graphql-stitching 0.1.0 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +17 -17
- data/docs/README.md +1 -1
- data/docs/composer.md +45 -4
- data/docs/executor.md +31 -12
- data/docs/images/library.png +0 -0
- data/docs/planner.md +11 -7
- data/docs/request.md +50 -0
- data/docs/supergraph.md +1 -1
- data/graphql-stitching.gemspec +1 -1
- data/lib/graphql/stitching/composer.rb +98 -16
- data/lib/graphql/stitching/executor.rb +88 -45
- data/lib/graphql/stitching/gateway.rb +18 -13
- data/lib/graphql/stitching/planner.rb +204 -151
- data/lib/graphql/stitching/remote_client.rb +4 -4
- data/lib/graphql/stitching/request.rb +133 -0
- data/lib/graphql/stitching/shaper.rb +7 -7
- data/lib/graphql/stitching/supergraph.rb +44 -10
- data/lib/graphql/stitching/util.rb +28 -35
- data/lib/graphql/stitching/version.rb +1 -1
- data/lib/graphql/stitching.rb +7 -1
- metadata +6 -6
- data/docs/document.md +0 -15
- 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
|
-
query_document =
|
18
|
-
query_variables = @executor.variables.slice(*op["variables"].keys)
|
19
|
-
result = @executor.supergraph.execute_at_location(op["location"], query_document, query_variables)
|
18
|
+
query_document = build_document(op, @executor.request.operation_name)
|
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,16 +25,27 @@ 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
|
+
# Builds root source documents
|
33
|
+
# "query MyOperation_1($var:VarType) { rootSelections ... }"
|
34
|
+
def build_document(op, operation_name = nil)
|
35
|
+
doc = String.new
|
36
|
+
doc << op["operation_type"]
|
37
|
+
|
38
|
+
if operation_name
|
39
|
+
doc << " " << operation_name << "_" << op["key"].to_s
|
40
|
+
end
|
41
|
+
|
31
42
|
if op["variables"].any?
|
32
43
|
variable_defs = op["variables"].map { |k, v| "$#{k}:#{v}" }.join(",")
|
33
|
-
"
|
34
|
-
else
|
35
|
-
"#{op["operation_type"]}#{op["selections"]}"
|
44
|
+
doc << "(" << variable_defs << ")"
|
36
45
|
end
|
46
|
+
|
47
|
+
doc << op["selections"]
|
48
|
+
doc
|
37
49
|
end
|
38
50
|
end
|
39
51
|
|
@@ -60,9 +72,9 @@ module GraphQL
|
|
60
72
|
end
|
61
73
|
|
62
74
|
if origin_sets_by_operation.any?
|
63
|
-
query_document, variable_names =
|
64
|
-
variables = @executor.variables.slice(*variable_names)
|
65
|
-
raw_result = @executor.supergraph.execute_at_location(@location, query_document, variables)
|
75
|
+
query_document, variable_names = build_document(origin_sets_by_operation, @executor.request.operation_name)
|
76
|
+
variables = @executor.request.variables.slice(*variable_names)
|
77
|
+
raw_result = @executor.supergraph.execute_at_location(@location, query_document, variables, @executor.request.context)
|
66
78
|
@executor.query_count += 1
|
67
79
|
|
68
80
|
merge_results!(origin_sets_by_operation, raw_result.dig("data"))
|
@@ -74,7 +86,14 @@ module GraphQL
|
|
74
86
|
ops.map { origin_sets_by_operation[_1] ? _1["key"] : nil }
|
75
87
|
end
|
76
88
|
|
77
|
-
|
89
|
+
# Builds batched boundary queries
|
90
|
+
# "query MyOperation_2_3($var:VarType) {
|
91
|
+
# _0_result: list(keys:["a","b","c"]) { boundarySelections... }
|
92
|
+
# _1_0_result: item(key:"x") { boundarySelections... }
|
93
|
+
# _1_1_result: item(key:"y") { boundarySelections... }
|
94
|
+
# _1_2_result: item(key:"z") { boundarySelections... }
|
95
|
+
# }"
|
96
|
+
def build_document(origin_sets_by_operation, operation_name = nil)
|
78
97
|
variable_defs = {}
|
79
98
|
query_fields = origin_sets_by_operation.map.with_index do |(op, origin_set), batch_index|
|
80
99
|
variable_defs.merge!(op["variables"])
|
@@ -92,14 +111,24 @@ module GraphQL
|
|
92
111
|
end
|
93
112
|
end
|
94
113
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
114
|
+
doc = String.new
|
115
|
+
doc << "query" # << boundary fulfillment always uses query
|
116
|
+
|
117
|
+
if operation_name
|
118
|
+
doc << " " << operation_name
|
119
|
+
origin_sets_by_operation.each_key do |op|
|
120
|
+
doc << "_" << op["key"].to_s
|
121
|
+
end
|
100
122
|
end
|
101
123
|
|
102
|
-
|
124
|
+
if variable_defs.any?
|
125
|
+
variable_str = variable_defs.map { |k, v| "$#{k}:#{v}" }.join(",")
|
126
|
+
doc << "(" << variable_str << ")"
|
127
|
+
end
|
128
|
+
|
129
|
+
doc << "{ " << query_fields.join(" ") << " }"
|
130
|
+
|
131
|
+
return doc, variable_defs.keys
|
103
132
|
end
|
104
133
|
|
105
134
|
def merge_results!(origin_sets_by_operation, raw_result)
|
@@ -165,23 +194,26 @@ module GraphQL
|
|
165
194
|
|
166
195
|
private
|
167
196
|
|
168
|
-
#
|
197
|
+
# traverse forward through origin data, expanding arrays to follow all paths
|
169
198
|
# any errors found for an origin object_id have their path prefixed by the object path
|
170
199
|
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]
|
200
|
+
current_path.push(forward_path.shift)
|
173
201
|
scope = root[current_path.last]
|
174
202
|
|
175
203
|
if forward_path.any? && scope.is_a?(Array)
|
176
204
|
scope.each_with_index do |element, index|
|
177
205
|
inner_elements = element.is_a?(Array) ? element.flatten : [element]
|
178
206
|
inner_elements.each do |inner_element|
|
179
|
-
|
207
|
+
current_path << index
|
208
|
+
repath_errors!(pathed_errors_by_object_id, forward_path, current_path, inner_element)
|
209
|
+
current_path.pop
|
180
210
|
end
|
181
211
|
end
|
182
212
|
|
183
213
|
elsif forward_path.any?
|
184
|
-
|
214
|
+
current_path << index
|
215
|
+
repath_errors!(pathed_errors_by_object_id, forward_path, current_path, scope)
|
216
|
+
current_path.pop
|
185
217
|
|
186
218
|
elsif scope.is_a?(Array)
|
187
219
|
scope.each_with_index do |element, index|
|
@@ -196,61 +228,72 @@ module GraphQL
|
|
196
228
|
errors = pathed_errors_by_object_id[scope.object_id]
|
197
229
|
errors.each { _1["path"] = [*current_path, *_1["path"]] } if errors
|
198
230
|
end
|
231
|
+
|
232
|
+
forward_path.unshift(current_path.pop)
|
199
233
|
end
|
200
234
|
end
|
201
235
|
|
202
|
-
attr_reader :supergraph, :
|
236
|
+
attr_reader :supergraph, :request, :data, :errors
|
203
237
|
attr_accessor :query_count
|
204
238
|
|
205
|
-
def initialize(supergraph:, plan:,
|
239
|
+
def initialize(supergraph:, request:, plan:, nonblocking: false)
|
206
240
|
@supergraph = supergraph
|
207
|
-
@
|
241
|
+
@request = request
|
208
242
|
@queue = plan["ops"]
|
209
243
|
@data = {}
|
210
244
|
@errors = []
|
211
245
|
@query_count = 0
|
246
|
+
@exec_cycles = 0
|
212
247
|
@dataloader = GraphQL::Dataloader.new(nonblocking: nonblocking)
|
213
248
|
end
|
214
249
|
|
215
|
-
def perform(
|
250
|
+
def perform(raw: false)
|
216
251
|
exec!
|
217
|
-
|
218
252
|
result = {}
|
219
|
-
result["data"] = @data if @data && @data.length > 0
|
220
|
-
result["errors"] = @errors if @errors.length > 0
|
221
253
|
|
222
|
-
if
|
223
|
-
GraphQL::Stitching::Shaper.new(
|
254
|
+
if @data && @data.length > 0
|
255
|
+
result["data"] = raw ? @data : GraphQL::Stitching::Shaper.new(
|
224
256
|
schema: @supergraph.schema,
|
225
|
-
|
226
|
-
).perform!(
|
227
|
-
else
|
228
|
-
result
|
257
|
+
request: @request,
|
258
|
+
).perform!(@data)
|
229
259
|
end
|
260
|
+
|
261
|
+
if @errors.length > 0
|
262
|
+
result["errors"] = @errors
|
263
|
+
end
|
264
|
+
|
265
|
+
result
|
230
266
|
end
|
231
267
|
|
232
268
|
private
|
233
269
|
|
234
270
|
def exec!(after_keys = [0])
|
271
|
+
if @exec_cycles > @queue.length
|
272
|
+
# sanity check... if we've exceeded queue size, then something went wrong.
|
273
|
+
raise StitchingError, "Too many execution requests attempted."
|
274
|
+
end
|
275
|
+
|
235
276
|
@dataloader.append_job do
|
236
|
-
|
277
|
+
tasks = @queue
|
237
278
|
.select { after_keys.include?(_1["after_key"]) }
|
238
|
-
.group_by { _1["location"] }
|
239
|
-
.map do |location, ops|
|
240
|
-
if
|
241
|
-
@dataloader.with(RootSource, self).request_all(ops)
|
279
|
+
.group_by { [_1["location"], _1["boundary"].nil?] }
|
280
|
+
.map do |(location, root_source), ops|
|
281
|
+
if root_source
|
282
|
+
@dataloader.with(RootSource, self, location).request_all(ops)
|
242
283
|
else
|
243
284
|
@dataloader.with(BoundarySource, self, location).request_all(ops)
|
244
285
|
end
|
245
286
|
end
|
246
287
|
|
247
|
-
|
288
|
+
tasks.each(&method(:exec_task))
|
248
289
|
end
|
290
|
+
|
291
|
+
@exec_cycles += 1
|
249
292
|
@dataloader.run
|
250
293
|
end
|
251
294
|
|
252
|
-
def
|
253
|
-
next_keys =
|
295
|
+
def exec_task(task)
|
296
|
+
next_keys = task.load
|
254
297
|
next_keys.compact!
|
255
298
|
exec!(next_keys) if next_keys.any?
|
256
299
|
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
|