graphql-stitching 1.7.3 → 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/Rakefile +11 -1
- data/benchmark/run.rb +279 -0
- data/graphql-stitching.gemspec +3 -3
- data/lib/graphql/stitching/executor/path_access.rb +81 -0
- data/lib/graphql/stitching/executor/root_source.rb +46 -31
- data/lib/graphql/stitching/executor/shaper.rb +16 -12
- data/lib/graphql/stitching/executor/type_resolver_source.rb +41 -67
- data/lib/graphql/stitching/executor.rb +1 -0
- data/lib/graphql/stitching/planner.rb +34 -19
- data/lib/graphql/stitching/version.rb +1 -1
- metadata +10 -8
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/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/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
|
|
@@ -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,42 +3,49 @@
|
|
|
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
|
|
@@ -69,13 +76,21 @@ module GraphQL::Stitching
|
|
|
69
76
|
end
|
|
70
77
|
|
|
71
78
|
# Format response errors without a document location (because it won't match the request doc),
|
|
72
|
-
# and prepend
|
|
73
|
-
def format_errors
|
|
74
|
-
errors.
|
|
75
|
-
err
|
|
76
|
-
|
|
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
|
|
77
93
|
end
|
|
78
|
-
errors
|
|
79
94
|
end
|
|
80
95
|
end
|
|
81
96
|
end
|
|
@@ -11,6 +11,7 @@ module GraphQL::Stitching
|
|
|
11
11
|
@request = request
|
|
12
12
|
@supergraph = request.supergraph
|
|
13
13
|
@root_type = nil
|
|
14
|
+
@possible_type_names_by_type = nil
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def perform!(raw)
|
|
@@ -24,6 +25,8 @@ module GraphQL::Stitching
|
|
|
24
25
|
return nil if raw_object.nil?
|
|
25
26
|
|
|
26
27
|
typename ||= raw_object[TypeResolver::TYPENAME_EXPORT_NODE.alias]
|
|
28
|
+
typename ||= parent_type.graphql_name unless parent_type.kind.abstract?
|
|
29
|
+
|
|
27
30
|
raw_object.reject! { |key, _v| TypeResolver.export_key?(key) }
|
|
28
31
|
|
|
29
32
|
selections.each do |node|
|
|
@@ -84,36 +87,37 @@ module GraphQL::Stitching
|
|
|
84
87
|
|
|
85
88
|
next_node_type = Util.unwrap_non_null(current_node_type).of_type
|
|
86
89
|
named_type = next_node_type.unwrap
|
|
87
|
-
|
|
90
|
+
|
|
91
|
+
if Util.is_leaf_type?(named_type)
|
|
92
|
+
return nil if next_node_type.non_null? && raw_list.include?(nil)
|
|
93
|
+
|
|
94
|
+
return raw_list
|
|
95
|
+
end
|
|
88
96
|
|
|
89
97
|
resolved_list = raw_list.map! do |raw_list_element|
|
|
90
98
|
result = if next_node_type.list?
|
|
91
99
|
resolve_list_scope(raw_list_element, next_node_type, selections)
|
|
92
|
-
elsif Util.is_leaf_type?(named_type)
|
|
93
|
-
raw_list_element
|
|
94
100
|
else
|
|
95
101
|
resolve_object_scope(raw_list_element, named_type, selections)
|
|
96
102
|
end
|
|
97
103
|
|
|
98
|
-
if result.nil?
|
|
99
|
-
contains_null = true
|
|
100
|
-
return nil if current_node_type.non_null?
|
|
101
|
-
end
|
|
104
|
+
return nil if result.nil? && next_node_type.non_null?
|
|
102
105
|
|
|
103
106
|
result
|
|
104
107
|
end
|
|
105
108
|
|
|
106
|
-
return nil if contains_null && next_node_type.non_null?
|
|
107
|
-
|
|
108
109
|
resolved_list
|
|
109
110
|
end
|
|
110
111
|
|
|
111
112
|
def typename_in_type?(typename, type)
|
|
112
113
|
return true if type.graphql_name == typename
|
|
114
|
+
return false unless typename && type.kind.abstract?
|
|
113
115
|
|
|
114
|
-
type.
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
possible_type_names(type).include?(typename)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def possible_type_names(type)
|
|
120
|
+
(@possible_type_names_by_type ||= {})[type.graphql_name] ||= @request.query.possible_types(type).map(&:graphql_name)
|
|
117
121
|
end
|
|
118
122
|
end
|
|
119
123
|
end
|
|
@@ -3,40 +3,39 @@
|
|
|
3
3
|
module GraphQL::Stitching
|
|
4
4
|
class Executor
|
|
5
5
|
class TypeResolverSource < GraphQL::Dataloader::Source
|
|
6
|
+
include PathAccess
|
|
7
|
+
|
|
6
8
|
def initialize(executor, location)
|
|
7
9
|
@executor = executor
|
|
8
10
|
@location = location
|
|
9
|
-
@variables = {}
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
def fetch(ops)
|
|
13
14
|
origin_sets_by_operation = ops.each_with_object({}.compare_by_identity) do |op, memo|
|
|
14
|
-
origin_set =
|
|
15
|
-
set.flat_map { |obj| obj && obj[path_segment] }.tap(&:compact!)
|
|
16
|
-
end
|
|
15
|
+
origin_set = path_objects(@executor.data, op.path)
|
|
17
16
|
|
|
18
17
|
if op.if_type
|
|
19
18
|
# operations planned around unused fragment conditions should not trigger requests
|
|
20
|
-
origin_set.select! {
|
|
19
|
+
origin_set.select! { |origin_obj| origin_obj[TypeResolver::TYPENAME_EXPORT_NODE.alias] == op.if_type }
|
|
21
20
|
end
|
|
22
21
|
|
|
23
22
|
memo[op] = origin_set unless origin_set.empty?
|
|
24
23
|
end
|
|
25
24
|
|
|
26
25
|
unless origin_sets_by_operation.empty?
|
|
27
|
-
query_document, variable_names = build_document(
|
|
26
|
+
query_document, variable_names, generated_variables = build_document(
|
|
28
27
|
origin_sets_by_operation,
|
|
29
28
|
@executor.request.operation_name,
|
|
30
29
|
@executor.request.operation_directives,
|
|
31
30
|
)
|
|
32
|
-
variables =
|
|
31
|
+
variables = generated_variables.merge(@executor.request.variables.slice(*variable_names))
|
|
33
32
|
raw_result = @executor.request.supergraph.execute_at_location(@location, query_document, variables, @executor.request)
|
|
34
33
|
@executor.query_count += 1
|
|
35
34
|
|
|
36
35
|
merge_results!(origin_sets_by_operation, raw_result.dig("data"))
|
|
37
36
|
|
|
38
37
|
errors = raw_result.dig("errors")
|
|
39
|
-
@executor.errors.concat(extract_errors!(origin_sets_by_operation, errors)) if errors
|
|
38
|
+
@executor.errors.concat(extract_errors!(origin_sets_by_operation, errors)) if errors && !errors.empty?
|
|
40
39
|
end
|
|
41
40
|
|
|
42
41
|
ops.map { origin_sets_by_operation[_1] ? _1.step : nil }
|
|
@@ -51,6 +50,7 @@ module GraphQL::Stitching
|
|
|
51
50
|
# }"
|
|
52
51
|
def build_document(origin_sets_by_operation, operation_name = nil, operation_directives = nil)
|
|
53
52
|
variable_defs = {}
|
|
53
|
+
generated_variables = {}
|
|
54
54
|
fields_buffer = String.new
|
|
55
55
|
|
|
56
56
|
origin_sets_by_operation.each_with_index do |(op, origin_set), batch_index|
|
|
@@ -65,7 +65,7 @@ module GraphQL::Stitching
|
|
|
65
65
|
fields_buffer << "," unless i.zero?
|
|
66
66
|
if arg.key?
|
|
67
67
|
variable_name = "_#{batch_index}_key_#{i}".freeze
|
|
68
|
-
|
|
68
|
+
generated_variables[variable_name] = origin_set.map { arg.build(_1) }
|
|
69
69
|
variable_defs[variable_name] = arg.to_type_signature
|
|
70
70
|
fields_buffer << arg.name << ":$" << variable_name
|
|
71
71
|
else
|
|
@@ -83,7 +83,7 @@ module GraphQL::Stitching
|
|
|
83
83
|
fields_buffer << "," unless i.zero?
|
|
84
84
|
if arg.key?
|
|
85
85
|
variable_name = "_#{batch_index}_#{index}_key_#{i}".freeze
|
|
86
|
-
|
|
86
|
+
generated_variables[variable_name] = arg.build(origin_obj)
|
|
87
87
|
variable_defs[variable_name] = arg.to_type_signature
|
|
88
88
|
fields_buffer << arg.name << ":$" << variable_name
|
|
89
89
|
else
|
|
@@ -120,9 +120,11 @@ module GraphQL::Stitching
|
|
|
120
120
|
|
|
121
121
|
doc_buffer << "{ " << fields_buffer << " }"
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
names.reject! {
|
|
123
|
+
variable_names = variable_defs.keys.tap do |names|
|
|
124
|
+
names.reject! { generated_variables.key?(_1) }
|
|
125
125
|
end
|
|
126
|
+
|
|
127
|
+
return doc_buffer, variable_names, generated_variables
|
|
126
128
|
end
|
|
127
129
|
|
|
128
130
|
def merge_results!(origin_sets_by_operation, raw_result)
|
|
@@ -145,85 +147,57 @@ module GraphQL::Stitching
|
|
|
145
147
|
end
|
|
146
148
|
|
|
147
149
|
# https://spec.graphql.org/June2018/#sec-Errors
|
|
148
|
-
def extract_errors!(origin_sets_by_operation, errors)
|
|
150
|
+
def extract_errors!(origin_sets_by_operation, errors, origin_paths_by_operation = nil)
|
|
149
151
|
ops = origin_sets_by_operation.keys
|
|
150
152
|
origin_sets = origin_sets_by_operation.values
|
|
151
|
-
|
|
153
|
+
origin_paths_by_operation ||= origin_sets_by_operation.each_with_object({}.compare_by_identity) do |(op, origin_set), memo|
|
|
154
|
+
memo[op] = paths_for_origin_set(op, origin_set)
|
|
155
|
+
end
|
|
152
156
|
|
|
153
|
-
|
|
154
|
-
err.delete("locations")
|
|
157
|
+
errors.each_with_object([]) do |err, memo|
|
|
155
158
|
path = err["path"]
|
|
156
159
|
|
|
157
160
|
if path && path.length > 0
|
|
158
161
|
result_alias = /^_(\d+)(?:_(\d+))?_result$/.match(path.first.to_s)
|
|
159
162
|
|
|
160
163
|
if result_alias
|
|
161
|
-
path =
|
|
164
|
+
path = path[1..-1]
|
|
165
|
+
batch_index = result_alias[1].to_i
|
|
162
166
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
elsif path[0].is_a?(Integer) || /\d
|
|
166
|
-
|
|
167
|
+
origin_index = if result_alias[2]
|
|
168
|
+
result_alias[2].to_i
|
|
169
|
+
elsif path[0].is_a?(Integer) || /\A\d+\z/.match?(path[0].to_s)
|
|
170
|
+
path.shift.to_i
|
|
167
171
|
end
|
|
172
|
+
origin_obj = origin_sets.dig(batch_index, origin_index) if origin_index
|
|
168
173
|
|
|
169
174
|
if origin_obj
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
175
|
+
op = ops[batch_index]
|
|
176
|
+
object_path = origin_paths_by_operation.dig(op, origin_index)
|
|
177
|
+
|
|
178
|
+
if object_path
|
|
179
|
+
memo << sanitized_error(err, path: object_path + path)
|
|
180
|
+
next
|
|
181
|
+
end
|
|
174
182
|
end
|
|
183
|
+
|
|
184
|
+
memo << sanitized_error(err, path: path)
|
|
185
|
+
next
|
|
175
186
|
end
|
|
176
187
|
end
|
|
177
188
|
|
|
178
|
-
memo << err
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
unless pathed_errors_by_op_index_and_object_id.empty?
|
|
182
|
-
pathed_errors_by_op_index_and_object_id.each do |op_index, pathed_errors_by_object_id|
|
|
183
|
-
repath_errors!(pathed_errors_by_object_id, ops[op_index].path)
|
|
184
|
-
errors_result.push(*pathed_errors_by_object_id.each_value)
|
|
185
|
-
end
|
|
189
|
+
memo << sanitized_error(err)
|
|
186
190
|
end
|
|
187
|
-
|
|
188
|
-
errors_result.tap(&:flatten!)
|
|
189
191
|
end
|
|
190
192
|
|
|
191
193
|
private
|
|
192
194
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
current_path.push(forward_path.shift)
|
|
197
|
-
scope = root[current_path.last]
|
|
198
|
-
|
|
199
|
-
if !forward_path.empty? && scope.is_a?(Array)
|
|
200
|
-
scope.each_with_index do |element, index|
|
|
201
|
-
inner_elements = element.is_a?(Array) ? element.flatten : [element]
|
|
202
|
-
inner_elements.each do |inner_element|
|
|
203
|
-
current_path << index
|
|
204
|
-
repath_errors!(pathed_errors_by_object_id, forward_path, current_path, inner_element)
|
|
205
|
-
current_path.pop
|
|
206
|
-
end
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
elsif !forward_path.empty?
|
|
210
|
-
repath_errors!(pathed_errors_by_object_id, forward_path, current_path, scope)
|
|
211
|
-
|
|
212
|
-
elsif scope.is_a?(Array)
|
|
213
|
-
scope.each_with_index do |element, index|
|
|
214
|
-
inner_elements = element.is_a?(Array) ? element.flatten : [element]
|
|
215
|
-
inner_elements.each do |inner_element|
|
|
216
|
-
errors = pathed_errors_by_object_id[inner_element.object_id]
|
|
217
|
-
errors.each { _1["path"] = [*current_path, index, *_1["path"]] } if errors
|
|
218
|
-
end
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
else
|
|
222
|
-
errors = pathed_errors_by_object_id[scope.object_id]
|
|
223
|
-
errors.each { _1["path"] = [*current_path, *_1["path"]] } if errors
|
|
195
|
+
def paths_for_origin_set(op, origin_set)
|
|
196
|
+
paths_by_object_id = path_entries(@executor.data, op.path).each_with_object(Hash.new { |h, k| h[k] = [] }) do |(object, path), memo|
|
|
197
|
+
memo[object.object_id] << path
|
|
224
198
|
end
|
|
225
199
|
|
|
226
|
-
|
|
200
|
+
origin_set.map { |origin_obj| paths_by_object_id[origin_obj.object_id].shift }
|
|
227
201
|
end
|
|
228
202
|
end
|
|
229
203
|
end
|
|
@@ -91,7 +91,7 @@ module GraphQL
|
|
|
91
91
|
path.each { entrypoint << "/" << _1 }
|
|
92
92
|
|
|
93
93
|
step = @steps_by_entrypoint[entrypoint]
|
|
94
|
-
next_index = step ?
|
|
94
|
+
next_index = step ? step.index : @planning_index += 1
|
|
95
95
|
|
|
96
96
|
unless selections.empty?
|
|
97
97
|
selections = extract_locale_selections(location, parent_type, next_index, selections, path, variables)
|
|
@@ -110,6 +110,7 @@ module GraphQL
|
|
|
110
110
|
resolver: resolver,
|
|
111
111
|
)
|
|
112
112
|
else
|
|
113
|
+
step.variables.merge!(variables)
|
|
113
114
|
step.selections.concat(selections)
|
|
114
115
|
step
|
|
115
116
|
end
|
|
@@ -142,7 +143,8 @@ module GraphQL
|
|
|
142
143
|
# A.2) Partition mutation fields by consecutive location for serial execution.
|
|
143
144
|
partitions = []
|
|
144
145
|
each_field_in_scope(parent_type, @request.operation.selections) do |node|
|
|
145
|
-
|
|
146
|
+
locations = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name] || SUPERGRAPH_LOCATIONS
|
|
147
|
+
next_location = locations.first
|
|
146
148
|
|
|
147
149
|
if partitions.none? || partitions.last.location != next_location
|
|
148
150
|
partitions << ScopePartition.new(location: next_location, selections: [])
|
|
@@ -238,7 +240,8 @@ module GraphQL
|
|
|
238
240
|
end
|
|
239
241
|
|
|
240
242
|
# B.3) Collect all variable definitions used within the filtered selection.
|
|
241
|
-
|
|
243
|
+
extract_node_argument_variables(node, locale_variables)
|
|
244
|
+
extract_node_directive_variables(node, locale_variables)
|
|
242
245
|
schema_fields = @supergraph.memoized_schema_fields(parent_type.graphql_name)
|
|
243
246
|
field_type = schema_fields[node.name].type.unwrap
|
|
244
247
|
|
|
@@ -256,7 +259,8 @@ module GraphQL
|
|
|
256
259
|
fragment_type = node.type ? @supergraph.memoized_schema_types[node.type.name] : parent_type
|
|
257
260
|
next unless @supergraph.locations_by_type[fragment_type.graphql_name].include?(current_location)
|
|
258
261
|
|
|
259
|
-
|
|
262
|
+
extract_node_directive_variables(node, locale_variables)
|
|
263
|
+
is_same_scope = fragment_type == parent_type && node.directives.empty?
|
|
260
264
|
selection_set = is_same_scope ? locale_selections : []
|
|
261
265
|
extract_locale_selections(current_location, fragment_type, parent_index, node.selections, path, locale_variables, selection_set)
|
|
262
266
|
|
|
@@ -269,14 +273,17 @@ module GraphQL
|
|
|
269
273
|
fragment = @request.fragment_definitions[node.name]
|
|
270
274
|
next unless @supergraph.locations_by_type[fragment.type.name].include?(current_location)
|
|
271
275
|
|
|
276
|
+
extract_node_directive_variables(node, locale_variables)
|
|
277
|
+
extract_node_directive_variables(fragment, locale_variables)
|
|
272
278
|
requires_typename = true
|
|
273
279
|
fragment_type = @supergraph.memoized_schema_types[fragment.type.name]
|
|
274
|
-
|
|
280
|
+
directives = fragment.directives.empty? && node.directives.empty? ? EMPTY_ARRAY : fragment.directives + node.directives
|
|
281
|
+
is_same_scope = fragment_type == parent_type && directives.empty?
|
|
275
282
|
selection_set = is_same_scope ? locale_selections : []
|
|
276
283
|
extract_locale_selections(current_location, fragment_type, parent_index, fragment.selections, path, locale_variables, selection_set)
|
|
277
284
|
|
|
278
285
|
unless is_same_scope
|
|
279
|
-
locale_selections << GraphQL::Language::Nodes::InlineFragment.new(type: fragment.type, selections: selection_set)
|
|
286
|
+
locale_selections << GraphQL::Language::Nodes::InlineFragment.new(type: fragment.type, directives: directives, selections: selection_set)
|
|
280
287
|
end
|
|
281
288
|
|
|
282
289
|
else
|
|
@@ -355,20 +362,28 @@ module GraphQL
|
|
|
355
362
|
|
|
356
363
|
# B.3) Collect all variable definitions used within the filtered selection.
|
|
357
364
|
# These specify which request variables to pass along with each step.
|
|
358
|
-
def
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
when GraphQL::Language::Nodes::InputObject
|
|
362
|
-
extract_node_variables(argument.value, variable_definitions)
|
|
363
|
-
when GraphQL::Language::Nodes::VariableIdentifier
|
|
364
|
-
variable_definitions[argument.value.name] ||= @request.variable_definitions[argument.value.name]
|
|
365
|
-
end
|
|
366
|
-
end
|
|
365
|
+
def extract_node_argument_variables(node, variable_definitions)
|
|
366
|
+
arguments = node.arguments
|
|
367
|
+
return if arguments.empty?
|
|
367
368
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
369
|
+
arguments.each { |argument| extract_value_variables(argument.value, variable_definitions) }
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def extract_node_directive_variables(node, variable_definitions)
|
|
373
|
+
directives = node.directives
|
|
374
|
+
return if directives.empty?
|
|
375
|
+
|
|
376
|
+
directives.each { |directive| extract_node_argument_variables(directive, variable_definitions) }
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def extract_value_variables(value, variable_definitions)
|
|
380
|
+
case value
|
|
381
|
+
when GraphQL::Language::Nodes::InputObject
|
|
382
|
+
extract_node_argument_variables(value, variable_definitions)
|
|
383
|
+
when GraphQL::Language::Nodes::VariableIdentifier
|
|
384
|
+
variable_definitions[value.name] ||= @request.variable_definitions[value.name]
|
|
385
|
+
when Array
|
|
386
|
+
value.each { extract_value_variables(_1, variable_definitions) }
|
|
372
387
|
end
|
|
373
388
|
end
|
|
374
389
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: graphql-stitching
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Greg MacWilliam
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-06-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: graphql
|
|
@@ -28,42 +28,42 @@ dependencies:
|
|
|
28
28
|
name: bundler
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
|
-
- - "
|
|
31
|
+
- - ">="
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
33
|
version: '2.0'
|
|
34
34
|
type: :development
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
|
-
- - "
|
|
38
|
+
- - ">="
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
40
|
version: '2.0'
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
42
|
name: rake
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
44
44
|
requirements:
|
|
45
|
-
- - "
|
|
45
|
+
- - ">="
|
|
46
46
|
- !ruby/object:Gem::Version
|
|
47
47
|
version: '12.0'
|
|
48
48
|
type: :development
|
|
49
49
|
prerelease: false
|
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
51
|
requirements:
|
|
52
|
-
- - "
|
|
52
|
+
- - ">="
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
54
|
version: '12.0'
|
|
55
55
|
- !ruby/object:Gem::Dependency
|
|
56
56
|
name: minitest
|
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
|
58
58
|
requirements:
|
|
59
|
-
- - "
|
|
59
|
+
- - ">="
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
61
|
version: '5.12'
|
|
62
62
|
type: :development
|
|
63
63
|
prerelease: false
|
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
65
|
requirements:
|
|
66
|
-
- - "
|
|
66
|
+
- - ">="
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
68
|
version: '5.12'
|
|
69
69
|
description: Combine GraphQL services into one unified graph
|
|
@@ -79,6 +79,7 @@ files:
|
|
|
79
79
|
- LICENSE
|
|
80
80
|
- README.md
|
|
81
81
|
- Rakefile
|
|
82
|
+
- benchmark/run.rb
|
|
82
83
|
- docs/README.md
|
|
83
84
|
- docs/composing_a_supergraph.md
|
|
84
85
|
- docs/error_handling.md
|
|
@@ -167,6 +168,7 @@ files:
|
|
|
167
168
|
- lib/graphql/stitching/composer/validate_type_resolvers.rb
|
|
168
169
|
- lib/graphql/stitching/directives.rb
|
|
169
170
|
- lib/graphql/stitching/executor.rb
|
|
171
|
+
- lib/graphql/stitching/executor/path_access.rb
|
|
170
172
|
- lib/graphql/stitching/executor/root_source.rb
|
|
171
173
|
- lib/graphql/stitching/executor/shaper.rb
|
|
172
174
|
- lib/graphql/stitching/executor/type_resolver_source.rb
|