graphql-stitching 1.6.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b6b2cc734796d7455701bcc5b376a8efb49534c43b7a75de53bc1f2e686bf54c
4
- data.tar.gz: 449c09b94257de6ae720b4b7938c13b8881da9f1f8dc7684aabdc4c493b2c6d2
3
+ metadata.gz: 43ca2665bc1d0e0a87eead760ee4f7b1caddf324861b66414bc011958901eeff
4
+ data.tar.gz: 18a14928e703744d29b57665c51ecdb6a17b1d9d2b7bc58fe8727a37b55fbcd5
5
5
  SHA512:
6
- metadata.gz: 94345a14cc9bcee462854188b543a170b3dbd65cc948e55773e1a07357f47026f41018ea35eedbacc8647d8a92634be1f143d86543f0ea092f48ddb1281a86f5
7
- data.tar.gz: ae7cb67b6f36ca209f327d43bd293fa6afc4e6a93e8e91e83f4acf2893b744831836959402cd236e789ade5602857bb39d484025ea58c7e16d19b9ec5eb69d67
6
+ metadata.gz: b5ee7817b699447b35c937eb3df872433f8c318310debab6fea1577513171260a39e900c64cf37b4bfcb6f4fde4b509b5216ddf95879dd53a9878a3924016b5d
7
+ data.tar.gz: 0ddda7a4df33d5b6946a3a6283a4388d507c18d7ff758db1b2f09179546d6be87b6e78390fa6f623442e0f6e79a91aac69b0922542c4134673175377c47cfc0f
data/README.md CHANGED
@@ -9,16 +9,29 @@ GraphQL stitching composes a single schema from multiple underlying GraphQL reso
9
9
  - Merged object and abstract types joining though multiple keys.
10
10
  - Shared objects, fields, enums, and inputs across locations.
11
11
  - Combining local and remote schemas.
12
- - [File uploads](./docs/http_executable.md) via multipart forms.
12
+ - [Visibility controls](./docs/visibility.md) for hiding schema elements.
13
+ - [File uploads](./docs/executables.md) via multipart forms.
13
14
  - Tested with all minor versions of `graphql-ruby`.
14
15
 
15
16
  **NOT Supported:**
16
17
  - Computed fields (ie: federation-style `@requires`).
17
18
  - Defer/stream.
18
19
 
19
- This Ruby implementation is designed as a generic library to join basic spec-compliant GraphQL schemas using their existing types and fields in a [DIY](https://dictionary.cambridge.org/us/dictionary/english/diy) capacity. The opportunity here is for a Ruby application to stitch its local schemas together or onto remote sources without requiring an additional proxy service running in another language. If your goal is a purely high-throughput federation gateway with managed schema deployments, consider more opinionated frameworks such as [Apollo Federation](https://www.apollographql.com/docs/federation/).
20
+ This Ruby implementation is designed as a generic library to join basic spec-compliant GraphQL schemas using their existing types and fields in a do-it-yourself capacity. The opportunity here is for a Ruby application to stitch its local schemas together or onto remote sources without requiring an additional proxy service running in another language. If your goal is a purely high-throughput federation gateway with managed schema deployments, consider more opinionated frameworks such as [Apollo Federation](https://www.apollographql.com/docs/federation/).
20
21
 
21
- ## Getting started
22
+ ## Documentation
23
+
24
+ 1. [Introduction](./docs/introduction.md)
25
+ 1. [Composing a supergraph](./docs/composing_a_supergraph.md)
26
+ 1. [Merged types](./docs/merged_types.md)
27
+ 1. [Executables & file uploads](./docs/executables.md)
28
+ 1. [Serving a supergraph](./docs/serving_a_supergraph.md)
29
+ 1. [Visibility controls](./docs/visibility.md)
30
+ 1. [Performance concerns](./docs/performance.md)
31
+ 1. [Error handling](./docs/error_handling.md)
32
+ 1. [Subscriptions](./docs/subscriptions.md)
33
+
34
+ ## Quick Start
22
35
 
23
36
  Add to your Gemfile:
24
37
 
@@ -32,468 +45,83 @@ Run `bundle install`, then require unless running an autoloading framework (Rail
32
45
  require "graphql/stitching"
33
46
  ```
34
47
 
35
- ## Usage
36
-
37
- The [`Client`](./docs/client.md) component builds a stitched graph wrapped in an executable workflow (with optional query plan caching hooks):
38
-
39
- ```ruby
40
- movies_schema = <<~GRAPHQL
41
- type Movie { id: ID! name: String! }
42
- type Query { movie(id: ID!): Movie }
43
- GRAPHQL
44
-
45
- showtimes_schema = <<~GRAPHQL
46
- type Showtime { id: ID! time: String! }
47
- type Query { showtime(id: ID!): Showtime }
48
- GRAPHQL
49
-
50
- client = GraphQL::Stitching::Client.new(locations: {
51
- movies: {
52
- schema: GraphQL::Schema.from_definition(movies_schema),
53
- executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3000"),
54
- },
55
- showtimes: {
56
- schema: GraphQL::Schema.from_definition(showtimes_schema),
57
- executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3001"),
58
- },
59
- my_local: {
60
- schema: MyLocal::GraphQL::Schema,
61
- },
62
- })
63
-
64
- result = client.execute(
65
- query: "query FetchFromAll($movieId:ID!, $showtimeId:ID!){
66
- movie(id:$movieId) { name }
67
- showtime(id:$showtimeId): { time }
68
- myLocalField
69
- }",
70
- variables: { "movieId" => "1", "showtimeId" => "2" },
71
- operation_name: "FetchFromAll"
72
- )
73
- ```
74
-
75
- Schemas provided in [location settings](./docs/composer.md#performing-composition) may be class-based schemas with local resolvers (locally-executable schemas), or schemas built from SDL strings (schema definition language parsed using `GraphQL::Schema.from_definition`) and mapped to remote locations via [executables](#executables).
76
-
77
- A Client bundles up the component parts of stitching, which are worth familiarizing with:
78
-
79
- - [Composer](./docs/composer.md) - merges and validates many schemas into one supergraph.
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) - manages the lifecycle of a stitched GraphQL request.
82
- - [HttpExecutable](./docs/http_executable.md) - proxies requests to remotes with multipart file upload support.
83
-
84
- ## Merged types
85
-
86
- `Object` and `Interface` types may exist with different fields in different graph locations, and will get merged together in the combined schema.
87
-
88
- ![Merging types](./docs/images/merging.png)
89
-
90
- 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`](./docs/federation_entities.md).
91
-
92
- ### Merged type keys
93
-
94
- Foreign keys in a GraphQL schema frequently look like the `Product.imageId` field here:
95
-
96
- ```graphql
97
- # -- Products schema:
98
-
99
- type Product {
100
- id: ID!
101
- imageId: ID!
102
- }
103
-
104
- # -- Images schema:
105
-
106
- type Image {
107
- id: ID!
108
- url: String!
109
- }
110
- ```
111
-
112
- 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:
113
-
114
- ```graphql
115
- # -- Products schema:
116
-
117
- type Product {
118
- id: ID!
119
- image: Image!
120
- }
121
-
122
- type Image {
123
- id: ID!
124
- }
125
-
126
- # -- Images schema:
127
-
128
- type Image {
129
- id: ID!
130
- url: String!
131
- }
132
- ```
133
-
134
- ### Merged type resolver queries
135
-
136
- 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:
48
+ A stitched schema is [_composed_](./docs/composing_a_supergraph.md) from many _subgraph_ schemas. These can be remote APIs expressed as Schema Definition Language (SDL), or local schemas built from Ruby classes. Subgraph type names that overlap become [_merged types_](./docs/merged_types.md), and require `@stitch` directives to identify where each variant of the type can be fetched and what key field links them:
137
49
 
50
+ _schemas/product_infos.graphql_
138
51
  ```graphql
139
52
  directive @stitch(key: String!, arguments: String) repeatable on FIELD_DEFINITION
140
- ```
141
53
 
142
- This directive tells stitching how to cross-reference and fetch types from across locations, for example:
143
-
144
- ```ruby
145
- products_schema = <<~GRAPHQL
146
- directive @stitch(key: String!, arguments: String) repeatable on FIELD_DEFINITION
147
-
148
- type Product {
149
- id: ID!
150
- name: String!
151
- }
152
-
153
- type Query {
154
- product(id: ID!): Product @stitch(key: "id")
155
- }
156
- GRAPHQL
157
-
158
- catalog_schema = <<~GRAPHQL
159
- directive @stitch(key: String!, arguments: String) repeatable on FIELD_DEFINITION
160
-
161
- type Product {
162
- id: ID!
163
- price: Float!
164
- }
165
-
166
- type Query {
167
- products(ids: [ID!]!): [Product]! @stitch(key: "id")
168
- }
169
- GRAPHQL
170
-
171
- client = GraphQL::Stitching::Client.new(locations: {
172
- products: {
173
- schema: GraphQL::Schema.from_definition(products_schema),
174
- executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3001"),
175
- },
176
- catalog: {
177
- schema: GraphQL::Schema.from_definition(catalog_schema),
178
- executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3002"),
179
- },
180
- })
181
- ```
182
-
183
- Focusing on the `@stitch` directive usage:
184
-
185
- ```graphql
186
54
  type Product {
187
55
  id: ID!
188
56
  name: String!
189
57
  }
190
- type Query {
191
- product(id: ID!): Product @stitch(key: "id")
192
- }
193
- ```
194
-
195
- * The `@stitch` directive marks a root query where the merged type may be accessed. The merged type identity is inferred from the field return. This identifier can also be provided as [static configuration](#sdl-based-schemas).
196
- * 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).
197
58
 
198
- Merged types must have a resolver query in each of their possible locations. The one exception to this requirement are [outbound-only types](./docs/mechanics.md#outbound-only-merged-types) that contain no exclusive data, such as foreign keys:
199
-
200
- ```graphql
201
- type Product {
202
- id: ID!
203
- }
204
- ```
205
-
206
- The above type contains nothing but a key field that is available in other locations. Therefore, this variant will never require an inbound request to fetch it, and its resolver query may be omitted from this location.
207
-
208
- #### List queries
209
-
210
- It's okay ([even preferable](#batching) in most circumstances) to provide a list accessor as a resolver query. The only requirement is that both the field argument and return type must be lists, and the query results are expected to be a mapped set with `null` holding the position of missing results.
211
-
212
- ```graphql
213
59
  type Query {
214
- products(ids: [ID!]!): [Product]! @stitch(key: "id")
215
- }
216
-
217
- # input: ["1", "2", "3"]
218
- # result: [{ id: "1" }, null, { id: "3" }]
219
- ```
220
-
221
- See [error handling](./docs/mechanics.md#stitched-errors) tips for list queries.
222
-
223
- #### Abstract queries
224
-
225
- 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.
226
-
227
- ```graphql
228
- interface Node {
229
- id: ID!
230
- }
231
- type Product implements Node {
232
- id: ID!
233
- name: String!
234
- }
235
- type Query {
236
- nodes(ids: [ID!]!): [Node]! @stitch(key: "id")
237
- }
238
- ```
239
-
240
- To customize which types an abstract query provides and their respective keys, you may extend the `@stitch` directive with a `typeName` constraint. This can be repeated to select multiple types.
241
-
242
- ```graphql
243
- directive @stitch(key: String!, arguments: String, typeName: String) repeatable on FIELD_DEFINITION
244
-
245
- type Product { sku: ID! }
246
- type Order { id: ID! }
247
- type Customer { id: ID! } # << not stitched
248
- union Entity = Product | Order | Customer
249
-
250
- type Query {
251
- entity(key: ID!): Entity
252
- @stitch(key: "sku", typeName: "Product")
253
- @stitch(key: "id", typeName: "Order")
254
- }
255
- ```
256
-
257
- #### Argument shapes
258
-
259
- Stitching infers which argument to use for queries with a single argument, or when the key name matches its intended argument. For custom mappings, the `arguments` option may specify a template of GraphQL arguments that insert key selections:
260
-
261
- ```graphql
262
- type Product {
263
- id: ID!
264
- }
265
- type Query {
266
- product(byId: ID, bySku: ID): Product
267
- @stitch(key: "id", arguments: "byId: $.id")
268
- }
269
- ```
270
-
271
- Key insertions are prefixed by `$` and specify a dot-notation path to any selections made by the resolver key, or `__typename`. This syntax allows sending multiple arguments that intermix stitching keys with complex input shapes and other static values:
272
-
273
- ```graphql
274
- type Product {
275
- id: ID!
276
- }
277
- union Entity = Product
278
- input EntityKey {
279
- id: ID!
280
- type: String!
281
- }
282
- enum EntitySource {
283
- DATABASE
284
- CACHE
285
- }
286
-
287
- type Query {
288
- entities(keys: [EntityKey!]!, source: EntitySource = DATABASE): [Entity]!
289
- @stitch(key: "id", arguments: "keys: { id: $.id, type: $.__typename }, source: CACHE")
290
- }
291
- ```
292
-
293
- See [resolver arguments](./docs/type_resolver.md#arguments) for full documentation on shaping input.
294
-
295
- #### Composite type keys
296
-
297
- Resolver keys may make composite selections for multiple key fields and/or nested scopes, for example:
298
-
299
- ```graphql
300
- interface FieldOwner {
301
- id: ID!
302
- }
303
- type CustomField {
304
- owner: FieldOwner!
305
- key: String!
306
- value: String
307
- }
308
- input CustomFieldLookup {
309
- ownerId: ID!
310
- ownerType: String!
311
- key: String!
312
- }
313
-
314
- type Query {
315
- customFields(lookups: [CustomFieldLookup!]!): [CustomField]! @stitch(
316
- key: "owner { id __typename } key",
317
- arguments: "lookups: { ownerId: $.owner.id, ownerType: $.owner.__typename, key: $.key }"
318
- )
319
- }
320
- ```
321
-
322
- 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.
323
-
324
- #### Multiple type keys
325
-
326
- A type may exist in multiple locations across the graph using different keys, for example:
327
-
328
- ```graphql
329
- type Product { id:ID! } # storefronts location
330
- type Product { id:ID! sku:ID! } # products location
331
- type Product { sku:ID! } # catelog location
332
- ```
333
-
334
- 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:
335
-
336
- ```graphql
337
- type Product {
338
- id: ID!
339
- sku: ID!
340
- }
341
- type Query {
342
- productById(id: ID!): Product @stitch(key: "id")
343
- productBySku(sku: ID!): Product @stitch(key: "sku")
344
- }
345
- ```
346
-
347
- The `@stitch` directive is also repeatable, allowing a single query to associate with multiple keys:
348
-
349
- ```graphql
350
- type Product {
351
- id: ID!
352
- sku: ID!
353
- }
354
- type Query {
355
- product(id: ID, sku: ID): Product @stitch(key: "id") @stitch(key: "sku")
60
+ product(id: ID!): Product @stitch(key: "id")
356
61
  }
357
62
  ```
358
63
 
359
- #### Class-based schemas
360
-
361
- The `@stitch` directive can be added to class-based schemas with a directive class:
362
-
64
+ _product_prices_schema.rb_
363
65
  ```ruby
364
- class StitchingResolver < GraphQL::Schema::Directive
365
- graphql_name "stitch"
366
- locations FIELD_DEFINITION
367
- repeatable true
368
- argument :key, String, required: true
369
- argument :arguments, String, required: false
66
+ class Product < GraphQL::Schema::Object
67
+ field :id, ID, null: false
68
+ field :price, Float, null: false
370
69
  end
371
70
 
372
71
  class Query < GraphQL::Schema::Object
373
- field :product, Product, null: false do
374
- directive StitchingResolver, key: "id"
375
- argument :id, ID, required: true
72
+ field :products, [Product, null: true], null: false do |f|
73
+ f.directive(GraphQL::Stitching::Directives::Stitch, key: "id")
74
+ f.argument(ids: [ID, null: false], required: true)
376
75
  end
377
- end
378
- ```
379
-
380
- The `@stitch` directive can be exported from a class-based schema to an SDL string by calling `schema.to_definition`.
381
-
382
- #### SDL-based schemas
383
-
384
- A clean schema may also have stitching directives applied via static configuration by passing a `stitch` array in [location settings](./docs/composer.md#performing-composition):
385
-
386
- ```ruby
387
- sdl_string = <<~GRAPHQL
388
- type Product {
389
- id: ID!
390
- sku: ID!
391
- }
392
- type Query {
393
- productById(id: ID!): Product
394
- productBySku(sku: ID!): Product
395
- }
396
- GRAPHQL
397
-
398
- supergraph = GraphQL::Stitching::Composer.new.perform({
399
- products: {
400
- schema: GraphQL::Schema.from_definition(sdl_string),
401
- executable: ->() { ... },
402
- stitch: [
403
- { field_name: "productById", key: "id" },
404
- { field_name: "productBySku", key: "sku", arguments: "mySku: $.sku" },
405
- ]
406
- },
407
- # ...
408
- })
409
- ```
410
-
411
- #### Custom directive names
412
-
413
- The library is configured to use a `@stitch` directive by default. You may customize this by setting a new name during initialization:
414
-
415
- ```ruby
416
- GraphQL::Stitching.stitch_directive = "resolver"
417
- ```
418
-
419
- ## Executables
420
-
421
- An executable resource performs location-specific GraphQL requests. Executables may be `GraphQL::Schema` classes, or any object that responds to `.call(request, source, variables)` and returns a raw GraphQL response:
422
76
 
423
- ```ruby
424
- class MyExecutable
425
- def call(request, source, variables)
426
- # process a GraphQL request...
427
- return {
428
- "data" => { ... },
429
- "errors" => [ ... ],
430
- }
77
+ def products(ids:)
78
+ products_by_id = ProductModel.where(id: ids).index_by(&:id)
79
+ ids.map { |id| products_by_id[id] }
431
80
  end
432
81
  end
82
+
83
+ class ProductPricesSchema < GraphQL::Schema
84
+ directive(GraphQL::Stitching::Directives::Stitch)
85
+ query(Query)
86
+ end
433
87
  ```
434
88
 
435
- A [Supergraph](./docs/supergraph.md) is composed with executable resources provided for each location. Any location that omits the `executable` option will use the provided `schema` as its default executable:
89
+ These subgraph schemas are composed into a _supergraph_, or, a single combined schema that can be queried as one. Remote schemas are mapped to their resolver locations using [_executables_](./docs/executables.md):
436
90
 
437
91
  ```ruby
438
- supergraph = GraphQL::Stitching::Composer.new.perform({
439
- first: {
440
- schema: FirstSchema,
441
- # executable:^^^^^^ delegates to FirstSchema,
442
- },
443
- second: {
444
- schema: SecondSchema,
445
- executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3001", headers: { ... }),
446
- },
447
- third: {
448
- schema: ThirdSchema,
449
- executable: MyExecutable.new,
92
+ client = GraphQL::Stitching::Client.new(locations: {
93
+ infos: {
94
+ schema: GraphQL::Schema.from_definition(File.read("schemas/product_infos.graphql")),
95
+ executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3001"),
450
96
  },
451
- fourth: {
452
- schema: FourthSchema,
453
- executable: ->(req, query, vars) { ... },
97
+ prices: {
98
+ schema: ProductPricesSchema,
454
99
  },
455
100
  })
456
101
  ```
457
102
 
458
- The `GraphQL::Stitching::HttpExecutable` class is provided as a simple executable wrapper around `Net::HTTP.post` with [file upload](./docs/http_executable.md#graphql-file-uploads) support. You should build your own executables to leverage your existing libraries and to add instrumentation. Note that you must manually assign all executables to a `Supergraph` when rehydrating it from cache ([see docs](./docs/supergraph.md)).
459
-
460
- ## Batching
103
+ A stitching client then acts as a drop-in replacement for [serving GraphQL queries](./docs/serving_a_supergraph.md) using the combined schema. Internally, a query is broken down by location and sequenced into multiple requests, then all results are merged and shaped to match the original query.
461
104
 
462
- The stitching executor automatically batches subgraph requests so that only one request is made per location per generation of data. This is done using batched queries that combine all data access for a given a location. For example:
105
+ ```ruby
106
+ query = %|
107
+ query FetchProduct($id: ID!) {
108
+ product(id: $id) {
109
+ name # from infos schema
110
+ price # from prices schema
111
+ }
112
+ }
113
+ |
463
114
 
464
- ```graphql
465
- query MyOperation_2($_0_key:[ID!]!, $_1_0_key:ID!, $_1_1_key:ID!, $_1_2_key:ID!) {
466
- _0_result: widgets(ids: $_0_key) { ... } # << 3 Widget
467
- _1_0_result: sprocket(id: $_1_0_key) { ... } # << 1 Sprocket
468
- _1_1_result: sprocket(id: $_1_1_key) { ... } # << 1 Sprocket
469
- _1_2_result: sprocket(id: $_1_2_key) { ... } # << 1 Sprocket
470
- }
115
+ result = client.execute(
116
+ query: query,
117
+ variables: { "id" => "1" },
118
+ operation_name: "FetchProduct",
119
+ )
471
120
  ```
472
121
 
473
- Tips:
474
-
475
- * List queries (like the `widgets` selection above) are generally 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.
476
- * Assure that root field resolvers across your subgraph implement batching to anticipate cases like the three `sprocket` selections above.
477
-
478
- Otherwise, there's no developer intervention necessary (or generally possible) to improve upon data access. Note that multiple generations of data may still force the executor to return to a previous location for more data.
479
-
480
- ## Concurrency
481
-
482
- The [Executor](./docs/executor.md) component 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.
483
-
484
- ## Additional topics
485
-
486
- - [Deploying a stitched schema](./docs/mechanics.md#deploying-a-stitched-schema)
487
- - [Schema composition merge patterns](./docs/composer.md#merge-patterns)
488
- - [Subscriptions tutorial](./docs/subscriptions.md)
489
- - [Field selection routing](./docs/mechanics.md#field-selection-routing)
490
- - [Root selection routing](./docs/mechanics.md#root-selection-routing)
491
- - [Stitched errors](./docs/mechanics.md#stitched-errors)
492
- - [Null results](./docs/mechanics.md#null-results)
493
-
494
122
  ## Examples
495
123
 
496
- This repo includes working examples of stitched schemas running across small Rack servers. Clone the repo, `cd` into each example and try running it following its README instructions.
124
+ Clone this repo, then `cd` into each example and follow its README instructions.
497
125
 
498
126
  - [Merged types](./examples/merged_types)
499
127
  - [File uploads](./examples/file_uploads)