graphql 1.12.8 → 1.12.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of graphql might be problematic. Click here for more details.

Files changed (44) 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 +1 -1
  7. data/lib/graphql/dataloader.rb +59 -15
  8. data/lib/graphql/dataloader/null_dataloader.rb +1 -0
  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/resolve.rb +6 -2
  13. data/lib/graphql/execution/interpreter/runtime.rb +496 -222
  14. data/lib/graphql/execution/lazy.rb +5 -1
  15. data/lib/graphql/introspection/schema_type.rb +1 -1
  16. data/lib/graphql/pagination/connections.rb +1 -1
  17. data/lib/graphql/query/null_context.rb +7 -1
  18. data/lib/graphql/rake_task.rb +3 -0
  19. data/lib/graphql/schema.rb +44 -218
  20. data/lib/graphql/schema/addition.rb +238 -0
  21. data/lib/graphql/schema/argument.rb +55 -36
  22. data/lib/graphql/schema/directive/transform.rb +13 -1
  23. data/lib/graphql/schema/enum.rb +10 -1
  24. data/lib/graphql/schema/input_object.rb +13 -17
  25. data/lib/graphql/schema/loader.rb +8 -0
  26. data/lib/graphql/schema/member/base_dsl_methods.rb +3 -15
  27. data/lib/graphql/schema/object.rb +19 -5
  28. data/lib/graphql/schema/printer.rb +11 -16
  29. data/lib/graphql/schema/resolver.rb +52 -25
  30. data/lib/graphql/schema/scalar.rb +3 -1
  31. data/lib/graphql/static_validation/rules/directives_are_defined.rb +1 -1
  32. data/lib/graphql/static_validation/rules/fields_will_merge.rb +17 -8
  33. data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +1 -1
  34. data/lib/graphql/static_validation/validator.rb +5 -0
  35. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +4 -3
  36. data/lib/graphql/subscriptions/serialize.rb +8 -1
  37. data/lib/graphql/types/relay/has_node_field.rb +1 -1
  38. data/lib/graphql/types/relay/has_nodes_field.rb +1 -1
  39. data/lib/graphql/types/relay/node_field.rb +2 -2
  40. data/lib/graphql/types/relay/nodes_field.rb +2 -2
  41. data/lib/graphql/version.rb +1 -1
  42. data/readme.md +0 -3
  43. metadata +7 -21
  44. 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: 2cad71b0084305ae219dd00e6c6ed48871f126d90c38cf47b9708db1eaf1feb6
4
+ data.tar.gz: afe00c0b1f14134068015098392817c0c0f0841f8016863789d319590dc5a9ba
5
5
  SHA512:
6
- metadata.gz: a79fde55cedcd391b7dfe7c141e22c51563aeefceb0fda494dd50f4281fefb0104f8663e688395268c95361ee10e01c7bfb69250368441ec8b8c9ad0d00ac279
7
- data.tar.gz: 49a7b0f5cdf9d3a64ff05b0898c7ef93c6cdfd210baf93affa25afe3f3d7b59187fe3e7727f467af9411a085c5f6e8e281fafd27352fe97128f98bd82ffde330
6
+ metadata.gz: 8fc7bee0a1a2bdc836358338d41d56c394f495dcc78d17403c60efd47ae8f807ad447fcb96a9c270137fa247fde43b5936961436efb6d1276768f945bd5be077
7
+ data.tar.gz: a0da8f32b54df7b1254dac3749ffe1e9a9b2d93ec273313280d755b5c2bf3910880f2a13f4861d7efb0b2e39a5aefc63491918a89bcdfa223893769a0a9a1ddf
@@ -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
@@ -22,7 +22,7 @@ module GraphQL
22
22
  when "execute_field", "execute_field_lazy"
23
23
  query = metadata[:query] || raise(ArgumentError, "Add `legacy: true` to use GraphQL::Backtrace without the interpreter runtime.")
24
24
  multiplex = query.multiplex
25
- push_key = metadata[:path].reject { |i| i.is_a?(Integer) }
25
+ push_key = metadata[:path]
26
26
  parent_frame = multiplex.context[:graphql_backtrace_contexts][push_key[0..-2]]
27
27
 
28
28
  if parent_frame.is_a?(GraphQL::Query)
@@ -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