graphql 1.12.10 → 1.13.4

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.
Files changed (169) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/core.rb +3 -1
  3. data/lib/generators/graphql/install_generator.rb +9 -2
  4. data/lib/generators/graphql/mutation_generator.rb +1 -1
  5. data/lib/generators/graphql/object_generator.rb +2 -1
  6. data/lib/generators/graphql/relay.rb +19 -11
  7. data/lib/generators/graphql/templates/schema.erb +14 -2
  8. data/lib/generators/graphql/type_generator.rb +0 -1
  9. data/lib/graphql/analysis/ast/field_usage.rb +28 -1
  10. data/lib/graphql/analysis/ast/query_complexity.rb +10 -14
  11. data/lib/graphql/analysis/ast/visitor.rb +4 -4
  12. data/lib/graphql/backtrace/table.rb +15 -3
  13. data/lib/graphql/backtrace/tracer.rb +7 -4
  14. data/lib/graphql/base_type.rb +4 -2
  15. data/lib/graphql/boolean_type.rb +1 -1
  16. data/lib/graphql/dataloader/null_dataloader.rb +1 -0
  17. data/lib/graphql/dataloader/source.rb +50 -2
  18. data/lib/graphql/dataloader.rb +110 -41
  19. data/lib/graphql/define/instance_definable.rb +1 -1
  20. data/lib/graphql/deprecated_dsl.rb +11 -3
  21. data/lib/graphql/deprecation.rb +1 -5
  22. data/lib/graphql/directive/deprecated_directive.rb +1 -1
  23. data/lib/graphql/directive/include_directive.rb +1 -1
  24. data/lib/graphql/directive/skip_directive.rb +1 -1
  25. data/lib/graphql/directive.rb +0 -4
  26. data/lib/graphql/enum_type.rb +5 -1
  27. data/lib/graphql/execution/errors.rb +1 -0
  28. data/lib/graphql/execution/execute.rb +1 -1
  29. data/lib/graphql/execution/interpreter/arguments.rb +1 -1
  30. data/lib/graphql/execution/interpreter/arguments_cache.rb +5 -4
  31. data/lib/graphql/execution/interpreter/resolve.rb +6 -2
  32. data/lib/graphql/execution/interpreter/runtime.rb +513 -213
  33. data/lib/graphql/execution/interpreter.rb +4 -8
  34. data/lib/graphql/execution/lazy.rb +5 -1
  35. data/lib/graphql/execution/lookahead.rb +2 -2
  36. data/lib/graphql/execution/multiplex.rb +4 -1
  37. data/lib/graphql/float_type.rb +1 -1
  38. data/lib/graphql/id_type.rb +1 -1
  39. data/lib/graphql/int_type.rb +1 -1
  40. data/lib/graphql/integer_encoding_error.rb +18 -2
  41. data/lib/graphql/introspection/directive_type.rb +1 -1
  42. data/lib/graphql/introspection/entry_points.rb +2 -2
  43. data/lib/graphql/introspection/enum_value_type.rb +2 -2
  44. data/lib/graphql/introspection/field_type.rb +2 -2
  45. data/lib/graphql/introspection/input_value_type.rb +10 -4
  46. data/lib/graphql/introspection/schema_type.rb +3 -3
  47. data/lib/graphql/introspection/type_type.rb +10 -10
  48. data/lib/graphql/language/block_string.rb +2 -6
  49. data/lib/graphql/language/document_from_schema_definition.rb +10 -4
  50. data/lib/graphql/language/lexer.rb +0 -3
  51. data/lib/graphql/language/lexer.rl +0 -4
  52. data/lib/graphql/language/nodes.rb +13 -3
  53. data/lib/graphql/language/parser.rb +442 -434
  54. data/lib/graphql/language/parser.y +5 -4
  55. data/lib/graphql/language/printer.rb +6 -1
  56. data/lib/graphql/language/sanitized_printer.rb +5 -5
  57. data/lib/graphql/language/token.rb +0 -4
  58. data/lib/graphql/name_validator.rb +0 -4
  59. data/lib/graphql/pagination/active_record_relation_connection.rb +43 -6
  60. data/lib/graphql/pagination/connections.rb +40 -16
  61. data/lib/graphql/pagination/relation_connection.rb +57 -27
  62. data/lib/graphql/query/arguments.rb +1 -1
  63. data/lib/graphql/query/arguments_cache.rb +1 -1
  64. data/lib/graphql/query/context.rb +15 -2
  65. data/lib/graphql/query/literal_input.rb +1 -1
  66. data/lib/graphql/query/null_context.rb +12 -7
  67. data/lib/graphql/query/serial_execution/field_resolution.rb +1 -1
  68. data/lib/graphql/query/validation_pipeline.rb +1 -1
  69. data/lib/graphql/query/variables.rb +5 -1
  70. data/lib/graphql/query.rb +5 -1
  71. data/lib/graphql/relay/edges_instrumentation.rb +0 -1
  72. data/lib/graphql/relay/global_id_resolve.rb +1 -1
  73. data/lib/graphql/relay/page_info.rb +1 -1
  74. data/lib/graphql/rubocop/graphql/base_cop.rb +36 -0
  75. data/lib/graphql/rubocop/graphql/default_null_true.rb +43 -0
  76. data/lib/graphql/rubocop/graphql/default_required_true.rb +43 -0
  77. data/lib/graphql/rubocop.rb +4 -0
  78. data/lib/graphql/schema/addition.rb +247 -0
  79. data/lib/graphql/schema/argument.rb +103 -45
  80. data/lib/graphql/schema/build_from_definition.rb +13 -7
  81. data/lib/graphql/schema/directive/feature.rb +1 -1
  82. data/lib/graphql/schema/directive/flagged.rb +2 -2
  83. data/lib/graphql/schema/directive/include.rb +1 -1
  84. data/lib/graphql/schema/directive/skip.rb +1 -1
  85. data/lib/graphql/schema/directive/transform.rb +14 -2
  86. data/lib/graphql/schema/directive.rb +7 -3
  87. data/lib/graphql/schema/enum.rb +70 -11
  88. data/lib/graphql/schema/enum_value.rb +6 -0
  89. data/lib/graphql/schema/field/connection_extension.rb +1 -1
  90. data/lib/graphql/schema/field.rb +243 -81
  91. data/lib/graphql/schema/field_extension.rb +89 -2
  92. data/lib/graphql/schema/find_inherited_value.rb +1 -0
  93. data/lib/graphql/schema/finder.rb +5 -5
  94. data/lib/graphql/schema/input_object.rb +39 -29
  95. data/lib/graphql/schema/interface.rb +11 -20
  96. data/lib/graphql/schema/introspection_system.rb +1 -1
  97. data/lib/graphql/schema/list.rb +3 -1
  98. data/lib/graphql/schema/member/accepts_definition.rb +15 -3
  99. data/lib/graphql/schema/member/build_type.rb +1 -4
  100. data/lib/graphql/schema/member/cached_graphql_definition.rb +29 -2
  101. data/lib/graphql/schema/member/has_arguments.rb +145 -57
  102. data/lib/graphql/schema/member/has_deprecation_reason.rb +1 -1
  103. data/lib/graphql/schema/member/has_fields.rb +76 -18
  104. data/lib/graphql/schema/member/has_interfaces.rb +90 -0
  105. data/lib/graphql/schema/member.rb +1 -0
  106. data/lib/graphql/schema/non_null.rb +7 -1
  107. data/lib/graphql/schema/object.rb +10 -75
  108. data/lib/graphql/schema/printer.rb +12 -17
  109. data/lib/graphql/schema/relay_classic_mutation.rb +37 -3
  110. data/lib/graphql/schema/resolver/has_payload_type.rb +27 -2
  111. data/lib/graphql/schema/resolver.rb +75 -65
  112. data/lib/graphql/schema/scalar.rb +2 -0
  113. data/lib/graphql/schema/subscription.rb +36 -8
  114. data/lib/graphql/schema/traversal.rb +1 -1
  115. data/lib/graphql/schema/type_expression.rb +1 -1
  116. data/lib/graphql/schema/type_membership.rb +18 -4
  117. data/lib/graphql/schema/union.rb +8 -1
  118. data/lib/graphql/schema/validator/allow_blank_validator.rb +29 -0
  119. data/lib/graphql/schema/validator/allow_null_validator.rb +26 -0
  120. data/lib/graphql/schema/validator/exclusion_validator.rb +3 -1
  121. data/lib/graphql/schema/validator/format_validator.rb +4 -5
  122. data/lib/graphql/schema/validator/inclusion_validator.rb +3 -1
  123. data/lib/graphql/schema/validator/length_validator.rb +5 -3
  124. data/lib/graphql/schema/validator/numericality_validator.rb +13 -2
  125. data/lib/graphql/schema/validator/required_validator.rb +29 -15
  126. data/lib/graphql/schema/validator.rb +33 -25
  127. data/lib/graphql/schema/warden.rb +116 -52
  128. data/lib/graphql/schema.rb +162 -227
  129. data/lib/graphql/static_validation/all_rules.rb +1 -0
  130. data/lib/graphql/static_validation/base_visitor.rb +8 -5
  131. data/lib/graphql/static_validation/definition_dependencies.rb +0 -1
  132. data/lib/graphql/static_validation/error.rb +3 -1
  133. data/lib/graphql/static_validation/literal_validator.rb +1 -1
  134. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
  135. data/lib/graphql/static_validation/rules/fields_will_merge.rb +52 -26
  136. data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +25 -4
  137. data/lib/graphql/static_validation/rules/fragments_are_finite.rb +2 -2
  138. data/lib/graphql/static_validation/rules/query_root_exists.rb +17 -0
  139. data/lib/graphql/static_validation/rules/query_root_exists_error.rb +26 -0
  140. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +3 -1
  141. data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +4 -4
  142. data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +13 -7
  143. data/lib/graphql/static_validation/validation_context.rb +8 -2
  144. data/lib/graphql/static_validation/validator.rb +15 -12
  145. data/lib/graphql/string_encoding_error.rb +13 -3
  146. data/lib/graphql/string_type.rb +1 -1
  147. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +36 -6
  148. data/lib/graphql/subscriptions/event.rb +68 -31
  149. data/lib/graphql/subscriptions/serialize.rb +23 -3
  150. data/lib/graphql/subscriptions.rb +17 -19
  151. data/lib/graphql/tracing/active_support_notifications_tracing.rb +6 -20
  152. data/lib/graphql/tracing/appsignal_tracing.rb +15 -0
  153. data/lib/graphql/tracing/notifications_tracing.rb +59 -0
  154. data/lib/graphql/types/big_int.rb +5 -1
  155. data/lib/graphql/types/int.rb +1 -1
  156. data/lib/graphql/types/relay/connection_behaviors.rb +26 -9
  157. data/lib/graphql/types/relay/default_relay.rb +5 -1
  158. data/lib/graphql/types/relay/edge_behaviors.rb +13 -2
  159. data/lib/graphql/types/relay/has_node_field.rb +2 -2
  160. data/lib/graphql/types/relay/has_nodes_field.rb +2 -2
  161. data/lib/graphql/types/relay/node_field.rb +15 -4
  162. data/lib/graphql/types/relay/nodes_field.rb +14 -4
  163. data/lib/graphql/types/string.rb +1 -1
  164. data/lib/graphql/unauthorized_error.rb +1 -1
  165. data/lib/graphql/version.rb +1 -1
  166. data/lib/graphql.rb +10 -28
  167. data/readme.md +1 -4
  168. metadata +17 -21
  169. data/lib/graphql/execution/interpreter/hash_response.rb +0 -46
@@ -8,6 +8,137 @@ module GraphQL
8
8
  #
9
9
  # @api private
10
10
  class Runtime
11
+
12
+ module GraphQLResult
13
+ def initialize(result_name, parent_result)
14
+ @graphql_parent = parent_result
15
+ if parent_result && parent_result.graphql_dead
16
+ @graphql_dead = true
17
+ end
18
+ @graphql_result_name = result_name
19
+ # Jump through some hoops to avoid creating this duplicate storage if at all possible.
20
+ @graphql_metadata = nil
21
+ end
22
+
23
+ attr_accessor :graphql_dead
24
+ attr_reader :graphql_parent, :graphql_result_name
25
+
26
+ # Although these are used by only one of the Result classes,
27
+ # it's handy to have the methods implemented on both (even though they just return `nil`)
28
+ # because it makes it easy to check if anything is assigned.
29
+ # @return [nil, Array<String>]
30
+ attr_accessor :graphql_non_null_field_names
31
+ # @return [nil, true]
32
+ attr_accessor :graphql_non_null_list_items
33
+
34
+ # @return [Hash] Plain-Ruby result data (`@graphql_metadata` contains Result wrapper objects)
35
+ attr_accessor :graphql_result_data
36
+ end
37
+
38
+ class GraphQLResultHash
39
+ def initialize(_result_name, _parent_result)
40
+ super
41
+ @graphql_result_data = {}
42
+ end
43
+
44
+ include GraphQLResult
45
+
46
+ attr_accessor :graphql_merged_into
47
+
48
+ def []=(key, value)
49
+ # This is a hack.
50
+ # Basically, this object is merged into the root-level result at some point.
51
+ # But the problem is, some lazies are created whose closures retain reference to _this_
52
+ # object. When those lazies are resolved, they cause an update to this object.
53
+ #
54
+ # In order to return a proper top-level result, we have to update that top-level result object.
55
+ # In order to return a proper partial result (eg, for a directive), we have to update this object, too.
56
+ # Yowza.
57
+ if (t = @graphql_merged_into)
58
+ t[key] = value
59
+ end
60
+
61
+ if value.respond_to?(:graphql_result_data)
62
+ @graphql_result_data[key] = value.graphql_result_data
63
+ # If we encounter some part of this response that requires metadata tracking,
64
+ # then create the metadata hash if necessary. It will be kept up-to-date after this.
65
+ (@graphql_metadata ||= @graphql_result_data.dup)[key] = value
66
+ else
67
+ @graphql_result_data[key] = value
68
+ # keep this up-to-date if it's been initialized
69
+ @graphql_metadata && @graphql_metadata[key] = value
70
+ end
71
+
72
+ value
73
+ end
74
+
75
+ def delete(key)
76
+ @graphql_metadata && @graphql_metadata.delete(key)
77
+ @graphql_result_data.delete(key)
78
+ end
79
+
80
+ def each
81
+ (@graphql_metadata || @graphql_result_data).each { |k, v| yield(k, v) }
82
+ end
83
+
84
+ def values
85
+ (@graphql_metadata || @graphql_result_data).values
86
+ end
87
+
88
+ def key?(k)
89
+ @graphql_result_data.key?(k)
90
+ end
91
+
92
+ def [](k)
93
+ (@graphql_metadata || @graphql_result_data)[k]
94
+ end
95
+ end
96
+
97
+ class GraphQLResultArray
98
+ include GraphQLResult
99
+
100
+ def initialize(_result_name, _parent_result)
101
+ super
102
+ @graphql_result_data = []
103
+ end
104
+
105
+ def graphql_skip_at(index)
106
+ # Mark this index as dead. It's tricky because some indices may already be storing
107
+ # `Lazy`s. So the runtime is still holding indexes _before_ skipping,
108
+ # this object has to coordinate incoming writes to account for any already-skipped indices.
109
+ @skip_indices ||= []
110
+ @skip_indices << index
111
+ offset_by = @skip_indices.count { |skipped_idx| skipped_idx < index}
112
+ delete_at_index = index - offset_by
113
+ @graphql_metadata && @graphql_metadata.delete_at(delete_at_index)
114
+ @graphql_result_data.delete_at(delete_at_index)
115
+ end
116
+
117
+ def []=(idx, value)
118
+ if @skip_indices
119
+ offset_by = @skip_indices.count { |skipped_idx| skipped_idx < idx }
120
+ idx -= offset_by
121
+ end
122
+ if value.respond_to?(:graphql_result_data)
123
+ @graphql_result_data[idx] = value.graphql_result_data
124
+ (@graphql_metadata ||= @graphql_result_data.dup)[idx] = value
125
+ else
126
+ @graphql_result_data[idx] = value
127
+ @graphql_metadata && @graphql_metadata[idx] = value
128
+ end
129
+
130
+ value
131
+ end
132
+
133
+ def values
134
+ (@graphql_metadata || @graphql_result_data)
135
+ end
136
+ end
137
+
138
+ class GraphQLSelectionSet < Hash
139
+ attr_accessor :graphql_directives
140
+ end
141
+
11
142
  # @return [GraphQL::Query]
12
143
  attr_reader :query
13
144
 
@@ -17,30 +148,48 @@ module GraphQL
17
148
  # @return [GraphQL::Query::Context]
18
149
  attr_reader :context
19
150
 
20
- def initialize(query:, response:)
151
+ def initialize(query:)
21
152
  @query = query
22
153
  @dataloader = query.multiplex.dataloader
23
154
  @schema = query.schema
24
155
  @context = query.context
25
156
  @multiplex_context = query.multiplex.context
26
157
  @interpreter_context = @context.namespace(:interpreter)
27
- @response = response
28
- @dead_paths = {}
29
- @types_at_paths = {}
158
+ @response = GraphQLResultHash.new(nil, nil)
159
+ # Identify runtime directives by checking which of this schema's directives have overridden `def self.resolve`
160
+ @runtime_directive_names = []
161
+ noop_resolve_owner = GraphQL::Schema::Directive.singleton_class
162
+ schema.directives.each do |name, dir_defn|
163
+ if dir_defn.method(:resolve).owner != noop_resolve_owner
164
+ @runtime_directive_names << name
165
+ end
166
+ end
30
167
  # A cache of { Class => { String => Schema::Field } }
31
168
  # Which assumes that MyObject.get_field("myField") will return the same field
32
169
  # during the lifetime of a query
33
170
  @fields_cache = Hash.new { |h, k| h[k] = {} }
171
+ # { Class => Boolean }
172
+ @lazy_cache = {}
34
173
  end
35
174
 
36
- def final_value
37
- @response.final_value
175
+ def final_result
176
+ @response && @response.graphql_result_data
38
177
  end
39
178
 
40
179
  def inspect
41
180
  "#<#{self.class.name} response=#{@response.inspect}>"
42
181
  end
43
182
 
183
+ def tap_or_each(obj_or_array)
184
+ if obj_or_array.is_a?(Array)
185
+ obj_or_array.each do |item|
186
+ yield(item, true)
187
+ end
188
+ else
189
+ yield(obj_or_array, false)
190
+ end
191
+ end
192
+
44
193
  # This _begins_ the execution. Some deferred work
45
194
  # might be stored up in lazies.
46
195
  # @return [void]
@@ -55,21 +204,43 @@ module GraphQL
55
204
 
56
205
  if object_proxy.nil?
57
206
  # Root .authorized? returned false.
58
- write_in_response(path, nil)
207
+ @response = nil
59
208
  else
60
- resolve_with_directives(object_proxy, root_operation) do # execute query level directives
209
+ resolve_with_directives(object_proxy, root_operation.directives) do # execute query level directives
61
210
  gathered_selections = gather_selections(object_proxy, root_type, root_operation.selections)
62
- # Make the first fiber which will begin execution
63
- @dataloader.append_job {
64
- evaluate_selections(
65
- path,
66
- context.scoped_context,
67
- object_proxy,
68
- root_type,
69
- root_op_type == "mutation",
70
- gathered_selections,
71
- )
72
- }
211
+ # This is kind of a hack -- `gathered_selections` is an Array if any of the selections
212
+ # require isolation during execution (because of runtime directives). In that case,
213
+ # make a new, isolated result hash for writing the result into. (That isolated response
214
+ # is eventually merged back into the main response)
215
+ #
216
+ # Otherwise, `gathered_selections` is a hash of selections which can be
217
+ # directly evaluated and the results can be written right into the main response hash.
218
+ tap_or_each(gathered_selections) do |selections, is_selection_array|
219
+ if is_selection_array
220
+ selection_response = GraphQLResultHash.new(nil, nil)
221
+ final_response = @response
222
+ else
223
+ selection_response = @response
224
+ final_response = nil
225
+ end
226
+
227
+ @dataloader.append_job {
228
+ set_all_interpreter_context(query.root_value, nil, nil, path)
229
+ resolve_with_directives(object_proxy, selections.graphql_directives) do
230
+ evaluate_selections(
231
+ path,
232
+ context.scoped_context,
233
+ object_proxy,
234
+ root_type,
235
+ root_op_type == "mutation",
236
+ selections,
237
+ selection_response,
238
+ final_response,
239
+ nil,
240
+ )
241
+ end
242
+ }
243
+ end
73
244
  end
74
245
  end
75
246
  delete_interpreter_context(:current_path)
@@ -79,15 +250,36 @@ module GraphQL
79
250
  nil
80
251
  end
81
252
 
82
- def gather_selections(owner_object, owner_type, selections, selections_by_name = {})
253
+ # @return [void]
254
+ def deep_merge_selection_result(from_result, into_result)
255
+ from_result.each do |key, value|
256
+ if !into_result.key?(key)
257
+ into_result[key] = value
258
+ else
259
+ case value
260
+ when GraphQLResultHash
261
+ deep_merge_selection_result(value, into_result[key])
262
+ else
263
+ # We have to assume that, since this passed the `fields_will_merge` selection,
264
+ # that the old and new values are the same.
265
+ # There's no special handling of arrays because currently, there's no way to split the execution
266
+ # of a list over several concurrent flows.
267
+ into_result[key] = value
268
+ end
269
+ end
270
+ end
271
+ from_result.graphql_merged_into = into_result
272
+ nil
273
+ end
274
+
275
+ def gather_selections(owner_object, owner_type, selections, selections_to_run = nil, selections_by_name = GraphQLSelectionSet.new)
83
276
  selections.each do |node|
84
277
  # Skip gathering this if the directive says so
85
278
  if !directives_include?(node, owner_object, owner_type)
86
279
  next
87
280
  end
88
281
 
89
- case node
90
- when GraphQL::Language::Nodes::Field
282
+ if node.is_a?(GraphQL::Language::Nodes::Field)
91
283
  response_key = node.alias || node.name
92
284
  selections = selections_by_name[response_key]
93
285
  # if there was already a selection of this field,
@@ -103,58 +295,84 @@ module GraphQL
103
295
  # No selection was found for this field yet
104
296
  selections_by_name[response_key] = node
105
297
  end
106
- when GraphQL::Language::Nodes::InlineFragment
107
- if node.type
108
- type_defn = schema.get_type(node.type.name)
109
- # Faster than .map{}.include?()
110
- query.warden.possible_types(type_defn).each do |t|
298
+ else
299
+ # This is an InlineFragment or a FragmentSpread
300
+ if @runtime_directive_names.any? && node.directives.any? { |d| @runtime_directive_names.include?(d.name) }
301
+ next_selections = GraphQLSelectionSet.new
302
+ next_selections.graphql_directives = node.directives
303
+ if selections_to_run
304
+ selections_to_run << next_selections
305
+ else
306
+ selections_to_run = []
307
+ selections_to_run << selections_by_name
308
+ selections_to_run << next_selections
309
+ end
310
+ else
311
+ next_selections = selections_by_name
312
+ end
313
+
314
+ case node
315
+ when GraphQL::Language::Nodes::InlineFragment
316
+ if node.type
317
+ type_defn = schema.get_type(node.type.name, context)
318
+
319
+ # Faster than .map{}.include?()
320
+ query.warden.possible_types(type_defn).each do |t|
321
+ if t == owner_type
322
+ gather_selections(owner_object, owner_type, node.selections, selections_to_run, next_selections)
323
+ break
324
+ end
325
+ end
326
+ else
327
+ # it's an untyped fragment, definitely continue
328
+ gather_selections(owner_object, owner_type, node.selections, selections_to_run, next_selections)
329
+ end
330
+ when GraphQL::Language::Nodes::FragmentSpread
331
+ fragment_def = query.fragments[node.name]
332
+ type_defn = query.get_type(fragment_def.type.name)
333
+ possible_types = query.warden.possible_types(type_defn)
334
+ possible_types.each do |t|
111
335
  if t == owner_type
112
- gather_selections(owner_object, owner_type, node.selections, selections_by_name)
336
+ gather_selections(owner_object, owner_type, fragment_def.selections, selections_to_run, next_selections)
113
337
  break
114
338
  end
115
339
  end
116
340
  else
117
- # it's an untyped fragment, definitely continue
118
- gather_selections(owner_object, owner_type, node.selections, selections_by_name)
341
+ raise "Invariant: unexpected selection class: #{node.class}"
119
342
  end
120
- when GraphQL::Language::Nodes::FragmentSpread
121
- fragment_def = query.fragments[node.name]
122
- type_defn = schema.get_type(fragment_def.type.name)
123
- possible_types = query.warden.possible_types(type_defn)
124
- possible_types.each do |t|
125
- if t == owner_type
126
- gather_selections(owner_object, owner_type, fragment_def.selections, selections_by_name)
127
- break
128
- end
129
- end
130
- else
131
- raise "Invariant: unexpected selection class: #{node.class}"
132
343
  end
133
344
  end
134
- selections_by_name
345
+ selections_to_run || selections_by_name
135
346
  end
136
347
 
137
348
  NO_ARGS = {}.freeze
138
349
 
139
350
  # @return [void]
140
- def evaluate_selections(path, scoped_context, owner_object, owner_type, is_eager_selection, gathered_selections)
351
+ def evaluate_selections(path, scoped_context, owner_object, owner_type, is_eager_selection, gathered_selections, selections_result, target_result, parent_object) # rubocop:disable Metrics/ParameterLists
141
352
  set_all_interpreter_context(owner_object, nil, nil, path)
142
353
 
354
+ finished_jobs = 0
355
+ enqueued_jobs = gathered_selections.size
143
356
  gathered_selections.each do |result_name, field_ast_nodes_or_ast_node|
144
357
  @dataloader.append_job {
145
358
  evaluate_selection(
146
- path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_selection
359
+ path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_selection, selections_result, parent_object
147
360
  )
361
+ finished_jobs += 1
362
+ if target_result && finished_jobs == enqueued_jobs
363
+ deep_merge_selection_result(selections_result, target_result)
364
+ end
148
365
  }
149
366
  end
150
367
 
151
- nil
368
+ selections_result
152
369
  end
153
370
 
154
371
  attr_reader :progress_path
155
372
 
156
373
  # @return [void]
157
- def evaluate_selection(path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_field)
374
+ def evaluate_selection(path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_field, selections_result, parent_object) # rubocop:disable Metrics/ParameterLists
375
+ return if dead_result?(selections_result)
158
376
  # As a performance optimization, the hash key will be a `Node` if
159
377
  # there's only one selection of the field. But if there are multiple
160
378
  # selections of the field, it will be an Array of nodes
@@ -166,7 +384,9 @@ module GraphQL
166
384
  ast_node = field_ast_nodes_or_ast_node
167
385
  end
168
386
  field_name = ast_node.name
169
- field_defn = @fields_cache[owner_type][field_name] ||= owner_type.get_field(field_name)
387
+ # This can't use `query.get_field` because it gets confused on introspection below if `field_defn` isn't `nil`,
388
+ # because of how `is_introspection` is used to call `.authorized_new` later on.
389
+ field_defn = @fields_cache[owner_type][field_name] ||= owner_type.get_field(field_name, @context)
170
390
  is_introspection = false
171
391
  if field_defn.nil?
172
392
  field_defn = if owner_type == schema.query && (entry_point_field = schema.introspection_system.entry_point(name: field_name))
@@ -188,7 +408,9 @@ module GraphQL
188
408
  # This seems janky, but we need to know
189
409
  # the field's return type at this path in order
190
410
  # to propagate `null`
191
- set_type_at_path(next_path, return_type)
411
+ if return_type.non_null?
412
+ (selections_result.graphql_non_null_field_names ||= []).push(result_name)
413
+ end
192
414
  # Set this before calling `run_with_directives`, so that the directive can have the latest path
193
415
  set_all_interpreter_context(nil, field_defn, nil, next_path)
194
416
 
@@ -199,24 +421,24 @@ module GraphQL
199
421
  object = authorized_new(field_defn.owner, object, context)
200
422
  end
201
423
 
202
- total_args_count = field_defn.arguments.size
424
+ total_args_count = field_defn.arguments(context).size
203
425
  if total_args_count == 0
204
- kwarg_arguments = GraphQL::Execution::Interpreter::Arguments::EMPTY
205
- evaluate_selection_with_args(kwarg_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field)
426
+ resolved_arguments = GraphQL::Execution::Interpreter::Arguments::EMPTY
427
+ evaluate_selection_with_args(resolved_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field, result_name, selections_result, parent_object)
206
428
  else
207
429
  # TODO remove all arguments(...) usages?
208
430
  @query.arguments_cache.dataload_for(ast_node, field_defn, object) do |resolved_arguments|
209
- evaluate_selection_with_args(resolved_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field)
431
+ evaluate_selection_with_args(resolved_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field, result_name, selections_result, parent_object)
210
432
  end
211
433
  end
212
434
  end
213
435
 
214
- def evaluate_selection_with_args(kwarg_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field) # rubocop:disable Metrics/ParameterLists
436
+ def evaluate_selection_with_args(arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field, result_name, selection_result, parent_object) # rubocop:disable Metrics/ParameterLists
215
437
  context.scoped_context = scoped_context
216
438
  return_type = field_defn.type
217
- after_lazy(kwarg_arguments, owner: owner_type, field: field_defn, path: next_path, ast_node: ast_node, scoped_context: context.scoped_context, owner_object: object, arguments: kwarg_arguments) do |resolved_arguments|
439
+ after_lazy(arguments, owner: owner_type, field: field_defn, path: next_path, ast_node: ast_node, scoped_context: context.scoped_context, owner_object: object, arguments: arguments, result_name: result_name, result: selection_result) do |resolved_arguments|
218
440
  if resolved_arguments.is_a?(GraphQL::ExecutionError) || resolved_arguments.is_a?(GraphQL::UnauthorizedError)
219
- continue_value(next_path, resolved_arguments, owner_type, field_defn, return_type.non_null?, ast_node)
441
+ continue_value(next_path, resolved_arguments, owner_type, field_defn, return_type.non_null?, ast_node, result_name, selection_result)
220
442
  next
221
443
  end
222
444
 
@@ -253,6 +475,8 @@ module GraphQL
253
475
  # This is used by `__typename` in order to support the legacy runtime,
254
476
  # but it has no use here (and it's always `nil`).
255
477
  # Stop adding it here to avoid the overhead of `.merge_extras` below.
478
+ when :parent
479
+ extra_args[:parent] = parent_object
256
480
  else
257
481
  extra_args[extra] = field_defn.fetch_extra(extra, context)
258
482
  end
@@ -263,17 +487,22 @@ module GraphQL
263
487
  resolved_arguments.keyword_arguments
264
488
  end
265
489
 
266
- set_all_interpreter_context(nil, nil, kwarg_arguments, nil)
490
+ set_all_interpreter_context(nil, nil, resolved_arguments, nil)
267
491
 
268
492
  # Optimize for the case that field is selected only once
269
493
  if field_ast_nodes.nil? || field_ast_nodes.size == 1
270
494
  next_selections = ast_node.selections
495
+ directives = ast_node.directives
271
496
  else
272
497
  next_selections = []
273
- field_ast_nodes.each { |f| next_selections.concat(f.selections) }
498
+ directives = []
499
+ field_ast_nodes.each { |f|
500
+ next_selections.concat(f.selections)
501
+ directives.concat(f.directives)
502
+ }
274
503
  end
275
504
 
276
- field_result = resolve_with_directives(object, ast_node) do
505
+ field_result = resolve_with_directives(object, directives) do
277
506
  # Actually call the field resolver and capture the result
278
507
  app_result = begin
279
508
  query.with_error_handling do
@@ -284,10 +513,10 @@ module GraphQL
284
513
  rescue GraphQL::ExecutionError => err
285
514
  err
286
515
  end
287
- after_lazy(app_result, owner: owner_type, field: field_defn, path: next_path, ast_node: ast_node, scoped_context: context.scoped_context, owner_object: object, arguments: kwarg_arguments) do |inner_result|
288
- continue_value = continue_value(next_path, inner_result, owner_type, field_defn, return_type.non_null?, ast_node)
516
+ 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: resolved_arguments, result_name: result_name, result: selection_result) do |inner_result|
517
+ continue_value = continue_value(next_path, inner_result, owner_type, field_defn, return_type.non_null?, ast_node, result_name, selection_result)
289
518
  if HALT != continue_value
290
- continue_field(next_path, continue_value, owner_type, field_defn, return_type, ast_node, next_selections, false, object, kwarg_arguments)
519
+ continue_field(next_path, continue_value, owner_type, field_defn, return_type, ast_node, next_selections, false, object, resolved_arguments, result_name, selection_result)
291
520
  end
292
521
  end
293
522
  end
@@ -304,43 +533,135 @@ module GraphQL
304
533
  end
305
534
  end
306
535
 
536
+ def dead_result?(selection_result)
537
+ selection_result.graphql_dead || ((parent = selection_result.graphql_parent) && parent.graphql_dead)
538
+ end
539
+
540
+ def set_result(selection_result, result_name, value)
541
+ if !dead_result?(selection_result)
542
+ if value.nil? &&
543
+ ( # there are two conditions under which `nil` is not allowed in the response:
544
+ (selection_result.graphql_non_null_list_items) || # this value would be written into a list that doesn't allow nils
545
+ ((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
546
+ )
547
+ # This is an invalid nil that should be propagated
548
+ # One caller of this method passes a block,
549
+ # namely when application code returns a `nil` to GraphQL and it doesn't belong there.
550
+ # The other possibility for reaching here is when a field returns an ExecutionError, so we write
551
+ # `nil` to the response, not knowing whether it's an invalid `nil` or not.
552
+ # (And in that case, we don't have to call the schema's handler, since it's not a bug in the application.)
553
+ # TODO the code is trying to tell me something.
554
+ yield if block_given?
555
+ parent = selection_result.graphql_parent
556
+ name_in_parent = selection_result.graphql_result_name
557
+ if parent.nil? # This is a top-level result hash
558
+ @response = nil
559
+ else
560
+ set_result(parent, name_in_parent, nil)
561
+ set_graphql_dead(selection_result)
562
+ end
563
+ else
564
+ selection_result[result_name] = value
565
+ end
566
+ end
567
+ end
568
+
569
+ # Mark this node and any already-registered children as dead,
570
+ # so that it accepts no more writes.
571
+ def set_graphql_dead(selection_result)
572
+ case selection_result
573
+ when GraphQLResultArray
574
+ selection_result.graphql_dead = true
575
+ selection_result.values.each { |v| set_graphql_dead(v) }
576
+ when GraphQLResultHash
577
+ selection_result.graphql_dead = true
578
+ selection_result.each { |k, v| set_graphql_dead(v) }
579
+ else
580
+ # It's a scalar, no way to mark it dead.
581
+ end
582
+ end
583
+
307
584
  HALT = Object.new
308
- def continue_value(path, value, parent_type, field, is_non_null, ast_node)
309
- if value.nil?
585
+ def continue_value(path, value, parent_type, field, is_non_null, ast_node, result_name, selection_result) # rubocop:disable Metrics/ParameterLists
586
+ case value
587
+ when nil
310
588
  if is_non_null
311
- err = parent_type::InvalidNullError.new(parent_type, field, value)
312
- write_invalid_null_in_response(path, err)
589
+ set_result(selection_result, result_name, nil) do
590
+ # This block is called if `result_name` is not dead. (Maybe a previous invalid nil caused it be marked dead.)
591
+ err = parent_type::InvalidNullError.new(parent_type, field, value)
592
+ schema.type_error(err, context)
593
+ end
313
594
  else
314
- write_in_response(path, nil)
595
+ set_result(selection_result, result_name, nil)
315
596
  end
316
597
  HALT
317
- elsif value.is_a?(GraphQL::ExecutionError)
318
- value.path ||= path
319
- value.ast_node ||= ast_node
320
- write_execution_errors_in_response(path, [value])
321
- HALT
322
- elsif value.is_a?(Array) && value.any? && value.all? { |v| v.is_a?(GraphQL::ExecutionError) }
323
- value.each_with_index do |error, index|
324
- error.ast_node ||= ast_node
325
- error.path ||= path + (field.type.list? ? [index] : [])
598
+ when GraphQL::Error
599
+ # Handle these cases inside a single `when`
600
+ # to avoid the overhead of checking three different classes
601
+ # every time.
602
+ if value.is_a?(GraphQL::ExecutionError)
603
+ if selection_result.nil? || !dead_result?(selection_result)
604
+ value.path ||= path
605
+ value.ast_node ||= ast_node
606
+ context.errors << value
607
+ if selection_result
608
+ set_result(selection_result, result_name, nil)
609
+ end
610
+ end
611
+ HALT
612
+ elsif value.is_a?(GraphQL::UnauthorizedError)
613
+ # this hook might raise & crash, or it might return
614
+ # a replacement value
615
+ next_value = begin
616
+ schema.unauthorized_object(value)
617
+ rescue GraphQL::ExecutionError => err
618
+ err
619
+ end
620
+ continue_value(path, next_value, parent_type, field, is_non_null, ast_node, result_name, selection_result)
621
+ elsif GraphQL::Execution::Execute::SKIP == value
622
+ # It's possible a lazy was already written here
623
+ case selection_result
624
+ when GraphQLResultHash
625
+ selection_result.delete(result_name)
626
+ when GraphQLResultArray
627
+ selection_result.graphql_skip_at(result_name)
628
+ when nil
629
+ # this can happen with directives
630
+ else
631
+ raise "Invariant: unexpected result class #{selection_result.class} (#{selection_result.inspect})"
632
+ end
633
+ HALT
634
+ else
635
+ # What could this actually _be_? Anyhow,
636
+ # preserve the default behavior of doing nothing with it.
637
+ value
326
638
  end
327
- write_execution_errors_in_response(path, value)
328
- HALT
329
- elsif value.is_a?(GraphQL::UnauthorizedError)
330
- # this hook might raise & crash, or it might return
331
- # a replacement value
332
- next_value = begin
333
- schema.unauthorized_object(value)
334
- rescue GraphQL::ExecutionError => err
335
- err
639
+ when Array
640
+ # It's an array full of execution errors; add them all.
641
+ if value.any? && value.all? { |v| v.is_a?(GraphQL::ExecutionError) }
642
+ list_type_at_all = (field && (field.type.list?))
643
+ if selection_result.nil? || !dead_result?(selection_result)
644
+ value.each_with_index do |error, index|
645
+ error.ast_node ||= ast_node
646
+ error.path ||= path + (list_type_at_all ? [index] : [])
647
+ context.errors << error
648
+ end
649
+ if selection_result
650
+ if list_type_at_all
651
+ result_without_errors = value.map { |v| v.is_a?(GraphQL::ExecutionError) ? nil : v }
652
+ set_result(selection_result, result_name, result_without_errors)
653
+ else
654
+ set_result(selection_result, result_name, nil)
655
+ end
656
+ end
657
+ end
658
+ HALT
659
+ else
660
+ value
336
661
  end
337
-
338
- continue_value(path, next_value, parent_type, field, is_non_null, ast_node)
339
- elsif GraphQL::Execution::Execute::SKIP == value
340
- HALT
341
- elsif value.is_a?(GraphQL::Execution::Interpreter::RawValue)
662
+ when GraphQL::Execution::Interpreter::RawValue
342
663
  # Write raw value directly to the response without resolving nested objects
343
- write_in_response(path, value.resolve)
664
+ set_result(selection_result, result_name, value.resolve)
344
665
  HALT
345
666
  else
346
667
  value
@@ -355,17 +676,22 @@ module GraphQL
355
676
  # Location information from `path` and `ast_node`.
356
677
  #
357
678
  # @return [Lazy, Array, Hash, Object] Lazy, Array, and Hash are all traversed to resolve lazy values later
358
- def continue_field(path, value, owner_type, field, current_type, ast_node, next_selections, is_non_null, owner_object, arguments) # rubocop:disable Metrics/ParameterLists
679
+ def continue_field(path, value, owner_type, field, current_type, ast_node, next_selections, is_non_null, owner_object, arguments, result_name, selection_result) # rubocop:disable Metrics/ParameterLists
680
+ if current_type.non_null?
681
+ current_type = current_type.of_type
682
+ is_non_null = true
683
+ end
684
+
359
685
  case current_type.kind.name
360
686
  when "SCALAR", "ENUM"
361
687
  r = current_type.coerce_result(value, context)
362
- write_in_response(path, r)
688
+ set_result(selection_result, result_name, r)
363
689
  r
364
690
  when "UNION", "INTERFACE"
365
691
  resolved_type_or_lazy, resolved_value = resolve_type(current_type, value, path)
366
692
  resolved_value ||= value
367
693
 
368
- after_lazy(resolved_type_or_lazy, owner: current_type, path: path, ast_node: ast_node, scoped_context: context.scoped_context, field: field, owner_object: owner_object, arguments: arguments, trace: false) do |resolved_type|
694
+ after_lazy(resolved_type_or_lazy, owner: current_type, path: path, ast_node: ast_node, scoped_context: context.scoped_context, field: field, owner_object: owner_object, arguments: arguments, trace: false, result_name: result_name, result: selection_result) do |resolved_type|
369
695
  possible_types = query.possible_types(current_type)
370
696
 
371
697
  if !possible_types.include?(resolved_type)
@@ -373,10 +699,10 @@ module GraphQL
373
699
  err_class = current_type::UnresolvedTypeError
374
700
  type_error = err_class.new(resolved_value, field, parent_type, resolved_type, possible_types)
375
701
  schema.type_error(type_error, context)
376
- write_in_response(path, nil)
702
+ set_result(selection_result, result_name, nil)
377
703
  nil
378
704
  else
379
- continue_field(path, resolved_value, owner_type, field, resolved_type, ast_node, next_selections, is_non_null, owner_object, arguments)
705
+ continue_field(path, resolved_value, owner_type, field, resolved_type, ast_node, next_selections, is_non_null, owner_object, arguments, result_name, selection_result)
380
706
  end
381
707
  end
382
708
  when "OBJECT"
@@ -385,34 +711,67 @@ module GraphQL
385
711
  rescue GraphQL::ExecutionError => err
386
712
  err
387
713
  end
388
- after_lazy(object_proxy, owner: current_type, path: path, ast_node: ast_node, scoped_context: context.scoped_context, field: field, owner_object: owner_object, arguments: arguments, trace: false) do |inner_object|
389
- continue_value = continue_value(path, inner_object, owner_type, field, is_non_null, ast_node)
714
+ after_lazy(object_proxy, owner: current_type, path: path, ast_node: ast_node, scoped_context: context.scoped_context, field: field, owner_object: owner_object, arguments: arguments, trace: false, result_name: result_name, result: selection_result) do |inner_object|
715
+ continue_value = continue_value(path, inner_object, owner_type, field, is_non_null, ast_node, result_name, selection_result)
390
716
  if HALT != continue_value
391
- response_hash = {}
392
- write_in_response(path, response_hash)
717
+ response_hash = GraphQLResultHash.new(result_name, selection_result)
718
+ set_result(selection_result, result_name, response_hash)
393
719
  gathered_selections = gather_selections(continue_value, current_type, next_selections)
394
- evaluate_selections(path, context.scoped_context, continue_value, current_type, false, gathered_selections)
395
- response_hash
720
+ # There are two possibilities for `gathered_selections`:
721
+ # 1. All selections of this object should be evaluated together (there are no runtime directives modifying execution).
722
+ # This case is handled below, and the result can be written right into the main `response_hash` above.
723
+ # In this case, `gathered_selections` is a hash of selections.
724
+ # 2. Some selections of this object have runtime directives that may or may not modify execution.
725
+ # That part of the selection is evaluated in an isolated way, writing into a sub-response object which is
726
+ # eventually merged into the final response. In this case, `gathered_selections` is an array of things to run in isolation.
727
+ # (Technically, it's possible that one of those entries _doesn't_ require isolation.)
728
+ tap_or_each(gathered_selections) do |selections, is_selection_array|
729
+ if is_selection_array
730
+ this_result = GraphQLResultHash.new(result_name, selection_result)
731
+ final_result = response_hash
732
+ else
733
+ this_result = response_hash
734
+ final_result = nil
735
+ end
736
+ set_all_interpreter_context(continue_value, nil, nil, path) # reset this mutable state
737
+ resolve_with_directives(continue_value, selections.graphql_directives) do
738
+ evaluate_selections(
739
+ path,
740
+ context.scoped_context,
741
+ continue_value,
742
+ current_type,
743
+ false,
744
+ selections,
745
+ this_result,
746
+ final_result,
747
+ owner_object.object,
748
+ )
749
+ this_result
750
+ end
751
+ end
396
752
  end
397
753
  end
398
754
  when "LIST"
399
- response_list = []
400
- write_in_response(path, response_list)
401
755
  inner_type = current_type.of_type
756
+ response_list = GraphQLResultArray.new(result_name, selection_result)
757
+ response_list.graphql_non_null_list_items = inner_type.non_null?
758
+ set_result(selection_result, result_name, response_list)
759
+
402
760
  idx = 0
403
761
  scoped_context = context.scoped_context
404
762
  begin
405
763
  value.each do |inner_value|
764
+ break if dead_result?(response_list)
406
765
  next_path = path.dup
407
766
  next_path << idx
767
+ this_idx = idx
408
768
  next_path.freeze
409
769
  idx += 1
410
- set_type_at_path(next_path, inner_type)
411
770
  # This will update `response_list` with the lazy
412
- after_lazy(inner_value, owner: inner_type, path: next_path, ast_node: ast_node, scoped_context: scoped_context, field: field, owner_object: owner_object, arguments: arguments) do |inner_inner_value|
413
- continue_value = continue_value(next_path, inner_inner_value, owner_type, field, inner_type.non_null?, ast_node)
771
+ after_lazy(inner_value, owner: inner_type, path: next_path, ast_node: ast_node, scoped_context: scoped_context, field: field, owner_object: owner_object, arguments: arguments, result_name: this_idx, result: response_list) do |inner_inner_value|
772
+ continue_value = continue_value(next_path, inner_inner_value, owner_type, field, inner_type.non_null?, ast_node, this_idx, response_list)
414
773
  if HALT != continue_value
415
- continue_field(next_path, continue_value, owner_type, field, inner_type, ast_node, next_selections, false, owner_object, arguments)
774
+ continue_field(next_path, continue_value, owner_type, field, inner_type, ast_node, next_selections, false, owner_object, arguments, this_idx, response_list)
416
775
  end
417
776
  end
418
777
  end
@@ -428,23 +787,18 @@ module GraphQL
428
787
  end
429
788
 
430
789
  response_list
431
- when "NON_NULL"
432
- inner_type = current_type.of_type
433
- # Don't `set_type_at_path` because we want the static type,
434
- # we're going to use that to determine whether a `nil` should be propagated or not.
435
- continue_field(path, value, owner_type, field, inner_type, ast_node, next_selections, true, owner_object, arguments)
436
790
  else
437
791
  raise "Invariant: Unhandled type kind #{current_type.kind} (#{current_type})"
438
792
  end
439
793
  end
440
794
 
441
- def resolve_with_directives(object, ast_node, &block)
442
- return yield if ast_node.directives.empty?
443
- run_directive(object, ast_node, 0, &block)
795
+ def resolve_with_directives(object, directives, &block)
796
+ return yield if directives.nil? || directives.empty?
797
+ run_directive(object, directives, 0, &block)
444
798
  end
445
799
 
446
- def run_directive(object, ast_node, idx, &block)
447
- dir_node = ast_node.directives[idx]
800
+ def run_directive(object, directives, idx, &block)
801
+ dir_node = directives[idx]
448
802
  if !dir_node
449
803
  yield
450
804
  else
@@ -452,9 +806,24 @@ module GraphQL
452
806
  if !dir_defn.is_a?(Class)
453
807
  dir_defn = dir_defn.type_class || raise("Only class-based directives are supported (not `@#{dir_node.name}`)")
454
808
  end
455
- dir_args = arguments(nil, dir_defn, dir_node).keyword_arguments
456
- dir_defn.resolve(object, dir_args, context) do
457
- run_directive(object, ast_node, idx + 1, &block)
809
+ raw_dir_args = arguments(nil, dir_defn, dir_node)
810
+ dir_args = continue_value(
811
+ @context[:current_path], # path
812
+ raw_dir_args, # value
813
+ dir_defn, # parent_type
814
+ nil, # field
815
+ false, # is_non_null
816
+ dir_node, # ast_node
817
+ nil, # result_name
818
+ nil, # selection_result
819
+ )
820
+
821
+ if dir_args == HALT
822
+ nil
823
+ else
824
+ dir_defn.resolve(object, dir_args, context) do
825
+ run_directive(object, directives, idx + 1, &block)
826
+ end
458
827
  end
459
828
  end
460
829
  end
@@ -463,7 +832,7 @@ module GraphQL
463
832
  def directives_include?(node, graphql_object, parent_type)
464
833
  node.directives.each do |dir_node|
465
834
  dir_defn = schema.directives.fetch(dir_node.name).type_class || raise("Only class-based directives are supported (not #{dir_node.name.inspect})")
466
- args = arguments(graphql_object, dir_defn, dir_node).keyword_arguments
835
+ args = arguments(graphql_object, dir_defn, dir_node)
467
836
  if !dir_defn.include?(graphql_object, args, context)
468
837
  return false
469
838
  end
@@ -492,9 +861,8 @@ module GraphQL
492
861
  # @param eager [Boolean] Set to `true` for mutation root fields only
493
862
  # @param trace [Boolean] If `false`, don't wrap this with field tracing
494
863
  # @return [GraphQL::Execution::Lazy, Object] If loading `object` will be deferred, it's a wrapper over it.
495
- def after_lazy(lazy_obj, owner:, field:, path:, scoped_context:, owner_object:, arguments:, ast_node:, eager: false, trace: true, &block)
496
- set_all_interpreter_context(owner_object, field, arguments, path)
497
- if schema.lazy?(lazy_obj)
864
+ def after_lazy(lazy_obj, owner:, field:, path:, scoped_context:, owner_object:, arguments:, ast_node:, result:, result_name:, eager: false, trace: true, &block)
865
+ if lazy?(lazy_obj)
498
866
  lazy = GraphQL::Execution::Lazy.new(path: path, field: field) do
499
867
  set_all_interpreter_context(owner_object, field, arguments, path)
500
868
  context.scoped_context = scoped_context
@@ -502,27 +870,32 @@ module GraphQL
502
870
  # but don't wrap the continuation below
503
871
  inner_obj = begin
504
872
  query.with_error_handling do
505
- if trace
506
- query.trace("execute_field_lazy", {owner: owner, field: field, path: path, query: query, object: owner_object, arguments: arguments, ast_node: ast_node}) do
873
+ begin
874
+ if trace
875
+ query.trace("execute_field_lazy", {owner: owner, field: field, path: path, query: query, object: owner_object, arguments: arguments, ast_node: ast_node}) do
876
+ schema.sync_lazy(lazy_obj)
877
+ end
878
+ else
507
879
  schema.sync_lazy(lazy_obj)
508
880
  end
509
- else
510
- schema.sync_lazy(lazy_obj)
881
+ rescue GraphQL::ExecutionError, GraphQL::UnauthorizedError => err
882
+ err
511
883
  end
512
884
  end
513
- rescue GraphQL::ExecutionError, GraphQL::UnauthorizedError => err
514
- err
885
+ rescue GraphQL::ExecutionError => ex_err
886
+ ex_err
515
887
  end
516
- after_lazy(inner_obj, owner: owner, field: field, path: path, ast_node: ast_node, scoped_context: context.scoped_context, owner_object: owner_object, arguments: arguments, eager: eager, trace: trace, &block)
888
+ yield(inner_obj)
517
889
  end
518
890
 
519
891
  if eager
520
892
  lazy.value
521
893
  else
522
- write_in_response(path, lazy)
894
+ set_result(result, result_name, lazy)
523
895
  lazy
524
896
  end
525
897
  else
898
+ set_all_interpreter_context(owner_object, field, arguments, path)
526
899
  yield(lazy_obj)
527
900
  end
528
901
  end
@@ -536,85 +909,6 @@ module GraphQL
536
909
  end
537
910
  end
538
911
 
539
- def write_invalid_null_in_response(path, invalid_null_error)
540
- if !dead_path?(path)
541
- schema.type_error(invalid_null_error, context)
542
- write_in_response(path, nil)
543
- add_dead_path(path)
544
- end
545
- end
546
-
547
- def write_execution_errors_in_response(path, errors)
548
- if !dead_path?(path)
549
- errors.each do |v|
550
- context.errors << v
551
- end
552
- write_in_response(path, nil)
553
- add_dead_path(path)
554
- end
555
- end
556
-
557
- def write_in_response(path, value)
558
- if dead_path?(path)
559
- return
560
- else
561
- if value.nil? && path.any? && type_at(path).non_null?
562
- # This nil is invalid, try writing it at the previous spot
563
- propagate_path = path[0..-2]
564
- write_in_response(propagate_path, value)
565
- add_dead_path(propagate_path)
566
- else
567
- @response.write(path, value)
568
- end
569
- end
570
- end
571
-
572
- def value_at(path)
573
- i = 0
574
- value = @response.final_value
575
- while value && (part = path[i])
576
- value = value[part]
577
- i += 1
578
- end
579
- value
580
- end
581
-
582
- # To propagate nulls, we have to know what the field type was
583
- # at previous parts of the response.
584
- # This hash matches the response
585
- def type_at(path)
586
- @types_at_paths.fetch(path)
587
- end
588
-
589
- def set_type_at_path(path, type)
590
- @types_at_paths[path] = type
591
- nil
592
- end
593
-
594
- # Mark `path` as having been permanently nulled out.
595
- # No values will be added beyond that path.
596
- def add_dead_path(path)
597
- dead = @dead_paths
598
- path.each do |part|
599
- dead = dead[part] ||= {}
600
- end
601
- dead[:__dead] = true
602
- end
603
-
604
- def dead_path?(path)
605
- res = @dead_paths
606
- path.each do |part|
607
- if res
608
- if res[:__dead]
609
- break
610
- else
611
- res = res[part]
612
- end
613
- end
614
- end
615
- res && res[:__dead]
616
- end
617
-
618
912
  # Set this pair in the Query context, but also in the interpeter namespace,
619
913
  # for compatibility.
620
914
  def set_interpreter_context(key, value)
@@ -633,7 +927,7 @@ module GraphQL
633
927
  query.resolve_type(type, value)
634
928
  end
635
929
 
636
- if schema.lazy?(resolved_type)
930
+ if lazy?(resolved_type)
637
931
  GraphQL::Execution::Lazy.new do
638
932
  query.trace("resolve_type_lazy", trace_payload) do
639
933
  schema.sync_lazy(resolved_type)
@@ -647,6 +941,12 @@ module GraphQL
647
941
  def authorized_new(type, value, context)
648
942
  type.authorized_new(value, context)
649
943
  end
944
+
945
+ def lazy?(object)
946
+ @lazy_cache.fetch(object.class) {
947
+ @lazy_cache[object.class] = @schema.lazy?(object)
948
+ }
949
+ end
650
950
  end
651
951
  end
652
952
  end