graphql-stitching 1.5.1 → 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 +54 -15
- 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 +6 -6
- data/docs/supergraph.md +5 -5
- 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.rb +7 -2
- data/lib/graphql/stitching/executor/shaper.rb +2 -2
- data/lib/graphql/stitching/executor/type_resolver_source.rb +2 -2
- data/lib/graphql/stitching/executor.rb +3 -3
- data/lib/graphql/stitching/planner.rb +7 -9
- data/lib/graphql/stitching/request.rb +1 -5
- data/lib/graphql/stitching/supergraph.rb +0 -5
- data/lib/graphql/stitching/version.rb +1 -1
- data/lib/graphql/stitching.rb +5 -0
- metadata +5 -5
- 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,14 +1,13 @@
|
|
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
8
|
- All operation types: query, mutation, and [subscription](./docs/subscriptions.md).
|
9
|
-
- Merged object and abstract types.
|
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
12
|
- [File uploads](./docs/http_executable.md) via multipart forms.
|
14
13
|
- Tested with all minor versions of `graphql-ruby`.
|
@@ -17,7 +16,7 @@ GraphQL stitching composes a single schema from multiple underlying GraphQL reso
|
|
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
|
|
@@ -88,17 +87,59 @@ While `Client` is sufficient for most usecases, the library offers several discr
|
|
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
|
|
@@ -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)
|
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
|
@@ -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 out through the stitching client.
|
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
|
|
@@ -188,7 +188,7 @@ class StitchedActionCableSubscriptions < GraphQL::Subscriptions::ActionCableSubs
|
|
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,
|
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
|
|
@@ -403,8 +403,13 @@ module GraphQL
|
|
403
403
|
next
|
404
404
|
end
|
405
405
|
|
406
|
-
# Getting double args sometimes
|
407
|
-
|
406
|
+
# Getting double args sometimes... why?
|
407
|
+
begin
|
408
|
+
next if owner.arguments(GraphQL::Query::NullContext.instance, false).key?(argument_name)
|
409
|
+
rescue ArgumentError
|
410
|
+
# pre- graphql v2.4.5
|
411
|
+
next if owner.arguments.key?(argument_name)
|
412
|
+
end
|
408
413
|
|
409
414
|
kwargs = {}
|
410
415
|
default_values_by_location = arguments_by_location.each_with_object({}) do |(location, argument), memo|
|
@@ -64,7 +64,7 @@ module GraphQL::Stitching
|
|
64
64
|
return nil if result.nil?
|
65
65
|
|
66
66
|
else
|
67
|
-
raise
|
67
|
+
raise DocumentError.new("selection node type")
|
68
68
|
end
|
69
69
|
end
|
70
70
|
|
@@ -118,7 +118,7 @@ module GraphQL::Stitching
|
|
118
118
|
def typename_in_type?(typename, type)
|
119
119
|
return true if type.graphql_name == typename
|
120
120
|
|
121
|
-
type.kind.abstract? && @
|
121
|
+
type.kind.abstract? && @supergraph.schema.possible_types(type).any? do |t|
|
122
122
|
t.graphql_name == typename
|
123
123
|
end
|
124
124
|
end
|
@@ -10,7 +10,7 @@ module GraphQL::Stitching
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def fetch(ops)
|
13
|
-
origin_sets_by_operation = ops.each_with_object({}) do |op, memo|
|
13
|
+
origin_sets_by_operation = ops.each_with_object({}.compare_by_identity) do |op, memo|
|
14
14
|
origin_set = op.path.reduce([@executor.data]) do |set, path_segment|
|
15
15
|
set.flat_map { |obj| obj && obj[path_segment] }.tap(&:compact!)
|
16
16
|
end
|
@@ -86,7 +86,7 @@ module GraphQL::Stitching
|
|
86
86
|
end
|
87
87
|
end
|
88
88
|
|
89
|
-
doc = String.new(
|
89
|
+
doc = String.new(QUERY_OP) # << resolver fulfillment always uses query
|
90
90
|
|
91
91
|
if operation_name
|
92
92
|
doc << " #{operation_name}"
|
@@ -27,7 +27,7 @@ module GraphQL
|
|
27
27
|
# Builds a new executor.
|
28
28
|
# @param request [Request] the stitching request to execute.
|
29
29
|
# @param nonblocking [Boolean] specifies if the dataloader should use async concurrency.
|
30
|
-
def initialize(request, data: {}, errors: [], after:
|
30
|
+
def initialize(request, data: {}, errors: [], after: Planner::ROOT_INDEX, nonblocking: false)
|
31
31
|
@request = request
|
32
32
|
@data = data
|
33
33
|
@errors = errors
|
@@ -38,7 +38,7 @@ module GraphQL
|
|
38
38
|
end
|
39
39
|
|
40
40
|
def perform(raw: false)
|
41
|
-
exec!
|
41
|
+
exec!([@after])
|
42
42
|
result = {}
|
43
43
|
|
44
44
|
if @data && @data.length > 0
|
@@ -54,7 +54,7 @@ module GraphQL
|
|
54
54
|
|
55
55
|
private
|
56
56
|
|
57
|
-
def exec!(next_steps
|
57
|
+
def exec!(next_steps)
|
58
58
|
if @exec_cycles > @request.plan.ops.length
|
59
59
|
# sanity check... if we've exceeded queue size, then something went wrong.
|
60
60
|
raise StitchingError, "Too many execution requests attempted."
|
@@ -20,7 +20,7 @@ module GraphQL
|
|
20
20
|
def perform
|
21
21
|
build_root_entrypoints
|
22
22
|
expand_abstract_resolvers
|
23
|
-
Plan.new(ops: steps.map(&:to_plan_op))
|
23
|
+
Plan.new(ops: steps.map!(&:to_plan_op))
|
24
24
|
end
|
25
25
|
|
26
26
|
def steps
|
@@ -76,9 +76,7 @@ module GraphQL
|
|
76
76
|
resolver: nil
|
77
77
|
)
|
78
78
|
# coalesce repeat parameters into a single entrypoint
|
79
|
-
entrypoint =
|
80
|
-
path.each { entrypoint << "/#{_1}" }
|
81
|
-
|
79
|
+
entrypoint = [parent_index, location, parent_type.graphql_name, resolver&.key&.to_definition, "#", *path].join("/")
|
82
80
|
step = @steps_by_entrypoint[entrypoint]
|
83
81
|
next_index = step ? parent_index : @planning_index += 1
|
84
82
|
|
@@ -156,7 +154,7 @@ module GraphQL
|
|
156
154
|
when SUBSCRIPTION_OP
|
157
155
|
# A.3) Permit exactly one subscription field.
|
158
156
|
each_field_in_scope(parent_type, @request.operation.selections) do |node|
|
159
|
-
raise
|
157
|
+
raise DocumentError.new("root field") unless @steps_by_entrypoint.empty?
|
160
158
|
|
161
159
|
locations = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name] || SUPERGRAPH_LOCATIONS
|
162
160
|
add_step(
|
@@ -169,7 +167,7 @@ module GraphQL
|
|
169
167
|
end
|
170
168
|
|
171
169
|
else
|
172
|
-
raise
|
170
|
+
raise DocumentError.new("operation type")
|
173
171
|
end
|
174
172
|
end
|
175
173
|
|
@@ -189,7 +187,7 @@ module GraphQL
|
|
189
187
|
each_field_in_scope(parent_type, fragment.selections, &block)
|
190
188
|
|
191
189
|
else
|
192
|
-
raise
|
190
|
+
raise DocumentError.new("selection node type")
|
193
191
|
end
|
194
192
|
end
|
195
193
|
end
|
@@ -271,7 +269,7 @@ module GraphQL
|
|
271
269
|
end
|
272
270
|
|
273
271
|
else
|
274
|
-
raise
|
272
|
+
raise DocumentError.new("selection node type")
|
275
273
|
end
|
276
274
|
end
|
277
275
|
|
@@ -333,7 +331,7 @@ module GraphQL
|
|
333
331
|
end
|
334
332
|
|
335
333
|
if expanded_selections
|
336
|
-
@
|
334
|
+
@supergraph.schema.possible_types(parent_type).each do |possible_type|
|
337
335
|
next unless @supergraph.locations_by_type[possible_type.graphql_name].include?(current_location)
|
338
336
|
|
339
337
|
type_name = GraphQL::Language::Nodes::TypeName.new(name: possible_type.graphql_name)
|
@@ -26,9 +26,6 @@ module GraphQL
|
|
26
26
|
# @return [Hash] contextual object passed through resolver flows.
|
27
27
|
attr_reader :context
|
28
28
|
|
29
|
-
# @return [GraphQL::Schema::Warden] a visibility warden for this request.
|
30
|
-
attr_reader :warden
|
31
|
-
|
32
29
|
# Creates a new supergraph request.
|
33
30
|
# @param supergraph [Supergraph] supergraph instance that resolves the request.
|
34
31
|
# @param document [String, GraphQL::Language::Nodes::Document] the request string or parsed AST.
|
@@ -58,7 +55,6 @@ module GraphQL
|
|
58
55
|
@variables = variables || {}
|
59
56
|
|
60
57
|
@query = GraphQL::Query.new(@supergraph.schema, document: @document, context: context)
|
61
|
-
@warden = @query.warden
|
62
58
|
@context = @query.context
|
63
59
|
@context[:request] = self
|
64
60
|
end
|
@@ -141,7 +137,7 @@ module GraphQL
|
|
141
137
|
# Validates the request using the combined supergraph schema.
|
142
138
|
# @return [Array<GraphQL::ExecutionError>] an array of static validation errors
|
143
139
|
def validate
|
144
|
-
result = @supergraph.static_validator.validate(@query)
|
140
|
+
result = @supergraph.schema.static_validator.validate(@query)
|
145
141
|
result[:errors]
|
146
142
|
end
|
147
143
|
|
@@ -50,11 +50,6 @@ module GraphQL
|
|
50
50
|
end.freeze
|
51
51
|
end
|
52
52
|
|
53
|
-
# @return [GraphQL::StaticValidation::Validator] static validator for the supergraph schema.
|
54
|
-
def static_validator
|
55
|
-
@static_validator ||= @schema.static_validator
|
56
|
-
end
|
57
|
-
|
58
53
|
def resolvers_by_version
|
59
54
|
@resolvers_by_version ||= resolvers.values.tap(&:flatten!).each_with_object({}) do |resolver, memo|
|
60
55
|
memo[resolver.version] = resolver
|
data/lib/graphql/stitching.rb
CHANGED
@@ -25,6 +25,11 @@ module GraphQL
|
|
25
25
|
class StitchingError < StandardError; end
|
26
26
|
class CompositionError < StitchingError; end
|
27
27
|
class ValidationError < CompositionError; end
|
28
|
+
class DocumentError < StandardError
|
29
|
+
def initialize(element)
|
30
|
+
super("Invalid #{element} encountered in document")
|
31
|
+
end
|
32
|
+
end
|
28
33
|
|
29
34
|
class << self
|
30
35
|
attr_writer :stitch_directive
|
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.5.
|
4
|
+
version: 1.5.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:
|
11
|
+
date: 2025-02-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: '2.0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: '2.0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -151,10 +151,10 @@ files:
|
|
151
151
|
- examples/subscriptions/public/apple-touch-icon.png
|
152
152
|
- examples/subscriptions/public/favicon.ico
|
153
153
|
- examples/subscriptions/public/robots.txt
|
154
|
-
- gemfiles/graphql_1.13.9.gemfile
|
155
154
|
- gemfiles/graphql_2.0.0.gemfile
|
156
155
|
- gemfiles/graphql_2.1.0.gemfile
|
157
156
|
- gemfiles/graphql_2.2.0.gemfile
|
157
|
+
- gemfiles/graphql_2.3.0.gemfile
|
158
158
|
- graphql-stitching.gemspec
|
159
159
|
- lib/graphql/stitching.rb
|
160
160
|
- lib/graphql/stitching/client.rb
|