graphql 1.12.8 → 1.12.14

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