graphql-stitching 0.1.0 → 0.2.2
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.
- 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
|