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