graphql 1.12.10 → 1.12.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of graphql might be problematic. Click here for more details.

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