Skip to content

Commit fa35e79

Browse files
authored
Merge pull request #5589 from rmosolgo/exec-next-subscriptions
exec-next: Subscriptions
2 parents becbe89 + 65a49f1 commit fa35e79

19 files changed

Lines changed: 345 additions & 185 deletions

lib/graphql/execution.rb

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@
1010
module GraphQL
1111
module Execution
1212
# @api private
13-
class Skip < GraphQL::Error; end
13+
class Skip < GraphQL::RuntimeError
14+
attr_accessor :path
15+
def ast_nodes=(_ignored); end
1416

15-
# Just a singleton for implementing {Query::Context#skip}
16-
# @api private
17-
SKIP = Skip.new
17+
def assign_graphql_result(query, result_data, key)
18+
result_data.delete(key)
19+
end
20+
end
1821
end
1922
end

lib/graphql/execution/interpreter.rb

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,6 @@ def run_all(schema, query_options, context: {}, max_complexity: schema.max_compl
5858
# Do as much eager evaluation of the query as possible
5959
results = []
6060
queries.each_with_index do |query, idx|
61-
if query.subscription? && !query.subscription_update?
62-
subs_namespace = query.context.namespace(:subscriptions)
63-
subs_namespace[:events] = []
64-
subs_namespace[:subscriptions] = {}
65-
end
6661
multiplex.dataloader.append_job {
6762
operation = query.selected_operation
6863
result = if operation.nil? || !query.valid? || !query.context.errors.empty?
@@ -74,7 +69,9 @@ def run_all(schema, query_options, context: {}, max_complexity: schema.max_compl
7469
# in particular, assign it here:
7570
runtime = Runtime.new(query: query)
7671
query.context.namespace(:interpreter_runtime)[:runtime] = runtime
77-
72+
if query.subscription? && !query.subscription_update?
73+
schema.subscriptions.initialize_subscriptions(query)
74+
end
7875
query.current_trace.execute_query(query: query) do
7976
runtime.run_eager
8077
end
@@ -91,9 +88,6 @@ def run_all(schema, query_options, context: {}, max_complexity: schema.max_compl
9188
# Then, find all errors and assign the result to the query object
9289
results.each_with_index do |data_result, idx|
9390
query = queries[idx]
94-
if (events = query.context.namespace(:subscriptions)[:events]) && !events.empty?
95-
schema.subscriptions.write_subscription(query, events)
96-
end
9791
# Assign the result so that it can be accessed in instrumentation
9892
query.result_values = if data_result.equal?(NO_OPERATION)
9993
if !query.valid? || !query.context.errors.empty?
@@ -103,6 +97,9 @@ def run_all(schema, query_options, context: {}, max_complexity: schema.max_compl
10397
data_result
10498
end
10599
else
100+
if query.subscription?
101+
schema.subscriptions.finish_subscriptions(query)
102+
end
106103
result = {}
107104

108105
if !query.context.errors.empty?

lib/graphql/execution/interpreter/runtime.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -603,7 +603,7 @@ def continue_value(value, field, is_non_null, ast_node, result_name, selection_r
603603
err
604604
end
605605
continue_value(next_value, field, is_non_null, ast_node, result_name, selection_result)
606-
elsif GraphQL::Execution::SKIP == value
606+
elsif value.is_a?(GraphQL::Execution::Skip)
607607
# It's possible a lazy was already written here
608608
case selection_result
609609
when GraphQLResultHash

lib/graphql/execution/lazy.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def value
3838
# (fewer clauses in a hot `case` block), but now it requires special handling here.
3939
# I think it's still worth it for the performance win, but if the number of special
4040
# cases grows, then maybe it's worth rethinking somehow.
41-
if @value.is_a?(StandardError) && @value != GraphQL::Execution::SKIP
41+
if @value.is_a?(StandardError) && !@value.is_a?(GraphQL::Execution::Skip)
4242
raise @value
4343
else
4444
@value

lib/graphql/execution/next.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,16 @@ def self.use(schema, authorization: true)
5252

5353
def self.run_all(schema, query_options, context: {}, max_complexity: schema.max_complexity)
5454
queries = query_options.map do |opts|
55-
case opts
55+
query = case opts
5656
when Hash
5757
schema.query_class.new(schema, nil, **opts)
5858
when GraphQL::Query, GraphQL::Query::Partial
5959
opts
6060
else
6161
raise "Expected Hash or GraphQL::Query, not #{opts.class} (#{opts.inspect})"
6262
end
63+
query.context[:__graphql_execute_next] = true
64+
query
6365
end
6466
multiplex = Execution::Multiplex.new(schema: schema, queries: queries, context: context, max_complexity: max_complexity)
6567
runner = Runner.new(multiplex, **schema.execution_next_options)

lib/graphql/execution/next/field_resolve_step.rb

Lines changed: 88 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -48,59 +48,85 @@ def append_selection(ast_node)
4848
nil
4949
end
5050

51-
def coerce_arguments(argument_owner, ast_arguments_or_hash)
51+
def coerce_arguments(argument_owner, ast_arguments_or_hash, run_loads = true)
5252
arg_defns = argument_owner.arguments(@selections_step.query.context)
5353
if arg_defns.empty?
5454
return EmptyObjects::EMPTY_HASH
5555
end
5656
args_hash = {}
57-
if ast_arguments_or_hash.is_a?(Hash)
58-
ast_arguments_or_hash.each do |key, value|
59-
key_s = nil
60-
arg_defn = arg_defns.each_value.find { |a|
61-
a.keyword == key || a.graphql_name == (key_s ||= String(key))
62-
}
63-
coerce_argument_value(args_hash, arg_defn, value)
64-
end
65-
else
66-
ast_arguments_or_hash.each { |arg_node|
67-
arg_defn = arg_defns[arg_node.name]
68-
coerce_argument_value(args_hash, arg_defn, arg_node.value)
69-
}
57+
if ast_arguments_or_hash.nil? # This can happen with `.trigger`
58+
return args_hash
7059
end
71-
# TODO refactor the loop above into this one
60+
61+
arg_inputs_are_h = ast_arguments_or_hash.is_a?(Hash)
62+
7263
arg_defns.each do |arg_graphql_name, arg_defn|
73-
if arg_defn.default_value? && !args_hash.key?(arg_defn.keyword)
74-
coerce_argument_value(args_hash, arg_defn, arg_defn.default_value)
64+
arg_value = nil
65+
was_found = false
66+
if arg_inputs_are_h
67+
ast_arguments_or_hash.each do |key, value|
68+
if key == arg_defn.keyword || key.to_s == arg_defn.graphql_name
69+
arg_value = value
70+
was_found = true
71+
break
72+
end
73+
end
74+
else
75+
ast_arguments_or_hash.each do |arg_node|
76+
if arg_node.name == arg_defn.graphql_name
77+
arg_value = arg_node.value
78+
was_found = true
79+
break
80+
end
81+
end
82+
end
83+
84+
if arg_value.is_a?(Language::Nodes::VariableIdentifier)
85+
vars = @selections_step.query.variables
86+
arg_value = if vars.key?(arg_value.name)
87+
vars[arg_value.name]
88+
elsif vars.key?(arg_value.name.to_sym)
89+
vars[arg_value.name.to_sym]
90+
else
91+
was_found = false
92+
nil
93+
end
94+
end
95+
96+
if !was_found && arg_defn.default_value?
97+
was_found = true
98+
arg_value = arg_defn.default_value
99+
end
100+
101+
if was_found
102+
coerce_argument_value(args_hash, arg_defn, arg_value, run_loads)
75103
end
76104
end
77105

78106
args_hash
79107
end
80108

81-
def coerce_argument_value(arguments, arg_defn, arg_value, target_keyword: arg_defn.keyword, as_type: nil)
109+
def coerce_argument_value(arguments, arg_defn, arg_value, run_loads, target_keyword: run_loads ? arg_defn.keyword : arg_defn.graphql_name, as_type: nil)
82110
arg_t = as_type || arg_defn.type
83111
if arg_t.non_null?
84112
arg_t = arg_t.of_type
85113
end
86114

87-
arg_value = if arg_value.is_a?(Language::Nodes::VariableIdentifier)
115+
if arg_value.is_a?(Language::Nodes::VariableIdentifier)
88116
vars = @selections_step.query.variables
89-
if vars.key?(arg_value.name)
117+
arg_value = if vars.key?(arg_value.name)
90118
vars[arg_value.name]
91119
elsif vars.key?(arg_value.name.to_sym)
92120
vars[arg_value.name.to_sym]
93121
else
94-
return # not present
122+
nil
95123
end
96-
elsif arg_value.is_a?(Language::Nodes::NullValue)
97-
nil
124+
end
125+
126+
if arg_value.is_a?(Language::Nodes::NullValue)
127+
arg_value = nil
98128
elsif arg_value.is_a?(Language::Nodes::Enum)
99-
arg_value.name
100-
elsif arg_value.is_a?(Language::Nodes::InputObject)
101-
arg_value.arguments # rubocop:disable Development/ContextIsPassedCop
102-
else
103-
arg_value
129+
arg_value = arg_value.name
104130
end
105131

106132
ctx = @selections_step.query.context
@@ -111,7 +137,7 @@ def coerce_argument_value(arguments, arg_defn, arg_value, target_keyword: arg_de
111137
arg_value = Array(arg_value)
112138
inner_t = arg_t.of_type
113139
result = Array.new(arg_value.size)
114-
arg_value.each_with_index { |v, i| coerce_argument_value(result, arg_defn, v, target_keyword: i, as_type: inner_t) }
140+
arg_value.each_with_index { |v, i| coerce_argument_value(result, arg_defn, v, run_loads, target_keyword: i, as_type: inner_t) }
115141
result
116142
end
117143
elsif arg_t.kind.leaf?
@@ -125,7 +151,8 @@ def coerce_argument_value(arguments, arg_defn, arg_value, target_keyword: arg_de
125151
end
126152
end
127153
elsif arg_t.kind.input_object?
128-
input_obj_args = coerce_arguments(arg_t, arg_value)
154+
input_obj_vals = arg_value.is_a?(Language::Nodes::InputObject) ? arg_value.arguments : arg_value # rubocop:disable Development/ContextIsPassedCop
155+
input_obj_args = coerce_arguments(arg_t, input_obj_vals)
129156
arg_t.new(nil, ruby_kwargs: input_obj_args, context: @selections_step.query.context, defaults_used: nil)
130157
else
131158
raise "Unsupported argument value: #{arg_t.to_type_signature} / #{arg_value.class} (#{arg_value.inspect})"
@@ -145,7 +172,7 @@ def coerce_argument_value(arguments, arg_defn, arg_value, target_keyword: arg_de
145172

146173
if arg_value.is_a?(GraphQL::Error)
147174
@arguments = arg_value
148-
elsif arg_defn.loads && as_type.nil? && !arg_value.nil?
175+
elsif run_loads && arg_defn.loads && as_type.nil? && !arg_value.nil?
149176
# This is for legacy compat:
150177
load_receiver = if (r = @field_definition.resolver)
151178
r.new(field: @field_definition, context: @selections_step.query.context, object: nil)
@@ -263,7 +290,8 @@ def build_arguments
263290
arguments = coerce_arguments(@field_definition, @ast_node.arguments) # rubocop:disable Development/ContextIsPassedCop
264291
@arguments ||= arguments # may have already been set to an error
265292

266-
if @pending_steps.nil? || @pending_steps.size == 0
293+
if (@pending_steps.nil? || @pending_steps.size == 0) &&
294+
@field_results.nil? # Make sure the arguments flow didn't already call through
267295
execute_field
268296
end
269297
end
@@ -323,6 +351,14 @@ def execute_field
323351
is_authed = @field_definition.authorized?(o, @arguments, ctx)
324352
if is_authed
325353
authorized_objects << o
354+
else
355+
begin
356+
err = GraphQL::UnauthorizedFieldError.new(object: o, type: @parent_type, context: ctx, field: @field_definition)
357+
authorized_objects << query.schema.unauthorized_object(err)
358+
is_authed = true
359+
rescue GraphQL::ExecutionError => exec_err
360+
add_graphql_error(exec_err)
361+
end
326362
end
327363
is_authed
328364
}
@@ -616,29 +652,35 @@ def resolve_batch(objects, context, args_hash)
616652
method_receiver = @field_definition.dynamic_introspection ? @field_definition.owner : @parent_type
617653
case @field_definition.execution_next_mode
618654
when :resolve_batch
619-
if args_hash.empty?
620-
method_receiver.public_send(@field_definition.execution_next_mode_key, objects, context)
621-
else
655+
begin
622656
method_receiver.public_send(@field_definition.execution_next_mode_key, objects, context, **args_hash)
657+
rescue GraphQL::ExecutionError => exec_err
658+
Array.new(objects.size, exec_err)
623659
end
624660
when :resolve_static
625-
result = if args_hash.empty?
626-
method_receiver.public_send(@field_definition.execution_next_mode_key, context)
627-
else
628-
method_receiver.public_send(@field_definition.execution_next_mode_key, context, **args_hash)
629-
end
661+
result = method_receiver.public_send(@field_definition.execution_next_mode_key, context, **args_hash)
630662
Array.new(objects.size, result)
631663
when :resolve_each
632-
if args_hash.empty?
633-
objects.map { |o| method_receiver.public_send(@field_definition.execution_next_mode_key, o, context) }
634-
else
635-
objects.map { |o| method_receiver.public_send(@field_definition.execution_next_mode_key, o, context, **args_hash) }
664+
objects.map do |o|
665+
method_receiver.public_send(@field_definition.execution_next_mode_key, o, context, **args_hash)
666+
rescue GraphQL::ExecutionError => err
667+
err
636668
end
637669
when :hash_key
638670
objects.map { |o| o[@field_definition.execution_next_mode_key] }
639671
when :direct_send
640672
if args_hash.empty?
641-
objects.map { |o| o.public_send(@field_definition.execution_next_mode_key) }
673+
objects.map do |o|
674+
o.public_send(@field_definition.execution_next_mode_key)
675+
rescue GraphQL::ExecutionError => err
676+
err
677+
rescue StandardError => stderr
678+
begin
679+
@selections_step.query.handle_or_reraise(stderr)
680+
rescue GraphQL::ExecutionError => ex_err
681+
ex_err
682+
end
683+
end
642684
else
643685
objects.map { |o| o.public_send(@field_definition.execution_next_mode_key, **args_hash) }
644686
end
@@ -684,17 +726,12 @@ def resolve_batch(objects, context, args_hash)
684726
if @field_definition.dynamic_introspection
685727
obj_inst = @owner.wrap(obj_inst, context)
686728
end
687-
if args_hash.empty?
688-
obj_inst.public_send(@field_definition.execution_next_mode_key)
689-
else
690-
obj_inst.public_send(@field_definition.execution_next_mode_key, **args_hash)
691-
end
729+
obj_inst.public_send(@field_definition.execution_next_mode_key, **args_hash)
692730
end
693731
else
694732
raise "Batching execution for #{path} not implemented (execution_next_mode: #{@execution_next_mode.inspect}); provide `resolve_static:`, `resolve_batch:`, `hash_key:`, `method:`, or use a compatibility plug-in"
695733
end
696734
end
697-
698735
end
699736

700737
class RawValueFieldResolveStep < FieldResolveStep

0 commit comments

Comments
 (0)