graphql 1.12.10 → 1.12.14

Sign up to get free protection for your applications and to get access to all the features.
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