graphql 2.0.20 → 2.0.22

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/backtrace/trace.rb +96 -0
  3. data/lib/graphql/backtrace.rb +6 -1
  4. data/lib/graphql/execution/interpreter/arguments.rb +1 -1
  5. data/lib/graphql/execution/interpreter/arguments_cache.rb +33 -33
  6. data/lib/graphql/execution/interpreter/runtime.rb +274 -209
  7. data/lib/graphql/execution/interpreter.rb +2 -3
  8. data/lib/graphql/execution/lookahead.rb +1 -1
  9. data/lib/graphql/filter.rb +8 -2
  10. data/lib/graphql/language/document_from_schema_definition.rb +37 -17
  11. data/lib/graphql/language/lexer.rb +5 -3
  12. data/lib/graphql/language/nodes.rb +2 -2
  13. data/lib/graphql/language/parser.rb +475 -458
  14. data/lib/graphql/language/parser.y +5 -1
  15. data/lib/graphql/pagination/connection.rb +5 -5
  16. data/lib/graphql/query/context.rb +22 -12
  17. data/lib/graphql/query/null_context.rb +4 -1
  18. data/lib/graphql/query.rb +25 -11
  19. data/lib/graphql/schema/argument.rb +12 -14
  20. data/lib/graphql/schema/build_from_definition.rb +15 -3
  21. data/lib/graphql/schema/enum_value.rb +2 -5
  22. data/lib/graphql/schema/field/connection_extension.rb +1 -1
  23. data/lib/graphql/schema/field.rb +17 -16
  24. data/lib/graphql/schema/field_extension.rb +1 -4
  25. data/lib/graphql/schema/find_inherited_value.rb +2 -7
  26. data/lib/graphql/schema/input_object.rb +1 -1
  27. data/lib/graphql/schema/member/has_arguments.rb +10 -8
  28. data/lib/graphql/schema/member/has_directives.rb +4 -6
  29. data/lib/graphql/schema/member/has_fields.rb +80 -36
  30. data/lib/graphql/schema/member/has_validators.rb +2 -2
  31. data/lib/graphql/schema/object.rb +1 -1
  32. data/lib/graphql/schema/printer.rb +3 -1
  33. data/lib/graphql/schema/relay_classic_mutation.rb +1 -1
  34. data/lib/graphql/schema/resolver.rb +8 -8
  35. data/lib/graphql/schema/validator.rb +1 -1
  36. data/lib/graphql/schema/warden.rb +11 -3
  37. data/lib/graphql/schema.rb +41 -12
  38. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +12 -4
  39. data/lib/graphql/static_validation/rules/fields_will_merge.rb +2 -2
  40. data/lib/graphql/tracing/appsignal_trace.rb +6 -0
  41. data/lib/graphql/tracing/legacy_trace.rb +65 -0
  42. data/lib/graphql/tracing/notifications_trace.rb +5 -1
  43. data/lib/graphql/tracing/platform_trace.rb +21 -19
  44. data/lib/graphql/tracing/prometheus_tracing/graphql_collector.rb +1 -1
  45. data/lib/graphql/tracing/trace.rb +75 -0
  46. data/lib/graphql/tracing.rb +4 -123
  47. data/lib/graphql/version.rb +1 -1
  48. data/lib/graphql.rb +4 -0
  49. data/readme.md +1 -1
  50. metadata +6 -3
@@ -8,14 +8,27 @@ module GraphQL
8
8
  #
9
9
  # @api private
10
10
  class Runtime
11
+ class CurrentState
12
+ def initialize
13
+ @current_object = nil
14
+ @current_field = nil
15
+ @current_arguments = nil
16
+ @current_result_name = nil
17
+ @current_result = nil
18
+ end
19
+
20
+ attr_accessor :current_result, :current_result_name,
21
+ :current_arguments, :current_field, :current_object
22
+ end
11
23
 
12
24
  module GraphQLResult
13
- def initialize(result_name, parent_result)
25
+ def initialize(result_name, parent_result, is_non_null_in_parent)
14
26
  @graphql_parent = parent_result
15
27
  if parent_result && parent_result.graphql_dead
16
28
  @graphql_dead = true
17
29
  end
18
30
  @graphql_result_name = result_name
31
+ @graphql_is_non_null_in_parent = is_non_null_in_parent
19
32
  # Jump through some hoops to avoid creating this duplicate storage if at all possible.
20
33
  @graphql_metadata = nil
21
34
  end
@@ -30,22 +43,14 @@ module GraphQL
30
43
  end
31
44
 
32
45
  attr_accessor :graphql_dead
33
- attr_reader :graphql_parent, :graphql_result_name
34
-
35
- # Although these are used by only one of the Result classes,
36
- # it's handy to have the methods implemented on both (even though they just return `nil`)
37
- # because it makes it easy to check if anything is assigned.
38
- # @return [nil, Array<String>]
39
- attr_accessor :graphql_non_null_field_names
40
- # @return [nil, true]
41
- attr_accessor :graphql_non_null_list_items
46
+ attr_reader :graphql_parent, :graphql_result_name, :graphql_is_non_null_in_parent
42
47
 
43
48
  # @return [Hash] Plain-Ruby result data (`@graphql_metadata` contains Result wrapper objects)
44
49
  attr_accessor :graphql_result_data
45
50
  end
46
51
 
47
52
  class GraphQLResultHash
48
- def initialize(_result_name, _parent_result)
53
+ def initialize(_result_name, _parent_result, _is_non_null_in_parent)
49
54
  super
50
55
  @graphql_result_data = {}
51
56
  end
@@ -54,7 +59,7 @@ module GraphQL
54
59
 
55
60
  attr_accessor :graphql_merged_into
56
61
 
57
- def []=(key, value)
62
+ def set_leaf(key, value)
58
63
  # This is a hack.
59
64
  # Basically, this object is merged into the root-level result at some point.
60
65
  # But the problem is, some lazies are created whose closures retain reference to _this_
@@ -64,20 +69,24 @@ module GraphQL
64
69
  # In order to return a proper partial result (eg, for a directive), we have to update this object, too.
65
70
  # Yowza.
66
71
  if (t = @graphql_merged_into)
67
- t[key] = value
72
+ t.set_leaf(key, value)
68
73
  end
69
74
 
70
- if value.respond_to?(:graphql_result_data)
71
- @graphql_result_data[key] = value.graphql_result_data
72
- # If we encounter some part of this response that requires metadata tracking,
73
- # then create the metadata hash if necessary. It will be kept up-to-date after this.
74
- (@graphql_metadata ||= @graphql_result_data.dup)[key] = value
75
- else
76
- @graphql_result_data[key] = value
77
- # keep this up-to-date if it's been initialized
78
- @graphql_metadata && @graphql_metadata[key] = value
79
- end
75
+ @graphql_result_data[key] = value
76
+ # keep this up-to-date if it's been initialized
77
+ @graphql_metadata && @graphql_metadata[key] = value
78
+
79
+ value
80
+ end
80
81
 
82
+ def set_child_result(key, value)
83
+ if (t = @graphql_merged_into)
84
+ t.set_child_result(key, value)
85
+ end
86
+ @graphql_result_data[key] = value.graphql_result_data
87
+ # If we encounter some part of this response that requires metadata tracking,
88
+ # then create the metadata hash if necessary. It will be kept up-to-date after this.
89
+ (@graphql_metadata ||= @graphql_result_data.dup)[key] = value
81
90
  value
82
91
  end
83
92
 
@@ -101,12 +110,35 @@ module GraphQL
101
110
  def [](k)
102
111
  (@graphql_metadata || @graphql_result_data)[k]
103
112
  end
113
+
114
+ def merge_into(into_result)
115
+ self.each do |key, value|
116
+ case value
117
+ when GraphQLResultHash
118
+ next_into = into_result[key]
119
+ if next_into
120
+ value.merge_into(next_into)
121
+ else
122
+ into_result.set_child_result(key, value)
123
+ end
124
+ when GraphQLResultArray
125
+ # There's no special handling of arrays because currently, there's no way to split the execution
126
+ # of a list over several concurrent flows.
127
+ next_result.set_child_result(key, value)
128
+ else
129
+ # We have to assume that, since this passed the `fields_will_merge` selection,
130
+ # that the old and new values are the same.
131
+ into_result.set_leaf(key, value)
132
+ end
133
+ end
134
+ @graphql_merged_into = into_result
135
+ end
104
136
  end
105
137
 
106
138
  class GraphQLResultArray
107
139
  include GraphQLResult
108
140
 
109
- def initialize(_result_name, _parent_result)
141
+ def initialize(_result_name, _parent_result, _is_non_null_in_parent)
110
142
  super
111
143
  @graphql_result_data = []
112
144
  end
@@ -123,19 +155,25 @@ module GraphQL
123
155
  @graphql_result_data.delete_at(delete_at_index)
124
156
  end
125
157
 
126
- def []=(idx, value)
158
+ def set_leaf(idx, value)
127
159
  if @skip_indices
128
160
  offset_by = @skip_indices.count { |skipped_idx| skipped_idx < idx }
129
161
  idx -= offset_by
130
162
  end
131
- if value.respond_to?(:graphql_result_data)
132
- @graphql_result_data[idx] = value.graphql_result_data
133
- (@graphql_metadata ||= @graphql_result_data.dup)[idx] = value
134
- else
135
- @graphql_result_data[idx] = value
136
- @graphql_metadata && @graphql_metadata[idx] = value
137
- end
163
+ @graphql_result_data[idx] = value
164
+ @graphql_metadata && @graphql_metadata[idx] = value
165
+ value
166
+ end
138
167
 
168
+ def set_child_result(idx, value)
169
+ if @skip_indices
170
+ offset_by = @skip_indices.count { |skipped_idx| skipped_idx < idx }
171
+ idx -= offset_by
172
+ end
173
+ @graphql_result_data[idx] = value.graphql_result_data
174
+ # If we encounter some part of this response that requires metadata tracking,
175
+ # then create the metadata hash if necessary. It will be kept up-to-date after this.
176
+ (@graphql_metadata ||= @graphql_result_data.dup)[idx] = value
139
177
  value
140
178
  end
141
179
 
@@ -144,10 +182,6 @@ module GraphQL
144
182
  end
145
183
  end
146
184
 
147
- class GraphQLSelectionSet < Hash
148
- attr_accessor :graphql_directives
149
- end
150
-
151
185
  # @return [GraphQL::Query]
152
186
  attr_reader :query
153
187
 
@@ -157,25 +191,14 @@ module GraphQL
157
191
  # @return [GraphQL::Query::Context]
158
192
  attr_reader :context
159
193
 
160
- def thread_info
161
- info = Thread.current[:__graphql_runtime_info]
162
- if !info
163
- new_ti = {}
164
- info = Thread.current[:__graphql_runtime_info] = new_ti
165
- end
166
- info
167
- end
168
-
169
194
  def initialize(query:, lazies_at_depth:)
170
195
  @query = query
196
+ @current_trace = query.current_trace
171
197
  @dataloader = query.multiplex.dataloader
172
198
  @lazies_at_depth = lazies_at_depth
173
199
  @schema = query.schema
174
200
  @context = query.context
175
- @multiplex_context = query.multiplex.context
176
- # Start this off empty:
177
- Thread.current[:__graphql_runtime_info] = nil
178
- @response = GraphQLResultHash.new(nil, nil)
201
+ @response = GraphQLResultHash.new(nil, nil, false)
179
202
  # Identify runtime directives by checking which of this schema's directives have overridden `def self.resolve`
180
203
  @runtime_directive_names = []
181
204
  noop_resolve_owner = GraphQL::Schema::Directive.singleton_class
@@ -189,8 +212,11 @@ module GraphQL
189
212
  # Which assumes that MyObject.get_field("myField") will return the same field
190
213
  # during the lifetime of a query
191
214
  @fields_cache = Hash.new { |h, k| h[k] = {} }
215
+ # this can by by-identity since owners are the same object, but not the sub-hash, which uses strings.
216
+ @fields_cache.compare_by_identity
192
217
  # { Class => Boolean }
193
218
  @lazy_cache = {}
219
+ @lazy_cache.compare_by_identity
194
220
  end
195
221
 
196
222
  def final_result
@@ -218,7 +244,9 @@ module GraphQL
218
244
  root_operation = query.selected_operation
219
245
  root_op_type = root_operation.operation_type || "query"
220
246
  root_type = schema.root_type_for_operation(root_op_type)
221
- set_all_interpreter_context(query.root_value, nil, nil, nil, @response)
247
+ st = get_current_runtime_state
248
+ st.current_object = query.root_value
249
+ st.current_result = @response
222
250
  object_proxy = authorized_new(root_type, query.root_value, context)
223
251
  object_proxy = schema.sync_lazy(object_proxy)
224
252
 
@@ -237,7 +265,7 @@ module GraphQL
237
265
  # directly evaluated and the results can be written right into the main response hash.
238
266
  tap_or_each(gathered_selections) do |selections, is_selection_array|
239
267
  if is_selection_array
240
- selection_response = GraphQLResultHash.new(nil, nil)
268
+ selection_response = GraphQLResultHash.new(nil, nil, false)
241
269
  final_response = @response
242
270
  else
243
271
  selection_response = @response
@@ -245,8 +273,14 @@ module GraphQL
245
273
  end
246
274
 
247
275
  @dataloader.append_job {
248
- set_all_interpreter_context(query.root_value, nil, nil, nil, selection_response)
249
- call_method_on_directives(:resolve, object_proxy, selections.graphql_directives) do
276
+ st = get_current_runtime_state
277
+ st.current_object = query.root_value
278
+ st.current_result = selection_response
279
+ # This is a less-frequent case; use a fast check since it's often not there.
280
+ if (directives = selections[:graphql_directives])
281
+ selections.delete(:graphql_directives)
282
+ end
283
+ call_method_on_directives(:resolve, object_proxy, directives) do
250
284
  evaluate_selections(
251
285
  object_proxy,
252
286
  root_type,
@@ -265,29 +299,7 @@ module GraphQL
265
299
  nil
266
300
  end
267
301
 
268
- # @return [void]
269
- def deep_merge_selection_result(from_result, into_result)
270
- from_result.each do |key, value|
271
- if !into_result.key?(key)
272
- into_result[key] = value
273
- else
274
- case value
275
- when GraphQLResultHash
276
- deep_merge_selection_result(value, into_result[key])
277
- else
278
- # We have to assume that, since this passed the `fields_will_merge` selection,
279
- # that the old and new values are the same.
280
- # There's no special handling of arrays because currently, there's no way to split the execution
281
- # of a list over several concurrent flows.
282
- into_result[key] = value
283
- end
284
- end
285
- end
286
- from_result.graphql_merged_into = into_result
287
- nil
288
- end
289
-
290
- def gather_selections(owner_object, owner_type, selections, selections_to_run = nil, selections_by_name = GraphQLSelectionSet.new)
302
+ def gather_selections(owner_object, owner_type, selections, selections_to_run = nil, selections_by_name = {})
291
303
  selections.each do |node|
292
304
  # Skip gathering this if the directive says so
293
305
  if !directives_include?(node, owner_object, owner_type)
@@ -313,8 +325,8 @@ module GraphQL
313
325
  else
314
326
  # This is an InlineFragment or a FragmentSpread
315
327
  if @runtime_directive_names.any? && node.directives.any? { |d| @runtime_directive_names.include?(d.name) }
316
- next_selections = GraphQLSelectionSet.new
317
- next_selections.graphql_directives = node.directives
328
+ next_selections = {}
329
+ next_selections[:graphql_directives] = node.directives
318
330
  if selections_to_run
319
331
  selections_to_run << next_selections
320
332
  else
@@ -360,11 +372,14 @@ module GraphQL
360
372
  selections_to_run || selections_by_name
361
373
  end
362
374
 
363
- NO_ARGS = {}.freeze
375
+ NO_ARGS = GraphQL::EmptyObjects::EMPTY_HASH
364
376
 
365
377
  # @return [void]
366
378
  def evaluate_selections(owner_object, owner_type, is_eager_selection, gathered_selections, selections_result, target_result, parent_object) # rubocop:disable Metrics/ParameterLists
367
- set_all_interpreter_context(owner_object, nil, nil, nil, selections_result)
379
+ st = get_current_runtime_state
380
+ st.current_object = owner_object
381
+ st.current_result_name = nil
382
+ st.current_result = selections_result
368
383
 
369
384
  finished_jobs = 0
370
385
  enqueued_jobs = gathered_selections.size
@@ -375,9 +390,15 @@ module GraphQL
375
390
  )
376
391
  finished_jobs += 1
377
392
  if target_result && finished_jobs == enqueued_jobs
378
- deep_merge_selection_result(selections_result, target_result)
393
+ selections_result.merge_into(target_result)
379
394
  end
380
395
  }
396
+ # Field resolution may pause the fiber,
397
+ # so it wouldn't get to the `Resolve` call that happens below.
398
+ # So instead trigger a run from this outer context.
399
+ if is_eager_selection
400
+ @dataloader.run
401
+ end
381
402
  end
382
403
 
383
404
  selections_result
@@ -418,11 +439,13 @@ module GraphQL
418
439
  # This seems janky, but we need to know
419
440
  # the field's return type at this path in order
420
441
  # to propagate `null`
421
- if return_type.non_null?
422
- (selections_result.graphql_non_null_field_names ||= []).push(result_name)
423
- end
442
+ return_type_non_null = return_type.non_null?
424
443
  # Set this before calling `run_with_directives`, so that the directive can have the latest path
425
- set_all_interpreter_context(nil, field_defn, nil, result_name, selections_result)
444
+ st = get_current_runtime_state
445
+ st.current_field = field_defn
446
+ st.current_result = selections_result
447
+ st.current_result_name = result_name
448
+
426
449
  object = owner_object
427
450
 
428
451
  if is_introspection
@@ -432,25 +455,34 @@ module GraphQL
432
455
  total_args_count = field_defn.arguments(context).size
433
456
  if total_args_count == 0
434
457
  resolved_arguments = GraphQL::Execution::Interpreter::Arguments::EMPTY
435
- evaluate_selection_with_args(resolved_arguments, field_defn, ast_node, field_ast_nodes, owner_type, object, is_eager_field, result_name, selections_result, parent_object, return_type)
458
+ if field_defn.extras.size == 0
459
+ evaluate_selection_with_resolved_keyword_args(
460
+ NO_ARGS, resolved_arguments, field_defn, ast_node, field_ast_nodes, owner_type, object, is_eager_field, result_name, selections_result, parent_object, return_type, return_type_non_null
461
+ )
462
+ else
463
+ evaluate_selection_with_args(resolved_arguments, field_defn, ast_node, field_ast_nodes, owner_type, object, is_eager_field, result_name, selections_result, parent_object, return_type, return_type_non_null)
464
+ end
436
465
  else
437
- # TODO remove all arguments(...) usages?
438
466
  @query.arguments_cache.dataload_for(ast_node, field_defn, object) do |resolved_arguments|
439
- evaluate_selection_with_args(resolved_arguments, field_defn, ast_node, field_ast_nodes, owner_type, object, is_eager_field, result_name, selections_result, parent_object, return_type)
467
+ evaluate_selection_with_args(resolved_arguments, field_defn, ast_node, field_ast_nodes, owner_type, object, is_eager_field, result_name, selections_result, parent_object, return_type, return_type_non_null)
440
468
  end
441
469
  end
442
470
  end
443
471
 
444
- def evaluate_selection_with_args(arguments, field_defn, ast_node, field_ast_nodes, owner_type, object, is_eager_field, result_name, selection_result, parent_object, return_type) # rubocop:disable Metrics/ParameterLists
445
- after_lazy(arguments, owner: owner_type, field: field_defn, ast_node: ast_node, owner_object: object, arguments: arguments, result_name: result_name, result: selection_result) do |resolved_arguments|
472
+ def evaluate_selection_with_args(arguments, field_defn, ast_node, field_ast_nodes, owner_type, object, is_eager_field, result_name, selection_result, parent_object, return_type, return_type_non_null) # rubocop:disable Metrics/ParameterLists
473
+ after_lazy(arguments, field: field_defn, ast_node: ast_node, owner_object: object, arguments: arguments, result_name: result_name, result: selection_result) do |resolved_arguments|
446
474
  if resolved_arguments.is_a?(GraphQL::ExecutionError) || resolved_arguments.is_a?(GraphQL::UnauthorizedError)
447
- continue_value(resolved_arguments, owner_type, field_defn, return_type.non_null?, ast_node, result_name, selection_result)
475
+ continue_value(resolved_arguments, owner_type, field_defn, return_type_non_null, ast_node, result_name, selection_result)
448
476
  next
449
477
  end
450
478
 
451
- kwarg_arguments = if resolved_arguments.empty? && field_defn.extras.empty?
452
- # We can avoid allocating the `{ Symbol => Object }` hash in this case
453
- NO_ARGS
479
+ kwarg_arguments = if field_defn.extras.empty?
480
+ if resolved_arguments.empty?
481
+ # We can avoid allocating the `{ Symbol => Object }` hash in this case
482
+ NO_ARGS
483
+ else
484
+ resolved_arguments.keyword_arguments
485
+ end
454
486
  else
455
487
  # Bundle up the extras, then make a new arguments instance
456
488
  # that includes the extras, too.
@@ -489,67 +521,72 @@ module GraphQL
489
521
  resolved_arguments.keyword_arguments
490
522
  end
491
523
 
492
- set_all_interpreter_context(nil, nil, resolved_arguments, result_name, selection_result)
524
+ evaluate_selection_with_resolved_keyword_args(kwarg_arguments, resolved_arguments, field_defn, ast_node, field_ast_nodes, owner_type, object, is_eager_field, result_name, selection_result, parent_object, return_type, return_type_non_null)
525
+ end
526
+ end
493
527
 
494
- # Optimize for the case that field is selected only once
495
- if field_ast_nodes.nil? || field_ast_nodes.size == 1
496
- next_selections = ast_node.selections
497
- directives = ast_node.directives
498
- else
499
- next_selections = []
500
- directives = []
501
- field_ast_nodes.each { |f|
502
- next_selections.concat(f.selections)
503
- directives.concat(f.directives)
504
- }
505
- end
506
-
507
- field_result = call_method_on_directives(:resolve, object, directives) do
508
- # Actually call the field resolver and capture the result
509
- app_result = begin
510
- query.current_trace.execute_field(field: field_defn, ast_node: ast_node, query: query, object: object, arguments: kwarg_arguments) do
511
- field_defn.resolve(object, kwarg_arguments, context)
512
- end
513
- rescue GraphQL::ExecutionError => err
514
- err
515
- rescue StandardError => err
516
- begin
517
- query.handle_or_reraise(err)
518
- rescue GraphQL::ExecutionError => ex_err
519
- ex_err
520
- end
528
+ def evaluate_selection_with_resolved_keyword_args(kwarg_arguments, resolved_arguments, field_defn, ast_node, field_ast_nodes, owner_type, object, is_eager_field, result_name, selection_result, parent_object, return_type, return_type_non_null) # rubocop:disable Metrics/ParameterLists
529
+ st = get_current_runtime_state
530
+ st.current_field = field_defn
531
+ st.current_object = object
532
+ st.current_arguments = resolved_arguments
533
+ st.current_result_name = result_name
534
+ st.current_result = selection_result
535
+ # Optimize for the case that field is selected only once
536
+ if field_ast_nodes.nil? || field_ast_nodes.size == 1
537
+ next_selections = ast_node.selections
538
+ directives = ast_node.directives
539
+ else
540
+ next_selections = []
541
+ directives = []
542
+ field_ast_nodes.each { |f|
543
+ next_selections.concat(f.selections)
544
+ directives.concat(f.directives)
545
+ }
546
+ end
547
+
548
+ field_result = call_method_on_directives(:resolve, object, directives) do
549
+ # Actually call the field resolver and capture the result
550
+ app_result = begin
551
+ @current_trace.execute_field(field: field_defn, ast_node: ast_node, query: query, object: object, arguments: kwarg_arguments) do
552
+ field_defn.resolve(object, kwarg_arguments, context)
521
553
  end
522
- after_lazy(app_result, owner: owner_type, field: field_defn, ast_node: ast_node, owner_object: object, arguments: resolved_arguments, result_name: result_name, result: selection_result) do |inner_result|
523
- continue_value = continue_value(inner_result, owner_type, field_defn, return_type.non_null?, ast_node, result_name, selection_result)
524
- if HALT != continue_value
525
- continue_field(continue_value, owner_type, field_defn, return_type, ast_node, next_selections, false, object, resolved_arguments, result_name, selection_result)
526
- end
554
+ rescue GraphQL::ExecutionError => err
555
+ err
556
+ rescue StandardError => err
557
+ begin
558
+ query.handle_or_reraise(err)
559
+ rescue GraphQL::ExecutionError => ex_err
560
+ ex_err
527
561
  end
528
562
  end
529
-
530
- # If this field is a root mutation field, immediately resolve
531
- # all of its child fields before moving on to the next root mutation field.
532
- # (Subselections of this mutation will still be resolved level-by-level.)
533
- if is_eager_field
534
- Interpreter::Resolve.resolve_all([field_result], @dataloader)
535
- else
536
- # Return this from `after_lazy` because it might be another lazy that needs to be resolved
537
- field_result
563
+ after_lazy(app_result, field: field_defn, ast_node: ast_node, owner_object: object, arguments: resolved_arguments, result_name: result_name, result: selection_result) do |inner_result|
564
+ continue_value = continue_value(inner_result, owner_type, field_defn, return_type_non_null, ast_node, result_name, selection_result)
565
+ if HALT != continue_value
566
+ continue_field(continue_value, owner_type, field_defn, return_type, ast_node, next_selections, false, object, resolved_arguments, result_name, selection_result)
567
+ end
538
568
  end
539
569
  end
570
+
571
+ # If this field is a root mutation field, immediately resolve
572
+ # all of its child fields before moving on to the next root mutation field.
573
+ # (Subselections of this mutation will still be resolved level-by-level.)
574
+ if is_eager_field
575
+ Interpreter::Resolve.resolve_all([field_result], @dataloader)
576
+ else
577
+ # Return this from `after_lazy` because it might be another lazy that needs to be resolved
578
+ field_result
579
+ end
540
580
  end
541
581
 
582
+
542
583
  def dead_result?(selection_result)
543
- selection_result.graphql_dead || ((parent = selection_result.graphql_parent) && parent.graphql_dead)
584
+ selection_result.graphql_dead # || ((parent = selection_result.graphql_parent) && parent.graphql_dead)
544
585
  end
545
586
 
546
- def set_result(selection_result, result_name, value)
587
+ def set_result(selection_result, result_name, value, is_child_result, is_non_null)
547
588
  if !dead_result?(selection_result)
548
- if value.nil? &&
549
- ( # there are two conditions under which `nil` is not allowed in the response:
550
- (selection_result.graphql_non_null_list_items) || # this value would be written into a list that doesn't allow nils
551
- ((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
552
- )
589
+ if value.nil? && is_non_null
553
590
  # This is an invalid nil that should be propagated
554
591
  # One caller of this method passes a block,
555
592
  # namely when application code returns a `nil` to GraphQL and it doesn't belong there.
@@ -559,15 +596,18 @@ module GraphQL
559
596
  # TODO the code is trying to tell me something.
560
597
  yield if block_given?
561
598
  parent = selection_result.graphql_parent
562
- name_in_parent = selection_result.graphql_result_name
563
599
  if parent.nil? # This is a top-level result hash
564
600
  @response = nil
565
601
  else
566
- set_result(parent, name_in_parent, nil)
602
+ name_in_parent = selection_result.graphql_result_name
603
+ is_non_null_in_parent = selection_result.graphql_is_non_null_in_parent
604
+ set_result(parent, name_in_parent, nil, false, is_non_null_in_parent)
567
605
  set_graphql_dead(selection_result)
568
606
  end
607
+ elsif is_child_result
608
+ selection_result.set_child_result(result_name, value)
569
609
  else
570
- selection_result[result_name] = value
610
+ selection_result.set_leaf(result_name, value)
571
611
  end
572
612
  end
573
613
  end
@@ -588,11 +628,10 @@ module GraphQL
588
628
  end
589
629
 
590
630
  def current_path
591
- ti = thread_info
592
- path = ti &&
593
- (result = ti[:current_result]) &&
594
- (result.path)
595
- if path && (rn = ti[:current_result_name])
631
+ st = get_current_runtime_state
632
+ result = st.current_result
633
+ path = result && result.path
634
+ if path && (rn = st.current_result_name)
596
635
  path = path.dup
597
636
  path.push(rn)
598
637
  end
@@ -604,13 +643,13 @@ module GraphQL
604
643
  case value
605
644
  when nil
606
645
  if is_non_null
607
- set_result(selection_result, result_name, nil) do
646
+ set_result(selection_result, result_name, nil, false, is_non_null) do
608
647
  # This block is called if `result_name` is not dead. (Maybe a previous invalid nil caused it be marked dead.)
609
648
  err = parent_type::InvalidNullError.new(parent_type, field, value)
610
649
  schema.type_error(err, context)
611
650
  end
612
651
  else
613
- set_result(selection_result, result_name, nil)
652
+ set_result(selection_result, result_name, nil, false, is_non_null)
614
653
  end
615
654
  HALT
616
655
  when GraphQL::Error
@@ -623,7 +662,7 @@ module GraphQL
623
662
  value.ast_node ||= ast_node
624
663
  context.errors << value
625
664
  if selection_result
626
- set_result(selection_result, result_name, nil)
665
+ set_result(selection_result, result_name, nil, false, is_non_null)
627
666
  end
628
667
  end
629
668
  HALT
@@ -677,9 +716,9 @@ module GraphQL
677
716
  if selection_result
678
717
  if list_type_at_all
679
718
  result_without_errors = value.map { |v| v.is_a?(GraphQL::ExecutionError) ? nil : v }
680
- set_result(selection_result, result_name, result_without_errors)
719
+ set_result(selection_result, result_name, result_without_errors, false, is_non_null)
681
720
  else
682
- set_result(selection_result, result_name, nil)
721
+ set_result(selection_result, result_name, nil, false, is_non_null)
683
722
  end
684
723
  end
685
724
  end
@@ -689,7 +728,7 @@ module GraphQL
689
728
  end
690
729
  when GraphQL::Execution::Interpreter::RawValue
691
730
  # Write raw value directly to the response without resolving nested objects
692
- set_result(selection_result, result_name, value.resolve)
731
+ set_result(selection_result, result_name, value.resolve, false, is_non_null)
693
732
  HALT
694
733
  else
695
734
  value
@@ -717,11 +756,11 @@ module GraphQL
717
756
  rescue StandardError => err
718
757
  schema.handle_or_reraise(context, err)
719
758
  end
720
- set_result(selection_result, result_name, r)
759
+ set_result(selection_result, result_name, r, false, is_non_null)
721
760
  r
722
761
  when "UNION", "INTERFACE"
723
762
  resolved_type_or_lazy = resolve_type(current_type, value)
724
- after_lazy(resolved_type_or_lazy, owner: current_type, ast_node: ast_node, field: field, owner_object: owner_object, arguments: arguments, trace: false, result_name: result_name, result: selection_result) do |resolved_type_result|
763
+ after_lazy(resolved_type_or_lazy, ast_node: ast_node, field: field, owner_object: owner_object, arguments: arguments, trace: false, result_name: result_name, result: selection_result) do |resolved_type_result|
725
764
  if resolved_type_result.is_a?(Array) && resolved_type_result.length == 2
726
765
  resolved_type, resolved_value = resolved_type_result
727
766
  else
@@ -735,7 +774,7 @@ module GraphQL
735
774
  err_class = current_type::UnresolvedTypeError
736
775
  type_error = err_class.new(resolved_value, field, parent_type, resolved_type, possible_types)
737
776
  schema.type_error(type_error, context)
738
- set_result(selection_result, result_name, nil)
777
+ set_result(selection_result, result_name, nil, false, is_non_null)
739
778
  nil
740
779
  else
741
780
  continue_field(resolved_value, owner_type, field, resolved_type, ast_node, next_selections, is_non_null, owner_object, arguments, result_name, selection_result)
@@ -747,11 +786,11 @@ module GraphQL
747
786
  rescue GraphQL::ExecutionError => err
748
787
  err
749
788
  end
750
- after_lazy(object_proxy, owner: current_type, ast_node: ast_node, field: field, owner_object: owner_object, arguments: arguments, trace: false, result_name: result_name, result: selection_result) do |inner_object|
789
+ after_lazy(object_proxy, ast_node: ast_node, field: field, owner_object: owner_object, arguments: arguments, trace: false, result_name: result_name, result: selection_result) do |inner_object|
751
790
  continue_value = continue_value(inner_object, owner_type, field, is_non_null, ast_node, result_name, selection_result)
752
791
  if HALT != continue_value
753
- response_hash = GraphQLResultHash.new(result_name, selection_result)
754
- set_result(selection_result, result_name, response_hash)
792
+ response_hash = GraphQLResultHash.new(result_name, selection_result, is_non_null)
793
+ set_result(selection_result, result_name, response_hash, true, is_non_null)
755
794
  gathered_selections = gather_selections(continue_value, current_type, next_selections)
756
795
  # There are two possibilities for `gathered_selections`:
757
796
  # 1. All selections of this object should be evaluated together (there are no runtime directives modifying execution).
@@ -763,15 +802,24 @@ module GraphQL
763
802
  # (Technically, it's possible that one of those entries _doesn't_ require isolation.)
764
803
  tap_or_each(gathered_selections) do |selections, is_selection_array|
765
804
  if is_selection_array
766
- this_result = GraphQLResultHash.new(result_name, selection_result)
805
+ this_result = GraphQLResultHash.new(result_name, selection_result, is_non_null)
767
806
  final_result = response_hash
768
807
  else
769
808
  this_result = response_hash
770
809
  final_result = nil
771
810
  end
772
- # Don't pass `result_name` here because it's already included in the new response hash
773
- set_all_interpreter_context(continue_value, nil, nil, nil, this_result) # reset this mutable state
774
- call_method_on_directives(:resolve, continue_value, selections.graphql_directives) do
811
+ # reset this mutable state
812
+ # Unset `result_name` here because it's already included in the new response hash
813
+ st = get_current_runtime_state
814
+ st.current_object = continue_value
815
+ st.current_result_name = nil
816
+ st.current_result = this_result
817
+
818
+ # This is a less-frequent case; use a fast check since it's often not there.
819
+ if (directives = selections[:graphql_directives])
820
+ selections.delete(:graphql_directives)
821
+ end
822
+ call_method_on_directives(:resolve, continue_value, directives) do
775
823
  evaluate_selections(
776
824
  continue_value,
777
825
  current_type,
@@ -790,21 +838,21 @@ module GraphQL
790
838
  inner_type = current_type.of_type
791
839
  # This is true for objects, unions, and interfaces
792
840
  use_dataloader_job = !inner_type.unwrap.kind.input?
793
- response_list = GraphQLResultArray.new(result_name, selection_result)
794
- response_list.graphql_non_null_list_items = inner_type.non_null?
795
- set_result(selection_result, result_name, response_list)
796
- idx = 0
841
+ inner_type_non_null = inner_type.non_null?
842
+ response_list = GraphQLResultArray.new(result_name, selection_result, is_non_null)
843
+ set_result(selection_result, result_name, response_list, true, is_non_null)
844
+ idx = nil
797
845
  list_value = begin
798
846
  value.each do |inner_value|
799
- break if dead_result?(response_list)
847
+ idx ||= 0
800
848
  this_idx = idx
801
849
  idx += 1
802
850
  if use_dataloader_job
803
851
  @dataloader.append_job do
804
- resolve_list_item(inner_value, inner_type, ast_node, field, owner_object, arguments, this_idx, response_list, next_selections, owner_type)
852
+ resolve_list_item(inner_value, inner_type, inner_type_non_null, ast_node, field, owner_object, arguments, this_idx, response_list, next_selections, owner_type)
805
853
  end
806
854
  else
807
- resolve_list_item(inner_value, inner_type, ast_node, field, owner_object, arguments, this_idx, response_list, next_selections, owner_type)
855
+ resolve_list_item(inner_value, inner_type, inner_type_non_null, ast_node, field, owner_object, arguments, this_idx, response_list, next_selections, owner_type)
808
856
  end
809
857
  end
810
858
 
@@ -827,19 +875,22 @@ module GraphQL
827
875
  ex_err
828
876
  end
829
877
  end
830
-
831
- continue_value(list_value, owner_type, field, inner_type.non_null?, ast_node, result_name, selection_result)
878
+ # Detect whether this error came while calling `.each` (before `idx` is set) or while running list *items* (after `idx` is set)
879
+ error_is_non_null = idx.nil? ? is_non_null : inner_type.non_null?
880
+ continue_value(list_value, owner_type, field, error_is_non_null, ast_node, result_name, selection_result)
832
881
  else
833
882
  raise "Invariant: Unhandled type kind #{current_type.kind} (#{current_type})"
834
883
  end
835
884
  end
836
885
 
837
- def resolve_list_item(inner_value, inner_type, ast_node, field, owner_object, arguments, this_idx, response_list, next_selections, owner_type) # rubocop:disable Metrics/ParameterLists
838
- set_all_interpreter_context(nil, nil, nil, this_idx, response_list)
886
+ def resolve_list_item(inner_value, inner_type, inner_type_non_null, ast_node, field, owner_object, arguments, this_idx, response_list, next_selections, owner_type) # rubocop:disable Metrics/ParameterLists
887
+ st = get_current_runtime_state
888
+ st.current_result_name = this_idx
889
+ st.current_result = response_list
839
890
  call_method_on_directives(:resolve_each, owner_object, ast_node.directives) do
840
891
  # This will update `response_list` with the lazy
841
- after_lazy(inner_value, owner: inner_type, ast_node: ast_node, field: field, owner_object: owner_object, arguments: arguments, result_name: this_idx, result: response_list) do |inner_inner_value|
842
- continue_value = continue_value(inner_inner_value, owner_type, field, inner_type.non_null?, ast_node, this_idx, response_list)
892
+ after_lazy(inner_value, ast_node: ast_node, field: field, owner_object: owner_object, arguments: arguments, result_name: this_idx, result: response_list) do |inner_inner_value|
893
+ continue_value = continue_value(inner_inner_value, owner_type, field, inner_type_non_null, ast_node, this_idx, response_list)
843
894
  if HALT != continue_value
844
895
  continue_field(continue_value, owner_type, field, inner_type, ast_node, next_selections, false, owner_object, arguments, this_idx, response_list)
845
896
  end
@@ -891,20 +942,25 @@ module GraphQL
891
942
  true
892
943
  end
893
944
 
894
- def set_all_interpreter_context(object, field, arguments, result_name, result)
895
- ti = thread_info
896
- if object
897
- ti[:current_object] = object
898
- end
899
- if field
900
- ti[:current_field] = field
901
- end
902
- if arguments
903
- ti[:current_arguments] = arguments
945
+ def get_current_runtime_state
946
+ current_state = Thread.current[:__graphql_runtime_info] ||= begin
947
+ per_query_state = {}
948
+ per_query_state.compare_by_identity
949
+ per_query_state
904
950
  end
905
- ti[:current_result_name] = result_name
906
- if result
907
- ti[:current_result] = result
951
+
952
+ current_state[@query] ||= CurrentState.new
953
+ end
954
+
955
+ def minimal_after_lazy(value, &block)
956
+ if lazy?(value)
957
+ GraphQL::Execution::Lazy.new do
958
+ result = @schema.sync_lazy(value)
959
+ # The returned result might also be lazy, so check it, too
960
+ minimal_after_lazy(result, &block)
961
+ end
962
+ else
963
+ yield(value)
908
964
  end
909
965
  end
910
966
 
@@ -913,16 +969,21 @@ module GraphQL
913
969
  # @param eager [Boolean] Set to `true` for mutation root fields only
914
970
  # @param trace [Boolean] If `false`, don't wrap this with field tracing
915
971
  # @return [GraphQL::Execution::Lazy, Object] If loading `object` will be deferred, it's a wrapper over it.
916
- def after_lazy(lazy_obj, owner:, field:, owner_object:, arguments:, ast_node:, result:, result_name:, eager: false, trace: true, &block)
972
+ def after_lazy(lazy_obj, field:, owner_object:, arguments:, ast_node:, result:, result_name:, eager: false, trace: true, &block)
917
973
  if lazy?(lazy_obj)
918
974
  orig_result = result
919
975
  lazy = GraphQL::Execution::Lazy.new(field: field) do
920
- set_all_interpreter_context(owner_object, field, arguments, result_name, orig_result)
976
+ st = get_current_runtime_state
977
+ st.current_object = owner_object
978
+ st.current_field = field
979
+ st.current_arguments = arguments
980
+ st.current_result_name = result_name
981
+ st.current_result = orig_result
921
982
  # Wrap the execution of _this_ method with tracing,
922
983
  # but don't wrap the continuation below
923
984
  inner_obj = begin
924
985
  if trace
925
- query.current_trace.execute_field_lazy(field: field, query: query, object: owner_object, arguments: arguments, ast_node: ast_node) do
986
+ @current_trace.execute_field_lazy(field: field, query: query, object: owner_object, arguments: arguments, ast_node: ast_node) do
926
987
  schema.sync_lazy(lazy_obj)
927
988
  end
928
989
  else
@@ -943,7 +1004,7 @@ module GraphQL
943
1004
  if eager
944
1005
  lazy.value
945
1006
  else
946
- set_result(result, result_name, lazy)
1007
+ set_result(result, result_name, lazy, false, false) # is_non_null is irrelevant here
947
1008
  current_depth = 0
948
1009
  while result
949
1010
  current_depth += 1
@@ -953,7 +1014,7 @@ module GraphQL
953
1014
  lazy
954
1015
  end
955
1016
  else
956
- set_all_interpreter_context(owner_object, field, arguments, result_name, result)
1017
+ # Don't need to reset state here because it _wasn't_ lazy.
957
1018
  yield(lazy_obj)
958
1019
  end
959
1020
  end
@@ -968,23 +1029,24 @@ module GraphQL
968
1029
  end
969
1030
 
970
1031
  def delete_all_interpreter_context
971
- if (ti = thread_info)
972
- ti.delete(:current_result)
973
- ti.delete(:current_result_name)
974
- ti.delete(:current_field)
975
- ti.delete(:current_object)
976
- ti.delete(:current_arguments)
1032
+ per_query_state = Thread.current[:__graphql_runtime_info]
1033
+ if per_query_state
1034
+ per_query_state.delete(@query)
1035
+ if per_query_state.size == 0
1036
+ Thread.current[:__graphql_runtime_info] = nil
1037
+ end
977
1038
  end
1039
+ nil
978
1040
  end
979
1041
 
980
1042
  def resolve_type(type, value)
981
- resolved_type, resolved_value = query.current_trace.resolve_type(query: query, type: type, object: value) do
1043
+ resolved_type, resolved_value = @current_trace.resolve_type(query: query, type: type, object: value) do
982
1044
  query.resolve_type(type, value)
983
1045
  end
984
1046
 
985
1047
  if lazy?(resolved_type)
986
1048
  GraphQL::Execution::Lazy.new do
987
- query.current_trace.resolve_type_lazy(query: query, type: type, object: value) do
1049
+ @current_trace.resolve_type_lazy(query: query, type: type, object: value) do
988
1050
  schema.sync_lazy(resolved_type)
989
1051
  end
990
1052
  end
@@ -998,9 +1060,12 @@ module GraphQL
998
1060
  end
999
1061
 
1000
1062
  def lazy?(object)
1001
- @lazy_cache.fetch(object.class) {
1002
- @lazy_cache[object.class] = @schema.lazy?(object)
1003
- }
1063
+ obj_class = object.class
1064
+ is_lazy = @lazy_cache[obj_class]
1065
+ if is_lazy.nil?
1066
+ is_lazy = @lazy_cache[obj_class] = @schema.lazy?(object)
1067
+ end
1068
+ is_lazy
1004
1069
  end
1005
1070
  end
1006
1071
  end