graphql 1.12.10 → 1.12.14

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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/backtrace/table.rb +14 -2
  3. data/lib/graphql/backtrace/tracer.rb +7 -4
  4. data/lib/graphql/cop/nullability.rb +28 -0
  5. data/lib/graphql/cop/resolve_methods.rb +28 -0
  6. data/lib/graphql/dataloader.rb +27 -14
  7. data/lib/graphql/dataloader/null_dataloader.rb +1 -0
  8. data/lib/graphql/execution/execute.rb +1 -1
  9. data/lib/graphql/execution/interpreter.rb +4 -8
  10. data/lib/graphql/execution/interpreter/arguments_cache.rb +3 -2
  11. data/lib/graphql/execution/interpreter/resolve.rb +6 -2
  12. data/lib/graphql/execution/interpreter/runtime.rb +482 -203
  13. data/lib/graphql/execution/lazy.rb +5 -1
  14. data/lib/graphql/introspection/schema_type.rb +1 -1
  15. data/lib/graphql/query.rb +1 -1
  16. data/lib/graphql/schema.rb +34 -200
  17. data/lib/graphql/schema/addition.rb +238 -0
  18. data/lib/graphql/schema/argument.rb +56 -39
  19. data/lib/graphql/schema/build_from_definition.rb +8 -2
  20. data/lib/graphql/schema/directive/transform.rb +13 -1
  21. data/lib/graphql/schema/enum.rb +10 -1
  22. data/lib/graphql/schema/input_object.rb +11 -15
  23. data/lib/graphql/schema/member/build_type.rb +1 -0
  24. data/lib/graphql/schema/printer.rb +11 -16
  25. data/lib/graphql/schema/resolver.rb +28 -3
  26. data/lib/graphql/types/relay/has_node_field.rb +1 -1
  27. data/lib/graphql/types/relay/has_nodes_field.rb +1 -1
  28. data/lib/graphql/types/relay/node_field.rb +2 -2
  29. data/lib/graphql/types/relay/nodes_field.rb +2 -2
  30. data/lib/graphql/version.rb +1 -1
  31. data/readme.md +0 -3
  32. metadata +5 -17
  33. data/lib/graphql/execution/interpreter/hash_response.rb +0 -46
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9d314c3a4dbbf6bded442bea6c2fcdd68b0cd7730ebb5e807d3677ffdc838268
4
- data.tar.gz: 60a24771181bb8a479f768a4b17f3a474ff9bbc339027b94e18bf92554df296c
3
+ metadata.gz: 74f73ae15eae0c31b63558b5d038091006422534ebee13cafe25b0fe07d1349a
4
+ data.tar.gz: c6f454d12ae887420e98f4e0563d74d7bedfa350d6b6dcd66beedd43e9cd5f5a
5
5
  SHA512:
6
- metadata.gz: 25f941249c01ce4dcfe3522dfc7b045b6f2bd3888e1f21b2e25a3ae5de74f0de5f9721c998d6ddd33eb681339c3e883edcdc9d76a196c148b688a1264b1761fa
7
- data.tar.gz: 91bde8d0102ae6874511656b6e11140b511aa1813d19b70b91a168cf58cbfc95eb83a6244137fc06abeae19f15694ca005c94c6e728085d0b8be3ad3bb964ebc
6
+ metadata.gz: 333b8424cc0c6cf3caccf68b27a66bd555311255a83fd336b8ccd569edf6ab4020bb3459b7ce1c0ab26542f003cd12e7c3d312d8ea45c6c2d52fe81a94471536
7
+ data.tar.gz: e31d1e33bd7da992d866749f53fe05e417ac27ee6488d0bd7d5cf0e7316e57c6bec6154f496e09eb87eb830b8540eb1ecebde1384f50d7deb41244fdc07dc5b9
@@ -83,7 +83,7 @@ module GraphQL
83
83
  value = if top && @override_value
84
84
  @override_value
85
85
  else
86
- @context.query.context.namespace(:interpreter)[:runtime].value_at(context_entry.path)
86
+ value_at(@context.query.context.namespace(:interpreter)[:runtime], context_entry.path)
87
87
  end
88
88
  rows << [
89
89
  "#{context_entry.ast_node ? context_entry.ast_node.position.join(":") : ""}",
@@ -130,7 +130,7 @@ module GraphQL
130
130
  if object.is_a?(GraphQL::Schema::Object)
131
131
  object = object.object
132
132
  end
133
- value = context_entry.namespace(:interpreter)[:runtime].value_at([])
133
+ value = value_at(context_entry.namespace(:interpreter)[:runtime], [])
134
134
  rows << [
135
135
  "#{position}",
136
136
  "#{op_type}#{op_name ? " #{op_name}" : ""}",
@@ -142,6 +142,18 @@ module GraphQL
142
142
  raise "Unexpected get_rows subject #{context_entry.class} (#{context_entry.inspect})"
143
143
  end
144
144
  end
145
+
146
+ def value_at(runtime, path)
147
+ response = runtime.final_result
148
+ path.each do |key|
149
+ if response && (response = response[key])
150
+ next
151
+ else
152
+ break
153
+ end
154
+ end
155
+ response
156
+ end
145
157
  end
146
158
  end
147
159
  end
@@ -15,14 +15,17 @@ module GraphQL
15
15
  # No query context yet
16
16
  nil
17
17
  when "validate", "analyze_query", "execute_query", "execute_query_lazy"
18
- query = metadata[:query] || metadata[:queries].first
19
18
  push_key = []
20
- push_data = query
21
- multiplex = query.multiplex
19
+ if (query = metadata[:query]) || ((queries = metadata[:queries]) && (query = queries.first))
20
+ push_data = query
21
+ multiplex = query.multiplex
22
+ elsif (multiplex = metadata[:multiplex])
23
+ push_data = multiplex.queries.first
24
+ end
22
25
  when "execute_field", "execute_field_lazy"
23
26
  query = metadata[:query] || raise(ArgumentError, "Add `legacy: true` to use GraphQL::Backtrace without the interpreter runtime.")
24
27
  multiplex = query.multiplex
25
- push_key = metadata[:path].reject { |i| i.is_a?(Integer) }
28
+ push_key = metadata[:path]
26
29
  parent_frame = multiplex.context[:graphql_backtrace_contexts][push_key[0..-2]]
27
30
 
28
31
  if parent_frame.is_a?(GraphQL::Query)
@@ -0,0 +1,28 @@
1
+ require "rubocop"
2
+
3
+ module GraphQL
4
+ module Cop
5
+ class Nullability < RuboCop::Cop::Base
6
+ extend AutoCorrector
7
+
8
+ NEEDLESS_NULL_FALSE = <<-ERR
9
+ `null: false` is the default, it can be removed.
10
+ ERR
11
+
12
+ NEEDLESS_REQUIRED_TRUE = <<-ERR
13
+ `required: true` is the default, it can be removed.
14
+ ERR
15
+
16
+ def on_send(node)
17
+ recv, method_name, args = *node
18
+ if recv.nil?
19
+ if method_name == :field
20
+
21
+ elsif method_name == :argument
22
+
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ require "rubocop"
2
+
3
+ module GraphQL
4
+ module Cop
5
+ class ResolveMethods < RuboCop::Cop::Base
6
+ extend AutoCorrector
7
+
8
+ NEEDLESS_NULL_FALSE = <<-ERR
9
+ `null: false` is the default, it can be removed.
10
+ ERR
11
+
12
+ NEEDLESS_REQUIRED_TRUE = <<-ERR
13
+ `required: true` is the default, it can be removed.
14
+ ERR
15
+
16
+ def on_send(node)
17
+ recv, method_name, args = *node
18
+ if recv.nil?
19
+ if method_name == :field
20
+
21
+ elsif method_name == :argument
22
+
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -77,6 +77,21 @@ module GraphQL
77
77
  nil
78
78
  end
79
79
 
80
+ # Use a self-contained queue for the work in the block.
81
+ def run_isolated
82
+ prev_queue = @pending_jobs
83
+ @pending_jobs = []
84
+ res = nil
85
+ # Make sure the block is inside a Fiber, so it can `Fiber.yield`
86
+ append_job {
87
+ res = yield
88
+ }
89
+ run
90
+ res
91
+ ensure
92
+ @pending_jobs = prev_queue
93
+ end
94
+
80
95
  # @api private Move along, move along
81
96
  def run
82
97
  # At a high level, the algorithm is:
@@ -136,26 +151,24 @@ module GraphQL
136
151
  # This is where an evented approach would be even better -- can we tell which
137
152
  # fibers are ready to continue, and continue execution there?
138
153
  #
139
- source_fiber_stack = if (first_source_fiber = create_source_fiber)
154
+ source_fiber_queue = if (first_source_fiber = create_source_fiber)
140
155
  [first_source_fiber]
141
156
  else
142
157
  nil
143
158
  end
144
159
 
145
- if source_fiber_stack
146
- # Use a stack with `.pop` here so that when a source causes another source to become pending,
147
- # that newly-pending source will run _before_ the one that depends on it.
148
- # (See below where the old fiber is pushed to the stack, then the new fiber is pushed on the stack.)
149
- while (outer_source_fiber = source_fiber_stack.pop)
160
+ if source_fiber_queue
161
+ while (outer_source_fiber = source_fiber_queue.shift)
150
162
  resume(outer_source_fiber)
151
163
 
152
- if outer_source_fiber.alive?
153
- source_fiber_stack << outer_source_fiber
154
- end
155
164
  # If this source caused more sources to become pending, run those before running this one again:
156
165
  next_source_fiber = create_source_fiber
157
166
  if next_source_fiber
158
- source_fiber_stack << next_source_fiber
167
+ source_fiber_queue << next_source_fiber
168
+ end
169
+
170
+ if outer_source_fiber.alive?
171
+ source_fiber_queue << outer_source_fiber
159
172
  end
160
173
  end
161
174
  end
@@ -224,16 +237,16 @@ module GraphQL
224
237
  #
225
238
  # @see https://github.com/rmosolgo/graphql-ruby/issues/3449
226
239
  def spawn_fiber
227
- fiber_locals = {}
240
+ fiber_locals = {}
228
241
 
229
242
  Thread.current.keys.each do |fiber_var_key|
230
243
  fiber_locals[fiber_var_key] = Thread.current[fiber_var_key]
231
- end
244
+ end
232
245
 
233
- Fiber.new do
246
+ Fiber.new do
234
247
  fiber_locals.each { |k, v| Thread.current[k] = v }
235
248
  yield
236
- end
249
+ end
237
250
  end
238
251
  end
239
252
  end
@@ -10,6 +10,7 @@ module GraphQL
10
10
  # These are all no-ops because code was
11
11
  # executed sychronously.
12
12
  def run; end
13
+ def run_isolated; yield; end
13
14
  def yield; end
14
15
 
15
16
  def append_job
@@ -6,7 +6,7 @@ module GraphQL
6
6
  class Execute
7
7
 
8
8
  # @api private
9
- class Skip; end
9
+ class Skip < GraphQL::Error; end
10
10
 
11
11
  # Just a singleton for implementing {Query::Context#skip}
12
12
  # @api private
@@ -4,7 +4,6 @@ require "graphql/execution/interpreter/argument_value"
4
4
  require "graphql/execution/interpreter/arguments"
5
5
  require "graphql/execution/interpreter/arguments_cache"
6
6
  require "graphql/execution/interpreter/execution_errors"
7
- require "graphql/execution/interpreter/hash_response"
8
7
  require "graphql/execution/interpreter/runtime"
9
8
  require "graphql/execution/interpreter/resolve"
10
9
  require "graphql/execution/interpreter/handles_raw_value"
@@ -19,7 +18,7 @@ module GraphQL
19
18
  def execute(_operation, _root_type, query)
20
19
  runtime = evaluate(query)
21
20
  sync_lazies(query: query)
22
- runtime.final_value
21
+ runtime.final_result
23
22
  end
24
23
 
25
24
  def self.use(schema_class)
@@ -57,7 +56,7 @@ module GraphQL
57
56
 
58
57
  def self.finish_query(query, _multiplex)
59
58
  {
60
- "data" => query.context.namespace(:interpreter)[:runtime].final_value
59
+ "data" => query.context.namespace(:interpreter)[:runtime].final_result
61
60
  }
62
61
  end
63
62
 
@@ -67,10 +66,7 @@ module GraphQL
67
66
  # Although queries in a multiplex _share_ an Interpreter instance,
68
67
  # they also have another item of state, which is private to that query
69
68
  # in particular, assign it here:
70
- runtime = Runtime.new(
71
- query: query,
72
- response: HashResponse.new,
73
- )
69
+ runtime = Runtime.new(query: query)
74
70
  query.context.namespace(:interpreter)[:runtime] = runtime
75
71
 
76
72
  query.trace("execute_query", {query: query}) do
@@ -91,7 +87,7 @@ module GraphQL
91
87
  final_values = queries.map do |query|
92
88
  runtime = query.context.namespace(:interpreter)[:runtime]
93
89
  # it might not be present if the query has an error
94
- runtime ? runtime.final_value : nil
90
+ runtime ? runtime.final_result : nil
95
91
  end
96
92
  final_values.compact!
97
93
  tracer.trace("execute_query_lazy", {multiplex: multiplex, query: query}) do
@@ -28,11 +28,12 @@ module GraphQL
28
28
  end
29
29
 
30
30
  def fetch(ast_node, argument_owner, parent_object)
31
- @storage[ast_node][argument_owner][parent_object]
32
31
  # If any jobs were enqueued, run them now,
33
32
  # since this might have been called outside of execution.
34
33
  # (The jobs are responsible for updating `result` in-place.)
35
- @dataloader.run
34
+ @dataloader.run_isolated do
35
+ @storage[ast_node][argument_owner][parent_object]
36
+ end
36
37
  # Ack, the _hash_ is updated, but the key is eventually
37
38
  # overridden with an immutable arguments instance.
38
39
  # The first call queues up the job,
@@ -34,7 +34,10 @@ module GraphQL
34
34
  next_results = []
35
35
  while results.any?
36
36
  result_value = results.shift
37
- if result_value.is_a?(Hash)
37
+ if result_value.is_a?(Runtime::GraphQLResultHash) || result_value.is_a?(Hash)
38
+ results.concat(result_value.values)
39
+ next
40
+ elsif result_value.is_a?(Runtime::GraphQLResultArray)
38
41
  results.concat(result_value.values)
39
42
  next
40
43
  elsif result_value.is_a?(Array)
@@ -46,7 +49,8 @@ module GraphQL
46
49
  # Since this field returned another lazy,
47
50
  # add it to the same queue
48
51
  results << loaded_value
49
- elsif loaded_value.is_a?(Hash) || loaded_value.is_a?(Array)
52
+ elsif loaded_value.is_a?(Runtime::GraphQLResultHash) || loaded_value.is_a?(Runtime::GraphQLResultArray) ||
53
+ loaded_value.is_a?(Hash) || loaded_value.is_a?(Array)
50
54
  # Add these values in wholesale --
51
55
  # they might be modified by later work in the dataloader.
52
56
  next_results << loaded_value
@@ -8,6 +8,128 @@ module GraphQL
8
8
  #
9
9
  # @api private
10
10
  class Runtime
11
+
12
+ module GraphQLResult
13
+ attr_accessor :graphql_dead, :graphql_parent, :graphql_result_name
14
+ # Although these are used by only one of the Result classes,
15
+ # it's handy to have the methods implemented on both (even though they just return `nil`)
16
+ # because it makes it easy to check if anything is assigned.
17
+ # @return [nil, Array<String>]
18
+ attr_accessor :graphql_non_null_field_names
19
+ # @return [nil, true]
20
+ attr_accessor :graphql_non_null_list_items
21
+
22
+ # @return [Hash] Plain-Ruby result data (`@graphql_metadata` contains Result wrapper objects)
23
+ attr_accessor :graphql_result_data
24
+ end
25
+
26
+ class GraphQLResultHash
27
+ def initialize
28
+ # Jump through some hoops to avoid creating this duplicate hash if at all possible.
29
+ @graphql_metadata = nil
30
+ @graphql_result_data = {}
31
+ end
32
+
33
+ include GraphQLResult
34
+
35
+ attr_accessor :graphql_merged_into
36
+
37
+ def []=(key, value)
38
+ # This is a hack.
39
+ # Basically, this object is merged into the root-level result at some point.
40
+ # But the problem is, some lazies are created whose closures retain reference to _this_
41
+ # object. When those lazies are resolved, they cause an update to this object.
42
+ #
43
+ # In order to return a proper top-level result, we have to update that top-level result object.
44
+ # In order to return a proper partial result (eg, for a directive), we have to update this object, too.
45
+ # Yowza.
46
+ if (t = @graphql_merged_into)
47
+ t[key] = value
48
+ end
49
+
50
+ if value.respond_to?(:graphql_result_data)
51
+ @graphql_result_data[key] = value.graphql_result_data
52
+ # If we encounter some part of this response that requires metadata tracking,
53
+ # then create the metadata hash if necessary. It will be kept up-to-date after this.
54
+ (@graphql_metadata ||= @graphql_result_data.dup)[key] = value
55
+ else
56
+ @graphql_result_data[key] = value
57
+ # keep this up-to-date if it's been initialized
58
+ @graphql_metadata && @graphql_metadata[key] = value
59
+ end
60
+
61
+ value
62
+ end
63
+
64
+ def delete(key)
65
+ @graphql_metadata && @graphql_metadata.delete(key)
66
+ @graphql_result_data.delete(key)
67
+ end
68
+
69
+ def each
70
+ (@graphql_metadata || @graphql_result_data).each { |k, v| yield(k, v) }
71
+ end
72
+
73
+ def values
74
+ (@graphql_metadata || @graphql_result_data).values
75
+ end
76
+
77
+ def key?(k)
78
+ @graphql_result_data.key?(k)
79
+ end
80
+
81
+ def [](k)
82
+ (@graphql_metadata || @graphql_result_data)[k]
83
+ end
84
+ end
85
+
86
+ class GraphQLResultArray
87
+ include GraphQLResult
88
+
89
+ def initialize
90
+ # Avoid this duplicate allocation if possible -
91
+ # but it will require some work to keep it up-to-date if it's created.
92
+ @graphql_metadata = nil
93
+ @graphql_result_data = []
94
+ end
95
+
96
+ def graphql_skip_at(index)
97
+ # Mark this index as dead. It's tricky because some indices may already be storing
98
+ # `Lazy`s. So the runtime is still holding indexes _before_ skipping,
99
+ # this object has to coordinate incoming writes to account for any already-skipped indices.
100
+ @skip_indices ||= []
101
+ @skip_indices << index
102
+ offset_by = @skip_indices.count { |skipped_idx| skipped_idx < index}
103
+ delete_at_index = index - offset_by
104
+ @graphql_metadata && @graphql_metadata.delete_at(delete_at_index)
105
+ @graphql_result_data.delete_at(delete_at_index)
106
+ end
107
+
108
+ def []=(idx, value)
109
+ if @skip_indices
110
+ offset_by = @skip_indices.count { |skipped_idx| skipped_idx < idx }
111
+ idx -= offset_by
112
+ end
113
+ if value.respond_to?(:graphql_result_data)
114
+ @graphql_result_data[idx] = value.graphql_result_data
115
+ (@graphql_metadata ||= @graphql_result_data.dup)[idx] = value
116
+ else
117
+ @graphql_result_data[idx] = value
118
+ @graphql_metadata && @graphql_metadata[idx] = value
119
+ end
120
+
121
+ value
122
+ end
123
+
124
+ def values
125
+ (@graphql_metadata || @graphql_result_data)
126
+ end
127
+ end
128
+
129
+ class GraphQLSelectionSet < Hash
130
+ attr_accessor :graphql_directives
131
+ end
132
+
11
133
  # @return [GraphQL::Query]
12
134
  attr_reader :query
13
135
 
@@ -17,30 +139,48 @@ module GraphQL
17
139
  # @return [GraphQL::Query::Context]
18
140
  attr_reader :context
19
141
 
20
- def initialize(query:, response:)
142
+ def initialize(query:)
21
143
  @query = query
22
144
  @dataloader = query.multiplex.dataloader
23
145
  @schema = query.schema
24
146
  @context = query.context
25
147
  @multiplex_context = query.multiplex.context
26
148
  @interpreter_context = @context.namespace(:interpreter)
27
- @response = response
28
- @dead_paths = {}
29
- @types_at_paths = {}
149
+ @response = GraphQLResultHash.new
150
+ # Identify runtime directives by checking which of this schema's directives have overridden `def self.resolve`
151
+ @runtime_directive_names = []
152
+ noop_resolve_owner = GraphQL::Schema::Directive.singleton_class
153
+ schema.directives.each do |name, dir_defn|
154
+ if dir_defn.method(:resolve).owner != noop_resolve_owner
155
+ @runtime_directive_names << name
156
+ end
157
+ end
30
158
  # A cache of { Class => { String => Schema::Field } }
31
159
  # Which assumes that MyObject.get_field("myField") will return the same field
32
160
  # during the lifetime of a query
33
161
  @fields_cache = Hash.new { |h, k| h[k] = {} }
162
+ # { Class => Boolean }
163
+ @lazy_cache = {}
34
164
  end
35
165
 
36
- def final_value
37
- @response.final_value
166
+ def final_result
167
+ @response && @response.graphql_result_data
38
168
  end
39
169
 
40
170
  def inspect
41
171
  "#<#{self.class.name} response=#{@response.inspect}>"
42
172
  end
43
173
 
174
+ def tap_or_each(obj_or_array)
175
+ if obj_or_array.is_a?(Array)
176
+ obj_or_array.each do |item|
177
+ yield(item, true)
178
+ end
179
+ else
180
+ yield(obj_or_array, false)
181
+ end
182
+ end
183
+
44
184
  # This _begins_ the execution. Some deferred work
45
185
  # might be stored up in lazies.
46
186
  # @return [void]
@@ -55,21 +195,42 @@ module GraphQL
55
195
 
56
196
  if object_proxy.nil?
57
197
  # Root .authorized? returned false.
58
- write_in_response(path, nil)
198
+ @response = nil
59
199
  else
60
- resolve_with_directives(object_proxy, root_operation) do # execute query level directives
200
+ resolve_with_directives(object_proxy, root_operation.directives) do # execute query level directives
61
201
  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
- }
202
+ # This is kind of a hack -- `gathered_selections` is an Array if any of the selections
203
+ # require isolation during execution (because of runtime directives). In that case,
204
+ # make a new, isolated result hash for writing the result into. (That isolated response
205
+ # is eventually merged back into the main response)
206
+ #
207
+ # Otherwise, `gathered_selections` is a hash of selections which can be
208
+ # directly evaluated and the results can be written right into the main response hash.
209
+ tap_or_each(gathered_selections) do |selections, is_selection_array|
210
+ if is_selection_array
211
+ selection_response = GraphQLResultHash.new
212
+ final_response = @response
213
+ else
214
+ selection_response = @response
215
+ final_response = nil
216
+ end
217
+
218
+ @dataloader.append_job {
219
+ set_all_interpreter_context(query.root_value, nil, nil, path)
220
+ resolve_with_directives(object_proxy, selections.graphql_directives) do
221
+ evaluate_selections(
222
+ path,
223
+ context.scoped_context,
224
+ object_proxy,
225
+ root_type,
226
+ root_op_type == "mutation",
227
+ selections,
228
+ selection_response,
229
+ final_response,
230
+ )
231
+ end
232
+ }
233
+ end
73
234
  end
74
235
  end
75
236
  delete_interpreter_context(:current_path)
@@ -79,15 +240,36 @@ module GraphQL
79
240
  nil
80
241
  end
81
242
 
82
- def gather_selections(owner_object, owner_type, selections, selections_by_name = {})
243
+ # @return [void]
244
+ def deep_merge_selection_result(from_result, into_result)
245
+ from_result.each do |key, value|
246
+ if !into_result.key?(key)
247
+ into_result[key] = value
248
+ else
249
+ case value
250
+ when GraphQLResultHash
251
+ deep_merge_selection_result(value, into_result[key])
252
+ else
253
+ # We have to assume that, since this passed the `fields_will_merge` selection,
254
+ # that the old and new values are the same.
255
+ # There's no special handling of arrays because currently, there's no way to split the execution
256
+ # of a list over several concurrent flows.
257
+ into_result[key] = value
258
+ end
259
+ end
260
+ end
261
+ from_result.graphql_merged_into = into_result
262
+ nil
263
+ end
264
+
265
+ def gather_selections(owner_object, owner_type, selections, selections_to_run = nil, selections_by_name = GraphQLSelectionSet.new)
83
266
  selections.each do |node|
84
267
  # Skip gathering this if the directive says so
85
268
  if !directives_include?(node, owner_object, owner_type)
86
269
  next
87
270
  end
88
271
 
89
- case node
90
- when GraphQL::Language::Nodes::Field
272
+ if node.is_a?(GraphQL::Language::Nodes::Field)
91
273
  response_key = node.alias || node.name
92
274
  selections = selections_by_name[response_key]
93
275
  # if there was already a selection of this field,
@@ -103,58 +285,83 @@ module GraphQL
103
285
  # No selection was found for this field yet
104
286
  selections_by_name[response_key] = node
105
287
  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|
288
+ else
289
+ # This is an InlineFragment or a FragmentSpread
290
+ if @runtime_directive_names.any? && node.directives.any? { |d| @runtime_directive_names.include?(d.name) }
291
+ next_selections = GraphQLSelectionSet.new
292
+ next_selections.graphql_directives = node.directives
293
+ if selections_to_run
294
+ selections_to_run << next_selections
295
+ else
296
+ selections_to_run = []
297
+ selections_to_run << selections_by_name
298
+ selections_to_run << next_selections
299
+ end
300
+ else
301
+ next_selections = selections_by_name
302
+ end
303
+
304
+ case node
305
+ when GraphQL::Language::Nodes::InlineFragment
306
+ if node.type
307
+ type_defn = schema.get_type(node.type.name)
308
+
309
+ # Faster than .map{}.include?()
310
+ query.warden.possible_types(type_defn).each do |t|
311
+ if t == owner_type
312
+ gather_selections(owner_object, owner_type, node.selections, selections_to_run, next_selections)
313
+ break
314
+ end
315
+ end
316
+ else
317
+ # it's an untyped fragment, definitely continue
318
+ gather_selections(owner_object, owner_type, node.selections, selections_to_run, next_selections)
319
+ end
320
+ when GraphQL::Language::Nodes::FragmentSpread
321
+ fragment_def = query.fragments[node.name]
322
+ type_defn = schema.get_type(fragment_def.type.name)
323
+ possible_types = query.warden.possible_types(type_defn)
324
+ possible_types.each do |t|
111
325
  if t == owner_type
112
- gather_selections(owner_object, owner_type, node.selections, selections_by_name)
326
+ gather_selections(owner_object, owner_type, fragment_def.selections, selections_to_run, next_selections)
113
327
  break
114
328
  end
115
329
  end
116
330
  else
117
- # it's an untyped fragment, definitely continue
118
- gather_selections(owner_object, owner_type, node.selections, selections_by_name)
331
+ raise "Invariant: unexpected selection class: #{node.class}"
119
332
  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
333
  end
133
334
  end
134
- selections_by_name
335
+ selections_to_run || selections_by_name
135
336
  end
136
337
 
137
338
  NO_ARGS = {}.freeze
138
339
 
139
340
  # @return [void]
140
- def evaluate_selections(path, scoped_context, owner_object, owner_type, is_eager_selection, gathered_selections)
341
+ def evaluate_selections(path, scoped_context, owner_object, owner_type, is_eager_selection, gathered_selections, selections_result, target_result) # rubocop:disable Metrics/ParameterLists
141
342
  set_all_interpreter_context(owner_object, nil, nil, path)
142
343
 
344
+ finished_jobs = 0
345
+ enqueued_jobs = gathered_selections.size
143
346
  gathered_selections.each do |result_name, field_ast_nodes_or_ast_node|
144
347
  @dataloader.append_job {
145
348
  evaluate_selection(
146
- path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_selection
349
+ path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_selection, selections_result
147
350
  )
351
+ finished_jobs += 1
352
+ if target_result && finished_jobs == enqueued_jobs
353
+ deep_merge_selection_result(selections_result, target_result)
354
+ end
148
355
  }
149
356
  end
150
357
 
151
- nil
358
+ selections_result
152
359
  end
153
360
 
154
361
  attr_reader :progress_path
155
362
 
156
363
  # @return [void]
157
- def evaluate_selection(path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_field)
364
+ def evaluate_selection(path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_field, selections_result) # rubocop:disable Metrics/ParameterLists
158
365
  # As a performance optimization, the hash key will be a `Node` if
159
366
  # there's only one selection of the field. But if there are multiple
160
367
  # selections of the field, it will be an Array of nodes
@@ -188,7 +395,9 @@ module GraphQL
188
395
  # This seems janky, but we need to know
189
396
  # the field's return type at this path in order
190
397
  # to propagate `null`
191
- set_type_at_path(next_path, return_type)
398
+ if return_type.non_null?
399
+ (selections_result.graphql_non_null_field_names ||= []).push(result_name)
400
+ end
192
401
  # Set this before calling `run_with_directives`, so that the directive can have the latest path
193
402
  set_all_interpreter_context(nil, field_defn, nil, next_path)
194
403
 
@@ -202,21 +411,21 @@ module GraphQL
202
411
  total_args_count = field_defn.arguments.size
203
412
  if total_args_count == 0
204
413
  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)
414
+ evaluate_selection_with_args(kwarg_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field, result_name, selections_result)
206
415
  else
207
416
  # TODO remove all arguments(...) usages?
208
417
  @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)
418
+ evaluate_selection_with_args(resolved_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field, result_name, selections_result)
210
419
  end
211
420
  end
212
421
  end
213
422
 
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
423
+ def evaluate_selection_with_args(kwarg_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field, result_name, selection_result) # rubocop:disable Metrics/ParameterLists
215
424
  context.scoped_context = scoped_context
216
425
  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|
426
+ after_lazy(kwarg_arguments, owner: owner_type, field: field_defn, path: next_path, ast_node: ast_node, scoped_context: context.scoped_context, owner_object: object, arguments: kwarg_arguments, result_name: result_name, result: selection_result) do |resolved_arguments|
218
427
  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)
428
+ continue_value(next_path, resolved_arguments, owner_type, field_defn, return_type.non_null?, ast_node, result_name, selection_result)
220
429
  next
221
430
  end
222
431
 
@@ -268,12 +477,17 @@ module GraphQL
268
477
  # Optimize for the case that field is selected only once
269
478
  if field_ast_nodes.nil? || field_ast_nodes.size == 1
270
479
  next_selections = ast_node.selections
480
+ directives = ast_node.directives
271
481
  else
272
482
  next_selections = []
273
- field_ast_nodes.each { |f| next_selections.concat(f.selections) }
483
+ directives = []
484
+ field_ast_nodes.each { |f|
485
+ next_selections.concat(f.selections)
486
+ directives.concat(f.directives)
487
+ }
274
488
  end
275
489
 
276
- field_result = resolve_with_directives(object, ast_node) do
490
+ field_result = resolve_with_directives(object, directives) do
277
491
  # Actually call the field resolver and capture the result
278
492
  app_result = begin
279
493
  query.with_error_handling do
@@ -284,10 +498,10 @@ module GraphQL
284
498
  rescue GraphQL::ExecutionError => err
285
499
  err
286
500
  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)
501
+ after_lazy(app_result, owner: owner_type, field: field_defn, path: next_path, ast_node: ast_node, scoped_context: context.scoped_context, owner_object: object, arguments: kwarg_arguments, result_name: result_name, result: selection_result) do |inner_result|
502
+ continue_value = continue_value(next_path, inner_result, owner_type, field_defn, return_type.non_null?, ast_node, result_name, selection_result)
289
503
  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)
504
+ continue_field(next_path, continue_value, owner_type, field_defn, return_type, ast_node, next_selections, false, object, kwarg_arguments, result_name, selection_result)
291
505
  end
292
506
  end
293
507
  end
@@ -304,43 +518,129 @@ module GraphQL
304
518
  end
305
519
  end
306
520
 
521
+ def dead_result?(selection_result)
522
+ selection_result.graphql_dead || ((parent = selection_result.graphql_parent) && parent.graphql_dead)
523
+ end
524
+
525
+ def set_result(selection_result, result_name, value)
526
+ if !dead_result?(selection_result)
527
+ if value.nil? &&
528
+ ( # there are two conditions under which `nil` is not allowed in the response:
529
+ (selection_result.graphql_non_null_list_items) || # this value would be written into a list that doesn't allow nils
530
+ ((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
531
+ )
532
+ # This is an invalid nil that should be propagated
533
+ # One caller of this method passes a block,
534
+ # namely when application code returns a `nil` to GraphQL and it doesn't belong there.
535
+ # The other possibility for reaching here is when a field returns an ExecutionError, so we write
536
+ # `nil` to the response, not knowing whether it's an invalid `nil` or not.
537
+ # (And in that case, we don't have to call the schema's handler, since it's not a bug in the application.)
538
+ # TODO the code is trying to tell me something.
539
+ yield if block_given?
540
+ parent = selection_result.graphql_parent
541
+ name_in_parent = selection_result.graphql_result_name
542
+ if parent.nil? # This is a top-level result hash
543
+ @response = nil
544
+ else
545
+ set_result(parent, name_in_parent, nil)
546
+ set_graphql_dead(selection_result)
547
+ end
548
+ else
549
+ selection_result[result_name] = value
550
+ end
551
+ end
552
+ end
553
+
554
+ # Mark this node and any already-registered children as dead,
555
+ # so that it accepts no more writes.
556
+ def set_graphql_dead(selection_result)
557
+ case selection_result
558
+ when GraphQLResultArray
559
+ selection_result.graphql_dead = true
560
+ selection_result.values.each { |v| set_graphql_dead(v) }
561
+ when GraphQLResultHash
562
+ selection_result.graphql_dead = true
563
+ selection_result.each { |k, v| set_graphql_dead(v) }
564
+ else
565
+ # It's a scalar, no way to mark it dead.
566
+ end
567
+ end
568
+
307
569
  HALT = Object.new
308
- def continue_value(path, value, parent_type, field, is_non_null, ast_node)
309
- if value.nil?
570
+ def continue_value(path, value, parent_type, field, is_non_null, ast_node, result_name, selection_result) # rubocop:disable Metrics/ParameterLists
571
+ case value
572
+ when nil
310
573
  if is_non_null
311
- err = parent_type::InvalidNullError.new(parent_type, field, value)
312
- write_invalid_null_in_response(path, err)
574
+ set_result(selection_result, result_name, nil) do
575
+ # This block is called if `result_name` is not dead. (Maybe a previous invalid nil caused it be marked dead.)
576
+ err = parent_type::InvalidNullError.new(parent_type, field, value)
577
+ schema.type_error(err, context)
578
+ end
313
579
  else
314
- write_in_response(path, nil)
580
+ set_result(selection_result, result_name, nil)
315
581
  end
316
582
  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] : [])
583
+ when GraphQL::Error
584
+ # Handle these cases inside a single `when`
585
+ # to avoid the overhead of checking three different classes
586
+ # every time.
587
+ if value.is_a?(GraphQL::ExecutionError)
588
+ if selection_result.nil? || !dead_result?(selection_result)
589
+ value.path ||= path
590
+ value.ast_node ||= ast_node
591
+ context.errors << value
592
+ if selection_result
593
+ set_result(selection_result, result_name, nil)
594
+ end
595
+ end
596
+ HALT
597
+ elsif value.is_a?(GraphQL::UnauthorizedError)
598
+ # this hook might raise & crash, or it might return
599
+ # a replacement value
600
+ next_value = begin
601
+ schema.unauthorized_object(value)
602
+ rescue GraphQL::ExecutionError => err
603
+ err
604
+ end
605
+ continue_value(path, next_value, parent_type, field, is_non_null, ast_node, result_name, selection_result)
606
+ elsif GraphQL::Execution::Execute::SKIP == value
607
+ # It's possible a lazy was already written here
608
+ case selection_result
609
+ when GraphQLResultHash
610
+ selection_result.delete(result_name)
611
+ when GraphQLResultArray
612
+ selection_result.graphql_skip_at(result_name)
613
+ when nil
614
+ # this can happen with directives
615
+ else
616
+ raise "Invariant: unexpected result class #{selection_result.class} (#{selection_result.inspect})"
617
+ end
618
+ HALT
619
+ else
620
+ # What could this actually _be_? Anyhow,
621
+ # preserve the default behavior of doing nothing with it.
622
+ value
326
623
  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
624
+ when Array
625
+ # It's an array full of execution errors; add them all.
626
+ if value.any? && value.all? { |v| v.is_a?(GraphQL::ExecutionError) }
627
+ if selection_result.nil? || !dead_result?(selection_result)
628
+ value.each_with_index do |error, index|
629
+ error.ast_node ||= ast_node
630
+ error.path ||= path + ((field && field.type.list?) ? [index] : [])
631
+ context.errors << error
632
+ end
633
+ if selection_result
634
+ set_result(selection_result, result_name, nil)
635
+ end
636
+ end
637
+ HALT
638
+ else
639
+ value
336
640
  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)
641
+ when GraphQL::Execution::Interpreter::RawValue
342
642
  # Write raw value directly to the response without resolving nested objects
343
- write_in_response(path, value.resolve)
643
+ set_result(selection_result, result_name, value.resolve)
344
644
  HALT
345
645
  else
346
646
  value
@@ -355,17 +655,22 @@ module GraphQL
355
655
  # Location information from `path` and `ast_node`.
356
656
  #
357
657
  # @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
658
+ 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
659
+ if current_type.non_null?
660
+ current_type = current_type.of_type
661
+ is_non_null = true
662
+ end
663
+
359
664
  case current_type.kind.name
360
665
  when "SCALAR", "ENUM"
361
666
  r = current_type.coerce_result(value, context)
362
- write_in_response(path, r)
667
+ set_result(selection_result, result_name, r)
363
668
  r
364
669
  when "UNION", "INTERFACE"
365
670
  resolved_type_or_lazy, resolved_value = resolve_type(current_type, value, path)
366
671
  resolved_value ||= value
367
672
 
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|
673
+ 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
674
  possible_types = query.possible_types(current_type)
370
675
 
371
676
  if !possible_types.include?(resolved_type)
@@ -373,10 +678,10 @@ module GraphQL
373
678
  err_class = current_type::UnresolvedTypeError
374
679
  type_error = err_class.new(resolved_value, field, parent_type, resolved_type, possible_types)
375
680
  schema.type_error(type_error, context)
376
- write_in_response(path, nil)
681
+ set_result(selection_result, result_name, nil)
377
682
  nil
378
683
  else
379
- continue_field(path, resolved_value, owner_type, field, resolved_type, ast_node, next_selections, is_non_null, owner_object, arguments)
684
+ 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
685
  end
381
686
  end
382
687
  when "OBJECT"
@@ -385,34 +690,71 @@ module GraphQL
385
690
  rescue GraphQL::ExecutionError => err
386
691
  err
387
692
  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)
693
+ 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|
694
+ continue_value = continue_value(path, inner_object, owner_type, field, is_non_null, ast_node, result_name, selection_result)
390
695
  if HALT != continue_value
391
- response_hash = {}
392
- write_in_response(path, response_hash)
696
+ response_hash = GraphQLResultHash.new
697
+ response_hash.graphql_parent = selection_result
698
+ response_hash.graphql_result_name = result_name
699
+ set_result(selection_result, result_name, response_hash)
393
700
  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
701
+ # There are two possibilities for `gathered_selections`:
702
+ # 1. All selections of this object should be evaluated together (there are no runtime directives modifying execution).
703
+ # This case is handled below, and the result can be written right into the main `response_hash` above.
704
+ # In this case, `gathered_selections` is a hash of selections.
705
+ # 2. Some selections of this object have runtime directives that may or may not modify execution.
706
+ # That part of the selection is evaluated in an isolated way, writing into a sub-response object which is
707
+ # eventually merged into the final response. In this case, `gathered_selections` is an array of things to run in isolation.
708
+ # (Technically, it's possible that one of those entries _doesn't_ require isolation.)
709
+ tap_or_each(gathered_selections) do |selections, is_selection_array|
710
+ if is_selection_array
711
+ this_result = GraphQLResultHash.new
712
+ this_result.graphql_parent = selection_result
713
+ this_result.graphql_result_name = result_name
714
+ final_result = response_hash
715
+ else
716
+ this_result = response_hash
717
+ final_result = nil
718
+ end
719
+ set_all_interpreter_context(continue_value, nil, nil, path) # reset this mutable state
720
+ resolve_with_directives(continue_value, selections.graphql_directives) do
721
+ evaluate_selections(
722
+ path,
723
+ context.scoped_context,
724
+ continue_value,
725
+ current_type,
726
+ false,
727
+ selections,
728
+ this_result,
729
+ final_result,
730
+ )
731
+ this_result
732
+ end
733
+ end
396
734
  end
397
735
  end
398
736
  when "LIST"
399
- response_list = []
400
- write_in_response(path, response_list)
401
737
  inner_type = current_type.of_type
738
+ response_list = GraphQLResultArray.new
739
+ response_list.graphql_non_null_list_items = inner_type.non_null?
740
+ response_list.graphql_parent = selection_result
741
+ response_list.graphql_result_name = result_name
742
+ set_result(selection_result, result_name, response_list)
743
+
402
744
  idx = 0
403
745
  scoped_context = context.scoped_context
404
746
  begin
405
747
  value.each do |inner_value|
406
748
  next_path = path.dup
407
749
  next_path << idx
750
+ this_idx = idx
408
751
  next_path.freeze
409
752
  idx += 1
410
- set_type_at_path(next_path, inner_type)
411
753
  # 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)
754
+ 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|
755
+ continue_value = continue_value(next_path, inner_inner_value, owner_type, field, inner_type.non_null?, ast_node, this_idx, response_list)
414
756
  if HALT != continue_value
415
- continue_field(next_path, continue_value, owner_type, field, inner_type, ast_node, next_selections, false, owner_object, arguments)
757
+ continue_field(next_path, continue_value, owner_type, field, inner_type, ast_node, next_selections, false, owner_object, arguments, this_idx, response_list)
416
758
  end
417
759
  end
418
760
  end
@@ -428,23 +770,18 @@ module GraphQL
428
770
  end
429
771
 
430
772
  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
773
  else
437
774
  raise "Invariant: Unhandled type kind #{current_type.kind} (#{current_type})"
438
775
  end
439
776
  end
440
777
 
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)
778
+ def resolve_with_directives(object, directives, &block)
779
+ return yield if directives.nil? || directives.empty?
780
+ run_directive(object, directives, 0, &block)
444
781
  end
445
782
 
446
- def run_directive(object, ast_node, idx, &block)
447
- dir_node = ast_node.directives[idx]
783
+ def run_directive(object, directives, idx, &block)
784
+ dir_node = directives[idx]
448
785
  if !dir_node
449
786
  yield
450
787
  else
@@ -452,9 +789,24 @@ module GraphQL
452
789
  if !dir_defn.is_a?(Class)
453
790
  dir_defn = dir_defn.type_class || raise("Only class-based directives are supported (not `@#{dir_node.name}`)")
454
791
  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)
792
+ raw_dir_args = arguments(nil, dir_defn, dir_node)
793
+ dir_args = continue_value(
794
+ @context[:current_path], # path
795
+ raw_dir_args, # value
796
+ dir_defn, # parent_type
797
+ nil, # field
798
+ false, # is_non_null
799
+ dir_node, # ast_node
800
+ nil, # result_name
801
+ nil, # selection_result
802
+ )
803
+
804
+ if dir_args == HALT
805
+ nil
806
+ else
807
+ dir_defn.resolve(object, dir_args, context) do
808
+ run_directive(object, directives, idx + 1, &block)
809
+ end
458
810
  end
459
811
  end
460
812
  end
@@ -463,7 +815,7 @@ module GraphQL
463
815
  def directives_include?(node, graphql_object, parent_type)
464
816
  node.directives.each do |dir_node|
465
817
  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
818
+ args = arguments(graphql_object, dir_defn, dir_node)
467
819
  if !dir_defn.include?(graphql_object, args, context)
468
820
  return false
469
821
  end
@@ -492,9 +844,8 @@ module GraphQL
492
844
  # @param eager [Boolean] Set to `true` for mutation root fields only
493
845
  # @param trace [Boolean] If `false`, don't wrap this with field tracing
494
846
  # @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)
847
+ def after_lazy(lazy_obj, owner:, field:, path:, scoped_context:, owner_object:, arguments:, ast_node:, result:, result_name:, eager: false, trace: true, &block)
848
+ if lazy?(lazy_obj)
498
849
  lazy = GraphQL::Execution::Lazy.new(path: path, field: field) do
499
850
  set_all_interpreter_context(owner_object, field, arguments, path)
500
851
  context.scoped_context = scoped_context
@@ -513,16 +864,17 @@ module GraphQL
513
864
  rescue GraphQL::ExecutionError, GraphQL::UnauthorizedError => err
514
865
  err
515
866
  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)
867
+ yield(inner_obj)
517
868
  end
518
869
 
519
870
  if eager
520
871
  lazy.value
521
872
  else
522
- write_in_response(path, lazy)
873
+ set_result(result, result_name, lazy)
523
874
  lazy
524
875
  end
525
876
  else
877
+ set_all_interpreter_context(owner_object, field, arguments, path)
526
878
  yield(lazy_obj)
527
879
  end
528
880
  end
@@ -536,85 +888,6 @@ module GraphQL
536
888
  end
537
889
  end
538
890
 
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
891
  # Set this pair in the Query context, but also in the interpeter namespace,
619
892
  # for compatibility.
620
893
  def set_interpreter_context(key, value)
@@ -633,7 +906,7 @@ module GraphQL
633
906
  query.resolve_type(type, value)
634
907
  end
635
908
 
636
- if schema.lazy?(resolved_type)
909
+ if lazy?(resolved_type)
637
910
  GraphQL::Execution::Lazy.new do
638
911
  query.trace("resolve_type_lazy", trace_payload) do
639
912
  schema.sync_lazy(resolved_type)
@@ -647,6 +920,12 @@ module GraphQL
647
920
  def authorized_new(type, value, context)
648
921
  type.authorized_new(value, context)
649
922
  end
923
+
924
+ def lazy?(object)
925
+ @lazy_cache.fetch(object.class) {
926
+ @lazy_cache[object.class] = @schema.lazy?(object)
927
+ }
928
+ end
650
929
  end
651
930
  end
652
931
  end