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.
- checksums.yaml +4 -4
- data/lib/generators/graphql/install_generator.rb +1 -1
- data/lib/generators/graphql/templates/graphql_controller.erb +2 -2
- data/lib/graphql.rb +10 -10
- data/lib/graphql/backtrace/table.rb +14 -2
- data/lib/graphql/backtrace/tracer.rb +1 -1
- data/lib/graphql/dataloader.rb +59 -15
- data/lib/graphql/dataloader/null_dataloader.rb +1 -0
- data/lib/graphql/execution/execute.rb +1 -1
- data/lib/graphql/execution/interpreter.rb +4 -8
- data/lib/graphql/execution/interpreter/arguments_cache.rb +3 -2
- data/lib/graphql/execution/interpreter/resolve.rb +6 -2
- data/lib/graphql/execution/interpreter/runtime.rb +496 -222
- data/lib/graphql/execution/lazy.rb +5 -1
- data/lib/graphql/introspection/schema_type.rb +1 -1
- data/lib/graphql/pagination/connections.rb +1 -1
- data/lib/graphql/query/null_context.rb +7 -1
- data/lib/graphql/rake_task.rb +3 -0
- data/lib/graphql/schema.rb +44 -218
- data/lib/graphql/schema/addition.rb +238 -0
- data/lib/graphql/schema/argument.rb +55 -36
- data/lib/graphql/schema/directive/transform.rb +13 -1
- data/lib/graphql/schema/enum.rb +10 -1
- data/lib/graphql/schema/input_object.rb +13 -17
- data/lib/graphql/schema/loader.rb +8 -0
- data/lib/graphql/schema/member/base_dsl_methods.rb +3 -15
- data/lib/graphql/schema/object.rb +19 -5
- data/lib/graphql/schema/printer.rb +11 -16
- data/lib/graphql/schema/resolver.rb +52 -25
- data/lib/graphql/schema/scalar.rb +3 -1
- data/lib/graphql/static_validation/rules/directives_are_defined.rb +1 -1
- data/lib/graphql/static_validation/rules/fields_will_merge.rb +17 -8
- data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +1 -1
- data/lib/graphql/static_validation/validator.rb +5 -0
- data/lib/graphql/subscriptions/action_cable_subscriptions.rb +4 -3
- data/lib/graphql/subscriptions/serialize.rb +8 -1
- data/lib/graphql/types/relay/has_node_field.rb +1 -1
- data/lib/graphql/types/relay/has_nodes_field.rb +1 -1
- data/lib/graphql/types/relay/node_field.rb +2 -2
- data/lib/graphql/types/relay/nodes_field.rb +2 -2
- data/lib/graphql/version.rb +1 -1
- data/readme.md +0 -3
- metadata +7 -21
- 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2cad71b0084305ae219dd00e6c6ed48871f126d90c38cf47b9708db1eaf1feb6
|
4
|
+
data.tar.gz: afe00c0b1f14134068015098392817c0c0f0841f8016863789d319590dc5a9ba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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]
|
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]
|
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]
|
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)
|
data/lib/graphql/dataloader.rb
CHANGED
@@ -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 =
|
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
|
-
|
47
|
-
|
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 =
|
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
|
-
|
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
|
134
|
-
|
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
|
-
|
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 =
|
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
|
@@ -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.
|
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].
|
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.
|
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.
|
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?(
|
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
|
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 =
|
28
|
-
|
29
|
-
@
|
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
|
37
|
-
@response.
|
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
|
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
|
-
|
198
|
+
@response = nil
|
58
199
|
else
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
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,
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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,
|
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
|
-
|
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
|
-
|
303
|
-
|
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
|
-
|
580
|
+
set_result(selection_result, result_name, nil)
|
306
581
|
end
|
307
582
|
HALT
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
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
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
386
|
-
|
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,
|
433
|
-
return yield if
|
434
|
-
run_directive(object,
|
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,
|
438
|
-
dir_node =
|
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
|
-
|
447
|
-
|
448
|
-
|
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)
|
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
|
-
|
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
|
-
|
867
|
+
yield(inner_obj)
|
508
868
|
end
|
509
869
|
|
510
870
|
if eager
|
511
871
|
lazy.value
|
512
872
|
else
|
513
|
-
|
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
|
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
|
639
|
-
|
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
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
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
|