graphql-stitching 0.0.1 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +8 -2
- data/.gitignore +2 -0
- data/README.md +15 -16
- data/docs/README.md +1 -1
- data/docs/composer.md +45 -4
- data/docs/executor.md +17 -6
- data/docs/images/library.png +0 -0
- data/docs/planner.md +7 -7
- data/docs/request.md +47 -0
- data/docs/supergraph.md +1 -1
- data/graphql-stitching.gemspec +2 -2
- data/lib/graphql/stitching/composer.rb +89 -7
- data/lib/graphql/stitching/executor.rb +49 -28
- data/lib/graphql/stitching/gateway.rb +18 -13
- data/lib/graphql/stitching/planner.rb +17 -11
- data/lib/graphql/stitching/remote_client.rb +4 -4
- data/lib/graphql/stitching/request.rb +133 -0
- data/lib/graphql/stitching/shaper.rb +60 -56
- data/lib/graphql/stitching/supergraph.rb +7 -6
- data/lib/graphql/stitching/util.rb +9 -1
- data/lib/graphql/stitching/version.rb +1 -1
- data/lib/graphql/stitching.rb +5 -1
- metadata +6 -9
- data/.ruby-version +0 -1
- data/Gemfile.lock +0 -49
- data/docs/document.md +0 -15
- data/docs/shaper.md +0 -20
- data/lib/graphql/stitching/document.rb +0 -59
@@ -7,16 +7,17 @@ module GraphQL
|
|
7
7
|
class Executor
|
8
8
|
|
9
9
|
class RootSource < GraphQL::Dataloader::Source
|
10
|
-
def initialize(executor)
|
10
|
+
def initialize(executor, location)
|
11
11
|
@executor = executor
|
12
|
+
@location = location
|
12
13
|
end
|
13
14
|
|
14
15
|
def fetch(ops)
|
15
16
|
op = ops.first # There should only ever be one per location at a time
|
16
17
|
|
17
18
|
query_document = build_query(op)
|
18
|
-
query_variables = @executor.variables.slice(*op["variables"].keys)
|
19
|
-
result = @executor.supergraph.execute_at_location(op["location"], query_document, query_variables)
|
19
|
+
query_variables = @executor.request.variables.slice(*op["variables"].keys)
|
20
|
+
result = @executor.supergraph.execute_at_location(op["location"], query_document, query_variables, @executor.request.context)
|
20
21
|
@executor.query_count += 1
|
21
22
|
|
22
23
|
@executor.data.merge!(result["data"]) if result["data"]
|
@@ -24,7 +25,8 @@ module GraphQL
|
|
24
25
|
result["errors"].each { _1.delete("locations") }
|
25
26
|
@executor.errors.concat(result["errors"])
|
26
27
|
end
|
27
|
-
|
28
|
+
|
29
|
+
ops.map { op["key"] }
|
28
30
|
end
|
29
31
|
|
30
32
|
def build_query(op)
|
@@ -61,8 +63,8 @@ module GraphQL
|
|
61
63
|
|
62
64
|
if origin_sets_by_operation.any?
|
63
65
|
query_document, variable_names = build_query(origin_sets_by_operation)
|
64
|
-
variables = @executor.variables.slice(*variable_names)
|
65
|
-
raw_result = @executor.supergraph.execute_at_location(@location, query_document, variables)
|
66
|
+
variables = @executor.request.variables.slice(*variable_names)
|
67
|
+
raw_result = @executor.supergraph.execute_at_location(@location, query_document, variables, @executor.request.context)
|
66
68
|
@executor.query_count += 1
|
67
69
|
|
68
70
|
merge_results!(origin_sets_by_operation, raw_result.dig("data"))
|
@@ -165,23 +167,26 @@ module GraphQL
|
|
165
167
|
|
166
168
|
private
|
167
169
|
|
168
|
-
#
|
170
|
+
# traverse forward through origin data, expanding arrays to follow all paths
|
169
171
|
# any errors found for an origin object_id have their path prefixed by the object path
|
170
172
|
def repath_errors!(pathed_errors_by_object_id, forward_path, current_path=[], root=@executor.data)
|
171
|
-
current_path
|
172
|
-
forward_path = forward_path[1..-1]
|
173
|
+
current_path.push(forward_path.shift)
|
173
174
|
scope = root[current_path.last]
|
174
175
|
|
175
176
|
if forward_path.any? && scope.is_a?(Array)
|
176
177
|
scope.each_with_index do |element, index|
|
177
178
|
inner_elements = element.is_a?(Array) ? element.flatten : [element]
|
178
179
|
inner_elements.each do |inner_element|
|
179
|
-
|
180
|
+
current_path << index
|
181
|
+
repath_errors!(pathed_errors_by_object_id, forward_path, current_path, inner_element)
|
182
|
+
current_path.pop
|
180
183
|
end
|
181
184
|
end
|
182
185
|
|
183
186
|
elsif forward_path.any?
|
184
|
-
|
187
|
+
current_path << index
|
188
|
+
repath_errors!(pathed_errors_by_object_id, forward_path, current_path, scope)
|
189
|
+
current_path.pop
|
185
190
|
|
186
191
|
elsif scope.is_a?(Array)
|
187
192
|
scope.each_with_index do |element, index|
|
@@ -196,56 +201,72 @@ module GraphQL
|
|
196
201
|
errors = pathed_errors_by_object_id[scope.object_id]
|
197
202
|
errors.each { _1["path"] = [*current_path, *_1["path"]] } if errors
|
198
203
|
end
|
204
|
+
|
205
|
+
forward_path.unshift(current_path.pop)
|
199
206
|
end
|
200
207
|
end
|
201
208
|
|
202
|
-
attr_reader :supergraph, :
|
209
|
+
attr_reader :supergraph, :request, :data, :errors
|
203
210
|
attr_accessor :query_count
|
204
211
|
|
205
|
-
def initialize(supergraph:, plan:,
|
212
|
+
def initialize(supergraph:, request:, plan:, nonblocking: false)
|
206
213
|
@supergraph = supergraph
|
207
|
-
@
|
214
|
+
@request = request
|
208
215
|
@queue = plan["ops"]
|
209
216
|
@data = {}
|
210
217
|
@errors = []
|
211
218
|
@query_count = 0
|
219
|
+
@exec_cycles = 0
|
212
220
|
@dataloader = GraphQL::Dataloader.new(nonblocking: nonblocking)
|
213
221
|
end
|
214
222
|
|
215
|
-
def perform(
|
223
|
+
def perform(raw: false)
|
216
224
|
exec!
|
217
|
-
|
218
225
|
result = {}
|
219
|
-
result["data"] = @data if @data && @data.length > 0
|
220
|
-
result["errors"] = @errors if @errors.length > 0
|
221
226
|
|
222
|
-
|
227
|
+
if @data && @data.length > 0
|
228
|
+
result["data"] = raw ? @data : GraphQL::Stitching::Shaper.new(
|
229
|
+
schema: @supergraph.schema,
|
230
|
+
request: @request,
|
231
|
+
).perform!(@data)
|
232
|
+
end
|
223
233
|
|
224
|
-
|
234
|
+
if @errors.length > 0
|
235
|
+
result["errors"] = @errors
|
236
|
+
end
|
237
|
+
|
238
|
+
result
|
225
239
|
end
|
226
240
|
|
227
241
|
private
|
228
242
|
|
229
243
|
def exec!(after_keys = [0])
|
244
|
+
if @exec_cycles > @queue.length
|
245
|
+
# sanity check... if we've exceeded queue size, then something went wrong.
|
246
|
+
raise StitchingError, "Too many execution requests attempted."
|
247
|
+
end
|
248
|
+
|
230
249
|
@dataloader.append_job do
|
231
|
-
|
250
|
+
tasks = @queue
|
232
251
|
.select { after_keys.include?(_1["after_key"]) }
|
233
|
-
.group_by { _1["location"] }
|
234
|
-
.map do |location, ops|
|
235
|
-
if
|
236
|
-
@dataloader.with(RootSource, self).request_all(ops)
|
252
|
+
.group_by { [_1["location"], _1["boundary"].nil?] }
|
253
|
+
.map do |(location, root_source), ops|
|
254
|
+
if root_source
|
255
|
+
@dataloader.with(RootSource, self, location).request_all(ops)
|
237
256
|
else
|
238
257
|
@dataloader.with(BoundarySource, self, location).request_all(ops)
|
239
258
|
end
|
240
259
|
end
|
241
260
|
|
242
|
-
|
261
|
+
tasks.each(&method(:exec_task))
|
243
262
|
end
|
263
|
+
|
264
|
+
@exec_cycles += 1
|
244
265
|
@dataloader.run
|
245
266
|
end
|
246
267
|
|
247
|
-
def
|
248
|
-
next_keys =
|
268
|
+
def exec_task(task)
|
269
|
+
next_keys = task.load
|
249
270
|
next_keys.compact!
|
250
271
|
exec!(next_keys) if next_keys.any?
|
251
272
|
end
|
@@ -7,8 +7,6 @@ module GraphQL
|
|
7
7
|
class Gateway
|
8
8
|
class GatewayError < StitchingError; end
|
9
9
|
|
10
|
-
EMPTY_CONTEXT = {}.freeze
|
11
|
-
|
12
10
|
attr_reader :supergraph
|
13
11
|
|
14
12
|
def initialize(locations: nil, supergraph: nil)
|
@@ -25,29 +23,36 @@ module GraphQL
|
|
25
23
|
end
|
26
24
|
end
|
27
25
|
|
28
|
-
def execute(query:, variables: nil, operation_name: nil, context:
|
29
|
-
|
26
|
+
def execute(query:, variables: nil, operation_name: nil, context: nil, validate: true)
|
27
|
+
request = GraphQL::Stitching::Request.new(
|
28
|
+
query,
|
29
|
+
operation_name: operation_name,
|
30
|
+
variables: variables,
|
31
|
+
context: context,
|
32
|
+
)
|
30
33
|
|
31
34
|
if validate
|
32
|
-
validation_errors = @supergraph.schema.validate(document
|
35
|
+
validation_errors = @supergraph.schema.validate(request.document)
|
33
36
|
return error_result(validation_errors) if validation_errors.any?
|
34
37
|
end
|
35
38
|
|
39
|
+
request.prepare!
|
40
|
+
|
36
41
|
begin
|
37
|
-
plan = fetch_plan(
|
42
|
+
plan = fetch_plan(request) do
|
38
43
|
GraphQL::Stitching::Planner.new(
|
39
44
|
supergraph: @supergraph,
|
40
|
-
|
45
|
+
request: request,
|
41
46
|
).perform.to_h
|
42
47
|
end
|
43
48
|
|
44
49
|
GraphQL::Stitching::Executor.new(
|
45
50
|
supergraph: @supergraph,
|
51
|
+
request: request,
|
46
52
|
plan: plan,
|
47
|
-
|
48
|
-
).perform(document)
|
53
|
+
).perform
|
49
54
|
rescue StandardError => e
|
50
|
-
custom_message = @on_error.call(e, context) if @on_error
|
55
|
+
custom_message = @on_error.call(e, request.context) if @on_error
|
51
56
|
error_result([{ "message" => custom_message || "An unexpected error occured." }])
|
52
57
|
end
|
53
58
|
end
|
@@ -91,16 +96,16 @@ module GraphQL
|
|
91
96
|
supergraph
|
92
97
|
end
|
93
98
|
|
94
|
-
def fetch_plan(
|
99
|
+
def fetch_plan(request)
|
95
100
|
if @on_cache_read
|
96
|
-
cached_plan = @on_cache_read.call(
|
101
|
+
cached_plan = @on_cache_read.call(request.digest, request.context)
|
97
102
|
return JSON.parse(cached_plan) if cached_plan
|
98
103
|
end
|
99
104
|
|
100
105
|
plan_json = yield
|
101
106
|
|
102
107
|
if @on_cache_write
|
103
|
-
@on_cache_write.call(
|
108
|
+
@on_cache_write.call(request.digest, JSON.generate(plan_json), request.context)
|
104
109
|
end
|
105
110
|
|
106
111
|
plan_json
|
@@ -6,9 +6,9 @@ module GraphQL
|
|
6
6
|
SUPERGRAPH_LOCATIONS = [Supergraph::LOCATION].freeze
|
7
7
|
TYPENAME_NODE = GraphQL::Language::Nodes::Field.new(alias: "_STITCH_typename", name: "__typename")
|
8
8
|
|
9
|
-
def initialize(supergraph:,
|
9
|
+
def initialize(supergraph:, request:)
|
10
10
|
@supergraph = supergraph
|
11
|
-
@
|
11
|
+
@request = request
|
12
12
|
@sequence_key = 0
|
13
13
|
@operations_by_grouping = {}
|
14
14
|
end
|
@@ -37,7 +37,11 @@ module GraphQL
|
|
37
37
|
extract_locale_selections(location, parent_type, selections, insertion_path, parent_key)
|
38
38
|
end
|
39
39
|
|
40
|
-
grouping =
|
40
|
+
grouping = String.new
|
41
|
+
grouping << after_key.to_s << "/" << location << "/" << parent_type.graphql_name
|
42
|
+
grouping = insertion_path.reduce(grouping) do |memo, segment|
|
43
|
+
memo << "/" << segment
|
44
|
+
end
|
41
45
|
|
42
46
|
if op = @operations_by_grouping[grouping]
|
43
47
|
op.selections += selection_set if selection_set
|
@@ -62,12 +66,12 @@ module GraphQL
|
|
62
66
|
end
|
63
67
|
|
64
68
|
def build_root_operations
|
65
|
-
case @
|
69
|
+
case @request.operation.operation_type
|
66
70
|
when "query"
|
67
71
|
# plan steps grouping all fields by location for async execution
|
68
72
|
parent_type = @supergraph.schema.query
|
69
73
|
|
70
|
-
selections_by_location = @
|
74
|
+
selections_by_location = @request.operation.selections.each_with_object({}) do |node, memo|
|
71
75
|
locations = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name] || SUPERGRAPH_LOCATIONS
|
72
76
|
memo[locations.last] ||= []
|
73
77
|
memo[locations.last] << node
|
@@ -82,7 +86,7 @@ module GraphQL
|
|
82
86
|
parent_type = @supergraph.schema.mutation
|
83
87
|
location_groups = []
|
84
88
|
|
85
|
-
@
|
89
|
+
@request.operation.selections.reduce(nil) do |last_location, node|
|
86
90
|
location = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name].last
|
87
91
|
if location != last_location
|
88
92
|
location_groups << {
|
@@ -161,8 +165,10 @@ module GraphQL
|
|
161
165
|
if Util.is_leaf_type?(field_type)
|
162
166
|
selections_result << node
|
163
167
|
else
|
164
|
-
|
165
|
-
selection_set, variables = extract_locale_selections(current_location, field_type, node.selections,
|
168
|
+
insertion_path.push(node.alias || node.name)
|
169
|
+
selection_set, variables = extract_locale_selections(current_location, field_type, node.selections, insertion_path, after_key)
|
170
|
+
insertion_path.pop
|
171
|
+
|
166
172
|
selections_result << node.merge(selections: selection_set)
|
167
173
|
variables_result.merge!(variables)
|
168
174
|
end
|
@@ -177,7 +183,7 @@ module GraphQL
|
|
177
183
|
implements_fragments = true
|
178
184
|
|
179
185
|
when GraphQL::Language::Nodes::FragmentSpread
|
180
|
-
fragment = @
|
186
|
+
fragment = @request.fragment_definitions[node.name]
|
181
187
|
next unless @supergraph.locations_by_type[fragment.type.name].include?(current_location)
|
182
188
|
|
183
189
|
fragment_type = @supergraph.schema.types[fragment.type.name]
|
@@ -260,7 +266,7 @@ module GraphQL
|
|
260
266
|
location: location,
|
261
267
|
selections: selections_by_location[location],
|
262
268
|
parent_type: parent_type,
|
263
|
-
insertion_path: insertion_path,
|
269
|
+
insertion_path: insertion_path.dup,
|
264
270
|
boundary: boundary,
|
265
271
|
after_key: after_key,
|
266
272
|
)
|
@@ -289,7 +295,7 @@ module GraphQL
|
|
289
295
|
when GraphQL::Language::Nodes::InputObject
|
290
296
|
extract_node_variables!(argument.value, memo)
|
291
297
|
when GraphQL::Language::Nodes::VariableIdentifier
|
292
|
-
memo[argument.value.name] ||= @
|
298
|
+
memo[argument.value.name] ||= @request.variable_definitions[argument.value.name]
|
293
299
|
end
|
294
300
|
end
|
295
301
|
end
|
@@ -9,14 +9,14 @@ module GraphQL
|
|
9
9
|
class RemoteClient
|
10
10
|
def initialize(url:, headers:{})
|
11
11
|
@url = url
|
12
|
-
@headers = headers
|
12
|
+
@headers = { "Content-Type" => "application/json" }.merge!(headers)
|
13
13
|
end
|
14
14
|
|
15
|
-
def call(
|
15
|
+
def call(_location, document, variables, _context)
|
16
16
|
response = Net::HTTP.post(
|
17
17
|
URI(@url),
|
18
|
-
{ "query" => document, "variables" => variables }
|
19
|
-
|
18
|
+
JSON.generate({ "query" => document, "variables" => variables }),
|
19
|
+
@headers,
|
20
20
|
)
|
21
21
|
JSON.parse(response.body)
|
22
22
|
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Stitching
|
5
|
+
class Request
|
6
|
+
SUPPORTED_OPERATIONS = ["query", "mutation"].freeze
|
7
|
+
EMPTY_CONTEXT = {}.freeze
|
8
|
+
|
9
|
+
class ApplyRuntimeDirectives < GraphQL::Language::Visitor
|
10
|
+
def initialize(document, variables)
|
11
|
+
@changed = false
|
12
|
+
@variables = variables
|
13
|
+
super(document)
|
14
|
+
end
|
15
|
+
|
16
|
+
def changed?
|
17
|
+
@changed
|
18
|
+
end
|
19
|
+
|
20
|
+
def on_field(node, parent)
|
21
|
+
delete_node = false
|
22
|
+
filtered_directives = if node.directives.any?
|
23
|
+
node.directives.select do |directive|
|
24
|
+
if directive.name == "skip"
|
25
|
+
delete_node = assess_argument_value(directive.arguments.first)
|
26
|
+
false
|
27
|
+
elsif directive.name == "include"
|
28
|
+
delete_node = !assess_argument_value(directive.arguments.first)
|
29
|
+
false
|
30
|
+
else
|
31
|
+
true
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
if delete_node
|
37
|
+
@changed = true
|
38
|
+
super(DELETE_NODE, parent)
|
39
|
+
elsif filtered_directives && filtered_directives.length != node.directives.length
|
40
|
+
@changed = true
|
41
|
+
super(node.merge(directives: filtered_directives), parent)
|
42
|
+
else
|
43
|
+
super
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def assess_argument_value(arg)
|
50
|
+
if arg.value.is_a?(GraphQL::Language::Nodes::VariableIdentifier)
|
51
|
+
return @variables[arg.value.name]
|
52
|
+
end
|
53
|
+
arg.value
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
attr_reader :document, :variables, :operation_name, :context
|
58
|
+
|
59
|
+
def initialize(document, operation_name: nil, variables: nil, context: nil)
|
60
|
+
@may_contain_runtime_directives = true
|
61
|
+
|
62
|
+
@document = if document.is_a?(String)
|
63
|
+
@may_contain_runtime_directives = document.include?("@")
|
64
|
+
GraphQL.parse(document)
|
65
|
+
else
|
66
|
+
document
|
67
|
+
end
|
68
|
+
|
69
|
+
@operation_name = operation_name
|
70
|
+
@variables = variables || {}
|
71
|
+
@context = context || EMPTY_CONTEXT
|
72
|
+
end
|
73
|
+
|
74
|
+
def string
|
75
|
+
@string ||= @document.to_query_string
|
76
|
+
end
|
77
|
+
|
78
|
+
def digest
|
79
|
+
@digest ||= Digest::SHA2.hexdigest(string)
|
80
|
+
end
|
81
|
+
|
82
|
+
def operation
|
83
|
+
@operation ||= begin
|
84
|
+
operation_defs = @document.definitions.select do |d|
|
85
|
+
next unless d.is_a?(GraphQL::Language::Nodes::OperationDefinition)
|
86
|
+
next unless SUPPORTED_OPERATIONS.include?(d.operation_type)
|
87
|
+
@operation_name ? d.name == @operation_name : true
|
88
|
+
end
|
89
|
+
|
90
|
+
if operation_defs.length < 1
|
91
|
+
raise GraphQL::ExecutionError, "Invalid root operation."
|
92
|
+
elsif operation_defs.length > 1
|
93
|
+
raise GraphQL::ExecutionError, "An operation name is required when sending multiple operations."
|
94
|
+
end
|
95
|
+
|
96
|
+
operation_defs.first
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def variable_definitions
|
101
|
+
@variable_definitions ||= operation.variables.each_with_object({}) do |v, memo|
|
102
|
+
memo[v.name] = v.type
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def fragment_definitions
|
107
|
+
@fragment_definitions ||= @document.definitions.each_with_object({}) do |d, memo|
|
108
|
+
memo[d.name] = d if d.is_a?(GraphQL::Language::Nodes::FragmentDefinition)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def prepare!
|
113
|
+
operation.variables.each do |v|
|
114
|
+
@variables[v.name] ||= v.default_value
|
115
|
+
end
|
116
|
+
|
117
|
+
return self unless @may_contain_runtime_directives
|
118
|
+
|
119
|
+
visitor = ApplyRuntimeDirectives.new(@document, @variables)
|
120
|
+
@document = visitor.visit
|
121
|
+
|
122
|
+
if visitor.changed?
|
123
|
+
@string = nil
|
124
|
+
@digest = nil
|
125
|
+
@operation = nil
|
126
|
+
@variable_definitions = nil
|
127
|
+
@fragment_definitions = nil
|
128
|
+
end
|
129
|
+
self
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -4,88 +4,92 @@
|
|
4
4
|
module GraphQL
|
5
5
|
module Stitching
|
6
6
|
class Shaper
|
7
|
-
def initialize(
|
8
|
-
@
|
9
|
-
@
|
10
|
-
@result = raw
|
11
|
-
@errors = []
|
7
|
+
def initialize(schema:, request:)
|
8
|
+
@schema = schema
|
9
|
+
@request = request
|
12
10
|
end
|
13
11
|
|
14
|
-
def perform!
|
15
|
-
|
16
|
-
|
17
|
-
# hate doing a second pass, but cannot remove _STITCH_ fields until the fragements are processed
|
18
|
-
clean_entry(@result["data"])
|
19
|
-
end
|
20
|
-
|
21
|
-
if @errors.length > 0
|
22
|
-
@result = [] unless @result.key?("errors")
|
23
|
-
@result["errors"] += @errors
|
24
|
-
end
|
25
|
-
|
26
|
-
@result
|
12
|
+
def perform!(raw)
|
13
|
+
root_type = @schema.public_send(@request.operation.operation_type)
|
14
|
+
resolve_object_scope(raw, root_type, @request.operation.selections)
|
27
15
|
end
|
28
16
|
|
29
17
|
private
|
30
18
|
|
31
|
-
def
|
19
|
+
def resolve_object_scope(raw_object, parent_type, selections, typename = nil)
|
20
|
+
return nil if raw_object.nil?
|
21
|
+
|
22
|
+
typename ||= raw_object["_STITCH_typename"]
|
23
|
+
raw_object.reject! { |k, _v| k.start_with?("_STITCH_") }
|
24
|
+
|
32
25
|
selections.each do |node|
|
33
26
|
case node
|
34
27
|
when GraphQL::Language::Nodes::Field
|
35
|
-
next if node.
|
28
|
+
next if node.name.start_with?("__")
|
36
29
|
|
37
|
-
|
30
|
+
field_name = node.alias || node.name
|
31
|
+
node_type = parent_type.fields[node.name].type
|
32
|
+
named_type = Util.get_named_type_for_field_node(@schema, parent_type, node)
|
33
|
+
|
34
|
+
raw_object[field_name] = if node_type.list?
|
35
|
+
resolve_list_scope(raw_object[field_name], Util.unwrap_non_null(node_type), node.selections)
|
36
|
+
elsif Util.is_leaf_type?(named_type)
|
37
|
+
raw_object[field_name]
|
38
|
+
else
|
39
|
+
resolve_object_scope(raw_object[field_name], named_type, node.selections)
|
40
|
+
end
|
41
|
+
|
42
|
+
return nil if raw_object[field_name].nil? && node_type.non_null?
|
38
43
|
|
39
44
|
when GraphQL::Language::Nodes::InlineFragment
|
40
|
-
next unless
|
41
|
-
fragment_type = @
|
42
|
-
|
45
|
+
next unless typename == node.type.name
|
46
|
+
fragment_type = @schema.types[node.type.name]
|
47
|
+
result = resolve_object_scope(raw_object, fragment_type, node.selections, typename)
|
48
|
+
return nil if result.nil?
|
43
49
|
|
44
50
|
when GraphQL::Language::Nodes::FragmentSpread
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
51
|
+
fragment = @request.fragment_definitions[node.name]
|
52
|
+
fragment_type = @schema.types[fragment.type.name]
|
53
|
+
next unless typename == fragment_type.graphql_name
|
54
|
+
|
55
|
+
result = resolve_object_scope(raw_object, fragment_type, fragment.selections, typename)
|
56
|
+
return nil if result.nil?
|
57
|
+
|
49
58
|
else
|
50
59
|
raise "Unexpected node of type #{node.class.name} in selection set."
|
51
60
|
end
|
52
61
|
end
|
62
|
+
|
63
|
+
raw_object
|
53
64
|
end
|
54
65
|
|
55
|
-
def
|
56
|
-
|
57
|
-
node_type = Util.get_named_type_for_field_node(@supergraph.schema, parent_type, node)
|
66
|
+
def resolve_list_scope(raw_list, current_node_type, selections)
|
67
|
+
return nil if raw_list.nil?
|
58
68
|
|
59
|
-
|
60
|
-
|
61
|
-
|
69
|
+
next_node_type = Util.unwrap_non_null(current_node_type).of_type
|
70
|
+
named_type = Util.get_named_type(next_node_type)
|
71
|
+
contains_null = false
|
62
72
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
73
|
+
resolved_list = raw_list.map! do |raw_list_element|
|
74
|
+
result = if next_node_type.list?
|
75
|
+
resolve_list_scope(raw_list_element, next_node_type, selections)
|
76
|
+
elsif Util.is_leaf_type?(named_type)
|
77
|
+
raw_list_element
|
78
|
+
else
|
79
|
+
resolve_object_scope(raw_list_element, named_type, selections)
|
69
80
|
end
|
70
|
-
elsif ! Util.is_leaf_type?(node_type)
|
71
|
-
munge_entry(child_entry, node.selections, node_type)
|
72
|
-
end
|
73
|
-
end
|
74
81
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
entry.each do |key, value|
|
79
|
-
if key.start_with? "_STITCH_"
|
80
|
-
entry.delete(key)
|
81
|
-
elsif value.is_a?(Array)
|
82
|
-
value.each do |item|
|
83
|
-
clean_entry(item)
|
84
|
-
end
|
85
|
-
elsif value.is_a?(Hash)
|
86
|
-
clean_entry(value)
|
82
|
+
if result.nil?
|
83
|
+
contains_null = true
|
84
|
+
return nil if current_node_type.non_null?
|
87
85
|
end
|
86
|
+
|
87
|
+
result
|
88
88
|
end
|
89
|
+
|
90
|
+
return nil if contains_null && next_node_type.non_null?
|
91
|
+
|
92
|
+
resolved_list
|
89
93
|
end
|
90
94
|
end
|
91
95
|
end
|
@@ -57,27 +57,28 @@ module GraphQL
|
|
57
57
|
def assign_executable(location, executable = nil, &block)
|
58
58
|
executable ||= block
|
59
59
|
unless executable.is_a?(Class) && executable <= GraphQL::Schema
|
60
|
-
raise "A client or block handler must be provided." unless executable
|
61
|
-
raise "A client must be callable" unless executable.respond_to?(:call)
|
60
|
+
raise StitchingError, "A client or block handler must be provided." unless executable
|
61
|
+
raise StitchingError, "A client must be callable" unless executable.respond_to?(:call)
|
62
62
|
end
|
63
63
|
@executables[location] = executable
|
64
64
|
end
|
65
65
|
|
66
|
-
def execute_at_location(location, query, variables)
|
66
|
+
def execute_at_location(location, query, variables, context)
|
67
67
|
executable = executables[location]
|
68
68
|
|
69
69
|
if executable.nil?
|
70
|
-
raise "No executable assigned for #{location} location."
|
70
|
+
raise StitchingError, "No executable assigned for #{location} location."
|
71
71
|
elsif executable.is_a?(Class) && executable <= GraphQL::Schema
|
72
72
|
executable.execute(
|
73
73
|
query: query,
|
74
74
|
variables: variables,
|
75
|
+
context: context,
|
75
76
|
validate: false,
|
76
77
|
)
|
77
78
|
elsif executable.respond_to?(:call)
|
78
|
-
executable.call(location, query, variables)
|
79
|
+
executable.call(location, query, variables, context)
|
79
80
|
else
|
80
|
-
raise "Missing valid executable for #{location} location."
|
81
|
+
raise StitchingError, "Missing valid executable for #{location} location."
|
81
82
|
end
|
82
83
|
end
|
83
84
|
|