graphql 1.12.7 → 1.12.12

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