graphql 1.12.10 → 1.12.12

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9d314c3a4dbbf6bded442bea6c2fcdd68b0cd7730ebb5e807d3677ffdc838268
4
- data.tar.gz: 60a24771181bb8a479f768a4b17f3a474ff9bbc339027b94e18bf92554df296c
3
+ metadata.gz: 296954a3b03f2fd5dae9fae56eaa08ea04b3dfb8f25923201ea27c67147c71fa
4
+ data.tar.gz: 4de323637c29b729a3eb3f07b620e2014515b37235c485002d016ead490d86e7
5
5
  SHA512:
6
- metadata.gz: 25f941249c01ce4dcfe3522dfc7b045b6f2bd3888e1f21b2e25a3ae5de74f0de5f9721c998d6ddd33eb681339c3e883edcdc9d76a196c148b688a1264b1761fa
7
- data.tar.gz: 91bde8d0102ae6874511656b6e11140b511aa1813d19b70b91a168cf58cbfc95eb83a6244137fc06abeae19f15694ca005c94c6e728085d0b8be3ad3bb964ebc
6
+ metadata.gz: fdf50f6c6f170aa4c50d46356e4089c40a939bf3d0dfd4ab7bdb667d169f01a38320c971cdec1dde32353a20823c1756196312df5e425af41a157caa1179e845
7
+ data.tar.gz: 4202ca4741998ee1f5535eea14ff93c1a8529ff5085625d67171e75736f65f55d0433b93512c272e16f4f9a48dd630eba49cf37a54e49b0e9e3dad5f0496930c
@@ -83,7 +83,7 @@ module GraphQL
83
83
  value = if top && @override_value
84
84
  @override_value
85
85
  else
86
- @context.query.context.namespace(:interpreter)[:runtime].value_at(context_entry.path)
86
+ value_at(@context.query.context.namespace(:interpreter)[:runtime], context_entry.path)
87
87
  end
88
88
  rows << [
89
89
  "#{context_entry.ast_node ? context_entry.ast_node.position.join(":") : ""}",
@@ -130,7 +130,7 @@ module GraphQL
130
130
  if object.is_a?(GraphQL::Schema::Object)
131
131
  object = object.object
132
132
  end
133
- value = context_entry.namespace(:interpreter)[:runtime].value_at([])
133
+ value = value_at(context_entry.namespace(:interpreter)[:runtime], [])
134
134
  rows << [
135
135
  "#{position}",
136
136
  "#{op_type}#{op_name ? " #{op_name}" : ""}",
@@ -142,6 +142,18 @@ module GraphQL
142
142
  raise "Unexpected get_rows subject #{context_entry.class} (#{context_entry.inspect})"
143
143
  end
144
144
  end
145
+
146
+ def value_at(runtime, path)
147
+ response = runtime.response
148
+ path.each do |key|
149
+ if response && (response = response[key])
150
+ next
151
+ else
152
+ break
153
+ end
154
+ end
155
+ response
156
+ end
145
157
  end
146
158
  end
147
159
  end
@@ -77,6 +77,21 @@ module GraphQL
77
77
  nil
78
78
  end
79
79
 
80
+ # Use a self-contained queue for the work in the block.
81
+ def run_isolated
82
+ prev_queue = @pending_jobs
83
+ @pending_jobs = []
84
+ res = nil
85
+ # Make sure the block is inside a Fiber, so it can `Fiber.yield`
86
+ append_job {
87
+ res = yield
88
+ }
89
+ run
90
+ res
91
+ ensure
92
+ @pending_jobs = prev_queue
93
+ end
94
+
80
95
  # @api private Move along, move along
81
96
  def run
82
97
  # At a high level, the algorithm is:
@@ -136,26 +151,24 @@ module GraphQL
136
151
  # This is where an evented approach would be even better -- can we tell which
137
152
  # fibers are ready to continue, and continue execution there?
138
153
  #
139
- source_fiber_stack = if (first_source_fiber = create_source_fiber)
154
+ source_fiber_queue = if (first_source_fiber = create_source_fiber)
140
155
  [first_source_fiber]
141
156
  else
142
157
  nil
143
158
  end
144
159
 
145
- if source_fiber_stack
146
- # Use a stack with `.pop` here so that when a source causes another source to become pending,
147
- # that newly-pending source will run _before_ the one that depends on it.
148
- # (See below where the old fiber is pushed to the stack, then the new fiber is pushed on the stack.)
149
- while (outer_source_fiber = source_fiber_stack.pop)
160
+ if source_fiber_queue
161
+ while (outer_source_fiber = source_fiber_queue.shift)
150
162
  resume(outer_source_fiber)
151
163
 
152
- if outer_source_fiber.alive?
153
- source_fiber_stack << outer_source_fiber
154
- end
155
164
  # If this source caused more sources to become pending, run those before running this one again:
156
165
  next_source_fiber = create_source_fiber
157
166
  if next_source_fiber
158
- source_fiber_stack << next_source_fiber
167
+ source_fiber_queue << next_source_fiber
168
+ end
169
+
170
+ if outer_source_fiber.alive?
171
+ source_fiber_queue << outer_source_fiber
159
172
  end
160
173
  end
161
174
  end
@@ -224,16 +237,16 @@ module GraphQL
224
237
  #
225
238
  # @see https://github.com/rmosolgo/graphql-ruby/issues/3449
226
239
  def spawn_fiber
227
- fiber_locals = {}
240
+ fiber_locals = {}
228
241
 
229
242
  Thread.current.keys.each do |fiber_var_key|
230
243
  fiber_locals[fiber_var_key] = Thread.current[fiber_var_key]
231
- end
244
+ end
232
245
 
233
- Fiber.new do
246
+ Fiber.new do
234
247
  fiber_locals.each { |k, v| Thread.current[k] = v }
235
248
  yield
236
- end
249
+ end
237
250
  end
238
251
  end
239
252
  end
@@ -10,6 +10,7 @@ module GraphQL
10
10
  # These are all no-ops because code was
11
11
  # executed sychronously.
12
12
  def run; end
13
+ def run_isolated; yield; end
13
14
  def yield; end
14
15
 
15
16
  def append_job
@@ -6,7 +6,7 @@ module GraphQL
6
6
  class Execute
7
7
 
8
8
  # @api private
9
- class Skip; end
9
+ class Skip < GraphQL::Error; end
10
10
 
11
11
  # Just a singleton for implementing {Query::Context#skip}
12
12
  # @api private
@@ -4,7 +4,6 @@ require "graphql/execution/interpreter/argument_value"
4
4
  require "graphql/execution/interpreter/arguments"
5
5
  require "graphql/execution/interpreter/arguments_cache"
6
6
  require "graphql/execution/interpreter/execution_errors"
7
- require "graphql/execution/interpreter/hash_response"
8
7
  require "graphql/execution/interpreter/runtime"
9
8
  require "graphql/execution/interpreter/resolve"
10
9
  require "graphql/execution/interpreter/handles_raw_value"
@@ -19,7 +18,7 @@ module GraphQL
19
18
  def execute(_operation, _root_type, query)
20
19
  runtime = evaluate(query)
21
20
  sync_lazies(query: query)
22
- runtime.final_value
21
+ runtime.response
23
22
  end
24
23
 
25
24
  def self.use(schema_class)
@@ -57,7 +56,7 @@ module GraphQL
57
56
 
58
57
  def self.finish_query(query, _multiplex)
59
58
  {
60
- "data" => query.context.namespace(:interpreter)[:runtime].final_value
59
+ "data" => query.context.namespace(:interpreter)[:runtime].response
61
60
  }
62
61
  end
63
62
 
@@ -67,10 +66,7 @@ module GraphQL
67
66
  # Although queries in a multiplex _share_ an Interpreter instance,
68
67
  # they also have another item of state, which is private to that query
69
68
  # in particular, assign it here:
70
- runtime = Runtime.new(
71
- query: query,
72
- response: HashResponse.new,
73
- )
69
+ runtime = Runtime.new(query: query)
74
70
  query.context.namespace(:interpreter)[:runtime] = runtime
75
71
 
76
72
  query.trace("execute_query", {query: query}) do
@@ -91,7 +87,7 @@ module GraphQL
91
87
  final_values = queries.map do |query|
92
88
  runtime = query.context.namespace(:interpreter)[:runtime]
93
89
  # it might not be present if the query has an error
94
- runtime ? runtime.final_value : nil
90
+ runtime ? runtime.response : nil
95
91
  end
96
92
  final_values.compact!
97
93
  tracer.trace("execute_query_lazy", {multiplex: multiplex, query: query}) do
@@ -28,11 +28,12 @@ module GraphQL
28
28
  end
29
29
 
30
30
  def fetch(ast_node, argument_owner, parent_object)
31
- @storage[ast_node][argument_owner][parent_object]
32
31
  # If any jobs were enqueued, run them now,
33
32
  # since this might have been called outside of execution.
34
33
  # (The jobs are responsible for updating `result` in-place.)
35
- @dataloader.run
34
+ @dataloader.run_isolated do
35
+ @storage[ast_node][argument_owner][parent_object]
36
+ end
36
37
  # Ack, the _hash_ is updated, but the key is eventually
37
38
  # overridden with an immutable arguments instance.
38
39
  # The first call queues up the job,
@@ -8,6 +8,49 @@ module GraphQL
8
8
  #
9
9
  # @api private
10
10
  class Runtime
11
+
12
+ module GraphQLResult
13
+ # These methods are private concerns of GraphQL-Ruby,
14
+ # they aren't guaranteed to continue working in the future.
15
+ attr_accessor :graphql_dead, :graphql_parent, :graphql_result_name
16
+ # Although these are used by only one of the Result classes,
17
+ # it's handy to have the methods implemented on both (even though they just return `nil`)
18
+ # because it makes it easy to check if anything is assigned.
19
+ # @return [nil, Array<String>]
20
+ attr_accessor :graphql_non_null_field_names
21
+ # @return [nil, true]
22
+ attr_accessor :graphql_non_null_list_items
23
+ end
24
+
25
+ class GraphQLResultHash < Hash
26
+ include GraphQLResult
27
+
28
+ attr_accessor :graphql_merged_into
29
+
30
+ def []=(key, value)
31
+ # This is a hack.
32
+ # Basically, this object is merged into the root-level result at some point.
33
+ # But the problem is, some lazies are created whose closures retain reference to _this_
34
+ # object. When those lazies are resolved, they cause an update to this object.
35
+ #
36
+ # In order to return a proper top-level result, we have to update that top-level result object.
37
+ # In order to return a proper partial result (eg, for a directive), we have to update this object, too.
38
+ # Yowza.
39
+ if (t = @graphql_merged_into)
40
+ t[key] = value
41
+ end
42
+ super
43
+ end
44
+ end
45
+
46
+ class GraphQLResultArray < Array
47
+ include GraphQLResult
48
+ end
49
+
50
+ class GraphQLSelectionSet < Hash
51
+ attr_accessor :graphql_directives
52
+ end
53
+
11
54
  # @return [GraphQL::Query]
12
55
  attr_reader :query
13
56
 
@@ -17,30 +60,47 @@ module GraphQL
17
60
  # @return [GraphQL::Query::Context]
18
61
  attr_reader :context
19
62
 
20
- def initialize(query:, response:)
63
+ # @return [Hash]
64
+ attr_reader :response
65
+
66
+ def initialize(query:)
21
67
  @query = query
22
68
  @dataloader = query.multiplex.dataloader
23
69
  @schema = query.schema
24
70
  @context = query.context
25
71
  @multiplex_context = query.multiplex.context
26
72
  @interpreter_context = @context.namespace(:interpreter)
27
- @response = response
28
- @dead_paths = {}
29
- @types_at_paths = {}
73
+ @response = GraphQLResultHash.new
74
+ # Identify runtime directives by checking which of this schema's directives have overridden `def self.resolve`
75
+ @runtime_directive_names = []
76
+ noop_resolve_owner = GraphQL::Schema::Directive.singleton_class
77
+ schema.directives.each do |name, dir_defn|
78
+ if dir_defn.method(:resolve).owner != noop_resolve_owner
79
+ @runtime_directive_names << name
80
+ end
81
+ end
30
82
  # A cache of { Class => { String => Schema::Field } }
31
83
  # Which assumes that MyObject.get_field("myField") will return the same field
32
84
  # during the lifetime of a query
33
85
  @fields_cache = Hash.new { |h, k| h[k] = {} }
34
- end
35
-
36
- def final_value
37
- @response.final_value
86
+ # { Class => Boolean }
87
+ @lazy_cache = {}
38
88
  end
39
89
 
40
90
  def inspect
41
91
  "#<#{self.class.name} response=#{@response.inspect}>"
42
92
  end
43
93
 
94
+ def tap_or_each(obj_or_array)
95
+ if obj_or_array.is_a?(Array)
96
+ obj_or_array.each do |item|
97
+ yield(item, true)
98
+ end
99
+ else
100
+ yield(obj_or_array, false)
101
+ end
102
+ end
103
+
44
104
  # This _begins_ the execution. Some deferred work
45
105
  # might be stored up in lazies.
46
106
  # @return [void]
@@ -55,21 +115,42 @@ module GraphQL
55
115
 
56
116
  if object_proxy.nil?
57
117
  # Root .authorized? returned false.
58
- write_in_response(path, nil)
118
+ @response = nil
59
119
  else
60
- resolve_with_directives(object_proxy, root_operation) do # execute query level directives
120
+ resolve_with_directives(object_proxy, root_operation.directives) do # execute query level directives
61
121
  gathered_selections = gather_selections(object_proxy, root_type, root_operation.selections)
62
- # Make the first fiber which will begin execution
63
- @dataloader.append_job {
64
- evaluate_selections(
65
- path,
66
- context.scoped_context,
67
- object_proxy,
68
- root_type,
69
- root_op_type == "mutation",
70
- gathered_selections,
71
- )
72
- }
122
+ # This is kind of a hack -- `gathered_selections` is an Array if any of the selections
123
+ # require isolation during execution (because of runtime directives). In that case,
124
+ # make a new, isolated result hash for writing the result into. (That isolated response
125
+ # is eventually merged back into the main response)
126
+ #
127
+ # Otherwise, `gathered_selections` is a hash of selections which can be
128
+ # directly evaluated and the results can be written right into the main response hash.
129
+ tap_or_each(gathered_selections) do |selections, is_selection_array|
130
+ if is_selection_array
131
+ selection_response = GraphQLResultHash.new
132
+ final_response = @response
133
+ else
134
+ selection_response = @response
135
+ final_response = nil
136
+ end
137
+
138
+ @dataloader.append_job {
139
+ set_all_interpreter_context(query.root_value, nil, nil, path)
140
+ resolve_with_directives(object_proxy, selections.graphql_directives) do
141
+ evaluate_selections(
142
+ path,
143
+ context.scoped_context,
144
+ object_proxy,
145
+ root_type,
146
+ root_op_type == "mutation",
147
+ selections,
148
+ selection_response,
149
+ final_response,
150
+ )
151
+ end
152
+ }
153
+ end
73
154
  end
74
155
  end
75
156
  delete_interpreter_context(:current_path)
@@ -79,15 +160,36 @@ module GraphQL
79
160
  nil
80
161
  end
81
162
 
82
- def gather_selections(owner_object, owner_type, selections, selections_by_name = {})
163
+ # @return [void]
164
+ def deep_merge_selection_result(from_result, into_result)
165
+ from_result.each do |key, value|
166
+ if !into_result.key?(key)
167
+ into_result[key] = value
168
+ else
169
+ case value
170
+ when Hash
171
+ deep_merge_selection_result(value, into_result[key])
172
+ else
173
+ # We have to assume that, since this passed the `fields_will_merge` selection,
174
+ # that the old and new values are the same.
175
+ # There's no special handling of arrays because currently, there's no way to split the execution
176
+ # of a list over several concurrent flows.
177
+ into_result[key] = value
178
+ end
179
+ end
180
+ end
181
+ from_result.graphql_merged_into = into_result
182
+ nil
183
+ end
184
+
185
+ def gather_selections(owner_object, owner_type, selections, selections_to_run = nil, selections_by_name = GraphQLSelectionSet.new)
83
186
  selections.each do |node|
84
187
  # Skip gathering this if the directive says so
85
188
  if !directives_include?(node, owner_object, owner_type)
86
189
  next
87
190
  end
88
191
 
89
- case node
90
- when GraphQL::Language::Nodes::Field
192
+ if node.is_a?(GraphQL::Language::Nodes::Field)
91
193
  response_key = node.alias || node.name
92
194
  selections = selections_by_name[response_key]
93
195
  # if there was already a selection of this field,
@@ -103,58 +205,83 @@ module GraphQL
103
205
  # No selection was found for this field yet
104
206
  selections_by_name[response_key] = node
105
207
  end
106
- when GraphQL::Language::Nodes::InlineFragment
107
- if node.type
108
- type_defn = schema.get_type(node.type.name)
109
- # Faster than .map{}.include?()
110
- query.warden.possible_types(type_defn).each do |t|
208
+ else
209
+ # This is an InlineFragment or a FragmentSpread
210
+ if @runtime_directive_names.any? && node.directives.any? { |d| @runtime_directive_names.include?(d.name) }
211
+ next_selections = GraphQLSelectionSet.new
212
+ next_selections.graphql_directives = node.directives
213
+ if selections_to_run
214
+ selections_to_run << next_selections
215
+ else
216
+ selections_to_run = []
217
+ selections_to_run << selections_by_name
218
+ selections_to_run << next_selections
219
+ end
220
+ else
221
+ next_selections = selections_by_name
222
+ end
223
+
224
+ case node
225
+ when GraphQL::Language::Nodes::InlineFragment
226
+ if node.type
227
+ type_defn = schema.get_type(node.type.name)
228
+
229
+ # Faster than .map{}.include?()
230
+ query.warden.possible_types(type_defn).each do |t|
231
+ if t == owner_type
232
+ gather_selections(owner_object, owner_type, node.selections, selections_to_run, next_selections)
233
+ break
234
+ end
235
+ end
236
+ else
237
+ # it's an untyped fragment, definitely continue
238
+ gather_selections(owner_object, owner_type, node.selections, selections_to_run, next_selections)
239
+ end
240
+ when GraphQL::Language::Nodes::FragmentSpread
241
+ fragment_def = query.fragments[node.name]
242
+ type_defn = schema.get_type(fragment_def.type.name)
243
+ possible_types = query.warden.possible_types(type_defn)
244
+ possible_types.each do |t|
111
245
  if t == owner_type
112
- gather_selections(owner_object, owner_type, node.selections, selections_by_name)
246
+ gather_selections(owner_object, owner_type, fragment_def.selections, selections_to_run, next_selections)
113
247
  break
114
248
  end
115
249
  end
116
250
  else
117
- # it's an untyped fragment, definitely continue
118
- gather_selections(owner_object, owner_type, node.selections, selections_by_name)
119
- end
120
- when GraphQL::Language::Nodes::FragmentSpread
121
- fragment_def = query.fragments[node.name]
122
- type_defn = schema.get_type(fragment_def.type.name)
123
- possible_types = query.warden.possible_types(type_defn)
124
- possible_types.each do |t|
125
- if t == owner_type
126
- gather_selections(owner_object, owner_type, fragment_def.selections, selections_by_name)
127
- break
128
- end
251
+ raise "Invariant: unexpected selection class: #{node.class}"
129
252
  end
130
- else
131
- raise "Invariant: unexpected selection class: #{node.class}"
132
253
  end
133
254
  end
134
- selections_by_name
255
+ selections_to_run || selections_by_name
135
256
  end
136
257
 
137
258
  NO_ARGS = {}.freeze
138
259
 
139
260
  # @return [void]
140
- def evaluate_selections(path, scoped_context, owner_object, owner_type, is_eager_selection, gathered_selections)
261
+ def evaluate_selections(path, scoped_context, owner_object, owner_type, is_eager_selection, gathered_selections, selections_result, target_result) # rubocop:disable Metrics/ParameterLists
141
262
  set_all_interpreter_context(owner_object, nil, nil, path)
142
263
 
264
+ finished_jobs = 0
265
+ enqueued_jobs = gathered_selections.size
143
266
  gathered_selections.each do |result_name, field_ast_nodes_or_ast_node|
144
267
  @dataloader.append_job {
145
268
  evaluate_selection(
146
- path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_selection
269
+ path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_selection, selections_result
147
270
  )
271
+ finished_jobs += 1
272
+ if target_result && finished_jobs == enqueued_jobs
273
+ deep_merge_selection_result(selections_result, target_result)
274
+ end
148
275
  }
149
276
  end
150
277
 
151
- nil
278
+ selections_result
152
279
  end
153
280
 
154
281
  attr_reader :progress_path
155
282
 
156
283
  # @return [void]
157
- def evaluate_selection(path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_field)
284
+ def evaluate_selection(path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_field, selections_result) # rubocop:disable Metrics/ParameterLists
158
285
  # As a performance optimization, the hash key will be a `Node` if
159
286
  # there's only one selection of the field. But if there are multiple
160
287
  # selections of the field, it will be an Array of nodes
@@ -188,7 +315,9 @@ module GraphQL
188
315
  # This seems janky, but we need to know
189
316
  # the field's return type at this path in order
190
317
  # to propagate `null`
191
- set_type_at_path(next_path, return_type)
318
+ if return_type.non_null?
319
+ (selections_result.graphql_non_null_field_names ||= []).push(result_name)
320
+ end
192
321
  # Set this before calling `run_with_directives`, so that the directive can have the latest path
193
322
  set_all_interpreter_context(nil, field_defn, nil, next_path)
194
323
 
@@ -202,21 +331,21 @@ module GraphQL
202
331
  total_args_count = field_defn.arguments.size
203
332
  if total_args_count == 0
204
333
  kwarg_arguments = GraphQL::Execution::Interpreter::Arguments::EMPTY
205
- evaluate_selection_with_args(kwarg_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field)
334
+ evaluate_selection_with_args(kwarg_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field, result_name, selections_result)
206
335
  else
207
336
  # TODO remove all arguments(...) usages?
208
337
  @query.arguments_cache.dataload_for(ast_node, field_defn, object) do |resolved_arguments|
209
- evaluate_selection_with_args(resolved_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field)
338
+ evaluate_selection_with_args(resolved_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field, result_name, selections_result)
210
339
  end
211
340
  end
212
341
  end
213
342
 
214
- def evaluate_selection_with_args(kwarg_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field) # rubocop:disable Metrics/ParameterLists
343
+ def evaluate_selection_with_args(kwarg_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field, result_name, selection_result) # rubocop:disable Metrics/ParameterLists
215
344
  context.scoped_context = scoped_context
216
345
  return_type = field_defn.type
217
- after_lazy(kwarg_arguments, owner: owner_type, field: field_defn, path: next_path, ast_node: ast_node, scoped_context: context.scoped_context, owner_object: object, arguments: kwarg_arguments) do |resolved_arguments|
346
+ after_lazy(kwarg_arguments, owner: owner_type, field: field_defn, path: next_path, ast_node: ast_node, scoped_context: context.scoped_context, owner_object: object, arguments: kwarg_arguments, result_name: result_name, result: selection_result) do |resolved_arguments|
218
347
  if resolved_arguments.is_a?(GraphQL::ExecutionError) || resolved_arguments.is_a?(GraphQL::UnauthorizedError)
219
- continue_value(next_path, resolved_arguments, owner_type, field_defn, return_type.non_null?, ast_node)
348
+ continue_value(next_path, resolved_arguments, owner_type, field_defn, return_type.non_null?, ast_node, result_name, selection_result)
220
349
  next
221
350
  end
222
351
 
@@ -268,12 +397,17 @@ module GraphQL
268
397
  # Optimize for the case that field is selected only once
269
398
  if field_ast_nodes.nil? || field_ast_nodes.size == 1
270
399
  next_selections = ast_node.selections
400
+ directives = ast_node.directives
271
401
  else
272
402
  next_selections = []
273
- field_ast_nodes.each { |f| next_selections.concat(f.selections) }
403
+ directives = []
404
+ field_ast_nodes.each { |f|
405
+ next_selections.concat(f.selections)
406
+ directives.concat(f.directives)
407
+ }
274
408
  end
275
409
 
276
- field_result = resolve_with_directives(object, ast_node) do
410
+ field_result = resolve_with_directives(object, directives) do
277
411
  # Actually call the field resolver and capture the result
278
412
  app_result = begin
279
413
  query.with_error_handling do
@@ -284,10 +418,10 @@ module GraphQL
284
418
  rescue GraphQL::ExecutionError => err
285
419
  err
286
420
  end
287
- after_lazy(app_result, owner: owner_type, field: field_defn, path: next_path, ast_node: ast_node, scoped_context: context.scoped_context, owner_object: object, arguments: kwarg_arguments) do |inner_result|
288
- continue_value = continue_value(next_path, inner_result, owner_type, field_defn, return_type.non_null?, ast_node)
421
+ after_lazy(app_result, owner: owner_type, field: field_defn, path: next_path, ast_node: ast_node, scoped_context: context.scoped_context, owner_object: object, arguments: kwarg_arguments, result_name: result_name, result: selection_result) do |inner_result|
422
+ continue_value = continue_value(next_path, inner_result, owner_type, field_defn, return_type.non_null?, ast_node, result_name, selection_result)
289
423
  if HALT != continue_value
290
- continue_field(next_path, continue_value, owner_type, field_defn, return_type, ast_node, next_selections, false, object, kwarg_arguments)
424
+ continue_field(next_path, continue_value, owner_type, field_defn, return_type, ast_node, next_selections, false, object, kwarg_arguments, result_name, selection_result)
291
425
  end
292
426
  end
293
427
  end
@@ -304,43 +438,109 @@ module GraphQL
304
438
  end
305
439
  end
306
440
 
441
+ def dead_result?(selection_result)
442
+ r = selection_result
443
+ while r
444
+ if r.graphql_dead
445
+ return true
446
+ else
447
+ r = r.graphql_parent
448
+ end
449
+ end
450
+ false
451
+ end
452
+
453
+ def set_result(selection_result, result_name, value)
454
+ if !dead_result?(selection_result)
455
+ if value.nil? &&
456
+ ( # there are two conditions under which `nil` is not allowed in the response:
457
+ (selection_result.graphql_non_null_list_items) || # this value would be written into a list that doesn't allow nils
458
+ ((nn = selection_result.graphql_non_null_field_names) && nn.include?(result_name)) # this value would be written into a field that doesn't allow nils
459
+ )
460
+ # This is an invalid nil that should be propagated
461
+ # One caller of this method passes a block,
462
+ # namely when application code returns a `nil` to GraphQL and it doesn't belong there.
463
+ # The other possibility for reaching here is when a field returns an ExecutionError, so we write
464
+ # `nil` to the response, not knowing whether it's an invalid `nil` or not.
465
+ # (And in that case, we don't have to call the schema's handler, since it's not a bug in the application.)
466
+ # TODO the code is trying to tell me something.
467
+ yield if block_given?
468
+ parent = selection_result.graphql_parent
469
+ name_in_parent = selection_result.graphql_result_name
470
+ if parent.nil? # This is a top-level result hash
471
+ @response = nil
472
+ else
473
+ set_result(parent, name_in_parent, nil)
474
+ # This is odd, but it's how it used to work. Even if `parent` _would_ accept
475
+ # a `nil`, it's marked dead. TODO: check the spec, is there a reason for this?
476
+ parent.graphql_dead = true
477
+ end
478
+ else
479
+ selection_result[result_name] = value
480
+ end
481
+ end
482
+ end
483
+
307
484
  HALT = Object.new
308
- def continue_value(path, value, parent_type, field, is_non_null, ast_node)
309
- if value.nil?
485
+ def continue_value(path, value, parent_type, field, is_non_null, ast_node, result_name, selection_result) # rubocop:disable Metrics/ParameterLists
486
+ case value
487
+ when nil
310
488
  if is_non_null
311
- err = parent_type::InvalidNullError.new(parent_type, field, value)
312
- write_invalid_null_in_response(path, err)
489
+ set_result(selection_result, result_name, nil) do
490
+ # This block is called if `result_name` is not dead. (Maybe a previous invalid nil caused it be marked dead.)
491
+ err = parent_type::InvalidNullError.new(parent_type, field, value)
492
+ schema.type_error(err, context)
493
+ end
313
494
  else
314
- write_in_response(path, nil)
495
+ set_result(selection_result, result_name, nil)
315
496
  end
316
497
  HALT
317
- elsif value.is_a?(GraphQL::ExecutionError)
318
- value.path ||= path
319
- value.ast_node ||= ast_node
320
- write_execution_errors_in_response(path, [value])
321
- HALT
322
- elsif value.is_a?(Array) && value.any? && value.all? { |v| v.is_a?(GraphQL::ExecutionError) }
323
- value.each_with_index do |error, index|
324
- error.ast_node ||= ast_node
325
- error.path ||= path + (field.type.list? ? [index] : [])
498
+ when GraphQL::Error
499
+ # Handle these cases inside a single `when`
500
+ # to avoid the overhead of checking three different classes
501
+ # every time.
502
+ if value.is_a?(GraphQL::ExecutionError)
503
+ if !dead_result?(selection_result)
504
+ value.path ||= path
505
+ value.ast_node ||= ast_node
506
+ context.errors << value
507
+ set_result(selection_result, result_name, nil)
508
+ end
509
+ HALT
510
+ elsif value.is_a?(GraphQL::UnauthorizedError)
511
+ # this hook might raise & crash, or it might return
512
+ # a replacement value
513
+ next_value = begin
514
+ schema.unauthorized_object(value)
515
+ rescue GraphQL::ExecutionError => err
516
+ err
517
+ end
518
+ continue_value(path, next_value, parent_type, field, is_non_null, ast_node, result_name, selection_result)
519
+ elsif GraphQL::Execution::Execute::SKIP == value
520
+ HALT
521
+ else
522
+ # What could this actually _be_? Anyhow,
523
+ # preserve the default behavior of doing nothing with it.
524
+ value
326
525
  end
327
- write_execution_errors_in_response(path, value)
328
- HALT
329
- elsif value.is_a?(GraphQL::UnauthorizedError)
330
- # this hook might raise & crash, or it might return
331
- # a replacement value
332
- next_value = begin
333
- schema.unauthorized_object(value)
334
- rescue GraphQL::ExecutionError => err
335
- err
526
+ when Array
527
+ # It's an array full of execution errors; add them all.
528
+ if value.any? && value.all? { |v| v.is_a?(GraphQL::ExecutionError) }
529
+ if !dead_result?(selection_result)
530
+ value.each_with_index do |error, index|
531
+ error.ast_node ||= ast_node
532
+ error.path ||= path + (field.type.list? ? [index] : [])
533
+ context.errors << error
534
+ end
535
+ set_result(selection_result, result_name, nil)
536
+ end
537
+ HALT
538
+ else
539
+ value
336
540
  end
337
-
338
- continue_value(path, next_value, parent_type, field, is_non_null, ast_node)
339
- elsif GraphQL::Execution::Execute::SKIP == value
340
- HALT
341
- elsif value.is_a?(GraphQL::Execution::Interpreter::RawValue)
541
+ when GraphQL::Execution::Interpreter::RawValue
342
542
  # Write raw value directly to the response without resolving nested objects
343
- write_in_response(path, value.resolve)
543
+ set_result(selection_result, result_name, value.resolve)
344
544
  HALT
345
545
  else
346
546
  value
@@ -355,17 +555,22 @@ module GraphQL
355
555
  # Location information from `path` and `ast_node`.
356
556
  #
357
557
  # @return [Lazy, Array, Hash, Object] Lazy, Array, and Hash are all traversed to resolve lazy values later
358
- def continue_field(path, value, owner_type, field, current_type, ast_node, next_selections, is_non_null, owner_object, arguments) # rubocop:disable Metrics/ParameterLists
558
+ def continue_field(path, value, owner_type, field, current_type, ast_node, next_selections, is_non_null, owner_object, arguments, result_name, selection_result) # rubocop:disable Metrics/ParameterLists
559
+ if current_type.non_null?
560
+ current_type = current_type.of_type
561
+ is_non_null = true
562
+ end
563
+
359
564
  case current_type.kind.name
360
565
  when "SCALAR", "ENUM"
361
566
  r = current_type.coerce_result(value, context)
362
- write_in_response(path, r)
567
+ set_result(selection_result, result_name, r)
363
568
  r
364
569
  when "UNION", "INTERFACE"
365
570
  resolved_type_or_lazy, resolved_value = resolve_type(current_type, value, path)
366
571
  resolved_value ||= value
367
572
 
368
- after_lazy(resolved_type_or_lazy, owner: current_type, path: path, ast_node: ast_node, scoped_context: context.scoped_context, field: field, owner_object: owner_object, arguments: arguments, trace: false) do |resolved_type|
573
+ after_lazy(resolved_type_or_lazy, owner: current_type, path: path, ast_node: ast_node, scoped_context: context.scoped_context, field: field, owner_object: owner_object, arguments: arguments, trace: false, result_name: result_name, result: selection_result) do |resolved_type|
369
574
  possible_types = query.possible_types(current_type)
370
575
 
371
576
  if !possible_types.include?(resolved_type)
@@ -373,10 +578,10 @@ module GraphQL
373
578
  err_class = current_type::UnresolvedTypeError
374
579
  type_error = err_class.new(resolved_value, field, parent_type, resolved_type, possible_types)
375
580
  schema.type_error(type_error, context)
376
- write_in_response(path, nil)
581
+ set_result(selection_result, result_name, nil)
377
582
  nil
378
583
  else
379
- continue_field(path, resolved_value, owner_type, field, resolved_type, ast_node, next_selections, is_non_null, owner_object, arguments)
584
+ continue_field(path, resolved_value, owner_type, field, resolved_type, ast_node, next_selections, is_non_null, owner_object, arguments, result_name, selection_result)
380
585
  end
381
586
  end
382
587
  when "OBJECT"
@@ -385,34 +590,71 @@ module GraphQL
385
590
  rescue GraphQL::ExecutionError => err
386
591
  err
387
592
  end
388
- after_lazy(object_proxy, owner: current_type, path: path, ast_node: ast_node, scoped_context: context.scoped_context, field: field, owner_object: owner_object, arguments: arguments, trace: false) do |inner_object|
389
- continue_value = continue_value(path, inner_object, owner_type, field, is_non_null, ast_node)
593
+ after_lazy(object_proxy, owner: current_type, path: path, ast_node: ast_node, scoped_context: context.scoped_context, field: field, owner_object: owner_object, arguments: arguments, trace: false, result_name: result_name, result: selection_result) do |inner_object|
594
+ continue_value = continue_value(path, inner_object, owner_type, field, is_non_null, ast_node, result_name, selection_result)
390
595
  if HALT != continue_value
391
- response_hash = {}
392
- write_in_response(path, response_hash)
596
+ response_hash = GraphQLResultHash.new
597
+ response_hash.graphql_parent = selection_result
598
+ response_hash.graphql_result_name = result_name
599
+ set_result(selection_result, result_name, response_hash)
393
600
  gathered_selections = gather_selections(continue_value, current_type, next_selections)
394
- evaluate_selections(path, context.scoped_context, continue_value, current_type, false, gathered_selections)
395
- response_hash
601
+ # There are two possibilities for `gathered_selections`:
602
+ # 1. All selections of this object should be evaluated together (there are no runtime directives modifying execution).
603
+ # This case is handled below, and the result can be written right into the main `response_hash` above.
604
+ # In this case, `gathered_selections` is a hash of selections.
605
+ # 2. Some selections of this object have runtime directives that may or may not modify execution.
606
+ # That part of the selection is evaluated in an isolated way, writing into a sub-response object which is
607
+ # eventually merged into the final response. In this case, `gathered_selections` is an array of things to run in isolation.
608
+ # (Technically, it's possible that one of those entries _doesn't_ require isolation.)
609
+ tap_or_each(gathered_selections) do |selections, is_selection_array|
610
+ if is_selection_array
611
+ this_result = GraphQLResultHash.new
612
+ this_result.graphql_parent = selection_result
613
+ this_result.graphql_result_name = result_name
614
+ final_result = response_hash
615
+ else
616
+ this_result = response_hash
617
+ final_result = nil
618
+ end
619
+ set_all_interpreter_context(continue_value, nil, nil, path) # reset this mutable state
620
+ resolve_with_directives(continue_value, selections.graphql_directives) do
621
+ evaluate_selections(
622
+ path,
623
+ context.scoped_context,
624
+ continue_value,
625
+ current_type,
626
+ false,
627
+ selections,
628
+ this_result,
629
+ final_result,
630
+ )
631
+ this_result
632
+ end
633
+ end
396
634
  end
397
635
  end
398
636
  when "LIST"
399
- response_list = []
400
- write_in_response(path, response_list)
401
637
  inner_type = current_type.of_type
638
+ response_list = GraphQLResultArray.new
639
+ response_list.graphql_non_null_list_items = inner_type.non_null?
640
+ response_list.graphql_parent = selection_result
641
+ response_list.graphql_result_name = result_name
642
+ set_result(selection_result, result_name, response_list)
643
+
402
644
  idx = 0
403
645
  scoped_context = context.scoped_context
404
646
  begin
405
647
  value.each do |inner_value|
406
648
  next_path = path.dup
407
649
  next_path << idx
650
+ this_idx = idx
408
651
  next_path.freeze
409
652
  idx += 1
410
- set_type_at_path(next_path, inner_type)
411
653
  # This will update `response_list` with the lazy
412
- after_lazy(inner_value, owner: inner_type, path: next_path, ast_node: ast_node, scoped_context: scoped_context, field: field, owner_object: owner_object, arguments: arguments) do |inner_inner_value|
413
- continue_value = continue_value(next_path, inner_inner_value, owner_type, field, inner_type.non_null?, ast_node)
654
+ after_lazy(inner_value, owner: inner_type, path: next_path, ast_node: ast_node, scoped_context: scoped_context, field: field, owner_object: owner_object, arguments: arguments, result_name: this_idx, result: response_list) do |inner_inner_value|
655
+ continue_value = continue_value(next_path, inner_inner_value, owner_type, field, inner_type.non_null?, ast_node, this_idx, response_list)
414
656
  if HALT != continue_value
415
- continue_field(next_path, continue_value, owner_type, field, inner_type, ast_node, next_selections, false, owner_object, arguments)
657
+ continue_field(next_path, continue_value, owner_type, field, inner_type, ast_node, next_selections, false, owner_object, arguments, this_idx, response_list)
416
658
  end
417
659
  end
418
660
  end
@@ -428,23 +670,18 @@ module GraphQL
428
670
  end
429
671
 
430
672
  response_list
431
- when "NON_NULL"
432
- inner_type = current_type.of_type
433
- # Don't `set_type_at_path` because we want the static type,
434
- # we're going to use that to determine whether a `nil` should be propagated or not.
435
- continue_field(path, value, owner_type, field, inner_type, ast_node, next_selections, true, owner_object, arguments)
436
673
  else
437
674
  raise "Invariant: Unhandled type kind #{current_type.kind} (#{current_type})"
438
675
  end
439
676
  end
440
677
 
441
- def resolve_with_directives(object, ast_node, &block)
442
- return yield if ast_node.directives.empty?
443
- run_directive(object, ast_node, 0, &block)
678
+ def resolve_with_directives(object, directives, &block)
679
+ return yield if directives.nil? || directives.empty?
680
+ run_directive(object, directives, 0, &block)
444
681
  end
445
682
 
446
- def run_directive(object, ast_node, idx, &block)
447
- dir_node = ast_node.directives[idx]
683
+ def run_directive(object, directives, idx, &block)
684
+ dir_node = directives[idx]
448
685
  if !dir_node
449
686
  yield
450
687
  else
@@ -452,9 +689,9 @@ module GraphQL
452
689
  if !dir_defn.is_a?(Class)
453
690
  dir_defn = dir_defn.type_class || raise("Only class-based directives are supported (not `@#{dir_node.name}`)")
454
691
  end
455
- dir_args = arguments(nil, dir_defn, dir_node).keyword_arguments
692
+ dir_args = arguments(nil, dir_defn, dir_node)
456
693
  dir_defn.resolve(object, dir_args, context) do
457
- run_directive(object, ast_node, idx + 1, &block)
694
+ run_directive(object, directives, idx + 1, &block)
458
695
  end
459
696
  end
460
697
  end
@@ -463,7 +700,7 @@ module GraphQL
463
700
  def directives_include?(node, graphql_object, parent_type)
464
701
  node.directives.each do |dir_node|
465
702
  dir_defn = schema.directives.fetch(dir_node.name).type_class || raise("Only class-based directives are supported (not #{dir_node.name.inspect})")
466
- args = arguments(graphql_object, dir_defn, dir_node).keyword_arguments
703
+ args = arguments(graphql_object, dir_defn, dir_node)
467
704
  if !dir_defn.include?(graphql_object, args, context)
468
705
  return false
469
706
  end
@@ -492,9 +729,8 @@ module GraphQL
492
729
  # @param eager [Boolean] Set to `true` for mutation root fields only
493
730
  # @param trace [Boolean] If `false`, don't wrap this with field tracing
494
731
  # @return [GraphQL::Execution::Lazy, Object] If loading `object` will be deferred, it's a wrapper over it.
495
- def after_lazy(lazy_obj, owner:, field:, path:, scoped_context:, owner_object:, arguments:, ast_node:, eager: false, trace: true, &block)
496
- set_all_interpreter_context(owner_object, field, arguments, path)
497
- if schema.lazy?(lazy_obj)
732
+ def after_lazy(lazy_obj, owner:, field:, path:, scoped_context:, owner_object:, arguments:, ast_node:, result:, result_name:, eager: false, trace: true, &block)
733
+ if lazy?(lazy_obj)
498
734
  lazy = GraphQL::Execution::Lazy.new(path: path, field: field) do
499
735
  set_all_interpreter_context(owner_object, field, arguments, path)
500
736
  context.scoped_context = scoped_context
@@ -513,16 +749,17 @@ module GraphQL
513
749
  rescue GraphQL::ExecutionError, GraphQL::UnauthorizedError => err
514
750
  err
515
751
  end
516
- after_lazy(inner_obj, owner: owner, field: field, path: path, ast_node: ast_node, scoped_context: context.scoped_context, owner_object: owner_object, arguments: arguments, eager: eager, trace: trace, &block)
752
+ yield(inner_obj)
517
753
  end
518
754
 
519
755
  if eager
520
756
  lazy.value
521
757
  else
522
- write_in_response(path, lazy)
758
+ set_result(result, result_name, lazy)
523
759
  lazy
524
760
  end
525
761
  else
762
+ set_all_interpreter_context(owner_object, field, arguments, path)
526
763
  yield(lazy_obj)
527
764
  end
528
765
  end
@@ -536,85 +773,6 @@ module GraphQL
536
773
  end
537
774
  end
538
775
 
539
- def write_invalid_null_in_response(path, invalid_null_error)
540
- if !dead_path?(path)
541
- schema.type_error(invalid_null_error, context)
542
- write_in_response(path, nil)
543
- add_dead_path(path)
544
- end
545
- end
546
-
547
- def write_execution_errors_in_response(path, errors)
548
- if !dead_path?(path)
549
- errors.each do |v|
550
- context.errors << v
551
- end
552
- write_in_response(path, nil)
553
- add_dead_path(path)
554
- end
555
- end
556
-
557
- def write_in_response(path, value)
558
- if dead_path?(path)
559
- return
560
- else
561
- if value.nil? && path.any? && type_at(path).non_null?
562
- # This nil is invalid, try writing it at the previous spot
563
- propagate_path = path[0..-2]
564
- write_in_response(propagate_path, value)
565
- add_dead_path(propagate_path)
566
- else
567
- @response.write(path, value)
568
- end
569
- end
570
- end
571
-
572
- def value_at(path)
573
- i = 0
574
- value = @response.final_value
575
- while value && (part = path[i])
576
- value = value[part]
577
- i += 1
578
- end
579
- value
580
- end
581
-
582
- # To propagate nulls, we have to know what the field type was
583
- # at previous parts of the response.
584
- # This hash matches the response
585
- def type_at(path)
586
- @types_at_paths.fetch(path)
587
- end
588
-
589
- def set_type_at_path(path, type)
590
- @types_at_paths[path] = type
591
- nil
592
- end
593
-
594
- # Mark `path` as having been permanently nulled out.
595
- # No values will be added beyond that path.
596
- def add_dead_path(path)
597
- dead = @dead_paths
598
- path.each do |part|
599
- dead = dead[part] ||= {}
600
- end
601
- dead[:__dead] = true
602
- end
603
-
604
- def dead_path?(path)
605
- res = @dead_paths
606
- path.each do |part|
607
- if res
608
- if res[:__dead]
609
- break
610
- else
611
- res = res[part]
612
- end
613
- end
614
- end
615
- res && res[:__dead]
616
- end
617
-
618
776
  # Set this pair in the Query context, but also in the interpeter namespace,
619
777
  # for compatibility.
620
778
  def set_interpreter_context(key, value)
@@ -633,7 +791,7 @@ module GraphQL
633
791
  query.resolve_type(type, value)
634
792
  end
635
793
 
636
- if schema.lazy?(resolved_type)
794
+ if lazy?(resolved_type)
637
795
  GraphQL::Execution::Lazy.new do
638
796
  query.trace("resolve_type_lazy", trace_payload) do
639
797
  schema.sync_lazy(resolved_type)
@@ -647,6 +805,12 @@ module GraphQL
647
805
  def authorized_new(type, value, context)
648
806
  type.authorized_new(value, context)
649
807
  end
808
+
809
+ def lazy?(object)
810
+ @lazy_cache.fetch(object.class) {
811
+ @lazy_cache[object.class] = @schema.lazy?(object)
812
+ }
813
+ end
650
814
  end
651
815
  end
652
816
  end