graphql-stitching 1.5.0 → 1.5.2
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/.github/workflows/ci.yml +6 -8
- data/Gemfile +1 -0
- data/README.md +60 -20
- data/docs/client.md +6 -0
- data/docs/composer.md +7 -6
- data/docs/federation_entities.md +1 -1
- data/docs/mechanics.md +1 -43
- data/docs/request.md +10 -10
- data/docs/subscriptions.md +11 -11
- data/docs/supergraph.md +5 -5
- data/docs/{resolver.md → type_resolver.md} +3 -3
- data/examples/subscriptions/app/graphql/subscriptions_schema.rb +3 -3
- data/gemfiles/graphql_2.0.0.gemfile +4 -1
- data/gemfiles/graphql_2.1.0.gemfile +4 -1
- data/gemfiles/graphql_2.2.0.gemfile +4 -1
- data/gemfiles/graphql_2.3.0.gemfile +9 -0
- data/graphql-stitching.gemspec +1 -1
- data/lib/graphql/stitching/client.rb +5 -7
- data/lib/graphql/stitching/composer/{resolver_config.rb → type_resolver_config.rb} +2 -2
- data/lib/graphql/stitching/composer/{validate_resolvers.rb → validate_type_resolvers.rb} +1 -1
- data/lib/graphql/stitching/composer.rb +28 -20
- data/lib/graphql/stitching/executor/shaper.rb +4 -4
- data/lib/graphql/stitching/executor/{resolver_source.rb → type_resolver_source.rb} +4 -4
- data/lib/graphql/stitching/executor.rb +5 -5
- data/lib/graphql/stitching/planner/step.rb +1 -1
- data/lib/graphql/stitching/planner.rb +16 -20
- data/lib/graphql/stitching/request/skip_include.rb +1 -1
- data/lib/graphql/stitching/request.rb +3 -7
- data/lib/graphql/stitching/supergraph/to_definition.rb +3 -3
- data/lib/graphql/stitching/supergraph.rb +1 -6
- data/lib/graphql/stitching/{resolver → type_resolver}/arguments.rb +6 -6
- data/lib/graphql/stitching/{resolver → type_resolver}/keys.rb +1 -1
- data/lib/graphql/stitching/{resolver.rb → type_resolver.rb} +4 -4
- data/lib/graphql/stitching/version.rb +1 -1
- data/lib/graphql/stitching.rb +20 -3
- metadata +12 -12
- data/gemfiles/graphql_1.13.9.gemfile +0 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cf60ae0ef85426a3011223bcc146a74a1664762e5488d8266d0afbf1c2143457
|
4
|
+
data.tar.gz: d20da5d4817193de7e156441cab88b0855f3fa93aeabe5e760f8fd66ac5b3547
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b89843bfdd353a6b4aec47fce17a6bd6524b29f7d5fa5dc0452570dc1b4bec63691171f7cd3f841c393015df763e20e1be7cf907cc660fcc576cc93e08c3d4dd
|
7
|
+
data.tar.gz: a20f68f8bac0a52271fdc76ec4f173347669acda7f49b18e177746c83df1ec42dc878e8d3af37545a99f5456acc6d9f7577600d79c4568925cd23837fe5c2d0a
|
data/.github/workflows/ci.yml
CHANGED
@@ -13,22 +13,20 @@ jobs:
|
|
13
13
|
matrix:
|
14
14
|
include:
|
15
15
|
- gemfile: Gemfile
|
16
|
+
ruby: 3.3
|
17
|
+
- gemfile: gemfiles/graphql_2.3.0.gemfile
|
16
18
|
ruby: 3.2
|
17
|
-
- gemfile: Gemfile
|
18
|
-
ruby: 3.1
|
19
|
-
- gemfile: Gemfile
|
20
|
-
ruby: 2.7
|
21
19
|
- gemfile: gemfiles/graphql_2.2.0.gemfile
|
22
20
|
ruby: 3.1
|
23
21
|
- gemfile: gemfiles/graphql_2.1.0.gemfile
|
24
22
|
ruby: 3.1
|
25
23
|
- gemfile: gemfiles/graphql_2.0.0.gemfile
|
26
24
|
ruby: 3.1
|
27
|
-
- gemfile: gemfiles/
|
28
|
-
ruby:
|
29
|
-
|
25
|
+
- gemfile: gemfiles/graphql_2.0.0.gemfile
|
26
|
+
ruby: 2.7
|
30
27
|
steps:
|
31
|
-
-
|
28
|
+
- run: echo BUNDLE_GEMFILE=${{ matrix.gemfile }} > $GITHUB_ENV
|
29
|
+
- uses: actions/checkout@v4
|
32
30
|
- name: Setup Ruby
|
33
31
|
uses: ruby/setup-ruby@v1
|
34
32
|
with:
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,23 +1,22 @@
|
|
1
1
|
## GraphQL Stitching for Ruby
|
2
2
|
|
3
|
-
GraphQL stitching composes a single schema from multiple underlying GraphQL resources, then smartly proxies portions of incoming requests to their respective locations in dependency order and returns the merged results. This allows an entire
|
3
|
+
GraphQL stitching composes a single schema from multiple underlying GraphQL resources, then smartly proxies portions of incoming requests to their respective locations in dependency order and returns the merged results. This allows an entire graph of locations to be queried through one combined GraphQL surface area.
|
4
4
|
|
5
5
|

|
6
6
|
|
7
7
|
**Supports:**
|
8
|
-
- All operation types: query, mutation, and subscription.
|
9
|
-
- Merged object and abstract types.
|
8
|
+
- All operation types: query, mutation, and [subscription](./docs/subscriptions.md).
|
9
|
+
- Merged object and abstract types joining though multiple keys.
|
10
10
|
- Shared objects, fields, enums, and inputs across locations.
|
11
|
-
- Multiple and composite type keys.
|
12
11
|
- Combining local and remote schemas.
|
13
|
-
- File uploads via
|
12
|
+
- [File uploads](./docs/http_executable.md) via multipart forms.
|
14
13
|
- Tested with all minor versions of `graphql-ruby`.
|
15
14
|
|
16
15
|
**NOT Supported:**
|
17
16
|
- Computed fields (ie: federation-style `@requires`).
|
18
17
|
- Defer/stream.
|
19
18
|
|
20
|
-
This Ruby implementation is
|
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/).
|
21
20
|
|
22
21
|
## Getting started
|
23
22
|
|
@@ -35,7 +34,7 @@ require "graphql/stitching"
|
|
35
34
|
|
36
35
|
## Usage
|
37
36
|
|
38
|
-
The
|
37
|
+
The [`Client`](./docs/client.md) component builds a stitched graph wrapped in an executable workflow (with optional query plan caching hooks):
|
39
38
|
|
40
39
|
```ruby
|
41
40
|
movies_schema = <<~GRAPHQL
|
@@ -75,7 +74,7 @@ result = client.execute(
|
|
75
74
|
|
76
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).
|
77
76
|
|
78
|
-
While
|
77
|
+
While `Client` is sufficient for most usecases, the library offers several discrete components that can be assembled into tailored workflows:
|
79
78
|
|
80
79
|
- [Composer](./docs/composer.md) - merges and validates many schemas into one supergraph.
|
81
80
|
- [Supergraph](./docs/supergraph.md) - manages the combined schema, location routing maps, and executable resources. Can be exported, cached, and rehydrated.
|
@@ -88,17 +87,59 @@ While the `Client` constructor is an easy quick start, the library also has seve
|
|
88
87
|
|
89
88
|

|
90
89
|
|
91
|
-
To facilitate this
|
90
|
+
To facilitate this, schemas should be designed around [merged type keys](#merged-type-keys) that stitching can cross-reference and fetch across locations using [type resolver queries](#merged-type-resolver-queries). For those in an Apollo ecosystem, there's also _limited_ support for merging types though a [federation `_entities` protocol](./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
|
+
```
|
92
133
|
|
93
134
|
### Merged type resolver queries
|
94
135
|
|
95
|
-
|
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:
|
96
137
|
|
97
138
|
```graphql
|
98
139
|
directive @stitch(key: String!, arguments: String) repeatable on FIELD_DEFINITION
|
99
140
|
```
|
100
141
|
|
101
|
-
This directive
|
142
|
+
This directive tells stitching how to cross-reference and fetch types from across locations, for example:
|
102
143
|
|
103
144
|
```ruby
|
104
145
|
products_schema = <<~GRAPHQL
|
@@ -151,10 +192,10 @@ type Query {
|
|
151
192
|
}
|
152
193
|
```
|
153
194
|
|
154
|
-
* The `@stitch` directive
|
155
|
-
* The `key: "id"` parameter indicates that an `{ id }` must be selected from prior locations so it
|
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).
|
156
197
|
|
157
|
-
|
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:
|
158
199
|
|
159
200
|
```graphql
|
160
201
|
type Product {
|
@@ -162,7 +203,7 @@ type Product {
|
|
162
203
|
}
|
163
204
|
```
|
164
205
|
|
165
|
-
The above
|
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.
|
166
207
|
|
167
208
|
#### List queries
|
168
209
|
|
@@ -249,7 +290,7 @@ type Query {
|
|
249
290
|
}
|
250
291
|
```
|
251
292
|
|
252
|
-
See [resolver arguments](./docs/
|
293
|
+
See [resolver arguments](./docs/type_resolver.md#arguments) for full documentation on shaping input.
|
253
294
|
|
254
295
|
#### Composite type keys
|
255
296
|
|
@@ -258,7 +299,6 @@ Resolver keys may make composite selections for multiple key fields and/or neste
|
|
258
299
|
```graphql
|
259
300
|
interface FieldOwner {
|
260
301
|
id: ID!
|
261
|
-
type: String!
|
262
302
|
}
|
263
303
|
type CustomField {
|
264
304
|
owner: FieldOwner!
|
@@ -273,8 +313,8 @@ input CustomFieldLookup {
|
|
273
313
|
|
274
314
|
type Query {
|
275
315
|
customFields(lookups: [CustomFieldLookup!]!): [CustomField]! @stitch(
|
276
|
-
key: "owner { id
|
277
|
-
arguments: "lookups: { ownerId: $.owner.id, ownerType: $.owner.
|
316
|
+
key: "owner { id __typename } key",
|
317
|
+
arguments: "lookups: { ownerId: $.owner.id, ownerType: $.owner.__typename, key: $.key }"
|
278
318
|
)
|
279
319
|
}
|
280
320
|
```
|
@@ -443,7 +483,6 @@ The [Executor](./docs/executor.md) component builds atop the Ruby fiber-based im
|
|
443
483
|
|
444
484
|
## Additional topics
|
445
485
|
|
446
|
-
- [Modeling foreign keys for stitching](./docs/mechanics.md##modeling-foreign-keys-for-stitching)
|
447
486
|
- [Deploying a stitched schema](./docs/mechanics.md#deploying-a-stitched-schema)
|
448
487
|
- [Schema composition merge patterns](./docs/composer.md#merge-patterns)
|
449
488
|
- [Subscriptions tutorial](./docs/subscriptions.md)
|
@@ -458,6 +497,7 @@ This repo includes working examples of stitched schemas running across small Rac
|
|
458
497
|
|
459
498
|
- [Merged types](./examples/merged_types)
|
460
499
|
- [File uploads](./examples/file_uploads)
|
500
|
+
- [Subscriptions](./examples/subscriptions)
|
461
501
|
|
462
502
|
## Tests
|
463
503
|
|
data/docs/client.md
CHANGED
@@ -68,6 +68,12 @@ client.on_cache_write do |request, payload|
|
|
68
68
|
end
|
69
69
|
```
|
70
70
|
|
71
|
+
All request digests use SHA2 by default. You can swap in [a faster algorithm](https://github.com/Shopify/blake3-rb) and/or add base scoping by reconfiguring the stitching library:
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
GraphQL::Stitching.digest { |str| Digest::MD5.hexdigest("v2/#{str}") }
|
75
|
+
```
|
76
|
+
|
71
77
|
Note that inlined input data works against caching, so you should _avoid_ these input literals when possible:
|
72
78
|
|
73
79
|
```graphql
|
data/docs/composer.md
CHANGED
@@ -10,6 +10,7 @@ A `Composer` may be constructed with optional settings that tune how it builds a
|
|
10
10
|
composer = GraphQL::Stitching::Composer.new(
|
11
11
|
query_name: "Query",
|
12
12
|
mutation_name: "Mutation",
|
13
|
+
subscription_name: "Subscription",
|
13
14
|
description_merger: ->(values_by_location, info) { values_by_location.values.join("\n") },
|
14
15
|
deprecation_merger: ->(values_by_location, info) { values_by_location.values.first },
|
15
16
|
default_value_merger: ->(values_by_location, info) { values_by_location.values.first },
|
@@ -24,6 +25,8 @@ Constructor arguments:
|
|
24
25
|
|
25
26
|
- **`mutation_name:`** _optional_, the name of the root mutation type in the composed schema; `Mutation` by default. The root mutation types from all location schemas will be merged into this type, regardless of their local names.
|
26
27
|
|
28
|
+
- **`subscription_name:`** _optional_, the name of the root subscription type in the composed schema; `Subscription` by default. The root subscription types from all location schemas will be merged into this type, regardless of their local names.
|
29
|
+
|
27
30
|
- **`description_merger:`** _optional_, a [value merger function](#value-merger-functions) for merging element description strings from across locations.
|
28
31
|
|
29
32
|
- **`deprecation_merger:`** _optional_, a [value merger function](#value-merger-functions) for merging element deprecation strings from across locations.
|
@@ -98,17 +101,16 @@ Location settings have top-level keys that specify arbitrary location names, eac
|
|
98
101
|
|
99
102
|
The strategy used to merge source schemas into the combined schema is based on each element type:
|
100
103
|
|
104
|
+
- Arguments of fields, directives, and `InputObject` types intersect for each parent element across locations (an element's arguments must appear in all locations):
|
105
|
+
- Arguments must share a value type, and the strictest nullability across locations is used.
|
106
|
+
- Composition fails if argument intersection would eliminate a non-null argument.
|
107
|
+
|
101
108
|
- `Object` and `Interface` types merge their fields and directives together:
|
102
109
|
- Common fields across locations must share a value type, and the weakest nullability is used.
|
103
|
-
- Field and directive arguments merge using the same rules as `InputObject`.
|
104
110
|
- Objects with unique fields across locations must implement [`@stitch` accessors](../README.md#merged-types).
|
105
111
|
- Shared object types without `@stitch` accessors must contain identical fields.
|
106
112
|
- Merged interfaces must remain compatible with all underlying implementations.
|
107
113
|
|
108
|
-
- `InputObject` types intersect arguments from across locations (arguments must appear in all locations):
|
109
|
-
- Arguments must share a value type, and the strictest nullability across locations is used.
|
110
|
-
- Composition fails if argument intersection would eliminate a non-null argument.
|
111
|
-
|
112
114
|
- `Enum` types merge their values based on how the enum is used:
|
113
115
|
- Enums used anywhere as an argument will intersect their values (common values across all locations).
|
114
116
|
- Enums used exclusively in read contexts will provide a union of values (all values across all locations).
|
@@ -118,7 +120,6 @@ The strategy used to merge source schemas into the combined schema is based on e
|
|
118
120
|
- `Scalar` types are added for all scalar names across all locations.
|
119
121
|
|
120
122
|
- `Directive` definitions are added for all distinct names across locations:
|
121
|
-
- Arguments merge using the same rules as `InputObject`.
|
122
123
|
- Stitching directives (both definitions and assignments) are omitted.
|
123
124
|
|
124
125
|
Note that the structure of a composed schema may change based on new schema additions and/or element usage (ie: changing input object arguments in one service may cause the intersection of arguments to change). Therefore, it's highly recommended that you use a [schema comparator](https://github.com/xuorig/graphql-schema_comparator) to flag regressions across composed schema versions.
|
data/docs/federation_entities.md
CHANGED
@@ -65,6 +65,6 @@ It's perfectly fine to mix and match schemas that implement an `_entities` query
|
|
65
65
|
|
66
66
|
### Federation features that will most definitly break
|
67
67
|
|
68
|
-
- `@external` fields will confuse the stitching query planner.
|
68
|
+
- `@external` fields will confuse the stitching query planner (as the fields aren't natively resolvable at the location).
|
69
69
|
- `@requires` fields will not be sent any dependencies.
|
70
70
|
- No support for Apollo composition directives.
|
data/docs/mechanics.md
CHANGED
@@ -1,47 +1,5 @@
|
|
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
|
-
|
45
3
|
### Deploying a stitched schema
|
46
4
|
|
47
5
|
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:
|
@@ -94,7 +52,7 @@ class GraphQlController
|
|
94
52
|
end
|
95
53
|
```
|
96
54
|
|
97
|
-
This process assures that composition always happens before deployment where failures can be detected. Hot reloading of the supergraph can also be accommodated by uploading the composed schema to a sync location (cloud storage, etc) that is polled by the application runtime. When the schema changes, load it into a new stitching client and swap that into the application.
|
55
|
+
This process assures that composition always happens before deployment where failures can be detected. Use CI to verify that the repo's supergraph output is always up to date. Hot reloading of the supergraph can also be accommodated by uploading the composed schema to a sync location (cloud storage, etc) that is polled by the application runtime. When the schema changes, load it into a new stitching client and swap that into the application.
|
98
56
|
|
99
57
|
### Field selection routing
|
100
58
|
|
data/docs/request.md
CHANGED
@@ -15,15 +15,15 @@ request = GraphQL::Stitching::Request.new(
|
|
15
15
|
|
16
16
|
A `Request` provides the following information:
|
17
17
|
|
18
|
-
- `req.document`: parsed AST of the GraphQL source
|
19
|
-
- `req.variables`: a hash of user-submitted variables
|
20
|
-
- `req.string`: the original GraphQL source string, or printed document
|
21
|
-
- `req.digest`: a
|
22
|
-
- `req.normalized_string`: printed document string with consistent whitespace
|
23
|
-
- `req.normalized_digest`: a
|
24
|
-
- `req.operation`: the operation definition selected for the request
|
25
|
-
- `req.variable_definitions`: a mapping of variable names to their type definitions
|
26
|
-
- `req.fragment_definitions`: a mapping of fragment names to their fragment definitions
|
18
|
+
- `req.document`: parsed AST of the GraphQL source.
|
19
|
+
- `req.variables`: a hash of user-submitted variables.
|
20
|
+
- `req.string`: the original GraphQL source string, or printed document.
|
21
|
+
- `req.digest`: a digest of the request string, hashed by the `Stitching.digest` implementation.
|
22
|
+
- `req.normalized_string`: printed document string with consistent whitespace.
|
23
|
+
- `req.normalized_digest`: a digest of the normalized string, hashed by the `Stitching.digest` implementation.
|
24
|
+
- `req.operation`: the operation definition selected for the request.
|
25
|
+
- `req.variable_definitions`: a mapping of variable names to their type definitions.
|
26
|
+
- `req.fragment_definitions`: a mapping of fragment names to their fragment definitions.
|
27
27
|
|
28
28
|
### Request lifecycle
|
29
29
|
|
@@ -32,5 +32,5 @@ component, or you may invoke them manually:
|
|
32
32
|
|
33
33
|
1. `request.validate`: runs static validations on the request using the combined schema.
|
34
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
|
35
|
+
3. `request.plan`: builds a plan for the request. May act as a setter for plans pulled from cache.
|
36
36
|
4. `request.execute`: executes the request, and returns the resulting data.
|
data/docs/subscriptions.md
CHANGED
@@ -4,7 +4,7 @@ Stitching is an interesting prospect for subscriptions because socket-based inte
|
|
4
4
|
|
5
5
|
### Composing a subscriptions schema
|
6
6
|
|
7
|
-
For simplicity, subscription resolvers
|
7
|
+
For simplicity, subscription resolvers are best kept together in a single schema (multiple schemas with subscriptions probably aren't worth the confusion). This subscriptions schema may provide basic entity types that will merge with other locations. For example, here's a bare-bones subscriptions schema:
|
8
8
|
|
9
9
|
```ruby
|
10
10
|
class SubscriptionSchema < GraphQL::Schema
|
@@ -88,7 +88,7 @@ class EntitiesSchema < GraphQL::Schema
|
|
88
88
|
end
|
89
89
|
```
|
90
90
|
|
91
|
-
These schemas can be composed as normal into a stitching client. The subscriptions schema must be locally-executable while other entity schema(s) may be served from anywhere:
|
91
|
+
These schemas can be composed as normal into a stitching client. The subscriptions schema must be locally-executable while the other entity schema(s) may be served from anywhere:
|
92
92
|
|
93
93
|
```ruby
|
94
94
|
StitchedSchema = GraphQL::Stitching::Client.new(locations: {
|
@@ -104,7 +104,7 @@ StitchedSchema = GraphQL::Stitching::Client.new(locations: {
|
|
104
104
|
|
105
105
|
### Serving stitched subscriptions
|
106
106
|
|
107
|
-
Once you've
|
107
|
+
Once you've composed a schema with subscriptions, it gets called as part of three workflows:
|
108
108
|
|
109
109
|
1. Controller - handles normal query and mutation requests recieved via HTTP.
|
110
110
|
2. Channel - handles subscription-create requests recieved through a socket connection.
|
@@ -122,7 +122,7 @@ class GraphqlController < ApplicationController
|
|
122
122
|
def execute
|
123
123
|
result = StitchedSchema.execute(
|
124
124
|
params[:query],
|
125
|
-
context: {},
|
125
|
+
context: {},
|
126
126
|
variables: params[:variables],
|
127
127
|
operation_name: params[:operationName],
|
128
128
|
)
|
@@ -164,7 +164,7 @@ class GraphqlChannel < ApplicationCable::Channel
|
|
164
164
|
|
165
165
|
def unsubscribed
|
166
166
|
@subscription_ids.each { |sid|
|
167
|
-
# Go directly through the subscriptions subschema
|
167
|
+
# Go directly through the subscriptions subschema
|
168
168
|
# when managing/triggering subscriptions:
|
169
169
|
SubscriptionSchema.subscriptions.delete_subscription(sid)
|
170
170
|
}
|
@@ -172,7 +172,7 @@ class GraphqlChannel < ApplicationCable::Channel
|
|
172
172
|
end
|
173
173
|
```
|
174
174
|
|
175
|
-
What happens behind the scenes here is that stitching filters the `execute` request down to just subscription selections, and passes those through to the subscriptions subschema where they register an event binding. The subscriber response gets stitched while passing back
|
175
|
+
What happens behind the scenes here is that stitching filters the `execute` request down to just subscription selections, and passes those through to the subscriptions subschema where they register an event binding. The subscriber response gets stitched while passing back out through the stitching client. The `unsubscribed` method works directly with the subschema where subscriptions are managed.
|
176
176
|
|
177
177
|
#### Plugin
|
178
178
|
|
@@ -181,14 +181,14 @@ Lastly, update events trigger with the filtered subscriptions selection, so must
|
|
181
181
|
```ruby
|
182
182
|
class StitchedActionCableSubscriptions < GraphQL::Subscriptions::ActionCableSubscriptions
|
183
183
|
def execute_update(subscription_id, event, object)
|
184
|
-
super(subscription_id, event, object)
|
185
|
-
|
186
|
-
|
184
|
+
result = super(subscription_id, event, object)
|
185
|
+
result.context[:stitch_subscription_update]&.call(result)
|
186
|
+
result
|
187
187
|
end
|
188
188
|
end
|
189
189
|
|
190
190
|
class SubscriptionSchema < GraphQL::Schema
|
191
|
-
# switch the plugin on the subscriptions schema to use the patched class...
|
191
|
+
# switch the plugin on the subscriptions schema to use the patched class...
|
192
192
|
use StitchedActionCableSubscriptions
|
193
193
|
end
|
194
194
|
```
|
@@ -200,7 +200,7 @@ Subscription update events are triggered as normal directly through the subscrip
|
|
200
200
|
```ruby
|
201
201
|
class Comment < ApplicationRecord
|
202
202
|
after_create :trigger_subscriptions
|
203
|
-
|
203
|
+
|
204
204
|
def trigger_subscriptions
|
205
205
|
SubscriptionsSchema.subscriptions.trigger(:comment_added_to_post, { post_id: post_id }, self)
|
206
206
|
end
|
data/docs/supergraph.md
CHANGED
@@ -9,17 +9,17 @@ A Supergraph is designed to be composed, cached, and restored. Calling `to_defin
|
|
9
9
|
```ruby
|
10
10
|
supergraph_sdl = supergraph.to_definition
|
11
11
|
|
12
|
-
#
|
13
|
-
$cache.set("cached_supergraph_sdl", supergraph_sdl)
|
14
|
-
|
15
|
-
# or, write the composed schema as a file into your repo...
|
12
|
+
# write the composed schema as a file into your repo...
|
16
13
|
File.write("supergraph/schema.graphql", supergraph_sdl)
|
14
|
+
|
15
|
+
# or, stash this composed schema in a cache...
|
16
|
+
$cache.set("cached_supergraph_sdl", supergraph_sdl)
|
17
17
|
```
|
18
18
|
|
19
19
|
To restore a Supergraph, call `from_definition` providing the cached SDL string and a hash of executables keyed by their location names:
|
20
20
|
|
21
21
|
```ruby
|
22
|
-
supergraph_sdl =
|
22
|
+
supergraph_sdl = File.read("supergraph/schema.graphql")
|
23
23
|
|
24
24
|
supergraph = GraphQL::Stitching::Supergraph.from_definition(
|
25
25
|
supergraph_sdl,
|
@@ -1,10 +1,10 @@
|
|
1
|
-
## GraphQL::Stitching::
|
1
|
+
## GraphQL::Stitching::TypeResolver
|
2
2
|
|
3
|
-
A `
|
3
|
+
A `TypeResolver` contains all information about a root query used by stitching to fetch location-specific variants of a merged type. Specifically, resolvers manage parsed keys and argument structures.
|
4
4
|
|
5
5
|
### Arguments
|
6
6
|
|
7
|
-
|
7
|
+
Type resolvers configure arguments through a template string of [GraphQL argument literal syntax](https://spec.graphql.org/October2021/#sec-Language.Arguments). This allows sending multiple arguments that intermix stitching keys with complex object shapes and other static values.
|
8
8
|
|
9
9
|
#### Key insertions
|
10
10
|
|
@@ -1,9 +1,9 @@
|
|
1
1
|
class SubscriptionsSchema < GraphQL::Schema
|
2
2
|
class StitchedActionCableSubscriptions < GraphQL::Subscriptions::ActionCableSubscriptions
|
3
3
|
def execute_update(subscription_id, event, object)
|
4
|
-
super(subscription_id, event, object)
|
5
|
-
|
6
|
-
|
4
|
+
result = super(subscription_id, event, object)
|
5
|
+
result.context[:stitch_subscription_update]&.call(result)
|
6
|
+
result
|
7
7
|
end
|
8
8
|
end
|
9
9
|
|
data/graphql-stitching.gemspec
CHANGED
@@ -26,7 +26,7 @@ Gem::Specification.new do |spec|
|
|
26
26
|
end
|
27
27
|
spec.require_paths = ['lib']
|
28
28
|
|
29
|
-
spec.add_runtime_dependency 'graphql', '>=
|
29
|
+
spec.add_runtime_dependency 'graphql', '>= 2.0'
|
30
30
|
|
31
31
|
spec.add_development_dependency 'bundler', '~> 2.0'
|
32
32
|
spec.add_development_dependency 'rake', '~> 12.0'
|
@@ -7,8 +7,6 @@ module GraphQL
|
|
7
7
|
# Client is an out-of-the-box helper that assembles all
|
8
8
|
# stitching components into a workflow that executes requests.
|
9
9
|
class Client
|
10
|
-
class ClientError < StitchingError; end
|
11
|
-
|
12
10
|
# @return [Supergraph] composed supergraph that services incoming requests.
|
13
11
|
attr_reader :supergraph
|
14
12
|
|
@@ -18,9 +16,9 @@ module GraphQL
|
|
18
16
|
# @param composer [Composer] optional, a pre-configured composer instance for use with `locations` configuration.
|
19
17
|
def initialize(locations: nil, supergraph: nil, composer: nil)
|
20
18
|
@supergraph = if locations && supergraph
|
21
|
-
raise
|
19
|
+
raise ArgumentError, "Cannot provide both locations and a supergraph."
|
22
20
|
elsif supergraph && !supergraph.is_a?(Supergraph)
|
23
|
-
raise
|
21
|
+
raise ArgumentError, "Provided supergraph must be a GraphQL::Stitching::Supergraph instance."
|
24
22
|
elsif supergraph
|
25
23
|
supergraph
|
26
24
|
else
|
@@ -58,17 +56,17 @@ module GraphQL
|
|
58
56
|
end
|
59
57
|
|
60
58
|
def on_cache_read(&block)
|
61
|
-
raise
|
59
|
+
raise ArgumentError, "A cache read block is required." unless block_given?
|
62
60
|
@on_cache_read = block
|
63
61
|
end
|
64
62
|
|
65
63
|
def on_cache_write(&block)
|
66
|
-
raise
|
64
|
+
raise ArgumentError, "A cache write block is required." unless block_given?
|
67
65
|
@on_cache_write = block
|
68
66
|
end
|
69
67
|
|
70
68
|
def on_error(&block)
|
71
|
-
raise
|
69
|
+
raise ArgumentError, "An error handler block is required." unless block_given?
|
72
70
|
@on_error = block
|
73
71
|
end
|
74
72
|
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module GraphQL::Stitching
|
4
4
|
class Composer
|
5
|
-
class
|
5
|
+
class TypeResolverConfig
|
6
6
|
ENTITY_TYPENAME = "_Entity"
|
7
7
|
ENTITIES_QUERY = "_entities"
|
8
8
|
|
@@ -30,7 +30,7 @@ module GraphQL::Stitching
|
|
30
30
|
entity_type.directives.each do |directive|
|
31
31
|
next unless directive.graphql_name == "key"
|
32
32
|
|
33
|
-
key =
|
33
|
+
key = TypeResolver.parse_key(directive.arguments.keyword_arguments.fetch(:fields))
|
34
34
|
key_fields = key.map { "#{_1.name}: $.#{_1.name}" }
|
35
35
|
field_path = "#{location}._entities"
|
36
36
|
|