graphql-stitching 1.7.2 → 1.8.0
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 +1 -1
- data/Rakefile +11 -1
- data/benchmark/run.rb +279 -0
- data/docs/performance.md +28 -0
- data/graphql-stitching.gemspec +3 -3
- data/lib/graphql/stitching/client.rb +2 -2
- data/lib/graphql/stitching/composer/type_resolver_config.rb +1 -1
- data/lib/graphql/stitching/composer.rb +1 -3
- data/lib/graphql/stitching/executor/path_access.rb +81 -0
- data/lib/graphql/stitching/executor/root_source.rb +59 -40
- data/lib/graphql/stitching/executor/shaper.rb +27 -18
- data/lib/graphql/stitching/executor/type_resolver_source.rb +78 -88
- data/lib/graphql/stitching/executor.rb +2 -1
- data/lib/graphql/stitching/http_executable.rb +1 -2
- data/lib/graphql/stitching/plan.rb +46 -13
- data/lib/graphql/stitching/planner.rb +62 -46
- data/lib/graphql/stitching/request/skip_include.rb +2 -2
- data/lib/graphql/stitching/request.rb +1 -1
- data/lib/graphql/stitching/supergraph.rb +16 -6
- data/lib/graphql/stitching/type_resolver.rb +12 -1
- data/lib/graphql/stitching/util.rb +21 -4
- data/lib/graphql/stitching/version.rb +1 -1
- metadata +11 -9
- /data/docs/{introduction.md → README.md} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 66715354401a5907a302fcecc6e5a285344e31357060f093f5804e6c27dab1d2
|
|
4
|
+
data.tar.gz: c51d4046ad4c6101edec196d97b7e44f083640ad739c81f9f27612067646c6fb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 641b5c2903c1d07b00d794ac445aadc96bb7be640ef0e5b2607842e8c5d296eebfd39ddde3e75a97978e2533994a0c9d0b587c68cfd76c8d2c5cde4fb0dc302a
|
|
7
|
+
data.tar.gz: 916a94f76a1d453cf24ea7e382fe3b1b41184390720dd55c5b0ef1ffe32306f40ee33de73574e7672a0a895461e049b9204ef3fbd1e907a2570f29e463b1c1c1
|
data/README.md
CHANGED
|
@@ -21,7 +21,7 @@ This Ruby implementation is designed as a generic library to join basic spec-com
|
|
|
21
21
|
|
|
22
22
|
## Documentation
|
|
23
23
|
|
|
24
|
-
1. [Introduction](./docs/
|
|
24
|
+
1. [Introduction](./docs/README.md)
|
|
25
25
|
1. [Composing a supergraph](./docs/composing_a_supergraph.md)
|
|
26
26
|
1. [Merged types](./docs/merged_types.md)
|
|
27
27
|
1. [Executables & file uploads](./docs/executables.md)
|
data/Rakefile
CHANGED
|
@@ -9,4 +9,14 @@ Rake::TestTask.new(:test) do |t, args|
|
|
|
9
9
|
t.test_files = FileList['test/**/*_test.rb']
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
namespace :benchmark do
|
|
13
|
+
desc "Benchmark planner throughput"
|
|
14
|
+
task :planner do
|
|
15
|
+
ruby "benchmark/run.rb"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
desc "Run benchmarks"
|
|
20
|
+
task benchmark: "benchmark:planner"
|
|
21
|
+
|
|
22
|
+
task :default => :test
|
data/benchmark/run.rb
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
4
|
+
|
|
5
|
+
require "bundler/setup"
|
|
6
|
+
require "graphql/stitching"
|
|
7
|
+
|
|
8
|
+
STITCH_DEFINITION = "directive @stitch(key: String!, arguments: String, typeName: String) repeatable on FIELD_DEFINITION\n"
|
|
9
|
+
|
|
10
|
+
def compose_definitions(locations)
|
|
11
|
+
locations = locations.each_with_object({}) do |(location, schema_config), memo|
|
|
12
|
+
schema_config = STITCH_DEFINITION + schema_config if schema_config.include?("@stitch")
|
|
13
|
+
memo[location.to_s] = { schema: GraphQL::Schema.from_definition(schema_config) }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
GraphQL::Stitching::Composer.new.perform(locations)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class PlannerBenchmark
|
|
20
|
+
Case = Struct.new(:name, :supergraph, :document, :variables, keyword_init: true)
|
|
21
|
+
|
|
22
|
+
MINIMUM_SECONDS = Float(ENV.fetch("BENCHMARK_SECONDS", 2.0))
|
|
23
|
+
WARMUP_ITERATIONS = Integer(ENV.fetch("BENCHMARK_WARMUP", 200))
|
|
24
|
+
|
|
25
|
+
def initialize(cases)
|
|
26
|
+
@cases = cases
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def run
|
|
30
|
+
width = @cases.map { |c| c.name.length }.max
|
|
31
|
+
|
|
32
|
+
puts "GraphQL::Stitching::Planner"
|
|
33
|
+
puts "ruby #{RUBY_VERSION}p#{RUBY_PATCHLEVEL} (#{RUBY_PLATFORM})"
|
|
34
|
+
puts "graphql #{GraphQL::VERSION}"
|
|
35
|
+
puts "minimum #{MINIMUM_SECONDS}s per case"
|
|
36
|
+
puts
|
|
37
|
+
|
|
38
|
+
@cases.each do |bench_case|
|
|
39
|
+
WARMUP_ITERATIONS.times { plan(bench_case) }
|
|
40
|
+
|
|
41
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
42
|
+
iterations = 0
|
|
43
|
+
elapsed = 0.0
|
|
44
|
+
|
|
45
|
+
loop do
|
|
46
|
+
plan(bench_case)
|
|
47
|
+
iterations += 1
|
|
48
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
|
49
|
+
break if elapsed >= MINIMUM_SECONDS
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
ips = iterations / elapsed
|
|
53
|
+
microseconds = 1_000_000.0 / ips
|
|
54
|
+
puts "%-#{width}s %10.1f i/s %10.2f us/i" % [bench_case.name, ips, microseconds]
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def plan(bench_case)
|
|
61
|
+
GraphQL::Stitching::Request.new(
|
|
62
|
+
bench_case.supergraph,
|
|
63
|
+
bench_case.document,
|
|
64
|
+
variables: bench_case.variables,
|
|
65
|
+
).plan
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
root_supergraph = compose_definitions({
|
|
70
|
+
"widgets" => %|
|
|
71
|
+
input MakeWidgetInput { name: String child: MakeWidgetInput }
|
|
72
|
+
type Widget { id: ID! name(lang: String): String size: Int color: String }
|
|
73
|
+
type Query { widget(id: ID!): Widget widgets(ids: [ID!]): [Widget!]! }
|
|
74
|
+
type Mutation { makeWidget(input: MakeWidgetInput!): Widget }
|
|
75
|
+
|,
|
|
76
|
+
"sprockets" => %|
|
|
77
|
+
input MakeSprocketInput { name: String child: MakeSprocketInput }
|
|
78
|
+
type Sprocket { id: ID! name(lang: String): String size: Int color: String }
|
|
79
|
+
type Query { sprocket(id: ID!): Sprocket sprockets(ids: [ID!]): [Sprocket!]! }
|
|
80
|
+
type Mutation { makeSprocket(input: MakeSprocketInput!): Sprocket }
|
|
81
|
+
|,
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
delegation_supergraph = compose_definitions({
|
|
85
|
+
"alpha" => %|
|
|
86
|
+
type Widget { id: ID! a: String! b: String! }
|
|
87
|
+
type Query { alpha(id: ID!): Widget @stitch(key: "id") }
|
|
88
|
+
|,
|
|
89
|
+
"bravo" => %|
|
|
90
|
+
type Widget { id: ID! a: String! b: String! }
|
|
91
|
+
type Query { bravo(id: ID!): Widget @stitch(key: "id") }
|
|
92
|
+
|,
|
|
93
|
+
"charlie" => %|
|
|
94
|
+
type Widget { id: ID! c: String! d: String! }
|
|
95
|
+
type Query { charlie(id: ID!): Widget @stitch(key: "id") }
|
|
96
|
+
|,
|
|
97
|
+
"delta" => %|
|
|
98
|
+
type Widget { id: ID! d: String! e: String! }
|
|
99
|
+
type Query { delta(id: ID!): Widget @stitch(key: "id") }
|
|
100
|
+
|,
|
|
101
|
+
"echo" => %|
|
|
102
|
+
type Widget { id: ID! d: String! e: String! f: String! }
|
|
103
|
+
type Query { echo(id: ID!): Widget @stitch(key: "id") }
|
|
104
|
+
|,
|
|
105
|
+
"foxtrot" => %|
|
|
106
|
+
type Widget { id: ID! d: String! f: String! }
|
|
107
|
+
type Query { foxtrot(id: ID!): Widget @stitch(key: "id") }
|
|
108
|
+
|,
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
resolver_supergraph = compose_definitions({
|
|
112
|
+
"storefronts" => %|
|
|
113
|
+
type Storefront {
|
|
114
|
+
id: ID!
|
|
115
|
+
name: String!
|
|
116
|
+
products: [Product]!
|
|
117
|
+
}
|
|
118
|
+
type Product {
|
|
119
|
+
upc: ID!
|
|
120
|
+
}
|
|
121
|
+
type Query {
|
|
122
|
+
storefront(id: ID!): Storefront
|
|
123
|
+
}
|
|
124
|
+
|,
|
|
125
|
+
"products" => %|
|
|
126
|
+
type Product {
|
|
127
|
+
upc: ID!
|
|
128
|
+
name: String!
|
|
129
|
+
price: Float!
|
|
130
|
+
manufacturer: Manufacturer!
|
|
131
|
+
}
|
|
132
|
+
type Manufacturer {
|
|
133
|
+
id: ID!
|
|
134
|
+
name: String!
|
|
135
|
+
products: [Product]!
|
|
136
|
+
}
|
|
137
|
+
type Query {
|
|
138
|
+
product(upc: ID!): Product @stitch(key: "upc")
|
|
139
|
+
productsManufacturer(id: ID!): Manufacturer @stitch(key: "id")
|
|
140
|
+
}
|
|
141
|
+
|,
|
|
142
|
+
"manufacturers" => %|
|
|
143
|
+
type Manufacturer {
|
|
144
|
+
id: ID!
|
|
145
|
+
name: String!
|
|
146
|
+
address: String!
|
|
147
|
+
}
|
|
148
|
+
type Query {
|
|
149
|
+
manufacturer(id: ID!): Manufacturer @stitch(key: "id")
|
|
150
|
+
}
|
|
151
|
+
|,
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
abstract_supergraph = compose_definitions({
|
|
155
|
+
"a" => %|
|
|
156
|
+
interface Buyable {
|
|
157
|
+
id: ID!
|
|
158
|
+
name: String!
|
|
159
|
+
price: Float!
|
|
160
|
+
}
|
|
161
|
+
type Product implements Buyable {
|
|
162
|
+
id: ID!
|
|
163
|
+
name: String!
|
|
164
|
+
price: Float!
|
|
165
|
+
}
|
|
166
|
+
type Query {
|
|
167
|
+
products(ids: [ID!]!): [Product]! @stitch(key: "id")
|
|
168
|
+
}
|
|
169
|
+
|,
|
|
170
|
+
"b" => %|
|
|
171
|
+
interface Buyable { id: ID! }
|
|
172
|
+
type Product implements Buyable { id: ID! }
|
|
173
|
+
type Bundle implements Buyable {
|
|
174
|
+
id: ID!
|
|
175
|
+
name: String!
|
|
176
|
+
price: Float!
|
|
177
|
+
products: [Product]!
|
|
178
|
+
}
|
|
179
|
+
type Query {
|
|
180
|
+
buyable(id: ID!): Buyable @stitch(key: "id")
|
|
181
|
+
}
|
|
182
|
+
|,
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
cases = [
|
|
186
|
+
PlannerBenchmark::Case.new(
|
|
187
|
+
name: "root groups",
|
|
188
|
+
supergraph: root_supergraph,
|
|
189
|
+
document: %|
|
|
190
|
+
query($wid: ID!, $sid: ID!, $ids: [ID!], $lang: String) {
|
|
191
|
+
a: widget(id: $wid) { id name(lang: $lang) size color }
|
|
192
|
+
b: sprocket(id: $sid) { id name(lang: $lang) size color }
|
|
193
|
+
c: widgets(ids: $ids) { id name size color }
|
|
194
|
+
d: sprockets(ids: $ids) { id name size color }
|
|
195
|
+
}
|
|
196
|
+
|,
|
|
197
|
+
variables: { "wid" => "1", "sid" => "2", "ids" => ["1", "2"], "lang" => "en" },
|
|
198
|
+
),
|
|
199
|
+
PlannerBenchmark::Case.new(
|
|
200
|
+
name: "variables",
|
|
201
|
+
supergraph: root_supergraph,
|
|
202
|
+
document: %|
|
|
203
|
+
mutation($wname1: String!, $wname2: String!, $sname1: String!, $sname2: String!, $lang: String) {
|
|
204
|
+
makeWidget(input: { name: $wname1, child: { name: $wname2 } }) { id name(lang: $lang) }
|
|
205
|
+
makeSprocket(input: { name: $sname1, child: { name: $sname2 } }) { id name(lang: $lang) }
|
|
206
|
+
}
|
|
207
|
+
|,
|
|
208
|
+
variables: {
|
|
209
|
+
"wname1" => "a",
|
|
210
|
+
"wname2" => "b",
|
|
211
|
+
"sname1" => "c",
|
|
212
|
+
"sname2" => "d",
|
|
213
|
+
"lang" => "en",
|
|
214
|
+
},
|
|
215
|
+
),
|
|
216
|
+
PlannerBenchmark::Case.new(
|
|
217
|
+
name: "delegation",
|
|
218
|
+
supergraph: delegation_supergraph,
|
|
219
|
+
document: %|query { alpha(id: "1") { a b c d e f } }|,
|
|
220
|
+
),
|
|
221
|
+
PlannerBenchmark::Case.new(
|
|
222
|
+
name: "nested resolvers",
|
|
223
|
+
supergraph: resolver_supergraph,
|
|
224
|
+
document: %|
|
|
225
|
+
query {
|
|
226
|
+
storefront(id: "1") {
|
|
227
|
+
name
|
|
228
|
+
products {
|
|
229
|
+
name
|
|
230
|
+
manufacturer {
|
|
231
|
+
address
|
|
232
|
+
products {
|
|
233
|
+
name
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|,
|
|
240
|
+
),
|
|
241
|
+
PlannerBenchmark::Case.new(
|
|
242
|
+
name: "fragments",
|
|
243
|
+
supergraph: resolver_supergraph,
|
|
244
|
+
document: %|
|
|
245
|
+
query {
|
|
246
|
+
storefront(id: "1") {
|
|
247
|
+
products {
|
|
248
|
+
...ProductAttrs
|
|
249
|
+
manufacturer {
|
|
250
|
+
...ManufacturerAttrs
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
fragment ProductAttrs on Product { name price }
|
|
256
|
+
fragment ManufacturerAttrs on Manufacturer {
|
|
257
|
+
name
|
|
258
|
+
address
|
|
259
|
+
products { ...ProductAttrs }
|
|
260
|
+
}
|
|
261
|
+
|,
|
|
262
|
+
),
|
|
263
|
+
PlannerBenchmark::Case.new(
|
|
264
|
+
name: "abstracts",
|
|
265
|
+
supergraph: abstract_supergraph,
|
|
266
|
+
document: %|
|
|
267
|
+
query {
|
|
268
|
+
buyable(id: "1") {
|
|
269
|
+
... {
|
|
270
|
+
...BuyableAttrs
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
fragment BuyableAttrs on Buyable { id name price }
|
|
275
|
+
|,
|
|
276
|
+
),
|
|
277
|
+
]
|
|
278
|
+
|
|
279
|
+
PlannerBenchmark.new(cases).run
|
data/docs/performance.md
CHANGED
|
@@ -57,6 +57,34 @@ query($id: ID!) {
|
|
|
57
57
|
# variables: { "id" => "1" }
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
### Subgraph validations
|
|
61
|
+
|
|
62
|
+
Requests are validated by the supergraph, and should always divide into valid subgraph documents. Therefore, you can skip redundant subgraph validations for requests sent by the supergraph, ex:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
exe = GraphQL::Stitching::HttpExecutable.new(
|
|
66
|
+
url: "http://localhost:3001",
|
|
67
|
+
headers: {
|
|
68
|
+
"Authorization" => "...",
|
|
69
|
+
"X-Supergraph-Secret" => "<shared-secret>",
|
|
70
|
+
},
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
A shared secret allows a subgraph location to trust the supergraph origin, at which time it can disable validations:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
def query
|
|
78
|
+
sg_header = request.headers["X-Supergraph-Secret"]
|
|
79
|
+
MySchema.execute(
|
|
80
|
+
query: params[:query],
|
|
81
|
+
variables: params[:variables],
|
|
82
|
+
operation_name: params[:operationName],
|
|
83
|
+
validate: sg_header.nil? || sg_header != Rails.env.credentials.supergraph,
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
60
88
|
### Digests
|
|
61
89
|
|
|
62
90
|
All computed digests use SHA2 hashing by default. You can swap in [a faster algorithm](https://github.com/Shopify/blake3-rb) and/or add base state by reconfiguring `Stitching.digest`:
|
data/graphql-stitching.gemspec
CHANGED
|
@@ -28,7 +28,7 @@ Gem::Specification.new do |spec|
|
|
|
28
28
|
|
|
29
29
|
spec.add_runtime_dependency 'graphql', '>= 2.0'
|
|
30
30
|
|
|
31
|
-
spec.add_development_dependency 'bundler', '
|
|
32
|
-
spec.add_development_dependency 'rake', '
|
|
33
|
-
spec.add_development_dependency 'minitest', '
|
|
31
|
+
spec.add_development_dependency 'bundler', '>= 2.0'
|
|
32
|
+
spec.add_development_dependency 'rake', '>= 12.0'
|
|
33
|
+
spec.add_development_dependency 'minitest', '>= 5.12'
|
|
34
34
|
end
|
|
@@ -25,7 +25,7 @@ module GraphQL
|
|
|
25
25
|
raise ArgumentError, "Cannot provide both locations and a supergraph."
|
|
26
26
|
elsif supergraph && !supergraph.is_a?(Supergraph)
|
|
27
27
|
raise ArgumentError, "Provided supergraph must be a GraphQL::Stitching::Supergraph instance."
|
|
28
|
-
elsif supergraph && composer_options.
|
|
28
|
+
elsif supergraph && !composer_options.empty?
|
|
29
29
|
raise ArgumentError, "Cannot provide composer options with a pre-built supergraph."
|
|
30
30
|
elsif supergraph
|
|
31
31
|
supergraph
|
|
@@ -50,7 +50,7 @@ module GraphQL
|
|
|
50
50
|
|
|
51
51
|
if validate
|
|
52
52
|
validation_errors = request.validate
|
|
53
|
-
return error_result(request, validation_errors)
|
|
53
|
+
return error_result(request, validation_errors) unless validation_errors.empty?
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
load_plan(request)
|
|
@@ -8,7 +8,7 @@ module GraphQL::Stitching
|
|
|
8
8
|
|
|
9
9
|
class << self
|
|
10
10
|
def extract_directive_assignments(schema, location, assignments)
|
|
11
|
-
return EMPTY_OBJECT unless assignments && assignments.
|
|
11
|
+
return EMPTY_OBJECT unless assignments && !assignments.empty?
|
|
12
12
|
|
|
13
13
|
assignments.each_with_object({}) do |kwargs, memo|
|
|
14
14
|
type = kwargs[:parent_type_name] ? schema.get_type(kwargs[:parent_type_name]) : schema.query
|
|
@@ -423,7 +423,7 @@ module GraphQL
|
|
|
423
423
|
memo[location] = argument.default_value
|
|
424
424
|
end
|
|
425
425
|
|
|
426
|
-
|
|
426
|
+
unless default_values_by_location.empty?
|
|
427
427
|
kwargs[:default_value] = @default_value_merger.call(default_values_by_location, {
|
|
428
428
|
type_name: type_name,
|
|
429
429
|
field_name: field_name,
|
|
@@ -468,8 +468,6 @@ module GraphQL
|
|
|
468
468
|
kwarg_values_by_name_location = directives_by_location.each_with_object({}) do |(location, directive), memo|
|
|
469
469
|
directive.arguments.keyword_arguments.each do |key, value|
|
|
470
470
|
key = key.to_s
|
|
471
|
-
next unless directive_class.arguments[key]
|
|
472
|
-
|
|
473
471
|
memo[key] ||= {}
|
|
474
472
|
memo[key][location] = value
|
|
475
473
|
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GraphQL::Stitching
|
|
4
|
+
class Executor
|
|
5
|
+
# Utilities for traversing aggregate executor data along planned paths.
|
|
6
|
+
module PathAccess
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def path_objects(root, path)
|
|
10
|
+
objects = []
|
|
11
|
+
each_path_object(root, path) { |object| objects << object }
|
|
12
|
+
objects
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def each_path_object(scope, path, &block)
|
|
16
|
+
return enum_for(:each_path_object, scope, path) unless block
|
|
17
|
+
return if scope.nil?
|
|
18
|
+
|
|
19
|
+
if path.empty?
|
|
20
|
+
each_leaf_object(scope, &block)
|
|
21
|
+
elsif scope.is_a?(Array)
|
|
22
|
+
scope.each { |element| each_path_object(element, path, &block) }
|
|
23
|
+
elsif scope.respond_to?(:[])
|
|
24
|
+
path_segment = path.first
|
|
25
|
+
each_path_object(scope[path_segment], path[1..-1], &block)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def each_leaf_object(scope, &block)
|
|
30
|
+
return if scope.nil?
|
|
31
|
+
|
|
32
|
+
if scope.is_a?(Array)
|
|
33
|
+
scope.each { |element| each_leaf_object(element, &block) }
|
|
34
|
+
else
|
|
35
|
+
yield(scope)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def path_entries(root, path)
|
|
40
|
+
entries = []
|
|
41
|
+
each_path_entry(root, path) { |object, response_path| entries << [object, response_path] }
|
|
42
|
+
entries
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def each_path_entry(scope, path, response_path = [], &block)
|
|
46
|
+
return enum_for(:each_path_entry, scope, path, response_path) unless block
|
|
47
|
+
return if scope.nil?
|
|
48
|
+
|
|
49
|
+
if path.empty?
|
|
50
|
+
each_leaf_entry(scope, response_path, &block)
|
|
51
|
+
elsif scope.is_a?(Array)
|
|
52
|
+
scope.each_with_index do |element, index|
|
|
53
|
+
each_path_entry(element, path, [*response_path, index], &block)
|
|
54
|
+
end
|
|
55
|
+
elsif scope.respond_to?(:[])
|
|
56
|
+
path_segment = path.first
|
|
57
|
+
each_path_entry(scope[path_segment], path[1..-1], [*response_path, path_segment], &block)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def each_leaf_entry(scope, response_path, &block)
|
|
62
|
+
return if scope.nil?
|
|
63
|
+
|
|
64
|
+
if scope.is_a?(Array)
|
|
65
|
+
scope.each_with_index do |element, index|
|
|
66
|
+
each_leaf_entry(element, [*response_path, index], &block)
|
|
67
|
+
end
|
|
68
|
+
else
|
|
69
|
+
yield(scope, response_path)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def sanitized_error(error, path: nil)
|
|
74
|
+
error.dup.tap do |formatted|
|
|
75
|
+
formatted.delete("locations")
|
|
76
|
+
formatted["path"] = path if path
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -3,75 +3,94 @@
|
|
|
3
3
|
module GraphQL::Stitching
|
|
4
4
|
class Executor
|
|
5
5
|
class RootSource < GraphQL::Dataloader::Source
|
|
6
|
+
include PathAccess
|
|
7
|
+
|
|
6
8
|
def initialize(executor, location)
|
|
7
9
|
@executor = executor
|
|
8
10
|
@location = location
|
|
9
11
|
end
|
|
10
12
|
|
|
11
13
|
def fetch(ops)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
14
|
+
ops.map do |op|
|
|
15
|
+
origin_set = op.path.empty? ? [@executor.data] : path_objects(@executor.data, op.path)
|
|
16
|
+
|
|
17
|
+
query_document = build_document(
|
|
18
|
+
op,
|
|
19
|
+
@executor.request.operation_name,
|
|
20
|
+
@executor.request.operation_directives,
|
|
21
|
+
)
|
|
22
|
+
query_variables = @executor.request.variables.slice(*op.variables.each_key)
|
|
23
|
+
result = @executor.request.supergraph.execute_at_location(op.location, query_document, query_variables, @executor.request)
|
|
24
|
+
@executor.query_count += 1
|
|
25
|
+
|
|
26
|
+
errors = result["errors"]
|
|
27
|
+
origin_entries = nil
|
|
28
|
+
|
|
29
|
+
if errors && !errors.empty?
|
|
30
|
+
origin_entries = op.path.empty? ? [[@executor.data, []]] : path_entries(@executor.data, op.path)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
if result["data"]
|
|
34
|
+
if op.path.empty?
|
|
35
|
+
# Actual root scopes merge directly into results data
|
|
36
|
+
@executor.data.merge!(result["data"])
|
|
37
|
+
elsif !origin_set.empty?
|
|
38
|
+
# Nested root scopes merge the same root payload into each pathed origin
|
|
39
|
+
origin_set.each { |origin_obj| origin_obj.merge!(result["data"]) }
|
|
28
40
|
end
|
|
41
|
+
end
|
|
29
42
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
# Actual root scopes merge directly into results data
|
|
33
|
-
@executor.data.merge!(result["data"])
|
|
43
|
+
if errors && !errors.empty?
|
|
44
|
+
@executor.errors.concat(format_errors(errors, origin_entries, op.path))
|
|
34
45
|
end
|
|
35
|
-
end
|
|
36
46
|
|
|
37
|
-
|
|
38
|
-
@executor.errors.concat(format_errors!(result["errors"], op.path))
|
|
47
|
+
op.step
|
|
39
48
|
end
|
|
40
|
-
|
|
41
|
-
ops.map(&:step)
|
|
42
49
|
end
|
|
43
50
|
|
|
44
51
|
# Builds root source documents
|
|
45
52
|
# "query MyOperation_1($var:VarType) { rootSelections ... }"
|
|
46
53
|
def build_document(op, operation_name = nil, operation_directives = nil)
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
doc_buffer = String.new
|
|
55
|
+
doc_buffer << op.operation_type
|
|
49
56
|
|
|
50
57
|
if operation_name
|
|
51
|
-
|
|
58
|
+
doc_buffer << " " << operation_name << "_" << op.step.to_s
|
|
52
59
|
end
|
|
53
60
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
unless op.variables.empty?
|
|
62
|
+
doc_buffer << "("
|
|
63
|
+
op.variables.each_with_index do |(k, v), i|
|
|
64
|
+
doc_buffer << "," unless i.zero?
|
|
65
|
+
doc_buffer << "$" << k << ":" << v
|
|
66
|
+
end
|
|
67
|
+
doc_buffer << ")"
|
|
57
68
|
end
|
|
58
69
|
|
|
59
70
|
if operation_directives
|
|
60
|
-
|
|
71
|
+
doc_buffer << " " << operation_directives << " "
|
|
61
72
|
end
|
|
62
73
|
|
|
63
|
-
|
|
64
|
-
|
|
74
|
+
doc_buffer << op.selections
|
|
75
|
+
doc_buffer
|
|
65
76
|
end
|
|
66
77
|
|
|
67
78
|
# Format response errors without a document location (because it won't match the request doc),
|
|
68
|
-
# and prepend
|
|
69
|
-
def format_errors
|
|
70
|
-
errors.
|
|
71
|
-
err
|
|
72
|
-
|
|
79
|
+
# and prepend all concrete insertion paths for nested scopes into error paths.
|
|
80
|
+
def format_errors(errors, origin_entries, fallback_path = [])
|
|
81
|
+
errors.flat_map do |err|
|
|
82
|
+
path = err["path"]
|
|
83
|
+
|
|
84
|
+
if path && !origin_entries.empty?
|
|
85
|
+
origin_entries.map do |_origin_obj, origin_path|
|
|
86
|
+
sanitized_error(err, path: origin_path + path)
|
|
87
|
+
end
|
|
88
|
+
elsif path && !fallback_path.empty?
|
|
89
|
+
sanitized_error(err, path: fallback_path + path)
|
|
90
|
+
else
|
|
91
|
+
sanitized_error(err)
|
|
92
|
+
end
|
|
73
93
|
end
|
|
74
|
-
errors
|
|
75
94
|
end
|
|
76
95
|
end
|
|
77
96
|
end
|