graphql 1.12.7 → 1.12.12

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/install_generator.rb +1 -1
  3. data/lib/generators/graphql/templates/graphql_controller.erb +2 -2
  4. data/lib/graphql.rb +10 -10
  5. data/lib/graphql/backtrace/table.rb +14 -2
  6. data/lib/graphql/dataloader.rb +59 -15
  7. data/lib/graphql/dataloader/null_dataloader.rb +1 -0
  8. data/lib/graphql/execution/errors.rb +4 -4
  9. data/lib/graphql/execution/execute.rb +1 -1
  10. data/lib/graphql/execution/interpreter.rb +4 -8
  11. data/lib/graphql/execution/interpreter/arguments_cache.rb +3 -2
  12. data/lib/graphql/execution/interpreter/runtime.rb +382 -223
  13. data/lib/graphql/introspection/schema_type.rb +1 -1
  14. data/lib/graphql/pagination/connections.rb +1 -1
  15. data/lib/graphql/query/null_context.rb +7 -1
  16. data/lib/graphql/rake_task.rb +3 -0
  17. data/lib/graphql/schema.rb +44 -218
  18. data/lib/graphql/schema/addition.rb +238 -0
  19. data/lib/graphql/schema/argument.rb +55 -36
  20. data/lib/graphql/schema/directive/transform.rb +13 -1
  21. data/lib/graphql/schema/input_object.rb +2 -2
  22. data/lib/graphql/schema/loader.rb +8 -0
  23. data/lib/graphql/schema/member/base_dsl_methods.rb +3 -15
  24. data/lib/graphql/schema/object.rb +19 -5
  25. data/lib/graphql/schema/resolver.rb +46 -24
  26. data/lib/graphql/schema/scalar.rb +3 -1
  27. data/lib/graphql/static_validation/rules/directives_are_defined.rb +1 -1
  28. data/lib/graphql/static_validation/rules/fields_will_merge.rb +17 -8
  29. data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +1 -1
  30. data/lib/graphql/static_validation/validator.rb +5 -0
  31. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +4 -3
  32. data/lib/graphql/subscriptions/serialize.rb +11 -1
  33. data/lib/graphql/version.rb +1 -1
  34. metadata +17 -3
  35. 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: a0b3eaaf2e8d1d263d8de4e509bbca1413c41981ac69d8f722eba16883fc9023
4
- data.tar.gz: e9d0a083c8553cb650a2aff619a4c04bd5a01fb345dc8ae560563db09fe4d0b4
3
+ metadata.gz: 296954a3b03f2fd5dae9fae56eaa08ea04b3dfb8f25923201ea27c67147c71fa
4
+ data.tar.gz: 4de323637c29b729a3eb3f07b620e2014515b37235c485002d016ead490d86e7
5
5
  SHA512:
6
- metadata.gz: c1580219b7da1f62566e65eff2b2c11f124415dad818243260b2ebefddb4de5dad2d2e1953bc671f2080d1523c98f31e7eee581edf5b6b830761a96b970796ca
7
- data.tar.gz: 1a886d5d664bb4352b9f75f1c448acb5e2d0b8299c6f1cf6debd441df2f700869912b8a7083363aac6c41874ae18dd5bd87955a9979e44b7e5e21811c25539f3
6
+ metadata.gz: fdf50f6c6f170aa4c50d46356e4089c40a939bf3d0dfd4ab7bdb667d169f01a38320c971cdec1dde32353a20823c1756196312df5e425af41a157caa1179e845
7
+ data.tar.gz: 4202ca4741998ee1f5535eea14ff93c1a8529ff5085625d67171e75736f65f55d0433b93512c272e16f4f9a48dd630eba49cf37a54e49b0e9e3dad5f0496930c
@@ -122,7 +122,7 @@ module Graphql
122
122
  if options.api?
123
123
  say("Skipped graphiql, as this rails project is API only")
124
124
  say(" You may wish to use GraphiQL.app for development: https://github.com/skevy/graphiql-app")
125
- elsif !options[:skip_graphiql]
125
+ elsif !options[:skip_graphiql] && !File.read(Rails.root.join("Gemfile")).include?("graphiql-rails")
126
126
  gem("graphiql-rails", group: :development)
127
127
 
128
128
  # This is a little cheat just to get cleaner shell output:
@@ -15,9 +15,9 @@ class GraphqlController < ApplicationController
15
15
  }
16
16
  result = <%= schema_name %>.execute(query, variables: variables, context: context, operation_name: operation_name)
17
17
  render json: result
18
- rescue => e
18
+ rescue StandardError => e
19
19
  raise e unless Rails.env.development?
20
- handle_error_in_development e
20
+ handle_error_in_development(e)
21
21
  end
22
22
 
23
23
  private
data/lib/graphql.rb CHANGED
@@ -81,10 +81,19 @@ end
81
81
  # Order matters for these:
82
82
 
83
83
  require "graphql/execution_error"
84
+ require "graphql/runtime_type_error"
85
+ require "graphql/unresolved_type_error"
86
+ require "graphql/invalid_null_error"
87
+ require "graphql/analysis_error"
88
+ require "graphql/coercion_error"
89
+ require "graphql/invalid_name_error"
90
+ require "graphql/integer_decoding_error"
91
+ require "graphql/integer_encoding_error"
92
+ require "graphql/string_encoding_error"
93
+
84
94
  require "graphql/define"
85
95
  require "graphql/base_type"
86
96
  require "graphql/object_type"
87
-
88
97
  require "graphql/enum_type"
89
98
  require "graphql/input_object_type"
90
99
  require "graphql/interface_type"
@@ -109,9 +118,6 @@ require "graphql/analysis"
109
118
  require "graphql/tracing"
110
119
  require "graphql/dig"
111
120
  require "graphql/execution"
112
- require "graphql/runtime_type_error"
113
- require "graphql/unresolved_type_error"
114
- require "graphql/invalid_null_error"
115
121
  require "graphql/pagination"
116
122
  require "graphql/schema"
117
123
  require "graphql/query"
@@ -133,12 +139,6 @@ require "graphql/static_validation"
133
139
  require "graphql/dataloader"
134
140
  require "graphql/introspection"
135
141
 
136
- require "graphql/analysis_error"
137
- require "graphql/coercion_error"
138
- require "graphql/invalid_name_error"
139
- require "graphql/integer_decoding_error"
140
- require "graphql/integer_encoding_error"
141
- require "graphql/string_encoding_error"
142
142
  require "graphql/version"
143
143
  require "graphql/compatibility"
144
144
  require "graphql/function"
@@ -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.response
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
@@ -29,7 +29,12 @@ module GraphQL
29
29
 
30
30
  def initialize
31
31
  @source_cache = Hash.new { |h, source_class| h[source_class] = Hash.new { |h2, batch_parameters|
32
- source = source_class.new(*batch_parameters)
32
+ source = if RUBY_VERSION < "3"
33
+ source_class.new(*batch_parameters)
34
+ else
35
+ batch_args, batch_kwargs = batch_parameters
36
+ source_class.new(*batch_args, **batch_kwargs)
37
+ end
33
38
  source.setup(self)
34
39
  h2[batch_parameters] = source
35
40
  }
@@ -43,8 +48,15 @@ module GraphQL
43
48
  # @param batch_parameters [Array<Object>]
44
49
  # @return [GraphQL::Dataloader::Source] An instance of {source_class}, initialized with `self, *batch_parameters`,
45
50
  # and cached for the lifetime of this {Multiplex}.
46
- def with(source_class, *batch_parameters)
47
- @source_cache[source_class][batch_parameters]
51
+ if RUBY_VERSION < "3"
52
+ def with(source_class, *batch_parameters)
53
+ @source_cache[source_class][batch_parameters]
54
+ end
55
+ else
56
+ def with(source_class, *batch_args, **batch_kwargs)
57
+ batch_parameters = [batch_args, batch_kwargs]
58
+ @source_cache[source_class][batch_parameters]
59
+ end
48
60
  end
49
61
 
50
62
  # Tell the dataloader that this fiber is waiting for data.
@@ -65,6 +77,21 @@ module GraphQL
65
77
  nil
66
78
  end
67
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
+
68
95
  # @api private Move along, move along
69
96
  def run
70
97
  # At a high level, the algorithm is:
@@ -104,7 +131,7 @@ module GraphQL
104
131
  while @pending_jobs.any?
105
132
  # Create a Fiber to consume jobs until one of the jobs yields
106
133
  # or jobs run out
107
- f = Fiber.new {
134
+ f = spawn_fiber {
108
135
  while (job = @pending_jobs.shift)
109
136
  job.call
110
137
  end
@@ -124,26 +151,24 @@ module GraphQL
124
151
  # This is where an evented approach would be even better -- can we tell which
125
152
  # fibers are ready to continue, and continue execution there?
126
153
  #
127
- source_fiber_stack = if (first_source_fiber = create_source_fiber)
154
+ source_fiber_queue = if (first_source_fiber = create_source_fiber)
128
155
  [first_source_fiber]
129
156
  else
130
157
  nil
131
158
  end
132
159
 
133
- if source_fiber_stack
134
- # Use a stack with `.pop` here so that when a source causes another source to become pending,
135
- # that newly-pending source will run _before_ the one that depends on it.
136
- # (See below where the old fiber is pushed to the stack, then the new fiber is pushed on the stack.)
137
- while (outer_source_fiber = source_fiber_stack.pop)
160
+ if source_fiber_queue
161
+ while (outer_source_fiber = source_fiber_queue.shift)
138
162
  resume(outer_source_fiber)
139
163
 
140
- if outer_source_fiber.alive?
141
- source_fiber_stack << outer_source_fiber
142
- end
143
164
  # If this source caused more sources to become pending, run those before running this one again:
144
165
  next_source_fiber = create_source_fiber
145
166
  if next_source_fiber
146
- 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
147
172
  end
148
173
  end
149
174
  end
@@ -191,7 +216,7 @@ module GraphQL
191
216
  #
192
217
  # This design could probably be improved by maintaining a `@pending_sources` queue which is shared by the fibers,
193
218
  # similar to `@pending_jobs`. That way, when a fiber is resumed, it would never pick up work that was finished by a different fiber.
194
- source_fiber = Fiber.new do
219
+ source_fiber = spawn_fiber do
195
220
  pending_sources.each(&:run_pending_keys)
196
221
  end
197
222
  end
@@ -204,5 +229,24 @@ module GraphQL
204
229
  rescue UncaughtThrowError => e
205
230
  throw e.tag, e.value
206
231
  end
232
+
233
+ # Copies the thread local vars into the fiber thread local vars. Many
234
+ # gems (such as RequestStore, MiniRacer, etc.) rely on thread local vars
235
+ # to keep track of execution context, and without this they do not
236
+ # behave as expected.
237
+ #
238
+ # @see https://github.com/rmosolgo/graphql-ruby/issues/3449
239
+ def spawn_fiber
240
+ fiber_locals = {}
241
+
242
+ Thread.current.keys.each do |fiber_var_key|
243
+ fiber_locals[fiber_var_key] = Thread.current[fiber_var_key]
244
+ end
245
+
246
+ Fiber.new do
247
+ fiber_locals.each { |k, v| Thread.current[k] = v }
248
+ yield
249
+ end
250
+ end
207
251
  end
208
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
@@ -115,7 +115,7 @@ module GraphQL
115
115
  if obj.is_a?(GraphQL::Schema::Object)
116
116
  obj = obj.object
117
117
  end
118
- handler.call(err, obj, args, ctx, field)
118
+ handler[:handler].call(err, obj, args, ctx, field)
119
119
  else
120
120
  raise err
121
121
  end
@@ -148,11 +148,11 @@ module GraphQL
148
148
  # If there's an inherited one, but not one defined here, use the inherited one.
149
149
  # Otherwise, there's no handler for this error, return `nil`.
150
150
  if parent_handler && handler && parent_handler[:class] < handler[:class]
151
- parent_handler[:handler]
151
+ parent_handler
152
152
  elsif handler
153
- handler[:handler]
153
+ handler
154
154
  elsif parent_handler
155
- parent_handler[:handler]
155
+ parent_handler
156
156
  else
157
157
  nil
158
158
  end
@@ -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.response
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].response
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.response : 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,
@@ -8,6 +8,49 @@ module GraphQL
8
8
  #
9
9
  # @api private
10
10
  class Runtime
11
+
12
+ module GraphQLResult
13
+ # These methods are private concerns of GraphQL-Ruby,
14
+ # they aren't guaranteed to continue working in the future.
15
+ attr_accessor :graphql_dead, :graphql_parent, :graphql_result_name
16
+ # Although these are used by only one of the Result classes,
17
+ # it's handy to have the methods implemented on both (even though they just return `nil`)
18
+ # because it makes it easy to check if anything is assigned.
19
+ # @return [nil, Array<String>]
20
+ attr_accessor :graphql_non_null_field_names
21
+ # @return [nil, true]
22
+ attr_accessor :graphql_non_null_list_items
23
+ end
24
+
25
+ class GraphQLResultHash < Hash
26
+ include GraphQLResult
27
+
28
+ attr_accessor :graphql_merged_into
29
+
30
+ def []=(key, value)
31
+ # This is a hack.
32
+ # Basically, this object is merged into the root-level result at some point.
33
+ # But the problem is, some lazies are created whose closures retain reference to _this_
34
+ # object. When those lazies are resolved, they cause an update to this object.
35
+ #
36
+ # In order to return a proper top-level result, we have to update that top-level result object.
37
+ # In order to return a proper partial result (eg, for a directive), we have to update this object, too.
38
+ # Yowza.
39
+ if (t = @graphql_merged_into)
40
+ t[key] = value
41
+ end
42
+ super
43
+ end
44
+ end
45
+
46
+ class GraphQLResultArray < Array
47
+ include GraphQLResult
48
+ end
49
+
50
+ class GraphQLSelectionSet < Hash
51
+ attr_accessor :graphql_directives
52
+ end
53
+
11
54
  # @return [GraphQL::Query]
12
55
  attr_reader :query
13
56
 
@@ -17,30 +60,47 @@ module GraphQL
17
60
  # @return [GraphQL::Query::Context]
18
61
  attr_reader :context
19
62
 
20
- def initialize(query:, response:)
63
+ # @return [Hash]
64
+ attr_reader :response
65
+
66
+ def initialize(query:)
21
67
  @query = query
22
68
  @dataloader = query.multiplex.dataloader
23
69
  @schema = query.schema
24
70
  @context = query.context
25
71
  @multiplex_context = query.multiplex.context
26
72
  @interpreter_context = @context.namespace(:interpreter)
27
- @response = response
28
- @dead_paths = {}
29
- @types_at_paths = {}
73
+ @response = GraphQLResultHash.new
74
+ # Identify runtime directives by checking which of this schema's directives have overridden `def self.resolve`
75
+ @runtime_directive_names = []
76
+ noop_resolve_owner = GraphQL::Schema::Directive.singleton_class
77
+ schema.directives.each do |name, dir_defn|
78
+ if dir_defn.method(:resolve).owner != noop_resolve_owner
79
+ @runtime_directive_names << name
80
+ end
81
+ end
30
82
  # A cache of { Class => { String => Schema::Field } }
31
83
  # Which assumes that MyObject.get_field("myField") will return the same field
32
84
  # during the lifetime of a query
33
85
  @fields_cache = Hash.new { |h, k| h[k] = {} }
34
- end
35
-
36
- def final_value
37
- @response.final_value
86
+ # { Class => Boolean }
87
+ @lazy_cache = {}
38
88
  end
39
89
 
40
90
  def inspect
41
91
  "#<#{self.class.name} response=#{@response.inspect}>"
42
92
  end
43
93
 
94
+ def tap_or_each(obj_or_array)
95
+ if obj_or_array.is_a?(Array)
96
+ obj_or_array.each do |item|
97
+ yield(item, true)
98
+ end
99
+ else
100
+ yield(obj_or_array, false)
101
+ end
102
+ end
103
+
44
104
  # This _begins_ the execution. Some deferred work
45
105
  # might be stored up in lazies.
46
106
  # @return [void]
@@ -50,24 +110,48 @@ module GraphQL
50
110
  root_type = schema.root_type_for_operation(root_op_type)
51
111
  path = []
52
112
  set_all_interpreter_context(query.root_value, nil, nil, path)
53
- object_proxy = authorized_new(root_type, query.root_value, context, path)
113
+ object_proxy = authorized_new(root_type, query.root_value, context)
54
114
  object_proxy = schema.sync_lazy(object_proxy)
115
+
55
116
  if object_proxy.nil?
56
117
  # Root .authorized? returned false.
57
- write_in_response(path, nil)
118
+ @response = nil
58
119
  else
59
- gathered_selections = gather_selections(object_proxy, root_type, root_operation.selections)
60
- # Make the first fiber which will begin execution
61
- @dataloader.append_job {
62
- evaluate_selections(
63
- path,
64
- context.scoped_context,
65
- object_proxy,
66
- root_type,
67
- root_op_type == "mutation",
68
- gathered_selections,
69
- )
70
- }
120
+ resolve_with_directives(object_proxy, root_operation.directives) do # execute query level directives
121
+ gathered_selections = gather_selections(object_proxy, root_type, root_operation.selections)
122
+ # This is kind of a hack -- `gathered_selections` is an Array if any of the selections
123
+ # require isolation during execution (because of runtime directives). In that case,
124
+ # make a new, isolated result hash for writing the result into. (That isolated response
125
+ # is eventually merged back into the main response)
126
+ #
127
+ # Otherwise, `gathered_selections` is a hash of selections which can be
128
+ # directly evaluated and the results can be written right into the main response hash.
129
+ tap_or_each(gathered_selections) do |selections, is_selection_array|
130
+ if is_selection_array
131
+ selection_response = GraphQLResultHash.new
132
+ final_response = @response
133
+ else
134
+ selection_response = @response
135
+ final_response = nil
136
+ end
137
+
138
+ @dataloader.append_job {
139
+ set_all_interpreter_context(query.root_value, nil, nil, path)
140
+ resolve_with_directives(object_proxy, selections.graphql_directives) do
141
+ evaluate_selections(
142
+ path,
143
+ context.scoped_context,
144
+ object_proxy,
145
+ root_type,
146
+ root_op_type == "mutation",
147
+ selections,
148
+ selection_response,
149
+ final_response,
150
+ )
151
+ end
152
+ }
153
+ end
154
+ end
71
155
  end
72
156
  delete_interpreter_context(:current_path)
73
157
  delete_interpreter_context(:current_field)
@@ -76,15 +160,36 @@ module GraphQL
76
160
  nil
77
161
  end
78
162
 
79
- def gather_selections(owner_object, owner_type, selections, selections_by_name = {})
163
+ # @return [void]
164
+ def deep_merge_selection_result(from_result, into_result)
165
+ from_result.each do |key, value|
166
+ if !into_result.key?(key)
167
+ into_result[key] = value
168
+ else
169
+ case value
170
+ when Hash
171
+ deep_merge_selection_result(value, into_result[key])
172
+ else
173
+ # We have to assume that, since this passed the `fields_will_merge` selection,
174
+ # that the old and new values are the same.
175
+ # There's no special handling of arrays because currently, there's no way to split the execution
176
+ # of a list over several concurrent flows.
177
+ into_result[key] = value
178
+ end
179
+ end
180
+ end
181
+ from_result.graphql_merged_into = into_result
182
+ nil
183
+ end
184
+
185
+ def gather_selections(owner_object, owner_type, selections, selections_to_run = nil, selections_by_name = GraphQLSelectionSet.new)
80
186
  selections.each do |node|
81
187
  # Skip gathering this if the directive says so
82
188
  if !directives_include?(node, owner_object, owner_type)
83
189
  next
84
190
  end
85
191
 
86
- case node
87
- when GraphQL::Language::Nodes::Field
192
+ if node.is_a?(GraphQL::Language::Nodes::Field)
88
193
  response_key = node.alias || node.name
89
194
  selections = selections_by_name[response_key]
90
195
  # if there was already a selection of this field,
@@ -100,58 +205,83 @@ module GraphQL
100
205
  # No selection was found for this field yet
101
206
  selections_by_name[response_key] = node
102
207
  end
103
- when GraphQL::Language::Nodes::InlineFragment
104
- if node.type
105
- type_defn = schema.get_type(node.type.name)
106
- # Faster than .map{}.include?()
107
- query.warden.possible_types(type_defn).each do |t|
208
+ else
209
+ # This is an InlineFragment or a FragmentSpread
210
+ if @runtime_directive_names.any? && node.directives.any? { |d| @runtime_directive_names.include?(d.name) }
211
+ next_selections = GraphQLSelectionSet.new
212
+ next_selections.graphql_directives = node.directives
213
+ if selections_to_run
214
+ selections_to_run << next_selections
215
+ else
216
+ selections_to_run = []
217
+ selections_to_run << selections_by_name
218
+ selections_to_run << next_selections
219
+ end
220
+ else
221
+ next_selections = selections_by_name
222
+ end
223
+
224
+ case node
225
+ when GraphQL::Language::Nodes::InlineFragment
226
+ if node.type
227
+ type_defn = schema.get_type(node.type.name)
228
+
229
+ # Faster than .map{}.include?()
230
+ query.warden.possible_types(type_defn).each do |t|
231
+ if t == owner_type
232
+ gather_selections(owner_object, owner_type, node.selections, selections_to_run, next_selections)
233
+ break
234
+ end
235
+ end
236
+ else
237
+ # it's an untyped fragment, definitely continue
238
+ gather_selections(owner_object, owner_type, node.selections, selections_to_run, next_selections)
239
+ end
240
+ when GraphQL::Language::Nodes::FragmentSpread
241
+ fragment_def = query.fragments[node.name]
242
+ type_defn = schema.get_type(fragment_def.type.name)
243
+ possible_types = query.warden.possible_types(type_defn)
244
+ possible_types.each do |t|
108
245
  if t == owner_type
109
- gather_selections(owner_object, owner_type, node.selections, selections_by_name)
246
+ gather_selections(owner_object, owner_type, fragment_def.selections, selections_to_run, next_selections)
110
247
  break
111
248
  end
112
249
  end
113
250
  else
114
- # it's an untyped fragment, definitely continue
115
- gather_selections(owner_object, owner_type, node.selections, selections_by_name)
116
- end
117
- when GraphQL::Language::Nodes::FragmentSpread
118
- fragment_def = query.fragments[node.name]
119
- type_defn = schema.get_type(fragment_def.type.name)
120
- possible_types = query.warden.possible_types(type_defn)
121
- possible_types.each do |t|
122
- if t == owner_type
123
- gather_selections(owner_object, owner_type, fragment_def.selections, selections_by_name)
124
- break
125
- end
251
+ raise "Invariant: unexpected selection class: #{node.class}"
126
252
  end
127
- else
128
- raise "Invariant: unexpected selection class: #{node.class}"
129
253
  end
130
254
  end
131
- selections_by_name
255
+ selections_to_run || selections_by_name
132
256
  end
133
257
 
134
258
  NO_ARGS = {}.freeze
135
259
 
136
260
  # @return [void]
137
- def evaluate_selections(path, scoped_context, owner_object, owner_type, is_eager_selection, gathered_selections)
261
+ def evaluate_selections(path, scoped_context, owner_object, owner_type, is_eager_selection, gathered_selections, selections_result, target_result) # rubocop:disable Metrics/ParameterLists
138
262
  set_all_interpreter_context(owner_object, nil, nil, path)
139
263
 
264
+ finished_jobs = 0
265
+ enqueued_jobs = gathered_selections.size
140
266
  gathered_selections.each do |result_name, field_ast_nodes_or_ast_node|
141
267
  @dataloader.append_job {
142
268
  evaluate_selection(
143
- path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_selection
269
+ path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_selection, selections_result
144
270
  )
271
+ finished_jobs += 1
272
+ if target_result && finished_jobs == enqueued_jobs
273
+ deep_merge_selection_result(selections_result, target_result)
274
+ end
145
275
  }
146
276
  end
147
277
 
148
- nil
278
+ selections_result
149
279
  end
150
280
 
151
281
  attr_reader :progress_path
152
282
 
153
283
  # @return [void]
154
- def evaluate_selection(path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_field)
284
+ 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
155
285
  # As a performance optimization, the hash key will be a `Node` if
156
286
  # there's only one selection of the field. But if there are multiple
157
287
  # selections of the field, it will be an Array of nodes
@@ -185,7 +315,9 @@ module GraphQL
185
315
  # This seems janky, but we need to know
186
316
  # the field's return type at this path in order
187
317
  # to propagate `null`
188
- set_type_at_path(next_path, return_type)
318
+ if return_type.non_null?
319
+ (selections_result.graphql_non_null_field_names ||= []).push(result_name)
320
+ end
189
321
  # Set this before calling `run_with_directives`, so that the directive can have the latest path
190
322
  set_all_interpreter_context(nil, field_defn, nil, next_path)
191
323
 
@@ -193,27 +325,27 @@ module GraphQL
193
325
  object = owner_object
194
326
 
195
327
  if is_introspection
196
- object = authorized_new(field_defn.owner, object, context, next_path)
328
+ object = authorized_new(field_defn.owner, object, context)
197
329
  end
198
330
 
199
331
  total_args_count = field_defn.arguments.size
200
332
  if total_args_count == 0
201
333
  kwarg_arguments = GraphQL::Execution::Interpreter::Arguments::EMPTY
202
- evaluate_selection_with_args(kwarg_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field)
334
+ 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)
203
335
  else
204
336
  # TODO remove all arguments(...) usages?
205
337
  @query.arguments_cache.dataload_for(ast_node, field_defn, object) do |resolved_arguments|
206
- evaluate_selection_with_args(resolved_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field)
338
+ 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)
207
339
  end
208
340
  end
209
341
  end
210
342
 
211
- 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
343
+ 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
212
344
  context.scoped_context = scoped_context
213
345
  return_type = field_defn.type
214
- 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|
346
+ 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|
215
347
  if resolved_arguments.is_a?(GraphQL::ExecutionError) || resolved_arguments.is_a?(GraphQL::UnauthorizedError)
216
- continue_value(next_path, resolved_arguments, owner_type, field_defn, return_type.non_null?, ast_node)
348
+ continue_value(next_path, resolved_arguments, owner_type, field_defn, return_type.non_null?, ast_node, result_name, selection_result)
217
349
  next
218
350
  end
219
351
 
@@ -246,11 +378,17 @@ module GraphQL
246
378
  # Use this flag to tell Interpreter::Arguments to add itself
247
379
  # to the keyword args hash _before_ freezing everything.
248
380
  extra_args[:argument_details] = :__arguments_add_self
381
+ when :irep_node
382
+ # This is used by `__typename` in order to support the legacy runtime,
383
+ # but it has no use here (and it's always `nil`).
384
+ # Stop adding it here to avoid the overhead of `.merge_extras` below.
249
385
  else
250
386
  extra_args[extra] = field_defn.fetch_extra(extra, context)
251
387
  end
252
388
  end
253
- resolved_arguments = resolved_arguments.merge_extras(extra_args)
389
+ if extra_args.any?
390
+ resolved_arguments = resolved_arguments.merge_extras(extra_args)
391
+ end
254
392
  resolved_arguments.keyword_arguments
255
393
  end
256
394
 
@@ -259,12 +397,17 @@ module GraphQL
259
397
  # Optimize for the case that field is selected only once
260
398
  if field_ast_nodes.nil? || field_ast_nodes.size == 1
261
399
  next_selections = ast_node.selections
400
+ directives = ast_node.directives
262
401
  else
263
402
  next_selections = []
264
- field_ast_nodes.each { |f| next_selections.concat(f.selections) }
403
+ directives = []
404
+ field_ast_nodes.each { |f|
405
+ next_selections.concat(f.selections)
406
+ directives.concat(f.directives)
407
+ }
265
408
  end
266
409
 
267
- field_result = resolve_with_directives(object, ast_node) do
410
+ field_result = resolve_with_directives(object, directives) do
268
411
  # Actually call the field resolver and capture the result
269
412
  app_result = begin
270
413
  query.with_error_handling do
@@ -275,10 +418,10 @@ module GraphQL
275
418
  rescue GraphQL::ExecutionError => err
276
419
  err
277
420
  end
278
- 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|
279
- continue_value = continue_value(next_path, inner_result, owner_type, field_defn, return_type.non_null?, ast_node)
421
+ 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|
422
+ continue_value = continue_value(next_path, inner_result, owner_type, field_defn, return_type.non_null?, ast_node, result_name, selection_result)
280
423
  if HALT != continue_value
281
- continue_field(next_path, continue_value, owner_type, field_defn, return_type, ast_node, next_selections, false, object, kwarg_arguments)
424
+ continue_field(next_path, continue_value, owner_type, field_defn, return_type, ast_node, next_selections, false, object, kwarg_arguments, result_name, selection_result)
282
425
  end
283
426
  end
284
427
  end
@@ -295,43 +438,109 @@ module GraphQL
295
438
  end
296
439
  end
297
440
 
441
+ def dead_result?(selection_result)
442
+ r = selection_result
443
+ while r
444
+ if r.graphql_dead
445
+ return true
446
+ else
447
+ r = r.graphql_parent
448
+ end
449
+ end
450
+ false
451
+ end
452
+
453
+ def set_result(selection_result, result_name, value)
454
+ if !dead_result?(selection_result)
455
+ if value.nil? &&
456
+ ( # there are two conditions under which `nil` is not allowed in the response:
457
+ (selection_result.graphql_non_null_list_items) || # this value would be written into a list that doesn't allow nils
458
+ ((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
459
+ )
460
+ # This is an invalid nil that should be propagated
461
+ # One caller of this method passes a block,
462
+ # namely when application code returns a `nil` to GraphQL and it doesn't belong there.
463
+ # The other possibility for reaching here is when a field returns an ExecutionError, so we write
464
+ # `nil` to the response, not knowing whether it's an invalid `nil` or not.
465
+ # (And in that case, we don't have to call the schema's handler, since it's not a bug in the application.)
466
+ # TODO the code is trying to tell me something.
467
+ yield if block_given?
468
+ parent = selection_result.graphql_parent
469
+ name_in_parent = selection_result.graphql_result_name
470
+ if parent.nil? # This is a top-level result hash
471
+ @response = nil
472
+ else
473
+ set_result(parent, name_in_parent, nil)
474
+ # This is odd, but it's how it used to work. Even if `parent` _would_ accept
475
+ # a `nil`, it's marked dead. TODO: check the spec, is there a reason for this?
476
+ parent.graphql_dead = true
477
+ end
478
+ else
479
+ selection_result[result_name] = value
480
+ end
481
+ end
482
+ end
483
+
298
484
  HALT = Object.new
299
- def continue_value(path, value, parent_type, field, is_non_null, ast_node)
300
- if value.nil?
485
+ def continue_value(path, value, parent_type, field, is_non_null, ast_node, result_name, selection_result) # rubocop:disable Metrics/ParameterLists
486
+ case value
487
+ when nil
301
488
  if is_non_null
302
- err = parent_type::InvalidNullError.new(parent_type, field, value)
303
- write_invalid_null_in_response(path, err)
489
+ set_result(selection_result, result_name, nil) do
490
+ # This block is called if `result_name` is not dead. (Maybe a previous invalid nil caused it be marked dead.)
491
+ err = parent_type::InvalidNullError.new(parent_type, field, value)
492
+ schema.type_error(err, context)
493
+ end
304
494
  else
305
- write_in_response(path, nil)
495
+ set_result(selection_result, result_name, nil)
306
496
  end
307
497
  HALT
308
- elsif value.is_a?(GraphQL::ExecutionError)
309
- value.path ||= path
310
- value.ast_node ||= ast_node
311
- write_execution_errors_in_response(path, [value])
312
- HALT
313
- elsif value.is_a?(Array) && value.any? && value.all? { |v| v.is_a?(GraphQL::ExecutionError) }
314
- value.each_with_index do |error, index|
315
- error.ast_node ||= ast_node
316
- error.path ||= path + (field.type.list? ? [index] : [])
498
+ when GraphQL::Error
499
+ # Handle these cases inside a single `when`
500
+ # to avoid the overhead of checking three different classes
501
+ # every time.
502
+ if value.is_a?(GraphQL::ExecutionError)
503
+ if !dead_result?(selection_result)
504
+ value.path ||= path
505
+ value.ast_node ||= ast_node
506
+ context.errors << value
507
+ set_result(selection_result, result_name, nil)
508
+ end
509
+ HALT
510
+ elsif value.is_a?(GraphQL::UnauthorizedError)
511
+ # this hook might raise & crash, or it might return
512
+ # a replacement value
513
+ next_value = begin
514
+ schema.unauthorized_object(value)
515
+ rescue GraphQL::ExecutionError => err
516
+ err
517
+ end
518
+ continue_value(path, next_value, parent_type, field, is_non_null, ast_node, result_name, selection_result)
519
+ elsif GraphQL::Execution::Execute::SKIP == value
520
+ HALT
521
+ else
522
+ # What could this actually _be_? Anyhow,
523
+ # preserve the default behavior of doing nothing with it.
524
+ value
317
525
  end
318
- write_execution_errors_in_response(path, value)
319
- HALT
320
- elsif value.is_a?(GraphQL::UnauthorizedError)
321
- # this hook might raise & crash, or it might return
322
- # a replacement value
323
- next_value = begin
324
- schema.unauthorized_object(value)
325
- rescue GraphQL::ExecutionError => err
326
- err
526
+ when Array
527
+ # It's an array full of execution errors; add them all.
528
+ if value.any? && value.all? { |v| v.is_a?(GraphQL::ExecutionError) }
529
+ if !dead_result?(selection_result)
530
+ value.each_with_index do |error, index|
531
+ error.ast_node ||= ast_node
532
+ error.path ||= path + (field.type.list? ? [index] : [])
533
+ context.errors << error
534
+ end
535
+ set_result(selection_result, result_name, nil)
536
+ end
537
+ HALT
538
+ else
539
+ value
327
540
  end
328
-
329
- continue_value(path, next_value, parent_type, field, is_non_null, ast_node)
330
- elsif GraphQL::Execution::Execute::SKIP == value
331
- HALT
332
- elsif value.is_a?(GraphQL::Execution::Interpreter::RawValue)
541
+ when GraphQL::Execution::Interpreter::RawValue
333
542
  # Write raw value directly to the response without resolving nested objects
334
- write_in_response(path, value.resolve)
543
+ set_result(selection_result, result_name, value.resolve)
335
544
  HALT
336
545
  else
337
546
  value
@@ -346,17 +555,22 @@ module GraphQL
346
555
  # Location information from `path` and `ast_node`.
347
556
  #
348
557
  # @return [Lazy, Array, Hash, Object] Lazy, Array, and Hash are all traversed to resolve lazy values later
349
- def continue_field(path, value, owner_type, field, current_type, ast_node, next_selections, is_non_null, owner_object, arguments) # rubocop:disable Metrics/ParameterLists
558
+ 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
559
+ if current_type.non_null?
560
+ current_type = current_type.of_type
561
+ is_non_null = true
562
+ end
563
+
350
564
  case current_type.kind.name
351
565
  when "SCALAR", "ENUM"
352
566
  r = current_type.coerce_result(value, context)
353
- write_in_response(path, r)
567
+ set_result(selection_result, result_name, r)
354
568
  r
355
569
  when "UNION", "INTERFACE"
356
570
  resolved_type_or_lazy, resolved_value = resolve_type(current_type, value, path)
357
571
  resolved_value ||= value
358
572
 
359
- 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|
573
+ 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|
360
574
  possible_types = query.possible_types(current_type)
361
575
 
362
576
  if !possible_types.include?(resolved_type)
@@ -364,46 +578,83 @@ module GraphQL
364
578
  err_class = current_type::UnresolvedTypeError
365
579
  type_error = err_class.new(resolved_value, field, parent_type, resolved_type, possible_types)
366
580
  schema.type_error(type_error, context)
367
- write_in_response(path, nil)
581
+ set_result(selection_result, result_name, nil)
368
582
  nil
369
583
  else
370
- continue_field(path, resolved_value, owner_type, field, resolved_type, ast_node, next_selections, is_non_null, owner_object, arguments)
584
+ continue_field(path, resolved_value, owner_type, field, resolved_type, ast_node, next_selections, is_non_null, owner_object, arguments, result_name, selection_result)
371
585
  end
372
586
  end
373
587
  when "OBJECT"
374
588
  object_proxy = begin
375
- authorized_new(current_type, value, context, path)
589
+ authorized_new(current_type, value, context)
376
590
  rescue GraphQL::ExecutionError => err
377
591
  err
378
592
  end
379
- 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|
380
- continue_value = continue_value(path, inner_object, owner_type, field, is_non_null, ast_node)
593
+ 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|
594
+ continue_value = continue_value(path, inner_object, owner_type, field, is_non_null, ast_node, result_name, selection_result)
381
595
  if HALT != continue_value
382
- response_hash = {}
383
- write_in_response(path, response_hash)
596
+ response_hash = GraphQLResultHash.new
597
+ response_hash.graphql_parent = selection_result
598
+ response_hash.graphql_result_name = result_name
599
+ set_result(selection_result, result_name, response_hash)
384
600
  gathered_selections = gather_selections(continue_value, current_type, next_selections)
385
- evaluate_selections(path, context.scoped_context, continue_value, current_type, false, gathered_selections)
386
- response_hash
601
+ # There are two possibilities for `gathered_selections`:
602
+ # 1. All selections of this object should be evaluated together (there are no runtime directives modifying execution).
603
+ # This case is handled below, and the result can be written right into the main `response_hash` above.
604
+ # In this case, `gathered_selections` is a hash of selections.
605
+ # 2. Some selections of this object have runtime directives that may or may not modify execution.
606
+ # That part of the selection is evaluated in an isolated way, writing into a sub-response object which is
607
+ # eventually merged into the final response. In this case, `gathered_selections` is an array of things to run in isolation.
608
+ # (Technically, it's possible that one of those entries _doesn't_ require isolation.)
609
+ tap_or_each(gathered_selections) do |selections, is_selection_array|
610
+ if is_selection_array
611
+ this_result = GraphQLResultHash.new
612
+ this_result.graphql_parent = selection_result
613
+ this_result.graphql_result_name = result_name
614
+ final_result = response_hash
615
+ else
616
+ this_result = response_hash
617
+ final_result = nil
618
+ end
619
+ set_all_interpreter_context(continue_value, nil, nil, path) # reset this mutable state
620
+ resolve_with_directives(continue_value, selections.graphql_directives) do
621
+ evaluate_selections(
622
+ path,
623
+ context.scoped_context,
624
+ continue_value,
625
+ current_type,
626
+ false,
627
+ selections,
628
+ this_result,
629
+ final_result,
630
+ )
631
+ this_result
632
+ end
633
+ end
387
634
  end
388
635
  end
389
636
  when "LIST"
390
- response_list = []
391
- write_in_response(path, response_list)
392
637
  inner_type = current_type.of_type
638
+ response_list = GraphQLResultArray.new
639
+ response_list.graphql_non_null_list_items = inner_type.non_null?
640
+ response_list.graphql_parent = selection_result
641
+ response_list.graphql_result_name = result_name
642
+ set_result(selection_result, result_name, response_list)
643
+
393
644
  idx = 0
394
645
  scoped_context = context.scoped_context
395
646
  begin
396
647
  value.each do |inner_value|
397
648
  next_path = path.dup
398
649
  next_path << idx
650
+ this_idx = idx
399
651
  next_path.freeze
400
652
  idx += 1
401
- set_type_at_path(next_path, inner_type)
402
653
  # This will update `response_list` with the lazy
403
- 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|
404
- continue_value = continue_value(next_path, inner_inner_value, owner_type, field, inner_type.non_null?, ast_node)
654
+ 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|
655
+ continue_value = continue_value(next_path, inner_inner_value, owner_type, field, inner_type.non_null?, ast_node, this_idx, response_list)
405
656
  if HALT != continue_value
406
- continue_field(next_path, continue_value, owner_type, field, inner_type, ast_node, next_selections, false, owner_object, arguments)
657
+ continue_field(next_path, continue_value, owner_type, field, inner_type, ast_node, next_selections, false, owner_object, arguments, this_idx, response_list)
407
658
  end
408
659
  end
409
660
  end
@@ -419,23 +670,18 @@ module GraphQL
419
670
  end
420
671
 
421
672
  response_list
422
- when "NON_NULL"
423
- inner_type = current_type.of_type
424
- # Don't `set_type_at_path` because we want the static type,
425
- # we're going to use that to determine whether a `nil` should be propagated or not.
426
- continue_field(path, value, owner_type, field, inner_type, ast_node, next_selections, true, owner_object, arguments)
427
673
  else
428
674
  raise "Invariant: Unhandled type kind #{current_type.kind} (#{current_type})"
429
675
  end
430
676
  end
431
677
 
432
- def resolve_with_directives(object, ast_node, &block)
433
- return yield if ast_node.directives.empty?
434
- run_directive(object, ast_node, 0, &block)
678
+ def resolve_with_directives(object, directives, &block)
679
+ return yield if directives.nil? || directives.empty?
680
+ run_directive(object, directives, 0, &block)
435
681
  end
436
682
 
437
- def run_directive(object, ast_node, idx, &block)
438
- dir_node = ast_node.directives[idx]
683
+ def run_directive(object, directives, idx, &block)
684
+ dir_node = directives[idx]
439
685
  if !dir_node
440
686
  yield
441
687
  else
@@ -443,9 +689,9 @@ module GraphQL
443
689
  if !dir_defn.is_a?(Class)
444
690
  dir_defn = dir_defn.type_class || raise("Only class-based directives are supported (not `@#{dir_node.name}`)")
445
691
  end
446
- dir_args = arguments(nil, dir_defn, dir_node).keyword_arguments
692
+ dir_args = arguments(nil, dir_defn, dir_node)
447
693
  dir_defn.resolve(object, dir_args, context) do
448
- run_directive(object, ast_node, idx + 1, &block)
694
+ run_directive(object, directives, idx + 1, &block)
449
695
  end
450
696
  end
451
697
  end
@@ -454,7 +700,7 @@ module GraphQL
454
700
  def directives_include?(node, graphql_object, parent_type)
455
701
  node.directives.each do |dir_node|
456
702
  dir_defn = schema.directives.fetch(dir_node.name).type_class || raise("Only class-based directives are supported (not #{dir_node.name.inspect})")
457
- args = arguments(graphql_object, dir_defn, dir_node).keyword_arguments
703
+ args = arguments(graphql_object, dir_defn, dir_node)
458
704
  if !dir_defn.include?(graphql_object, args, context)
459
705
  return false
460
706
  end
@@ -483,9 +729,8 @@ module GraphQL
483
729
  # @param eager [Boolean] Set to `true` for mutation root fields only
484
730
  # @param trace [Boolean] If `false`, don't wrap this with field tracing
485
731
  # @return [GraphQL::Execution::Lazy, Object] If loading `object` will be deferred, it's a wrapper over it.
486
- def after_lazy(lazy_obj, owner:, field:, path:, scoped_context:, owner_object:, arguments:, ast_node:, eager: false, trace: true, &block)
487
- set_all_interpreter_context(owner_object, field, arguments, path)
488
- if schema.lazy?(lazy_obj)
732
+ def after_lazy(lazy_obj, owner:, field:, path:, scoped_context:, owner_object:, arguments:, ast_node:, result:, result_name:, eager: false, trace: true, &block)
733
+ if lazy?(lazy_obj)
489
734
  lazy = GraphQL::Execution::Lazy.new(path: path, field: field) do
490
735
  set_all_interpreter_context(owner_object, field, arguments, path)
491
736
  context.scoped_context = scoped_context
@@ -504,16 +749,17 @@ module GraphQL
504
749
  rescue GraphQL::ExecutionError, GraphQL::UnauthorizedError => err
505
750
  err
506
751
  end
507
- 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)
752
+ yield(inner_obj)
508
753
  end
509
754
 
510
755
  if eager
511
756
  lazy.value
512
757
  else
513
- write_in_response(path, lazy)
758
+ set_result(result, result_name, lazy)
514
759
  lazy
515
760
  end
516
761
  else
762
+ set_all_interpreter_context(owner_object, field, arguments, path)
517
763
  yield(lazy_obj)
518
764
  end
519
765
  end
@@ -527,85 +773,6 @@ module GraphQL
527
773
  end
528
774
  end
529
775
 
530
- def write_invalid_null_in_response(path, invalid_null_error)
531
- if !dead_path?(path)
532
- schema.type_error(invalid_null_error, context)
533
- write_in_response(path, nil)
534
- add_dead_path(path)
535
- end
536
- end
537
-
538
- def write_execution_errors_in_response(path, errors)
539
- if !dead_path?(path)
540
- errors.each do |v|
541
- context.errors << v
542
- end
543
- write_in_response(path, nil)
544
- add_dead_path(path)
545
- end
546
- end
547
-
548
- def write_in_response(path, value)
549
- if dead_path?(path)
550
- return
551
- else
552
- if value.nil? && path.any? && type_at(path).non_null?
553
- # This nil is invalid, try writing it at the previous spot
554
- propagate_path = path[0..-2]
555
- write_in_response(propagate_path, value)
556
- add_dead_path(propagate_path)
557
- else
558
- @response.write(path, value)
559
- end
560
- end
561
- end
562
-
563
- def value_at(path)
564
- i = 0
565
- value = @response.final_value
566
- while value && (part = path[i])
567
- value = value[part]
568
- i += 1
569
- end
570
- value
571
- end
572
-
573
- # To propagate nulls, we have to know what the field type was
574
- # at previous parts of the response.
575
- # This hash matches the response
576
- def type_at(path)
577
- @types_at_paths.fetch(path)
578
- end
579
-
580
- def set_type_at_path(path, type)
581
- @types_at_paths[path] = type
582
- nil
583
- end
584
-
585
- # Mark `path` as having been permanently nulled out.
586
- # No values will be added beyond that path.
587
- def add_dead_path(path)
588
- dead = @dead_paths
589
- path.each do |part|
590
- dead = dead[part] ||= {}
591
- end
592
- dead[:__dead] = true
593
- end
594
-
595
- def dead_path?(path)
596
- res = @dead_paths
597
- path.each do |part|
598
- if res
599
- if res[:__dead]
600
- break
601
- else
602
- res = res[part]
603
- end
604
- end
605
- end
606
- res && res[:__dead]
607
- end
608
-
609
776
  # Set this pair in the Query context, but also in the interpeter namespace,
610
777
  # for compatibility.
611
778
  def set_interpreter_context(key, value)
@@ -624,7 +791,7 @@ module GraphQL
624
791
  query.resolve_type(type, value)
625
792
  end
626
793
 
627
- if schema.lazy?(resolved_type)
794
+ if lazy?(resolved_type)
628
795
  GraphQL::Execution::Lazy.new do
629
796
  query.trace("resolve_type_lazy", trace_payload) do
630
797
  schema.sync_lazy(resolved_type)
@@ -635,22 +802,14 @@ module GraphQL
635
802
  end
636
803
  end
637
804
 
638
- def authorized_new(type, value, context, path)
639
- trace_payload = { context: context, type: type, object: value, path: path }
640
-
641
- auth_val = context.query.trace("authorized", trace_payload) do
642
- type.authorized_new(value, context)
643
- end
805
+ def authorized_new(type, value, context)
806
+ type.authorized_new(value, context)
807
+ end
644
808
 
645
- if context.schema.lazy?(auth_val)
646
- GraphQL::Execution::Lazy.new do
647
- context.query.trace("authorized_lazy", trace_payload) do
648
- context.schema.sync_lazy(auth_val)
649
- end
650
- end
651
- else
652
- auth_val
653
- end
809
+ def lazy?(object)
810
+ @lazy_cache.fetch(object.class) {
811
+ @lazy_cache[object.class] = @schema.lazy?(object)
812
+ }
654
813
  end
655
814
  end
656
815
  end