graphql 2.0.20 → 2.0.22

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.

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