graphql-stitching 1.2.1 → 1.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 +5 -6
- data/docs/README.md +0 -2
- data/docs/mechanics.md +42 -0
- data/docs/request.md +7 -32
- data/lib/graphql/stitching/composer.rb +4 -8
- data/lib/graphql/stitching/executor/root_source.rb +25 -3
- data/lib/graphql/stitching/planner.rb +14 -12
- data/lib/graphql/stitching/request.rb +13 -2
- data/lib/graphql/stitching/supergraph.rb +22 -9
- data/lib/graphql/stitching/util.rb +1 -1
- data/lib/graphql/stitching/version.rb +1 -1
- metadata +2 -4
- data/docs/executor.md +0 -68
- data/docs/planner.md +0 -45
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6753e205aac88d3dd3be1c906b743cc49ae0de84ff8ab56c7e9be6e889429aa3
|
4
|
+
data.tar.gz: 59440f9d70cb365ef124604c42012bf655934555dfb76905328368353f5e310b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ac218d2218c14b8debff552ffb11e83e00293dfebb404d25296ed9e408232e488c44575c701ee3fd933331d84755062517f2c2da8df2db98ac7c5e8594595846
|
7
|
+
data.tar.gz: ec4f5051ebd511e72536c8e3246302079b2eb65ab5edab37c7c7d7de5488cdc7581318c69a0ea54047bd79ff42bf9e9c5af3434a27fc5667cf029340527c8117
|
data/README.md
CHANGED
@@ -78,9 +78,7 @@ While the `Client` constructor is an easy quick start, the library also has seve
|
|
78
78
|
|
79
79
|
- [Composer](./docs/composer.md) - merges and validates many schemas into one supergraph.
|
80
80
|
- [Supergraph](./docs/supergraph.md) - manages the combined schema, location routing maps, and executable resources. Can be exported, cached, and rehydrated.
|
81
|
-
- [Request](./docs/request.md) -
|
82
|
-
- [Planner](./docs/planner.md) - builds a cacheable query plan for a request document.
|
83
|
-
- [Executor](./docs/executor.md) - executes a query plan with given request variables.
|
81
|
+
- [Request](./docs/request.md) - manages the lifecycle of a stitched GraphQL request.
|
84
82
|
- [HttpExecutable](./docs/http_executable.md) - proxies requests to remotes with multipart file upload support.
|
85
83
|
|
86
84
|
## Merged types
|
@@ -134,7 +132,7 @@ client = GraphQL::Stitching::Client.new(locations: {
|
|
134
132
|
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3001"),
|
135
133
|
},
|
136
134
|
catalog: {
|
137
|
-
schema: GraphQL::Schema.from_definition(
|
135
|
+
schema: GraphQL::Schema.from_definition(catalog_schema),
|
138
136
|
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3002"),
|
139
137
|
},
|
140
138
|
})
|
@@ -155,7 +153,7 @@ type Query {
|
|
155
153
|
* The `@stitch` directive is applied to a root query where the merged type may be accessed. The merged type identity is inferred from the field return.
|
156
154
|
* The `key: "id"` parameter indicates that an `{ id }` must be selected from prior locations so it may be submitted as an argument to this query. The query argument used to send the key is inferred when possible ([more on arguments](#multiple-query-arguments) later).
|
157
155
|
|
158
|
-
Each location that provides a unique variant of a type must provide at least one stitching query. The exception to this requirement are types that contain only a single key field:
|
156
|
+
Each location that provides a unique variant of a type must provide at least one stitching query for the type. The exception to this requirement are [foreign key types](./docs/mechanics.md##modeling-foreign-keys-for-stitching) that contain only a single key field:
|
159
157
|
|
160
158
|
```graphql
|
161
159
|
type Product {
|
@@ -163,7 +161,7 @@ type Product {
|
|
163
161
|
}
|
164
162
|
```
|
165
163
|
|
166
|
-
The above representation of a `Product` type provides no unique data beyond a key that is available in other locations. Thus, this representation will never require an inbound request to fetch it, and its stitching query may be omitted.
|
164
|
+
The above representation of a `Product` type provides no unique data beyond a key that is available in other locations. Thus, this representation will never require an inbound request to fetch it, and its stitching query may be omitted.
|
167
165
|
|
168
166
|
#### List queries
|
169
167
|
|
@@ -427,6 +425,7 @@ The [Executor](./docs/executor.md) component builds atop the Ruby fiber-based im
|
|
427
425
|
|
428
426
|
## Additional topics
|
429
427
|
|
428
|
+
- [Modeling foreign keys for stitching](./docs/mechanics.md##modeling-foreign-keys-for-stitching)
|
430
429
|
- [Deploying a stitched schema](./docs/mechanics.md#deploying-a-stitched-schema)
|
431
430
|
- [Field selection routing](./docs/mechanics.md#field-selection-routing)
|
432
431
|
- [Root selection routing](./docs/mechanics.md#root-selection-routing)
|
data/docs/README.md
CHANGED
@@ -10,8 +10,6 @@ Major components include:
|
|
10
10
|
- [Composer](./composer.md) - merges and validates many schemas into one graph.
|
11
11
|
- [Supergraph](./supergraph.md) - manages the combined schema and location routing maps. Can be exported, cached, and rehydrated.
|
12
12
|
- [Request](./request.md) - prepares a requested GraphQL document and variables for stitching.
|
13
|
-
- [Planner](./planner.md) - builds a cacheable query plan for a request document.
|
14
|
-
- [Executor](./executor.md) - executes a query plan with given request variables.
|
15
13
|
- [HttpExecutable](./http_executable.md) - proxies requests to remotes with multipart file upload support.
|
16
14
|
|
17
15
|
Additional topics:
|
data/docs/mechanics.md
CHANGED
@@ -1,5 +1,47 @@
|
|
1
1
|
## Schema Stitching, mechanics
|
2
2
|
|
3
|
+
### Modeling foreign keys for stitching
|
4
|
+
|
5
|
+
Foreign keys in a GraphQL schema typically look like the `Product.imageId` field here:
|
6
|
+
|
7
|
+
```graphql
|
8
|
+
# -- Products schema:
|
9
|
+
|
10
|
+
type Product {
|
11
|
+
id: ID!
|
12
|
+
imageId: ID!
|
13
|
+
}
|
14
|
+
|
15
|
+
# -- Images schema:
|
16
|
+
|
17
|
+
type Image {
|
18
|
+
id: ID!
|
19
|
+
url: String!
|
20
|
+
}
|
21
|
+
```
|
22
|
+
|
23
|
+
However, this design does not lend itself to stitching where types need to _merge_ across locations. A simple schema refactor makes this foreign key more expressive as an entity type, and turns the key into an _object_ that will merge with analogous object types in other locations:
|
24
|
+
|
25
|
+
```graphql
|
26
|
+
# -- Products schema:
|
27
|
+
|
28
|
+
type Product {
|
29
|
+
id: ID!
|
30
|
+
image: Image!
|
31
|
+
}
|
32
|
+
|
33
|
+
type Image {
|
34
|
+
id: ID!
|
35
|
+
}
|
36
|
+
|
37
|
+
# -- Images schema:
|
38
|
+
|
39
|
+
type Image {
|
40
|
+
id: ID!
|
41
|
+
url: String!
|
42
|
+
}
|
43
|
+
```
|
44
|
+
|
3
45
|
### Deploying a stitched schema
|
4
46
|
|
5
47
|
Among the simplest and most effective ways to manage a stitched schema is to compose it locally, write the composed SDL as a `.graphql` file in your repo, and then load the composed schema into a stitching client at runtime. For example, setup a `rake` task that loads/fetches subgraph schemas, composes them, and then writes the composed schema definition as a file committed to the repo:
|
data/docs/request.md
CHANGED
@@ -25,37 +25,12 @@ A `Request` provides the following information:
|
|
25
25
|
- `req.variable_definitions`: a mapping of variable names to their type definitions
|
26
26
|
- `req.fragment_definitions`: a mapping of fragment names to their fragment definitions
|
27
27
|
|
28
|
-
###
|
28
|
+
### Request lifecycle
|
29
29
|
|
30
|
-
A request
|
30
|
+
A request manages the flow of stitching behaviors. These are sequenced by the `Client`
|
31
|
+
component, or you may invoke them manually:
|
31
32
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
id
|
37
|
-
title(lang: $lang)
|
38
|
-
showtimes @include(if: $withShowtimes) {
|
39
|
-
time
|
40
|
-
}
|
41
|
-
}
|
42
|
-
}
|
43
|
-
GRAPHQL
|
44
|
-
|
45
|
-
request = GraphQL::Stitching::Request.new(
|
46
|
-
supergraph,
|
47
|
-
document,
|
48
|
-
variables: { "id" => "1" },
|
49
|
-
operation_name: "FetchMovie",
|
50
|
-
)
|
51
|
-
|
52
|
-
errors = MySchema.validate(request.document)
|
53
|
-
# return early with any static validation errors...
|
54
|
-
|
55
|
-
request.prepare!
|
56
|
-
```
|
57
|
-
|
58
|
-
Preparing a request will apply several destructive transformations:
|
59
|
-
|
60
|
-
- Default values from variable definitions will be added to request variables.
|
61
|
-
- The document will be pre-shaped based on `@skip` and `@include` directives.
|
33
|
+
1. `request.validate`: runs static validations on the request using the combined schema.
|
34
|
+
2. `request.prepare!`: inserts variable defaults and pre-renders skip/include conditional shaping.
|
35
|
+
3. `request.plan`: builds a plan for the request. May act as a setting for plans pulled from cache.
|
36
|
+
4. `request.execute`: executes the request, and returns the resulting data.
|
@@ -263,7 +263,6 @@ module GraphQL
|
|
263
263
|
enum_values_by_name_location = types_by_location.each_with_object({}) do |(location, type_candidate), memo|
|
264
264
|
type_candidate.enum_values.each do |enum_value_candidate|
|
265
265
|
memo[enum_value_candidate.graphql_name] ||= {}
|
266
|
-
memo[enum_value_candidate.graphql_name][location] ||= {}
|
267
266
|
memo[enum_value_candidate.graphql_name][location] = enum_value_candidate
|
268
267
|
end
|
269
268
|
end
|
@@ -376,7 +375,6 @@ module GraphQL
|
|
376
375
|
@field_map[type_name][field_candidate.name] << location
|
377
376
|
|
378
377
|
memo[field_name] ||= {}
|
379
|
-
memo[field_name][location] ||= {}
|
380
378
|
memo[field_name][location] = field_candidate
|
381
379
|
end
|
382
380
|
end
|
@@ -406,7 +404,6 @@ module GraphQL
|
|
406
404
|
args_by_name_location = members_by_location.each_with_object({}) do |(location, member_candidate), memo|
|
407
405
|
member_candidate.arguments.each do |argument_name, argument|
|
408
406
|
memo[argument_name] ||= {}
|
409
|
-
memo[argument_name][location] ||= {}
|
410
407
|
memo[argument_name][location] = argument
|
411
408
|
end
|
412
409
|
end
|
@@ -461,7 +458,6 @@ module GraphQL
|
|
461
458
|
directives_by_name_location = members_by_location.each_with_object({}) do |(location, member_candidate), memo|
|
462
459
|
member_candidate.directives.each do |directive|
|
463
460
|
memo[directive.graphql_name] ||= {}
|
464
|
-
memo[directive.graphql_name][location] ||= {}
|
465
461
|
memo[directive.graphql_name][location] = directive
|
466
462
|
end
|
467
463
|
end
|
@@ -652,22 +648,22 @@ module GraphQL
|
|
652
648
|
|
653
649
|
schemas.each do |schema|
|
654
650
|
introspection_types = schema.introspection_system.types.keys
|
655
|
-
schema.types.
|
651
|
+
schema.types.each_value do |type|
|
656
652
|
next if introspection_types.include?(type.graphql_name)
|
657
653
|
|
658
654
|
if type.kind.object? || type.kind.interface?
|
659
|
-
type.fields.
|
655
|
+
type.fields.each_value do |field|
|
660
656
|
field_type = field.type.unwrap
|
661
657
|
reads << field_type.graphql_name if field_type.kind.enum?
|
662
658
|
|
663
|
-
field.arguments.
|
659
|
+
field.arguments.each_value do |argument|
|
664
660
|
argument_type = argument.type.unwrap
|
665
661
|
writes << argument_type.graphql_name if argument_type.kind.enum?
|
666
662
|
end
|
667
663
|
end
|
668
664
|
|
669
665
|
elsif type.kind.input_object?
|
670
|
-
type.arguments.
|
666
|
+
type.arguments.each_value do |argument|
|
671
667
|
argument_type = argument.type.unwrap
|
672
668
|
writes << argument_type.graphql_name if argument_type.kind.enum?
|
673
669
|
end
|
@@ -20,10 +20,22 @@ module GraphQL::Stitching
|
|
20
20
|
result = @executor.request.supergraph.execute_at_location(op.location, query_document, query_variables, @executor.request)
|
21
21
|
@executor.query_count += 1
|
22
22
|
|
23
|
-
|
23
|
+
if result["data"]
|
24
|
+
if op.path.any?
|
25
|
+
# Nested root scopes must expand their pathed origin set
|
26
|
+
origin_set = op.path.reduce([@executor.data]) do |set, ns|
|
27
|
+
set.flat_map { |obj| obj && obj[ns] }.tap(&:compact!)
|
28
|
+
end
|
29
|
+
|
30
|
+
origin_set.each { _1.merge!(result["data"]) }
|
31
|
+
else
|
32
|
+
# Actual root scopes merge directly into results data
|
33
|
+
@executor.data.merge!(result["data"])
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
24
37
|
if result["errors"]&.any?
|
25
|
-
result["errors"]
|
26
|
-
@executor.errors.concat(result["errors"])
|
38
|
+
@executor.errors.concat(format_errors!(result["errors"], op.path))
|
27
39
|
end
|
28
40
|
|
29
41
|
ops.map(&:step)
|
@@ -51,6 +63,16 @@ module GraphQL::Stitching
|
|
51
63
|
doc << op.selections
|
52
64
|
doc
|
53
65
|
end
|
66
|
+
|
67
|
+
# Format response errors without a document location (because it won't match the request doc),
|
68
|
+
# and prepend any insertion path for the scope into error paths.
|
69
|
+
def format_errors!(errors, path)
|
70
|
+
errors.each do |err|
|
71
|
+
err.delete("locations")
|
72
|
+
err["path"].unshift(*path) if err["path"] && path.any?
|
73
|
+
end
|
74
|
+
errors
|
75
|
+
end
|
54
76
|
end
|
55
77
|
end
|
56
78
|
end
|
@@ -276,19 +276,21 @@ module GraphQL
|
|
276
276
|
routes.each_value do |route|
|
277
277
|
route.reduce(locale_selections) do |parent_selections, boundary|
|
278
278
|
# E.1) Add the key of each boundary query into the prior location's selection set.
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
279
|
+
if boundary.key
|
280
|
+
foreign_key = ExportSelection.key(boundary.key)
|
281
|
+
has_key = false
|
282
|
+
has_typename = false
|
283
|
+
|
284
|
+
parent_selections.each do |node|
|
285
|
+
next unless node.is_a?(GraphQL::Language::Nodes::Field)
|
286
|
+
has_key ||= node.alias == foreign_key
|
287
|
+
has_typename ||= node.alias == ExportSelection.typename_node.alias
|
288
|
+
end
|
289
|
+
|
290
|
+
parent_selections << ExportSelection.key_node(boundary.key) unless has_key
|
291
|
+
parent_selections << ExportSelection.typename_node unless has_typename
|
287
292
|
end
|
288
293
|
|
289
|
-
parent_selections << ExportSelection.key_node(boundary.key) unless has_key
|
290
|
-
parent_selections << ExportSelection.typename_node unless has_typename
|
291
|
-
|
292
294
|
# E.2) Add a planner step for each new entrypoint location.
|
293
295
|
add_step(
|
294
296
|
location: boundary.location,
|
@@ -296,7 +298,7 @@ module GraphQL
|
|
296
298
|
parent_type: parent_type,
|
297
299
|
selections: remote_selections_by_location[boundary.location] || [],
|
298
300
|
path: path.dup,
|
299
|
-
boundary: boundary,
|
301
|
+
boundary: boundary.key ? boundary : nil,
|
300
302
|
).selections
|
301
303
|
end
|
302
304
|
end
|
@@ -120,12 +120,13 @@ module GraphQL
|
|
120
120
|
end
|
121
121
|
|
122
122
|
# Validates the request using the combined supergraph schema.
|
123
|
+
# @return [Array<GraphQL::ExecutionError>] an array of static validation errors
|
123
124
|
def validate
|
124
125
|
result = @supergraph.static_validator.validate(@query)
|
125
126
|
result[:errors]
|
126
127
|
end
|
127
128
|
|
128
|
-
# Prepares the request for stitching by
|
129
|
+
# Prepares the request for stitching by inserting variable defaults and applying @skip/@include conditionals.
|
129
130
|
def prepare!
|
130
131
|
operation.variables.each do |v|
|
131
132
|
@variables[v.name] = v.default_value if @variables[v.name].nil? && !v.default_value.nil?
|
@@ -143,7 +144,17 @@ module GraphQL
|
|
143
144
|
self
|
144
145
|
end
|
145
146
|
|
146
|
-
# Gets and sets the query plan for the request. Assigned query plans may pull from cache
|
147
|
+
# Gets and sets the query plan for the request. Assigned query plans may pull from a cache,
|
148
|
+
# which is useful for redundant GraphQL documents (commonly sent by frontend clients).
|
149
|
+
# ```ruby
|
150
|
+
# if cached_plan = $cache.get(request.digest)
|
151
|
+
# plan = GraphQL::Stitching::Plan.from_json(JSON.parse(cached_plan))
|
152
|
+
# request.plan(plan)
|
153
|
+
# else
|
154
|
+
# plan = request.plan
|
155
|
+
# $cache.set(request.digest, JSON.generate(plan.as_json))
|
156
|
+
# end
|
157
|
+
# ```
|
147
158
|
# @param new_plan [Plan, nil] a cached query plan for the request.
|
148
159
|
# @return [Plan] query plan for the request.
|
149
160
|
def plan(new_plan = nil)
|
@@ -245,7 +245,11 @@ module GraphQL
|
|
245
245
|
# ("Type") => ["id", ...]
|
246
246
|
def possible_keys_for_type(type_name)
|
247
247
|
@possible_keys_by_type[type_name] ||= begin
|
248
|
-
@
|
248
|
+
if type_name == @schema.query.graphql_name
|
249
|
+
GraphQL::Stitching::EMPTY_ARRAY
|
250
|
+
else
|
251
|
+
@boundaries[type_name].map(&:key).tap(&:uniq!)
|
252
|
+
end
|
249
253
|
end
|
250
254
|
end
|
251
255
|
|
@@ -262,16 +266,25 @@ module GraphQL
|
|
262
266
|
# For a given type, route from one origin location to one or more remote locations
|
263
267
|
# used to connect a partial type across locations via boundary queries
|
264
268
|
def route_type_to_locations(type_name, start_location, goal_locations)
|
265
|
-
|
269
|
+
key_count = possible_keys_for_type(type_name).length
|
270
|
+
|
271
|
+
if key_count.zero?
|
272
|
+
# nested root scopes have no boundary keys and just return a location
|
273
|
+
goal_locations.each_with_object({}) do |goal_location, memo|
|
274
|
+
memo[goal_location] = [Boundary.new(location: goal_location)]
|
275
|
+
end
|
276
|
+
|
277
|
+
elsif key_count > 1
|
266
278
|
# multiple keys use an A* search to traverse intermediary locations
|
267
|
-
|
268
|
-
end
|
279
|
+
route_type_to_locations_via_search(type_name, start_location, goal_locations)
|
269
280
|
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
281
|
+
else
|
282
|
+
# types with a single key attribute must all be within a single hop of each other,
|
283
|
+
# so can use a simple match to collect boundaries for the goal locations.
|
284
|
+
@boundaries[type_name].each_with_object({}) do |boundary, memo|
|
285
|
+
if goal_locations.include?(boundary.location)
|
286
|
+
memo[boundary.location] = [boundary]
|
287
|
+
end
|
275
288
|
end
|
276
289
|
end
|
277
290
|
end
|
@@ -54,7 +54,7 @@ module GraphQL
|
|
54
54
|
return parent_type.possible_types if parent_type.kind.union?
|
55
55
|
|
56
56
|
result = []
|
57
|
-
schema.types.
|
57
|
+
schema.types.each_value do |type|
|
58
58
|
next unless type <= GraphQL::Schema::Interface && type != parent_type
|
59
59
|
next unless type.interfaces.include?(parent_type)
|
60
60
|
result << type
|
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.2.
|
4
|
+
version: 1.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Greg MacWilliam
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-02-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|
@@ -82,13 +82,11 @@ files:
|
|
82
82
|
- docs/README.md
|
83
83
|
- docs/client.md
|
84
84
|
- docs/composer.md
|
85
|
-
- docs/executor.md
|
86
85
|
- docs/http_executable.md
|
87
86
|
- docs/images/library.png
|
88
87
|
- docs/images/merging.png
|
89
88
|
- docs/images/stitching.png
|
90
89
|
- docs/mechanics.md
|
91
|
-
- docs/planner.md
|
92
90
|
- docs/request.md
|
93
91
|
- docs/supergraph.md
|
94
92
|
- examples/file_uploads/Gemfile
|
data/docs/executor.md
DELETED
@@ -1,68 +0,0 @@
|
|
1
|
-
## GraphQL::Stitching::Executor
|
2
|
-
|
3
|
-
An `Executor` accepts a [`Supergraph`](./supergraph.md), a [query plan hash](./planner.md), and optional request variables. It handles executing requests and merging results collected from across graph locations.
|
4
|
-
|
5
|
-
```ruby
|
6
|
-
query = <<~GRAPHQL
|
7
|
-
query MyQuery($id: ID!) {
|
8
|
-
product(id:$id) {
|
9
|
-
title
|
10
|
-
brands { name }
|
11
|
-
}
|
12
|
-
}
|
13
|
-
GRAPHQL
|
14
|
-
|
15
|
-
request = GraphQL::Stitching::Request.new(
|
16
|
-
supergraph,
|
17
|
-
query,
|
18
|
-
variables: { "id" => "123" },
|
19
|
-
operation_name: "MyQuery",
|
20
|
-
context: { ... },
|
21
|
-
)
|
22
|
-
|
23
|
-
# Via Request:
|
24
|
-
result = request.execute
|
25
|
-
|
26
|
-
# Via Executor:
|
27
|
-
result = GraphQL::Stitching::Executor.new(request).perform
|
28
|
-
```
|
29
|
-
|
30
|
-
### Raw results
|
31
|
-
|
32
|
-
By default, execution results are always returned with document shaping (stitching additions removed, missing fields added, null bubbling applied). You may access the raw execution result by calling the `perform` method with a `raw: true` argument:
|
33
|
-
|
34
|
-
```ruby
|
35
|
-
# get the raw result without shaping using either form:
|
36
|
-
raw_result = request.execute(raw: true)
|
37
|
-
raw_result = GraphQL::Stitching::Executor.new(request).perform(raw: true)
|
38
|
-
```
|
39
|
-
|
40
|
-
The raw result will contain many irregularities from the stitching process, however may be insightful when debugging inconsistencies in results:
|
41
|
-
|
42
|
-
```ruby
|
43
|
-
{
|
44
|
-
"data" => {
|
45
|
-
"product" => {
|
46
|
-
"upc" => "1",
|
47
|
-
"_export_upc" => "1",
|
48
|
-
"_export_typename" => "Product",
|
49
|
-
"name" => "iPhone",
|
50
|
-
"price" => nil,
|
51
|
-
}
|
52
|
-
}
|
53
|
-
}
|
54
|
-
```
|
55
|
-
|
56
|
-
### Batching
|
57
|
-
|
58
|
-
The Executor batches together as many requests as possible to a given location at a given time. Batched queries are written with the operation name suffixed by all operation keys in the batch, and root stitching fields are each prefixed by their batch index and collection index (for non-list fields):
|
59
|
-
|
60
|
-
```graphql
|
61
|
-
query MyOperation_2_3($lang:String!,$currency:Currency!){
|
62
|
-
_0_result: storefronts(ids:["7","8"]) { name(lang:$lang) }
|
63
|
-
_1_0_result: product(upc:"abc") { price(currency:$currency) }
|
64
|
-
_1_1_result: product(upc:"xyz") { price(currency:$currency) }
|
65
|
-
}
|
66
|
-
```
|
67
|
-
|
68
|
-
All told, the executor will make one request per location per generation of data. Generations started on separate forks of the resolution tree will be resolved independently.
|
data/docs/planner.md
DELETED
@@ -1,45 +0,0 @@
|
|
1
|
-
## GraphQL::Stitching::Planner
|
2
|
-
|
3
|
-
A `Planner` generates a query plan for a given [`Supergraph`](./supergraph.md) and [`Request`](./request.md). The generated plan breaks down all the discrete GraphQL operations that must be delegated across locations and their sequencing.
|
4
|
-
|
5
|
-
```ruby
|
6
|
-
document = <<~GRAPHQL
|
7
|
-
query MyQuery($id: ID!) {
|
8
|
-
product(id:$id) {
|
9
|
-
title
|
10
|
-
brands { name }
|
11
|
-
}
|
12
|
-
}
|
13
|
-
GRAPHQL
|
14
|
-
|
15
|
-
request = GraphQL::Stitching::Request.new(
|
16
|
-
supergraph,
|
17
|
-
document,
|
18
|
-
variables: { "id" => "1" },
|
19
|
-
operation_name: "MyQuery",
|
20
|
-
).prepare!
|
21
|
-
|
22
|
-
# Via Request:
|
23
|
-
plan = request.plan
|
24
|
-
|
25
|
-
# Via Planner:
|
26
|
-
plan = GraphQL::Stitching::Planner.new(request).perform
|
27
|
-
```
|
28
|
-
|
29
|
-
### Caching
|
30
|
-
|
31
|
-
Plans are designed to be cacheable. This is very useful for redundant GraphQL documents (commonly sent by frontend clients) where there's no sense in planning every request individually. It's far more efficient to generate a plan once and cache it, then simply retreive the plan and execute it for future requests.
|
32
|
-
|
33
|
-
```ruby
|
34
|
-
cached_plan = $cache.get(request.digest)
|
35
|
-
|
36
|
-
if cached_plan
|
37
|
-
plan = GraphQL::Stitching::Plan.from_json(JSON.parse(cached_plan))
|
38
|
-
request.plan(plan)
|
39
|
-
else
|
40
|
-
plan = request.plan
|
41
|
-
$cache.set(request.digest, JSON.generate(plan.as_json))
|
42
|
-
end
|
43
|
-
|
44
|
-
# execute the plan...
|
45
|
-
```
|