graphql-stitching 1.7.0 → 1.7.1
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 +58 -424
- data/docs/composing_a_supergraph.md +215 -0
- data/docs/error_handling.md +69 -0
- data/docs/executables.md +112 -0
- data/docs/introduction.md +17 -0
- data/docs/merged_types.md +457 -0
- data/docs/{federation_entities.md → merged_types_apollo.md} +1 -1
- data/docs/performance.md +71 -0
- data/docs/query_planning.md +102 -0
- data/docs/serving_a_supergraph.md +152 -0
- data/docs/subscriptions.md +1 -1
- data/docs/visibility.md +21 -11
- data/lib/graphql/stitching/client.rb +6 -0
- data/lib/graphql/stitching/composer.rb +18 -10
- data/lib/graphql/stitching/request.rb +4 -0
- data/lib/graphql/stitching/supergraph.rb +4 -2
- data/lib/graphql/stitching/version.rb +1 -1
- metadata +11 -11
- data/docs/README.md +0 -19
- data/docs/client.md +0 -107
- data/docs/composer.md +0 -125
- data/docs/http_executable.md +0 -51
- data/docs/mechanics.md +0 -306
- data/docs/request.md +0 -34
- data/docs/supergraph.md +0 -31
- data/docs/type_resolver.md +0 -101
@@ -0,0 +1,457 @@
|
|
1
|
+
## Merged Types
|
2
|
+
|
3
|
+
`Object` and `Interface` types may exist with different fields in different graph locations, and will get merged together in the combined supergraph schema.
|
4
|
+
|
5
|
+

|
6
|
+
|
7
|
+
To facilitate this, schemas should be designed around **merged type keys** that stitching can cross-reference and fetch across locations using **type resolver queries** (discussed below). For those in an Apollo ecosystem, there's also _limited_ support for merging types though [federation `_entities`](./merged_types_apollo.md).
|
8
|
+
|
9
|
+
### Merged type keys
|
10
|
+
|
11
|
+
Foreign keys in a GraphQL schema frequently look like the `Product.imageId` field here:
|
12
|
+
|
13
|
+
```graphql
|
14
|
+
# -- Products schema:
|
15
|
+
|
16
|
+
type Product {
|
17
|
+
id: ID!
|
18
|
+
imageId: ID!
|
19
|
+
}
|
20
|
+
|
21
|
+
# -- Images schema:
|
22
|
+
|
23
|
+
type Image {
|
24
|
+
id: ID!
|
25
|
+
url: String!
|
26
|
+
}
|
27
|
+
```
|
28
|
+
|
29
|
+
However, this design does not lend itself to merging types 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 objects in other locations:
|
30
|
+
|
31
|
+
```graphql
|
32
|
+
# -- Products schema:
|
33
|
+
|
34
|
+
type Product {
|
35
|
+
id: ID!
|
36
|
+
image: Image!
|
37
|
+
}
|
38
|
+
|
39
|
+
type Image {
|
40
|
+
id: ID!
|
41
|
+
}
|
42
|
+
|
43
|
+
# -- Images schema:
|
44
|
+
|
45
|
+
type Image {
|
46
|
+
id: ID!
|
47
|
+
url: String!
|
48
|
+
}
|
49
|
+
```
|
50
|
+
|
51
|
+
### Merged type resolver queries
|
52
|
+
|
53
|
+
Each location that provides a unique variant of a type must provide at least one _resolver query_ for accessing it. Type resolvers are root queries identified by a `@stitch` directive:
|
54
|
+
|
55
|
+
```graphql
|
56
|
+
directive @stitch(key: String!, arguments: String, typeName: String) repeatable on FIELD_DEFINITION
|
57
|
+
```
|
58
|
+
|
59
|
+
This directive tells stitching how to cross-reference and fetch types from across locations, for example:
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
products_schema = <<~GRAPHQL
|
63
|
+
directive @stitch(key: String!, arguments: String) repeatable on FIELD_DEFINITION
|
64
|
+
|
65
|
+
type Product {
|
66
|
+
id: ID!
|
67
|
+
name: String!
|
68
|
+
}
|
69
|
+
|
70
|
+
type Query {
|
71
|
+
product(id: ID!): Product @stitch(key: "id")
|
72
|
+
}
|
73
|
+
GRAPHQL
|
74
|
+
|
75
|
+
catalog_schema = <<~GRAPHQL
|
76
|
+
directive @stitch(key: String!, arguments: String) repeatable on FIELD_DEFINITION
|
77
|
+
|
78
|
+
type Product {
|
79
|
+
id: ID!
|
80
|
+
price: Float!
|
81
|
+
}
|
82
|
+
|
83
|
+
type Query {
|
84
|
+
products(ids: [ID!]!): [Product]! @stitch(key: "id")
|
85
|
+
}
|
86
|
+
GRAPHQL
|
87
|
+
|
88
|
+
client = GraphQL::Stitching::Client.new(locations: {
|
89
|
+
products: {
|
90
|
+
schema: GraphQL::Schema.from_definition(products_schema),
|
91
|
+
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3001"),
|
92
|
+
},
|
93
|
+
catalog: {
|
94
|
+
schema: GraphQL::Schema.from_definition(catalog_schema),
|
95
|
+
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3002"),
|
96
|
+
},
|
97
|
+
})
|
98
|
+
```
|
99
|
+
|
100
|
+
Focusing on the `@stitch` directive usage:
|
101
|
+
|
102
|
+
```graphql
|
103
|
+
type Product {
|
104
|
+
id: ID!
|
105
|
+
name: String!
|
106
|
+
}
|
107
|
+
type Query {
|
108
|
+
product(id: ID!): Product @stitch(key: "id")
|
109
|
+
}
|
110
|
+
```
|
111
|
+
|
112
|
+
* The `@stitch` directive marks a root query where the merged type may be accessed. The merged type identity is inferred from the field return.
|
113
|
+
* The `key: "id"` parameter indicates that an `{ id }` must be selected from prior locations so it can be submitted as an argument to this query. The query argument used to send the key is inferred when possible ([more on arguments](#argument-shapes) later).
|
114
|
+
|
115
|
+
Merged types must have a resolver query in each of their possible locations. The one exception to this requirement are [outbound-only types](#outbound-only-merged-types) that contain no exclusive data; these may omit their resolver because they never require an inbound request to fetch them.
|
116
|
+
|
117
|
+
#### List queries
|
118
|
+
|
119
|
+
It's generally preferable to provide a list accessor as a resolver query for optimal batching. The only requirement is that both the field argument and the return type must be lists, and the query results are expected to be a mapped set with `null` holding the position of missing results.
|
120
|
+
|
121
|
+
```graphql
|
122
|
+
type Query {
|
123
|
+
products(ids: [ID!]!): [Product]! @stitch(key: "id")
|
124
|
+
}
|
125
|
+
|
126
|
+
# input: ["1", "2", "3"]
|
127
|
+
# result: [{ id: "1" }, null, { id: "3" }]
|
128
|
+
```
|
129
|
+
|
130
|
+
See [error handling](./error_handling.md#list-queries) tips for list queries.
|
131
|
+
|
132
|
+
#### Abstract queries
|
133
|
+
|
134
|
+
It's okay for resolver queries to be implemented through abstract types. An abstract query will provide access to all of its possible types by default, each of which must implement the key.
|
135
|
+
|
136
|
+
```graphql
|
137
|
+
interface Node {
|
138
|
+
id: ID!
|
139
|
+
}
|
140
|
+
type Product implements Node {
|
141
|
+
id: ID!
|
142
|
+
name: String!
|
143
|
+
}
|
144
|
+
type Query {
|
145
|
+
nodes(ids: [ID!]!): [Node]! @stitch(key: "id")
|
146
|
+
}
|
147
|
+
```
|
148
|
+
|
149
|
+
To customize which types an abstract query provides and their respective keys, add a `typeName` constraint. This can be repeated to select multiple types from an abstract.
|
150
|
+
|
151
|
+
```graphql
|
152
|
+
type Product { sku: ID! }
|
153
|
+
type Order { id: ID! }
|
154
|
+
type Customer { id: ID! } # << not stitched
|
155
|
+
union Entity = Product | Order | Customer
|
156
|
+
|
157
|
+
type Query {
|
158
|
+
entity(key: ID!): Entity
|
159
|
+
@stitch(key: "sku", typeName: "Product")
|
160
|
+
@stitch(key: "id", typeName: "Order")
|
161
|
+
}
|
162
|
+
```
|
163
|
+
|
164
|
+
#### Argument shapes
|
165
|
+
|
166
|
+
Stitching infers which argument to use for queries with a single argument, or when the key name matches its intended argument. For custom mappings, use the `arguments` option:
|
167
|
+
|
168
|
+
```graphql
|
169
|
+
type Product {
|
170
|
+
id: ID!
|
171
|
+
}
|
172
|
+
union Entity = Product
|
173
|
+
|
174
|
+
type Query {
|
175
|
+
entity(key: ID!, type: String!): Entity @stitch(
|
176
|
+
key: "id",
|
177
|
+
arguments: "key: $.id, type: $.__typename",
|
178
|
+
typeName: "Product",
|
179
|
+
)
|
180
|
+
}
|
181
|
+
```
|
182
|
+
|
183
|
+
The `arguments` option specifies a template of [GraphQL arguments](https://spec.graphql.org/October2021/#sec-Language.Arguments) (or, GraphQL syntax that would normally be written into an arguments closure). This template may include key insertions prefixed by `$` with dot-notation paths to any selections made by the resolver `key`. A `__typename` key selection is also always available. This arguments syntax allows sending multiple arguments that intermix stitching keys with complex input shapes and/or static values.
|
184
|
+
|
185
|
+
<details>
|
186
|
+
<summary>All argument patterns</summary>
|
187
|
+
|
188
|
+
---
|
189
|
+
|
190
|
+
**List arguments**
|
191
|
+
|
192
|
+
List arguments may specify input just like non-list arguments, and [GraphQL list input coercion](https://spec.graphql.org/October2021/#sec-List.Input-Coercion) will assume the shape represents a list item:
|
193
|
+
|
194
|
+
```graphql
|
195
|
+
type Query {
|
196
|
+
product(ids: [ID!]!, organization: ID!): [Product]!
|
197
|
+
@stitch(key: "id", arguments: "ids: $.id, organization: '1'")
|
198
|
+
}
|
199
|
+
```
|
200
|
+
|
201
|
+
List resolvers (that return list types) may _only_ insert keys into repeatable list arguments, while non-list arguments may only contain static values. Nested list inputs are neither common nor practical, so are not supported.
|
202
|
+
|
203
|
+
**Scalar & Enum arguments**
|
204
|
+
|
205
|
+
Built-in scalars are written as normal literal values. For convenience, string literals may be enclosed in single quotes rather than escaped double-quotes:
|
206
|
+
|
207
|
+
```graphql
|
208
|
+
enum DataSource { CACHE }
|
209
|
+
type Query {
|
210
|
+
product(id: ID!, source: String!): Product
|
211
|
+
@stitch(key: "id", arguments: "id: $.id, source: 'cache'")
|
212
|
+
|
213
|
+
variant(id: ID!, source: DataSource!): Variant
|
214
|
+
@stitch(key: "id", arguments: "id: $.id, source: CACHE")
|
215
|
+
}
|
216
|
+
```
|
217
|
+
|
218
|
+
**InputObject arguments**
|
219
|
+
|
220
|
+
Input objects may be provided anywhere in the input, even as nested structures. The stitching resolver will build the specified object shape:
|
221
|
+
|
222
|
+
```graphql
|
223
|
+
input ComplexKey {
|
224
|
+
id: ID
|
225
|
+
nested: ComplexKey
|
226
|
+
}
|
227
|
+
type Query {
|
228
|
+
product(key: ComplexKey!): [Product]!
|
229
|
+
@stitch(key: "id", arguments: "key: { nested: { id: $.id } }")
|
230
|
+
}
|
231
|
+
```
|
232
|
+
|
233
|
+
**Custom scalar arguments**
|
234
|
+
|
235
|
+
Custom scalar keys allow any input shape to be submitted, from primitive scalars to complex object structures. These values will be sent and recieved as untyped JSON input, which makes them flexible but quite lax with validation:
|
236
|
+
|
237
|
+
```graphql
|
238
|
+
type Product {
|
239
|
+
id: ID!
|
240
|
+
}
|
241
|
+
union Entity = Product
|
242
|
+
scalar Key
|
243
|
+
|
244
|
+
type Query {
|
245
|
+
entities(representations: [Key!]!): [Entity]!
|
246
|
+
@stitch(key: "id", arguments: "representations: { id: $.id, __typename: $.__typename }")
|
247
|
+
}
|
248
|
+
```
|
249
|
+
|
250
|
+
---
|
251
|
+
</details>
|
252
|
+
|
253
|
+
#### Composite type keys
|
254
|
+
|
255
|
+
Resolver keys may make composite selections for multiple key fields and/or nested scopes, for example:
|
256
|
+
|
257
|
+
```graphql
|
258
|
+
interface FieldOwner {
|
259
|
+
id: ID!
|
260
|
+
}
|
261
|
+
type CustomField {
|
262
|
+
owner: FieldOwner!
|
263
|
+
key: String!
|
264
|
+
value: String
|
265
|
+
}
|
266
|
+
input CustomFieldLookup {
|
267
|
+
ownerId: ID!
|
268
|
+
ownerType: String!
|
269
|
+
key: String!
|
270
|
+
}
|
271
|
+
|
272
|
+
type Query {
|
273
|
+
customFields(lookups: [CustomFieldLookup!]!): [CustomField]! @stitch(
|
274
|
+
key: "owner { id __typename } key",
|
275
|
+
arguments: "lookups: { ownerId: $.owner.id, ownerType: $.owner.__typename, key: $.key }"
|
276
|
+
)
|
277
|
+
}
|
278
|
+
```
|
279
|
+
|
280
|
+
Note that composite key selections may _not_ be distributed across locations. The complete selection criteria must be available in each location that provides the key.
|
281
|
+
|
282
|
+
#### Multiple type keys
|
283
|
+
|
284
|
+
A type may exist in multiple locations across the graph using different keys, for example:
|
285
|
+
|
286
|
+
```graphql
|
287
|
+
type Product { id:ID! } # storefronts location
|
288
|
+
type Product { id:ID! sku:ID! } # products location
|
289
|
+
type Product { sku:ID! } # catelog location
|
290
|
+
```
|
291
|
+
|
292
|
+
In the above graph, the `storefronts` and `catelog` locations have different keys that join through an intermediary. This pattern is perfectly valid and resolvable as long as the intermediary provides resolver queries for each possible key:
|
293
|
+
|
294
|
+
```graphql
|
295
|
+
type Product {
|
296
|
+
id: ID!
|
297
|
+
sku: ID!
|
298
|
+
}
|
299
|
+
type Query {
|
300
|
+
productById(id: ID!): Product @stitch(key: "id")
|
301
|
+
productBySku(sku: ID!): Product @stitch(key: "sku")
|
302
|
+
}
|
303
|
+
```
|
304
|
+
|
305
|
+
The `@stitch` directive is also repeatable, allowing a single query to associate with multiple keys:
|
306
|
+
|
307
|
+
```graphql
|
308
|
+
type Product {
|
309
|
+
id: ID!
|
310
|
+
sku: ID!
|
311
|
+
}
|
312
|
+
type Query {
|
313
|
+
product(id: ID, sku: ID): Product @stitch(key: "id") @stitch(key: "sku")
|
314
|
+
}
|
315
|
+
```
|
316
|
+
|
317
|
+
#### Null merges
|
318
|
+
|
319
|
+
It's okay for a merged type resolver to return `null` for an object as long as all unique fields of the type allow null. For example, the following merge works:
|
320
|
+
|
321
|
+
```graphql
|
322
|
+
# -- Request
|
323
|
+
|
324
|
+
query {
|
325
|
+
movieA(id: "23") {
|
326
|
+
id
|
327
|
+
title
|
328
|
+
rating
|
329
|
+
}
|
330
|
+
}
|
331
|
+
|
332
|
+
# -- Location A
|
333
|
+
|
334
|
+
type Movie {
|
335
|
+
id: String!
|
336
|
+
title: String!
|
337
|
+
}
|
338
|
+
|
339
|
+
type Query {
|
340
|
+
movieA(id: ID!): Movie @stitch(key: "id")
|
341
|
+
# (id: "23") -> { id: "23", title: "Jurassic Park" }
|
342
|
+
}
|
343
|
+
|
344
|
+
# -- Location B
|
345
|
+
|
346
|
+
type Movie {
|
347
|
+
id: String!
|
348
|
+
rating: Int
|
349
|
+
}
|
350
|
+
|
351
|
+
type Query {
|
352
|
+
movieB(id: ID!): Movie @stitch(key: "id")
|
353
|
+
# (id: "23") -> null
|
354
|
+
}
|
355
|
+
```
|
356
|
+
|
357
|
+
And produces this result:
|
358
|
+
|
359
|
+
```json
|
360
|
+
{
|
361
|
+
"data": {
|
362
|
+
"id": "23",
|
363
|
+
"title": "Jurassic Park",
|
364
|
+
"rating": null
|
365
|
+
}
|
366
|
+
}
|
367
|
+
```
|
368
|
+
|
369
|
+
Location B is allowed to return `null` here because its one unique field (`rating`) is nullable. If `rating` were non-null, then null bubbling would invalidate the response object.
|
370
|
+
|
371
|
+
### Adding @stitch directives
|
372
|
+
|
373
|
+
The `@stitch` directive can be added to class-based schemas using the provided definition:
|
374
|
+
|
375
|
+
```ruby
|
376
|
+
class Query < GraphQL::Schema::Object
|
377
|
+
field :product, Product, null: false do
|
378
|
+
directive(GraphQL::Stitching::Directives::Stitch, key: "id")
|
379
|
+
argument(:id, ID, required: true)
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
class Schema < GraphQL::Schema
|
384
|
+
directive(GraphQL::Stitching::Directives::Stitch)
|
385
|
+
query(Query)
|
386
|
+
end
|
387
|
+
```
|
388
|
+
|
389
|
+
Alternatively, a clean schema can have stitching directives applied from static configuration passed as a location's `stitch` option:
|
390
|
+
|
391
|
+
```ruby
|
392
|
+
sdl_string = <<~GRAPHQL
|
393
|
+
type Product {
|
394
|
+
id: ID!
|
395
|
+
sku: ID!
|
396
|
+
}
|
397
|
+
type Query {
|
398
|
+
productById(id: ID!): Product
|
399
|
+
productBySku(sku: ID!): Product
|
400
|
+
}
|
401
|
+
GRAPHQL
|
402
|
+
|
403
|
+
client = GraphQL::Stitching::Client.new(locations: {
|
404
|
+
products: {
|
405
|
+
schema: GraphQL::Schema.from_definition(sdl_string),
|
406
|
+
executable: ->() { ... },
|
407
|
+
stitch: [
|
408
|
+
{ field_name: "productById", key: "id" },
|
409
|
+
{ field_name: "productBySku", key: "sku", arguments: "mySku: $.sku" },
|
410
|
+
]
|
411
|
+
},
|
412
|
+
# ...
|
413
|
+
})
|
414
|
+
```
|
415
|
+
|
416
|
+
### Outbound-only merged types
|
417
|
+
|
418
|
+
Merged types do not always require a resolver query. For example:
|
419
|
+
|
420
|
+
```graphql
|
421
|
+
# -- Location A
|
422
|
+
|
423
|
+
type Widget {
|
424
|
+
id: ID!
|
425
|
+
name: String
|
426
|
+
price: Float
|
427
|
+
}
|
428
|
+
|
429
|
+
type Query {
|
430
|
+
widgetA(id: ID!): Widget @stitch(key: "id")
|
431
|
+
}
|
432
|
+
|
433
|
+
# -- Location B
|
434
|
+
|
435
|
+
type Widget {
|
436
|
+
id: ID!
|
437
|
+
size: Float
|
438
|
+
}
|
439
|
+
|
440
|
+
type Query {
|
441
|
+
widgetB(id: ID!): Widget @stitch(key: "id")
|
442
|
+
}
|
443
|
+
|
444
|
+
# -- Location C
|
445
|
+
|
446
|
+
type Widget {
|
447
|
+
id: ID!
|
448
|
+
name: String
|
449
|
+
size: Float
|
450
|
+
}
|
451
|
+
|
452
|
+
type Query {
|
453
|
+
featuredWidget: Widget
|
454
|
+
}
|
455
|
+
```
|
456
|
+
|
457
|
+
In this graph, `Widget` is a merged type without a resolver query in location C. This works because all of its fields are resolvable in other locations; that means location C can provide outbound representations of this type without ever needing to resolve inbound requests for it. Outbound types do still require a shared key field (such as `id` above) that allow them to join with data in other resolver locations (such as `price` above).
|
@@ -8,7 +8,7 @@ To avoid confusion, using [basic resolver queries](../README.md#merged-type-reso
|
|
8
8
|
|
9
9
|
The following subset of the federation spec is supported:
|
10
10
|
|
11
|
-
- `@key(fields: "id")` (repeatable) specifies a key field for an object type.
|
11
|
+
- `@key(fields: "id")` (repeatable) specifies a key field for an object type.
|
12
12
|
- `_Entity` is a union type that must contain all types that implement a `@key`.
|
13
13
|
- `_Any` is a scalar that recieves raw JSON objects; each object representation contains a `__typename` and the type's key field.
|
14
14
|
- `_entities(representations: [_Any!]!): [_Entity]!` is a root query for local entity types.
|
data/docs/performance.md
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
## Performance
|
2
|
+
|
3
|
+
There are many considerations that can aid in the performance of a stitched schema.
|
4
|
+
|
5
|
+
### Batching
|
6
|
+
|
7
|
+
The stitching executor automatically batches subgraph requests so that only one request is made per location per generation of data (multiple generations can still force the executor to return to a location for more data). This is done using batched queries that combine all data access for a given a location. For example:
|
8
|
+
|
9
|
+
```graphql
|
10
|
+
query MyOperation_2($_0_key:[ID!]!, $_1_0_key:ID!, $_1_1_key:ID!, $_1_2_key:ID!) {
|
11
|
+
_0_result: widgets(ids: $_0_key) { ... } # << 3 Widget
|
12
|
+
_1_0_result: sprocket(id: $_1_0_key) { ... } # << 1 Sprocket
|
13
|
+
_1_1_result: sprocket(id: $_1_1_key) { ... } # << 1 Sprocket
|
14
|
+
_1_2_result: sprocket(id: $_1_2_key) { ... } # << 1 Sprocket
|
15
|
+
}
|
16
|
+
```
|
17
|
+
|
18
|
+
You can make optimal use of this batching behavior by following some best-practices:
|
19
|
+
|
20
|
+
1. List queries (like the `widgets` selection above) are preferable as resolver queries because they keep the batched document consistent regardless of set size, and make for smaller documents that parse and validate faster.
|
21
|
+
|
22
|
+
2. Root subgraph fields used as merged type resolvers (like the three `sprocket` selections above) should implement [batch loading](https://github.com/Shopify/graphql-batch) to anticipate repeated selections. Never assume that a root field will only be selected once per request.
|
23
|
+
|
24
|
+
### Query plan caching
|
25
|
+
|
26
|
+
A stitching client provides caching hooks for saving query plans. Without caching, every request made to the client will be planned individually. With caching, a query may be planned once, cached, and then executed from cache on subsequent requests. The provided `request` object includes digests for use in cache keys:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
client = GraphQL::Stitching::Client.new(locations: { ... })
|
30
|
+
|
31
|
+
client.on_cache_read do |request|
|
32
|
+
# get a cached query plan...
|
33
|
+
$cache.get(request.digest)
|
34
|
+
end
|
35
|
+
|
36
|
+
client.on_cache_write do |request, payload|
|
37
|
+
# write a computed query plan...
|
38
|
+
$cache.set(request.digest, payload)
|
39
|
+
end
|
40
|
+
```
|
41
|
+
|
42
|
+
Note that inlined input data (such as the `id: "1"` argument below) works against caching, so you should _avoid_ these input literals when possible:
|
43
|
+
|
44
|
+
```graphql
|
45
|
+
query {
|
46
|
+
product(id: "1") { name }
|
47
|
+
}
|
48
|
+
```
|
49
|
+
|
50
|
+
Instead, leverage query variables so that the document body remains consistent across requests:
|
51
|
+
|
52
|
+
```graphql
|
53
|
+
query($id: ID!) {
|
54
|
+
product(id: $id) { name }
|
55
|
+
}
|
56
|
+
|
57
|
+
# variables: { "id" => "1" }
|
58
|
+
```
|
59
|
+
|
60
|
+
### Digests
|
61
|
+
|
62
|
+
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`:
|
63
|
+
|
64
|
+
_config/initializers/graphql_stitching.rb_
|
65
|
+
```ruby
|
66
|
+
GraphQL::Stitching.digest { |str| Digest::Blake3.hexdigest("v2/#{str}") }
|
67
|
+
```
|
68
|
+
|
69
|
+
### Concurrency
|
70
|
+
|
71
|
+
The stitching executor builds atop the Ruby fiber-based implementation of `GraphQL::Dataloader`. Non-blocking concurrency requires setting a fiber scheduler via `Fiber.set_scheduler`, see [graphql-ruby docs](https://graphql-ruby.org/dataloader/nonblocking.html). You may also need to build your own remote clients using corresponding HTTP libraries.
|
@@ -0,0 +1,102 @@
|
|
1
|
+
## Query Planning
|
2
|
+
|
3
|
+
|
4
|
+
### Root selection routing
|
5
|
+
|
6
|
+
It's okay if root field names are repeated across locations. The entrypoint location will be used when routing root selections:
|
7
|
+
|
8
|
+
```graphql
|
9
|
+
# -- Location A
|
10
|
+
|
11
|
+
type Movie {
|
12
|
+
id: String!
|
13
|
+
rating: Int!
|
14
|
+
}
|
15
|
+
|
16
|
+
type Query {
|
17
|
+
movie(id: ID!): Movie @stitch(key: "id") # << set as root entrypoint
|
18
|
+
}
|
19
|
+
|
20
|
+
# -- Location B
|
21
|
+
|
22
|
+
type Movie {
|
23
|
+
id: String!
|
24
|
+
reviews: [String!]!
|
25
|
+
}
|
26
|
+
|
27
|
+
type Query {
|
28
|
+
movie(id: ID!): Movie @stitch(key: "id")
|
29
|
+
}
|
30
|
+
|
31
|
+
# -- Request
|
32
|
+
|
33
|
+
query {
|
34
|
+
movie(id: "23") { id } # routes to Location A
|
35
|
+
}
|
36
|
+
```
|
37
|
+
|
38
|
+
Note that primary location routing _only_ applies to selections in the root scope. If the `Query` type appears again lower in the graph, then its fields are resolved as normal object fields outside of root context, for example:
|
39
|
+
|
40
|
+
```graphql
|
41
|
+
schema {
|
42
|
+
query: Query # << root query, uses primary locations
|
43
|
+
}
|
44
|
+
|
45
|
+
type Query {
|
46
|
+
subquery: Query # << subquery, acts as a normal object type
|
47
|
+
}
|
48
|
+
```
|
49
|
+
|
50
|
+
Also note that stitching queries (denoted by the `@stitch` directive) are completely separate from field routing concerns. A `@stitch` directive establishes a contract for resolving a given type in a given location. This contract is always used to collect stitching data, regardless of how request routing selected the location for use.
|
51
|
+
|
52
|
+
### Field selection routing
|
53
|
+
|
54
|
+
Fields of a merged type may exist in multiple locations. For example, the `title` field below is provided by both locations:
|
55
|
+
|
56
|
+
```graphql
|
57
|
+
# -- Location A
|
58
|
+
|
59
|
+
type Movie {
|
60
|
+
id: String!
|
61
|
+
title: String! # shared
|
62
|
+
rating: Int!
|
63
|
+
}
|
64
|
+
|
65
|
+
type Query {
|
66
|
+
movieA(id: ID!): Movie @stitch(key: "id")
|
67
|
+
}
|
68
|
+
|
69
|
+
# -- Location B
|
70
|
+
|
71
|
+
type Movie {
|
72
|
+
id: String!
|
73
|
+
title: String! # shared
|
74
|
+
reviews: [String!]!
|
75
|
+
}
|
76
|
+
|
77
|
+
type Query {
|
78
|
+
movieB(id: ID!): Movie @stitch(key: "id")
|
79
|
+
}
|
80
|
+
```
|
81
|
+
|
82
|
+
When planning a request, field selections always attempt to use the current routing location that originates from the selection root, for example:
|
83
|
+
|
84
|
+
```graphql
|
85
|
+
query GetTitleFromA {
|
86
|
+
movieA(id: "23") { # <- enter via Location A
|
87
|
+
title # <- source from Location A
|
88
|
+
}
|
89
|
+
}
|
90
|
+
|
91
|
+
query GetTitleFromB {
|
92
|
+
movieB(id: "23") { # <- enter via Location B
|
93
|
+
title # <- source from Location B
|
94
|
+
}
|
95
|
+
}
|
96
|
+
```
|
97
|
+
|
98
|
+
Field selections that are NOT available in the current routing location delegate to new locations as follows:
|
99
|
+
|
100
|
+
1. Fields with only one location automatically use that location.
|
101
|
+
2. Fields with multiple locations attempt to use a location added during step-1.
|
102
|
+
3. Any remaining fields pick a location based on their highest availability among locations.
|