graphql 1.11.4 → 1.12.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (160) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/install_generator.rb +5 -5
  3. data/lib/generators/graphql/object_generator.rb +2 -0
  4. data/lib/generators/graphql/relay_generator.rb +63 -0
  5. data/lib/generators/graphql/templates/base_connection.erb +8 -0
  6. data/lib/generators/graphql/templates/base_edge.erb +8 -0
  7. data/lib/generators/graphql/templates/node_type.erb +9 -0
  8. data/lib/generators/graphql/templates/object.erb +1 -1
  9. data/lib/generators/graphql/templates/query_type.erb +1 -3
  10. data/lib/generators/graphql/templates/schema.erb +8 -35
  11. data/lib/generators/graphql/templates/union.erb +1 -1
  12. data/lib/graphql.rb +55 -4
  13. data/lib/graphql/analysis/analyze_query.rb +7 -0
  14. data/lib/graphql/analysis/ast.rb +11 -2
  15. data/lib/graphql/analysis/ast/visitor.rb +9 -1
  16. data/lib/graphql/argument.rb +3 -3
  17. data/lib/graphql/backtrace.rb +28 -19
  18. data/lib/graphql/backtrace/legacy_tracer.rb +56 -0
  19. data/lib/graphql/backtrace/table.rb +22 -2
  20. data/lib/graphql/backtrace/tracer.rb +40 -8
  21. data/lib/graphql/backwards_compatibility.rb +1 -0
  22. data/lib/graphql/compatibility/execution_specification.rb +1 -0
  23. data/lib/graphql/compatibility/lazy_execution_specification.rb +2 -0
  24. data/lib/graphql/compatibility/query_parser_specification.rb +2 -0
  25. data/lib/graphql/compatibility/schema_parser_specification.rb +2 -0
  26. data/lib/graphql/dataloader.rb +198 -0
  27. data/lib/graphql/dataloader/null_dataloader.rb +21 -0
  28. data/lib/graphql/dataloader/request.rb +24 -0
  29. data/lib/graphql/dataloader/request_all.rb +22 -0
  30. data/lib/graphql/dataloader/source.rb +93 -0
  31. data/lib/graphql/define/assign_global_id_field.rb +2 -2
  32. data/lib/graphql/define/instance_definable.rb +32 -2
  33. data/lib/graphql/define/type_definer.rb +5 -5
  34. data/lib/graphql/deprecated_dsl.rb +5 -0
  35. data/lib/graphql/enum_type.rb +2 -0
  36. data/lib/graphql/execution/errors.rb +4 -0
  37. data/lib/graphql/execution/execute.rb +7 -0
  38. data/lib/graphql/execution/interpreter.rb +20 -6
  39. data/lib/graphql/execution/interpreter/arguments.rb +57 -5
  40. data/lib/graphql/execution/interpreter/arguments_cache.rb +8 -0
  41. data/lib/graphql/execution/interpreter/handles_raw_value.rb +0 -7
  42. data/lib/graphql/execution/interpreter/runtime.rb +236 -120
  43. data/lib/graphql/execution/multiplex.rb +20 -6
  44. data/lib/graphql/function.rb +4 -0
  45. data/lib/graphql/input_object_type.rb +2 -0
  46. data/lib/graphql/integer_decoding_error.rb +17 -0
  47. data/lib/graphql/interface_type.rb +3 -1
  48. data/lib/graphql/introspection.rb +96 -0
  49. data/lib/graphql/introspection/field_type.rb +7 -3
  50. data/lib/graphql/introspection/input_value_type.rb +6 -0
  51. data/lib/graphql/introspection/introspection_query.rb +6 -92
  52. data/lib/graphql/introspection/type_type.rb +7 -3
  53. data/lib/graphql/invalid_null_error.rb +1 -1
  54. data/lib/graphql/language/block_string.rb +24 -5
  55. data/lib/graphql/language/document_from_schema_definition.rb +50 -23
  56. data/lib/graphql/language/lexer.rb +7 -3
  57. data/lib/graphql/language/lexer.rl +7 -3
  58. data/lib/graphql/language/nodes.rb +1 -1
  59. data/lib/graphql/language/parser.rb +107 -103
  60. data/lib/graphql/language/parser.y +4 -0
  61. data/lib/graphql/language/sanitized_printer.rb +59 -26
  62. data/lib/graphql/name_validator.rb +6 -7
  63. data/lib/graphql/object_type.rb +2 -0
  64. data/lib/graphql/pagination/connection.rb +5 -1
  65. data/lib/graphql/pagination/connections.rb +15 -17
  66. data/lib/graphql/query.rb +8 -3
  67. data/lib/graphql/query/context.rb +18 -3
  68. data/lib/graphql/query/serial_execution.rb +1 -0
  69. data/lib/graphql/query/validation_pipeline.rb +1 -1
  70. data/lib/graphql/relay/array_connection.rb +2 -2
  71. data/lib/graphql/relay/base_connection.rb +7 -0
  72. data/lib/graphql/relay/connection_instrumentation.rb +4 -4
  73. data/lib/graphql/relay/connection_type.rb +1 -1
  74. data/lib/graphql/relay/mutation.rb +1 -0
  75. data/lib/graphql/relay/node.rb +3 -0
  76. data/lib/graphql/relay/range_add.rb +14 -5
  77. data/lib/graphql/relay/type_extensions.rb +2 -0
  78. data/lib/graphql/scalar_type.rb +2 -0
  79. data/lib/graphql/schema.rb +104 -39
  80. data/lib/graphql/schema/argument.rb +74 -5
  81. data/lib/graphql/schema/build_from_definition.rb +203 -86
  82. data/lib/graphql/schema/default_type_error.rb +2 -0
  83. data/lib/graphql/schema/directive.rb +76 -0
  84. data/lib/graphql/schema/directive/deprecated.rb +1 -1
  85. data/lib/graphql/schema/directive/flagged.rb +57 -0
  86. data/lib/graphql/schema/enum.rb +3 -0
  87. data/lib/graphql/schema/enum_value.rb +12 -6
  88. data/lib/graphql/schema/field.rb +59 -24
  89. data/lib/graphql/schema/field/connection_extension.rb +10 -8
  90. data/lib/graphql/schema/field/scope_extension.rb +1 -1
  91. data/lib/graphql/schema/input_object.rb +38 -25
  92. data/lib/graphql/schema/interface.rb +2 -1
  93. data/lib/graphql/schema/late_bound_type.rb +2 -2
  94. data/lib/graphql/schema/loader.rb +1 -0
  95. data/lib/graphql/schema/member.rb +4 -0
  96. data/lib/graphql/schema/member/base_dsl_methods.rb +1 -0
  97. data/lib/graphql/schema/member/build_type.rb +17 -7
  98. data/lib/graphql/schema/member/has_arguments.rb +70 -51
  99. data/lib/graphql/schema/member/has_deprecation_reason.rb +25 -0
  100. data/lib/graphql/schema/member/has_directives.rb +98 -0
  101. data/lib/graphql/schema/member/has_fields.rb +2 -2
  102. data/lib/graphql/schema/member/has_validators.rb +31 -0
  103. data/lib/graphql/schema/member/type_system_helpers.rb +3 -3
  104. data/lib/graphql/schema/object.rb +11 -0
  105. data/lib/graphql/schema/printer.rb +5 -4
  106. data/lib/graphql/schema/relay_classic_mutation.rb +4 -2
  107. data/lib/graphql/schema/resolver.rb +7 -0
  108. data/lib/graphql/schema/resolver/has_payload_type.rb +2 -0
  109. data/lib/graphql/schema/subscription.rb +19 -1
  110. data/lib/graphql/schema/timeout.rb +29 -15
  111. data/lib/graphql/schema/timeout_middleware.rb +2 -0
  112. data/lib/graphql/schema/unique_within_type.rb +1 -2
  113. data/lib/graphql/schema/validation.rb +10 -0
  114. data/lib/graphql/schema/validator.rb +163 -0
  115. data/lib/graphql/schema/validator/exclusion_validator.rb +31 -0
  116. data/lib/graphql/schema/validator/format_validator.rb +49 -0
  117. data/lib/graphql/schema/validator/inclusion_validator.rb +33 -0
  118. data/lib/graphql/schema/validator/length_validator.rb +57 -0
  119. data/lib/graphql/schema/validator/numericality_validator.rb +71 -0
  120. data/lib/graphql/schema/validator/required_validator.rb +68 -0
  121. data/lib/graphql/static_validation.rb +1 -0
  122. data/lib/graphql/static_validation/all_rules.rb +1 -0
  123. data/lib/graphql/static_validation/rules/fields_will_merge.rb +25 -17
  124. data/lib/graphql/static_validation/rules/input_object_names_are_unique.rb +30 -0
  125. data/lib/graphql/static_validation/rules/input_object_names_are_unique_error.rb +30 -0
  126. data/lib/graphql/static_validation/validation_timeout_error.rb +25 -0
  127. data/lib/graphql/static_validation/validator.rb +33 -7
  128. data/lib/graphql/subscriptions.rb +18 -23
  129. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +21 -7
  130. data/lib/graphql/tracing.rb +2 -2
  131. data/lib/graphql/tracing/appoptics_tracing.rb +3 -1
  132. data/lib/graphql/tracing/platform_tracing.rb +4 -2
  133. data/lib/graphql/tracing/prometheus_tracing/graphql_collector.rb +4 -1
  134. data/lib/graphql/tracing/skylight_tracing.rb +1 -1
  135. data/lib/graphql/types/int.rb +9 -2
  136. data/lib/graphql/types/relay.rb +11 -3
  137. data/lib/graphql/types/relay/base_connection.rb +2 -91
  138. data/lib/graphql/types/relay/base_edge.rb +2 -34
  139. data/lib/graphql/types/relay/connection_behaviors.rb +123 -0
  140. data/lib/graphql/types/relay/default_relay.rb +27 -0
  141. data/lib/graphql/types/relay/edge_behaviors.rb +42 -0
  142. data/lib/graphql/types/relay/has_node_field.rb +41 -0
  143. data/lib/graphql/types/relay/has_nodes_field.rb +41 -0
  144. data/lib/graphql/types/relay/node.rb +2 -4
  145. data/lib/graphql/types/relay/node_behaviors.rb +15 -0
  146. data/lib/graphql/types/relay/node_field.rb +1 -19
  147. data/lib/graphql/types/relay/nodes_field.rb +1 -19
  148. data/lib/graphql/types/relay/page_info.rb +2 -14
  149. data/lib/graphql/types/relay/page_info_behaviors.rb +25 -0
  150. data/lib/graphql/types/string.rb +7 -1
  151. data/lib/graphql/unauthorized_error.rb +1 -1
  152. data/lib/graphql/union_type.rb +2 -0
  153. data/lib/graphql/upgrader/member.rb +1 -0
  154. data/lib/graphql/upgrader/schema.rb +1 -0
  155. data/lib/graphql/version.rb +1 -1
  156. data/readme.md +1 -1
  157. metadata +49 -6
  158. data/lib/graphql/types/relay/base_field.rb +0 -22
  159. data/lib/graphql/types/relay/base_interface.rb +0 -29
  160. data/lib/graphql/types/relay/base_object.rb +0 -26
@@ -13,13 +13,6 @@ module GraphQL
13
13
  @object
14
14
  end
15
15
  end
16
-
17
- # Allows to return "raw" value from the resolver
18
- module HandlesRawValue
19
- def raw_value(obj)
20
- RawValue.new(obj)
21
- end
22
- end
23
16
  end
24
17
  end
25
18
  end
@@ -19,8 +19,10 @@ module GraphQL
19
19
 
20
20
  def initialize(query:, response:)
21
21
  @query = query
22
+ @dataloader = query.multiplex.dataloader
22
23
  @schema = query.schema
23
24
  @context = query.context
25
+ @multiplex_context = query.multiplex.context
24
26
  @interpreter_context = @context.namespace(:interpreter)
25
27
  @response = response
26
28
  @dead_paths = {}
@@ -43,26 +45,63 @@ module GraphQL
43
45
  # might be stored up in lazies.
44
46
  # @return [void]
45
47
  def run_eager
46
-
47
48
  root_operation = query.selected_operation
48
49
  root_op_type = root_operation.operation_type || "query"
49
50
  root_type = schema.root_type_for_operation(root_op_type)
50
51
  path = []
51
- @interpreter_context[:current_object] = query.root_value
52
- @interpreter_context[:current_path] = path
52
+ set_all_interpreter_context(query.root_value, nil, nil, path)
53
53
  object_proxy = authorized_new(root_type, query.root_value, context, path)
54
54
  object_proxy = schema.sync_lazy(object_proxy)
55
55
  if object_proxy.nil?
56
56
  # Root .authorized? returned false.
57
57
  write_in_response(path, nil)
58
- nil
59
58
  else
60
- evaluate_selections(path, context.scoped_context, object_proxy, root_type, root_operation.selections, root_operation_type: root_op_type)
61
- nil
59
+ # Prepare this runtime state to be encapsulated in a Fiber
60
+ @progress_path = path
61
+ @progress_scoped_context = context.scoped_context
62
+ @progress_object = object_proxy
63
+ @progress_object_type = root_type
64
+ @progress_index = nil
65
+ @progress_is_eager_selection = root_op_type == "mutation"
66
+ @progress_selections = gather_selections(object_proxy, root_type, root_operation.selections)
67
+
68
+ # Make the first fiber which will begin execution
69
+ enqueue_selections_fiber
62
70
  end
71
+ delete_interpreter_context(:current_path)
72
+ delete_interpreter_context(:current_field)
73
+ delete_interpreter_context(:current_object)
74
+ delete_interpreter_context(:current_arguments)
75
+ nil
76
+ end
77
+
78
+ # Use `@dataloader` to enqueue a fiber that will pick up from the current point.
79
+ # @return [void]
80
+ def enqueue_selections_fiber
81
+ # Read these into local variables so that later assignments don't affect the block below.
82
+ path = @progress_path
83
+ scoped_context = @progress_scoped_context
84
+ owner_object = @progress_object
85
+ owner_type = @progress_object_type
86
+ idx = @progress_index
87
+ is_eager_selection = @progress_is_eager_selection
88
+ gathered_selections = @progress_selections
89
+
90
+ @dataloader.enqueue {
91
+ evaluate_selections(
92
+ path,
93
+ scoped_context,
94
+ owner_object,
95
+ owner_type,
96
+ is_eager_selection: is_eager_selection,
97
+ after: idx,
98
+ gathered_selections: gathered_selections,
99
+ )
100
+ }
101
+ nil
63
102
  end
64
103
 
65
- def gather_selections(owner_object, owner_type, selections, selections_by_name)
104
+ def gather_selections(owner_object, owner_type, selections, selections_by_name = {})
66
105
  selections.each do |node|
67
106
  # Skip gathering this if the directive says so
68
107
  if !directives_include?(node, owner_object, owner_type)
@@ -114,139 +153,186 @@ module GraphQL
114
153
  raise "Invariant: unexpected selection class: #{node.class}"
115
154
  end
116
155
  end
156
+ selections_by_name
117
157
  end
118
158
 
119
- def evaluate_selections(path, scoped_context, owner_object, owner_type, selections, root_operation_type: nil)
120
- @interpreter_context[:current_object] = owner_object
121
- @interpreter_context[:current_path] = path
122
- selections_by_name = {}
123
- gather_selections(owner_object, owner_type, selections, selections_by_name)
124
- selections_by_name.each do |result_name, field_ast_nodes_or_ast_node|
125
- # As a performance optimization, the hash key will be a `Node` if
126
- # there's only one selection of the field. But if there are multiple
127
- # selections of the field, it will be an Array of nodes
128
- if field_ast_nodes_or_ast_node.is_a?(Array)
129
- field_ast_nodes = field_ast_nodes_or_ast_node
130
- ast_node = field_ast_nodes.first
131
- else
132
- field_ast_nodes = nil
133
- ast_node = field_ast_nodes_or_ast_node
159
+ NO_ARGS = {}.freeze
160
+
161
+ # @return [void]
162
+ def evaluate_selections(path, scoped_context, owner_object, owner_type, is_eager_selection:, gathered_selections:, after:)
163
+ set_all_interpreter_context(owner_object, nil, nil, path)
164
+
165
+ @progress_path = path
166
+ @progress_scoped_context = scoped_context
167
+ @progress_object = owner_object
168
+ @progress_object_type = owner_type
169
+ @progress_index = nil
170
+ @progress_is_eager_selection = is_eager_selection
171
+ @progress_selections = gathered_selections
172
+
173
+ # Track `idx` manually to avoid an allocation on this hot path
174
+ idx = 0
175
+ gathered_selections.each do |result_name, field_ast_nodes_or_ast_node|
176
+ prev_idx = idx
177
+ idx += 1
178
+ # TODO: this is how a `progress` resumes where this left off.
179
+ # Is there a better way to seek in the hash?
180
+ # I think we could also use the array of keys; it supports seeking just fine.
181
+ if after && prev_idx <= after
182
+ next
134
183
  end
135
- field_name = ast_node.name
136
- field_defn = @fields_cache[owner_type][field_name] ||= owner_type.get_field(field_name)
137
- is_introspection = false
138
- if field_defn.nil?
139
- field_defn = if owner_type == schema.query && (entry_point_field = schema.introspection_system.entry_point(name: field_name))
140
- is_introspection = true
141
- entry_point_field
142
- elsif (dynamic_field = schema.introspection_system.dynamic_field(name: field_name))
143
- is_introspection = true
144
- dynamic_field
145
- else
146
- raise "Invariant: no field for #{owner_type}.#{field_name}"
147
- end
184
+ @progress_index = prev_idx
185
+ # This is how the current runtime gives itself to `dataloader`
186
+ # so that the dataloader can enqueue another fiber to resume if needed.
187
+ @dataloader.current_runtime = self
188
+ evaluate_selection(path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_selection)
189
+ # The dataloader knows if ^^ that selection halted and later selections were executed in another fiber.
190
+ # If that's the case, then don't continue execution here.
191
+ if @dataloader.yielded?(path)
192
+ break
148
193
  end
149
- return_type = field_defn.type
194
+ end
195
+ nil
196
+ end
150
197
 
151
- next_path = path.dup
152
- next_path << result_name
153
- next_path.freeze
198
+ attr_reader :progress_path
154
199
 
155
- # This seems janky, but we need to know
156
- # the field's return type at this path in order
157
- # to propagate `null`
158
- set_type_at_path(next_path, return_type)
159
- # Set this before calling `run_with_directives`, so that the directive can have the latest path
160
- @interpreter_context[:current_path] = next_path
161
- @interpreter_context[:current_field] = field_defn
200
+ # @return [void]
201
+ def evaluate_selection(path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_field)
202
+ # As a performance optimization, the hash key will be a `Node` if
203
+ # there's only one selection of the field. But if there are multiple
204
+ # selections of the field, it will be an Array of nodes
205
+ if field_ast_nodes_or_ast_node.is_a?(Array)
206
+ field_ast_nodes = field_ast_nodes_or_ast_node
207
+ ast_node = field_ast_nodes.first
208
+ else
209
+ field_ast_nodes = nil
210
+ ast_node = field_ast_nodes_or_ast_node
211
+ end
212
+ field_name = ast_node.name
213
+ field_defn = @fields_cache[owner_type][field_name] ||= owner_type.get_field(field_name)
214
+ is_introspection = false
215
+ if field_defn.nil?
216
+ field_defn = if owner_type == schema.query && (entry_point_field = schema.introspection_system.entry_point(name: field_name))
217
+ is_introspection = true
218
+ entry_point_field
219
+ elsif (dynamic_field = schema.introspection_system.dynamic_field(name: field_name))
220
+ is_introspection = true
221
+ dynamic_field
222
+ else
223
+ raise "Invariant: no field for #{owner_type}.#{field_name}"
224
+ end
225
+ end
226
+ return_type = field_defn.type
162
227
 
163
- context.scoped_context = scoped_context
164
- object = owner_object
228
+ next_path = path.dup
229
+ next_path << result_name
230
+ next_path.freeze
165
231
 
166
- if is_introspection
167
- object = authorized_new(field_defn.owner, object, context, next_path)
168
- end
232
+ # This seems janky, but we need to know
233
+ # the field's return type at this path in order
234
+ # to propagate `null`
235
+ set_type_at_path(next_path, return_type)
236
+ # Set this before calling `run_with_directives`, so that the directive can have the latest path
237
+ set_all_interpreter_context(nil, field_defn, nil, next_path)
169
238
 
170
- begin
171
- kwarg_arguments = arguments(object, field_defn, ast_node)
172
- rescue GraphQL::ExecutionError => e
173
- continue_value(next_path, e, owner_type, field_defn, return_type.non_null?, ast_node)
174
- next
175
- end
239
+ context.scoped_context = scoped_context
240
+ object = owner_object
176
241
 
177
- after_lazy(kwarg_arguments, owner: owner_type, field: field_defn, path: next_path, scoped_context: context.scoped_context, owner_object: object, arguments: kwarg_arguments) do |resolved_arguments|
178
- if resolved_arguments.is_a? GraphQL::ExecutionError
179
- continue_value(next_path, resolved_arguments, owner_type, field_defn, return_type.non_null?, ast_node)
180
- next
181
- end
242
+ if is_introspection
243
+ object = authorized_new(field_defn.owner, object, context, next_path)
244
+ end
245
+
246
+ begin
247
+ kwarg_arguments = arguments(object, field_defn, ast_node)
248
+ rescue GraphQL::ExecutionError, GraphQL::UnauthorizedError => e
249
+ continue_value(next_path, e, owner_type, field_defn, return_type.non_null?, ast_node)
250
+ return
251
+ end
182
252
 
183
- kwarg_arguments = resolved_arguments.keyword_arguments
253
+ 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|
254
+ if resolved_arguments.is_a?(GraphQL::ExecutionError) || resolved_arguments.is_a?(GraphQL::UnauthorizedError)
255
+ continue_value(next_path, resolved_arguments, owner_type, field_defn, return_type.non_null?, ast_node)
256
+ next
257
+ end
184
258
 
259
+ kwarg_arguments = if resolved_arguments.empty? && field_defn.extras.empty?
260
+ # We can avoid allocating the `{ Symbol => Object }` hash in this case
261
+ NO_ARGS
262
+ else
263
+ # Bundle up the extras, then make a new arguments instance
264
+ # that includes the extras, too.
265
+ extra_args = {}
185
266
  field_defn.extras.each do |extra|
186
267
  case extra
187
268
  when :ast_node
188
- kwarg_arguments[:ast_node] = ast_node
269
+ extra_args[:ast_node] = ast_node
189
270
  when :execution_errors
190
- kwarg_arguments[:execution_errors] = ExecutionErrors.new(context, ast_node, next_path)
271
+ extra_args[:execution_errors] = ExecutionErrors.new(context, ast_node, next_path)
191
272
  when :path
192
- kwarg_arguments[:path] = next_path
273
+ extra_args[:path] = next_path
193
274
  when :lookahead
194
275
  if !field_ast_nodes
195
276
  field_ast_nodes = [ast_node]
196
277
  end
197
- kwarg_arguments[:lookahead] = Execution::Lookahead.new(
278
+
279
+ extra_args[:lookahead] = Execution::Lookahead.new(
198
280
  query: query,
199
281
  ast_nodes: field_ast_nodes,
200
282
  field: field_defn,
201
283
  )
202
284
  when :argument_details
203
- kwarg_arguments[:argument_details] = resolved_arguments
285
+ # Use this flag to tell Interpreter::Arguments to add itself
286
+ # to the keyword args hash _before_ freezing everything.
287
+ extra_args[:argument_details] = :__arguments_add_self
204
288
  else
205
- kwarg_arguments[extra] = field_defn.fetch_extra(extra, context)
289
+ extra_args[extra] = field_defn.fetch_extra(extra, context)
206
290
  end
207
291
  end
292
+ resolved_arguments = resolved_arguments.merge_extras(extra_args)
293
+ resolved_arguments.keyword_arguments
294
+ end
208
295
 
209
- @interpreter_context[:current_arguments] = kwarg_arguments
296
+ set_all_interpreter_context(nil, nil, kwarg_arguments, nil)
210
297
 
211
- # Optimize for the case that field is selected only once
212
- if field_ast_nodes.nil? || field_ast_nodes.size == 1
213
- next_selections = ast_node.selections
214
- else
215
- next_selections = []
216
- field_ast_nodes.each { |f| next_selections.concat(f.selections) }
217
- end
298
+ # Optimize for the case that field is selected only once
299
+ if field_ast_nodes.nil? || field_ast_nodes.size == 1
300
+ next_selections = ast_node.selections
301
+ else
302
+ next_selections = []
303
+ field_ast_nodes.each { |f| next_selections.concat(f.selections) }
304
+ end
218
305
 
219
- field_result = resolve_with_directives(object, ast_node) do
220
- # Actually call the field resolver and capture the result
221
- app_result = begin
222
- query.with_error_handling do
223
- query.trace("execute_field", {owner: owner_type, field: field_defn, path: next_path, query: query, object: object, arguments: kwarg_arguments}) do
224
- field_defn.resolve(object, kwarg_arguments, context)
225
- end
306
+ field_result = resolve_with_directives(object, ast_node) do
307
+ # Actually call the field resolver and capture the result
308
+ app_result = begin
309
+ query.with_error_handling do
310
+ query.trace("execute_field", {owner: owner_type, field: field_defn, path: next_path, ast_node: ast_node, query: query, object: object, arguments: kwarg_arguments}) do
311
+ field_defn.resolve(object, kwarg_arguments, context)
226
312
  end
227
- rescue GraphQL::ExecutionError => err
228
- err
229
313
  end
230
- after_lazy(app_result, owner: owner_type, field: field_defn, path: next_path, scoped_context: context.scoped_context, owner_object: object, arguments: kwarg_arguments) do |inner_result|
231
- continue_value = continue_value(next_path, inner_result, owner_type, field_defn, return_type.non_null?, ast_node)
232
- if RawValue === continue_value
233
- # Write raw value directly to the response without resolving nested objects
234
- write_in_response(next_path, continue_value.resolve)
235
- elsif HALT != continue_value
236
- continue_field(next_path, continue_value, owner_type, field_defn, return_type, ast_node, next_selections, false, object, kwarg_arguments)
237
- end
314
+ rescue GraphQL::ExecutionError => err
315
+ err
316
+ end
317
+ 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|
318
+ continue_value = continue_value(next_path, inner_result, owner_type, field_defn, return_type.non_null?, ast_node)
319
+ if RawValue === continue_value
320
+ # Write raw value directly to the response without resolving nested objects
321
+ write_in_response(next_path, continue_value.resolve)
322
+ elsif HALT != continue_value
323
+ continue_field(next_path, continue_value, owner_type, field_defn, return_type, ast_node, next_selections, false, object, kwarg_arguments)
238
324
  end
239
325
  end
326
+ end
240
327
 
241
- # If this field is a root mutation field, immediately resolve
242
- # all of its child fields before moving on to the next root mutation field.
243
- # (Subselections of this mutation will still be resolved level-by-level.)
244
- if root_operation_type == "mutation"
245
- Interpreter::Resolve.resolve_all([field_result])
246
- else
247
- field_result
248
- end
328
+ # If this field is a root mutation field, immediately resolve
329
+ # all of its child fields before moving on to the next root mutation field.
330
+ # (Subselections of this mutation will still be resolved level-by-level.)
331
+ if is_eager_field
332
+ Interpreter::Resolve.resolve_all([field_result])
249
333
  end
334
+
335
+ nil
250
336
  end
251
337
  end
252
338
 
@@ -307,7 +393,7 @@ module GraphQL
307
393
  resolved_type_or_lazy, resolved_value = resolve_type(current_type, value, path)
308
394
  resolved_value ||= value
309
395
 
310
- after_lazy(resolved_type_or_lazy, owner: current_type, path: path, scoped_context: context.scoped_context, field: field, owner_object: owner_object, arguments: arguments, trace: false) do |resolved_type|
396
+ 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|
311
397
  possible_types = query.possible_types(current_type)
312
398
 
313
399
  if !possible_types.include?(resolved_type)
@@ -327,12 +413,13 @@ module GraphQL
327
413
  rescue GraphQL::ExecutionError => err
328
414
  err
329
415
  end
330
- after_lazy(object_proxy, owner: current_type, path: path, scoped_context: context.scoped_context, field: field, owner_object: owner_object, arguments: arguments, trace: false) do |inner_object|
416
+ 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|
331
417
  continue_value = continue_value(path, inner_object, owner_type, field, is_non_null, ast_node)
332
418
  if HALT != continue_value
333
419
  response_hash = {}
334
420
  write_in_response(path, response_hash)
335
- evaluate_selections(path, context.scoped_context, continue_value, current_type, next_selections)
421
+ gathered_selections = gather_selections(continue_value, current_type, next_selections)
422
+ evaluate_selections(path, context.scoped_context, continue_value, current_type, is_eager_selection: false, gathered_selections: gathered_selections, after: nil)
336
423
  response_hash
337
424
  end
338
425
  end
@@ -350,7 +437,7 @@ module GraphQL
350
437
  idx += 1
351
438
  set_type_at_path(next_path, inner_type)
352
439
  # This will update `response_list` with the lazy
353
- after_lazy(inner_value, owner: inner_type, path: next_path, scoped_context: scoped_context, field: field, owner_object: owner_object, arguments: arguments) do |inner_inner_value|
440
+ 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|
354
441
  continue_value = continue_value(next_path, inner_inner_value, owner_type, field, inner_type.non_null?, ast_node)
355
442
  if HALT != continue_value
356
443
  continue_field(next_path, continue_value, owner_type, field, inner_type, ast_node, next_selections, false, owner_object, arguments)
@@ -412,30 +499,39 @@ module GraphQL
412
499
  true
413
500
  end
414
501
 
502
+ def set_all_interpreter_context(object, field, arguments, path)
503
+ if object
504
+ @context[:current_object] = @interpreter_context[:current_object] = object
505
+ end
506
+ if field
507
+ @context[:current_field] = @interpreter_context[:current_field] = field
508
+ end
509
+ if arguments
510
+ @context[:current_arguments] = @interpreter_context[:current_arguments] = arguments
511
+ end
512
+ if path
513
+ @context[:current_path] = @interpreter_context[:current_path] = path
514
+ end
515
+ end
516
+
415
517
  # @param obj [Object] Some user-returned value that may want to be batched
416
518
  # @param path [Array<String>]
417
519
  # @param field [GraphQL::Schema::Field]
418
520
  # @param eager [Boolean] Set to `true` for mutation root fields only
419
521
  # @param trace [Boolean] If `false`, don't wrap this with field tracing
420
522
  # @return [GraphQL::Execution::Lazy, Object] If loading `object` will be deferred, it's a wrapper over it.
421
- def after_lazy(lazy_obj, owner:, field:, path:, scoped_context:, owner_object:, arguments:, eager: false, trace: true, &block)
422
- @interpreter_context[:current_object] = owner_object
423
- @interpreter_context[:current_arguments] = arguments
424
- @interpreter_context[:current_path] = path
425
- @interpreter_context[:current_field] = field
523
+ def after_lazy(lazy_obj, owner:, field:, path:, scoped_context:, owner_object:, arguments:, ast_node:, eager: false, trace: true, &block)
524
+ set_all_interpreter_context(owner_object, field, arguments, path)
426
525
  if schema.lazy?(lazy_obj)
427
526
  lazy = GraphQL::Execution::Lazy.new(path: path, field: field) do
428
- @interpreter_context[:current_path] = path
429
- @interpreter_context[:current_field] = field
430
- @interpreter_context[:current_object] = owner_object
431
- @interpreter_context[:current_arguments] = arguments
527
+ set_all_interpreter_context(owner_object, field, arguments, path)
432
528
  context.scoped_context = scoped_context
433
529
  # Wrap the execution of _this_ method with tracing,
434
530
  # but don't wrap the continuation below
435
531
  inner_obj = begin
436
532
  query.with_error_handling do
437
533
  if trace
438
- query.trace("execute_field_lazy", {owner: owner, field: field, path: path, query: query, object: owner_object, arguments: arguments}) do
534
+ query.trace("execute_field_lazy", {owner: owner, field: field, path: path, query: query, object: owner_object, arguments: arguments, ast_node: ast_node}) do
439
535
  schema.sync_lazy(lazy_obj)
440
536
  end
441
537
  else
@@ -445,7 +541,7 @@ module GraphQL
445
541
  rescue GraphQL::ExecutionError, GraphQL::UnauthorizedError => err
446
542
  err
447
543
  end
448
- after_lazy(inner_obj, owner: owner, field: field, path: path, scoped_context: context.scoped_context, owner_object: owner_object, arguments: arguments, eager: eager, trace: trace, &block)
544
+ 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)
449
545
  end
450
546
 
451
547
  if eager
@@ -460,9 +556,7 @@ module GraphQL
460
556
  end
461
557
 
462
558
  def arguments(graphql_object, arg_owner, ast_node)
463
- # Don't cache arguments if field extras or extensions are requested since they can mutate the argument data structure
464
- if arg_owner.arguments_statically_coercible? &&
465
- (!arg_owner.is_a?(GraphQL::Schema::Field) || (arg_owner.extras.empty? && arg_owner.extensions.empty?))
559
+ if arg_owner.arguments_statically_coercible?
466
560
  query.arguments_for(ast_node, arg_owner)
467
561
  else
468
562
  # The arguments must be prepared in the context of the given object
@@ -503,6 +597,16 @@ module GraphQL
503
597
  end
504
598
  end
505
599
 
600
+ def value_at(path)
601
+ i = 0
602
+ value = @response.final_value
603
+ while value && (part = path[i])
604
+ value = value[part]
605
+ i += 1
606
+ end
607
+ value
608
+ end
609
+
506
610
  # To propagate nulls, we have to know what the field type was
507
611
  # at previous parts of the response.
508
612
  # This hash matches the response
@@ -539,6 +643,18 @@ module GraphQL
539
643
  res && res[:__dead]
540
644
  end
541
645
 
646
+ # Set this pair in the Query context, but also in the interpeter namespace,
647
+ # for compatibility.
648
+ def set_interpreter_context(key, value)
649
+ @interpreter_context[key] = value
650
+ @context[key] = value
651
+ end
652
+
653
+ def delete_interpreter_context(key)
654
+ @interpreter_context.delete(key)
655
+ @context.delete(key)
656
+ end
657
+
542
658
  def resolve_type(type, value, path)
543
659
  trace_payload = { context: context, type: type, object: value, path: path }
544
660
  resolved_type, resolved_value = query.trace("resolve_type", trace_payload) do