graphql-stitching 1.2.2 → 1.2.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +8 -2
- data/README.md +40 -78
- data/docs/federation_entities.md +70 -0
- data/gemfiles/graphql_1.13.9.gemfile +1 -1
- data/gemfiles/graphql_2.0.0.gemfile +6 -0
- data/gemfiles/graphql_2.1.0.gemfile +6 -0
- data/gemfiles/graphql_2.2.0.gemfile +6 -0
- data/lib/graphql/stitching/composer/boundary_config.rb +73 -0
- data/lib/graphql/stitching/composer.rb +28 -44
- data/lib/graphql/stitching/supergraph/resolver_directive.rb +1 -0
- data/lib/graphql/stitching/supergraph.rb +2 -1
- data/lib/graphql/stitching/version.rb +1 -1
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5c1c6ae95fd6ec036a7678356c8bc48191d8cf5cfd9e4b2936e79fdb72b52f48
|
4
|
+
data.tar.gz: 7f8002d013e26f69df5a7023e147f98b86a46cc3027d2753b6d704bfe75a835b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 734fe2a73f5412f30ccc6d89a20e62c1da376422de65a816a9680f02330cb2df93c39803cc182c5eed1c310624509beccb0a69162c1dad407ccaf6ada69178dd
|
7
|
+
data.tar.gz: c293a3de403d0888d15047e2de120a8187e4fee598229572f37905dd4e4b3429017ee958ae8ab2a2c16561d25761128b9a6ffb954cbf49b5a310c92aa77f7ffe
|
data/.github/workflows/ci.yml
CHANGED
@@ -16,10 +16,16 @@ jobs:
|
|
16
16
|
ruby: 3.2
|
17
17
|
- gemfile: Gemfile
|
18
18
|
ruby: 3.1
|
19
|
-
- gemfile: gemfiles/graphql_1.13.9.gemfile
|
20
|
-
ruby: 3.1
|
21
19
|
- gemfile: Gemfile
|
22
20
|
ruby: 2.7
|
21
|
+
- gemfile: gemfiles/graphql_2.2.0.gemfile
|
22
|
+
ruby: 3.1
|
23
|
+
- gemfile: gemfiles/graphql_2.1.0.gemfile
|
24
|
+
ruby: 3.1
|
25
|
+
- gemfile: gemfiles/graphql_2.0.0.gemfile
|
26
|
+
ruby: 3.1
|
27
|
+
- gemfile: gemfiles/graphql_1.13.9.gemfile
|
28
|
+
ruby: 3.1
|
23
29
|
|
24
30
|
steps:
|
25
31
|
- uses: actions/checkout@v2
|
data/README.md
CHANGED
@@ -9,14 +9,14 @@ GraphQL stitching composes a single schema from multiple underlying GraphQL reso
|
|
9
9
|
- Multiple keys per merged type.
|
10
10
|
- Shared objects, fields, enums, and inputs across locations.
|
11
11
|
- Combining local and remote schemas.
|
12
|
-
- Type merging via arbitrary queries or federation `_entities` protocol.
|
13
12
|
- File uploads via [multipart form spec](https://github.com/jaydenseric/graphql-multipart-request-spec).
|
13
|
+
- Tested with all minor versions of `graphql-ruby`.
|
14
14
|
|
15
15
|
**NOT Supported:**
|
16
16
|
- Computed fields (ie: federation-style `@requires`).
|
17
17
|
- Subscriptions, defer/stream.
|
18
18
|
|
19
|
-
This Ruby implementation is a sibling to [GraphQL Tools](https://the-guild.dev/graphql/stitching) (JS) and [Bramble](https://movio.github.io/bramble/) (Go), and its capabilities fall somewhere in between them. GraphQL stitching is similar in concept to [Apollo Federation](https://www.apollographql.com/docs/federation/), though more generic.
|
19
|
+
This Ruby implementation is a sibling to [GraphQL Tools](https://the-guild.dev/graphql/stitching) (JS) and [Bramble](https://movio.github.io/bramble/) (Go), and its capabilities fall somewhere in between them. GraphQL stitching is similar in concept to [Apollo Federation](https://www.apollographql.com/docs/federation/), though more generic. 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 to build a purely high-performance API gateway, consider not using Ruby.
|
20
20
|
|
21
21
|
## Getting started
|
22
22
|
|
@@ -34,7 +34,7 @@ require "graphql/stitching"
|
|
34
34
|
|
35
35
|
## Usage
|
36
36
|
|
37
|
-
The quickest way to start is to use the provided [`Client`](./docs/client.md) component that wraps a stitched graph in an executable workflow with
|
37
|
+
The quickest way to start is to use the provided [`Client`](./docs/client.md) component that wraps a stitched graph in an executable workflow (with optional query plan caching hooks):
|
38
38
|
|
39
39
|
```ruby
|
40
40
|
movies_schema = <<~GRAPHQL
|
@@ -72,7 +72,7 @@ result = client.execute(
|
|
72
72
|
)
|
73
73
|
```
|
74
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
|
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
76
|
|
77
77
|
While the `Client` constructor is an easy quick start, the library also has several discrete components that can be assembled into custom workflows:
|
78
78
|
|
@@ -87,11 +87,11 @@ While the `Client` constructor is an easy quick start, the library also has seve
|
|
87
87
|
|
88
88
|
![Merging types](./docs/images/merging.png)
|
89
89
|
|
90
|
-
To facilitate this merging of types, stitching must know how to cross-reference and fetch each variant of a type from its source location
|
90
|
+
To facilitate this merging of types, stitching must know how to cross-reference and fetch each variant of a type from its source location using [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
91
|
|
92
|
-
### Merged
|
92
|
+
### Merged type resolver queries
|
93
93
|
|
94
|
-
Types
|
94
|
+
Types merge through resolver queries identified by a `@stitch` directive:
|
95
95
|
|
96
96
|
```graphql
|
97
97
|
directive @stitch(key: String!) repeatable on FIELD_DEFINITION
|
@@ -180,7 +180,7 @@ See [error handling](./docs/mechanics.md#stitched-errors) tips for list queries.
|
|
180
180
|
|
181
181
|
#### Abstract queries
|
182
182
|
|
183
|
-
It's okay for stitching queries to be implemented through abstract types. An abstract query will provide access to all of its possible types
|
183
|
+
It's okay for stitching 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.
|
184
184
|
|
185
185
|
```graphql
|
186
186
|
interface Node {
|
@@ -195,13 +195,33 @@ type Query {
|
|
195
195
|
}
|
196
196
|
```
|
197
197
|
|
198
|
+
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.
|
199
|
+
|
200
|
+
```graphql
|
201
|
+
directive @stitch(key: String!, typeName: String) repeatable on FIELD_DEFINITION
|
202
|
+
|
203
|
+
type Product { sku: ID! }
|
204
|
+
type Order { id: ID! }
|
205
|
+
type Customer { id: ID! } # << not stitched
|
206
|
+
union Entity = Product | Order | Customer
|
207
|
+
|
208
|
+
type Query {
|
209
|
+
entity(key: ID!): Entity
|
210
|
+
@stitch(key: "sku", typeName: "Product")
|
211
|
+
@stitch(key: "id", typeName: "Order")
|
212
|
+
}
|
213
|
+
```
|
214
|
+
|
198
215
|
#### Multiple query arguments
|
199
216
|
|
200
|
-
Stitching infers which argument to use for queries with a single argument. For queries that accept multiple arguments, the key
|
217
|
+
Stitching infers which argument to use for queries with a single argument, or when the key name matches its intended argument. For queries that accept multiple arguments with unmatched names, the key may provide an argument mapping specified as `"<arg>:<key>"`.
|
201
218
|
|
202
219
|
```graphql
|
220
|
+
type Product {
|
221
|
+
id: ID!
|
222
|
+
}
|
203
223
|
type Query {
|
204
|
-
product(
|
224
|
+
product(by_id: ID, by_sku: ID): Product @stitch(key: "by_id:id")
|
205
225
|
}
|
206
226
|
```
|
207
227
|
|
@@ -211,8 +231,8 @@ A type may exist in multiple locations across the graph using different keys, fo
|
|
211
231
|
|
212
232
|
```graphql
|
213
233
|
type Product { id:ID! } # storefronts location
|
214
|
-
type Product { id:ID!
|
215
|
-
type Product {
|
234
|
+
type Product { id:ID! sku:ID! } # products location
|
235
|
+
type Product { sku:ID! } # catelog location
|
216
236
|
```
|
217
237
|
|
218
238
|
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 stitching queries for each possible key:
|
@@ -220,11 +240,11 @@ In the above graph, the `storefronts` and `catelog` locations have different key
|
|
220
240
|
```graphql
|
221
241
|
type Product {
|
222
242
|
id: ID!
|
223
|
-
|
243
|
+
sku: ID!
|
224
244
|
}
|
225
245
|
type Query {
|
226
246
|
productById(id: ID!): Product @stitch(key: "id")
|
227
|
-
productByUpc(
|
247
|
+
productByUpc(sku: ID!): Product @stitch(key: "sku")
|
228
248
|
}
|
229
249
|
```
|
230
250
|
|
@@ -233,10 +253,10 @@ The `@stitch` directive is also repeatable, allowing a single query to associate
|
|
233
253
|
```graphql
|
234
254
|
type Product {
|
235
255
|
id: ID!
|
236
|
-
|
256
|
+
sku: ID!
|
237
257
|
}
|
238
258
|
type Query {
|
239
|
-
product(id: ID,
|
259
|
+
product(id: ID, sku: ID): Product @stitch(key: "id") @stitch(key: "sku")
|
240
260
|
}
|
241
261
|
```
|
242
262
|
|
@@ -270,11 +290,11 @@ A clean SDL string may also have stitching directives applied via static configu
|
|
270
290
|
sdl_string = <<~GRAPHQL
|
271
291
|
type Product {
|
272
292
|
id: ID!
|
273
|
-
|
293
|
+
sku: ID!
|
274
294
|
}
|
275
295
|
type Query {
|
276
296
|
productById(id: ID!): Product
|
277
|
-
productByUpc(
|
297
|
+
productByUpc(sku: ID!): Product
|
278
298
|
}
|
279
299
|
GRAPHQL
|
280
300
|
|
@@ -284,7 +304,7 @@ supergraph = GraphQL::Stitching::Composer.new.perform({
|
|
284
304
|
executable: ->() { ... },
|
285
305
|
stitch: [
|
286
306
|
{ field_name: "productById", key: "id" },
|
287
|
-
{ field_name: "productByUpc", key: "
|
307
|
+
{ field_name: "productByUpc", key: "sku" },
|
288
308
|
]
|
289
309
|
},
|
290
310
|
# ...
|
@@ -299,65 +319,6 @@ The library is configured to use a `@stitch` directive by default. You may custo
|
|
299
319
|
GraphQL::Stitching.stitch_directive = "merge"
|
300
320
|
```
|
301
321
|
|
302
|
-
### Merged types via Federation entities
|
303
|
-
|
304
|
-
The [Apollo Federation specification](https://www.apollographql.com/docs/federation/subgraph-spec/) defines a standard interface for accessing merged type variants across locations. Stitching can utilize a _subset_ of this interface to facilitate basic type merging. The following spec is supported:
|
305
|
-
|
306
|
-
- `@key(fields: "id")` (repeatable) specifies a key field for an object type. The key `fields` argument may only contain one field selection.
|
307
|
-
- `_Entity` is a union type that must contain all types that implement a `@key`.
|
308
|
-
- `_Any` is a scalar that recieves raw JSON objects; each object representation contains a `__typename` and the type's key field.
|
309
|
-
- `_entities(representations: [_Any!]!): [_Entity]!` is a root query for local entity types.
|
310
|
-
|
311
|
-
The composer will automatcially detect and stitch schemas with an `_entities` query, for example:
|
312
|
-
|
313
|
-
```ruby
|
314
|
-
products_schema = <<~GRAPHQL
|
315
|
-
directive @key(fields: String!) repeatable on OBJECT
|
316
|
-
|
317
|
-
type Product @key(fields: "id") {
|
318
|
-
id: ID!
|
319
|
-
name: String!
|
320
|
-
}
|
321
|
-
|
322
|
-
union _Entity = Product
|
323
|
-
scalar _Any
|
324
|
-
|
325
|
-
type Query {
|
326
|
-
user(id: ID!): User
|
327
|
-
_entities(representations: [_Any!]!): [_Entity]!
|
328
|
-
}
|
329
|
-
GRAPHQL
|
330
|
-
|
331
|
-
catalog_schema = <<~GRAPHQL
|
332
|
-
directive @key(fields: String!) repeatable on OBJECT
|
333
|
-
|
334
|
-
type Product @key(fields: "id") {
|
335
|
-
id: ID!
|
336
|
-
price: Float!
|
337
|
-
}
|
338
|
-
|
339
|
-
union _Entity = Product
|
340
|
-
scalar _Any
|
341
|
-
|
342
|
-
type Query {
|
343
|
-
_entities(representations: [_Any!]!): [_Entity]!
|
344
|
-
}
|
345
|
-
GRAPHQL
|
346
|
-
|
347
|
-
client = GraphQL::Stitching::Client.new(locations: {
|
348
|
-
products: {
|
349
|
-
schema: GraphQL::Schema.from_definition(products_schema),
|
350
|
-
executable: ...,
|
351
|
-
},
|
352
|
-
catalog: {
|
353
|
-
schema: GraphQL::Schema.from_definition(catalog_schema),
|
354
|
-
executable: ...,
|
355
|
-
},
|
356
|
-
})
|
357
|
-
```
|
358
|
-
|
359
|
-
It's perfectly fine to mix and match schemas that implement an `_entities` query with schemas that implement `@stitch` directives; the protocols achieve the same result. Note that stitching is much simpler than Apollo Federation by design, and that Federation's advanced routing features (such as the `@requires` and `@external` directives) will not work with stitching.
|
360
|
-
|
361
322
|
## Executables
|
362
323
|
|
363
324
|
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:
|
@@ -427,6 +388,7 @@ The [Executor](./docs/executor.md) component builds atop the Ruby fiber-based im
|
|
427
388
|
|
428
389
|
- [Modeling foreign keys for stitching](./docs/mechanics.md##modeling-foreign-keys-for-stitching)
|
429
390
|
- [Deploying a stitched schema](./docs/mechanics.md#deploying-a-stitched-schema)
|
391
|
+
- [Schema composition merge patterns](./docs/composer.md#merge-patterns)
|
430
392
|
- [Field selection routing](./docs/mechanics.md#field-selection-routing)
|
431
393
|
- [Root selection routing](./docs/mechanics.md#root-selection-routing)
|
432
394
|
- [Stitched errors](./docs/mechanics.md#stitched-errors)
|
@@ -0,0 +1,70 @@
|
|
1
|
+
## Merged types via Apollo Federation `_entities`
|
2
|
+
|
3
|
+
The [Apollo Federation specification](https://www.apollographql.com/docs/federation/subgraph-spec/) defines a standard interface for accessing merged type variants across locations. Stitching can utilize a _subset_ of this interface to facilitate basic type merging; the full spec is NOT supported and therefore is not fully interchangable with an Apollo Gateway.
|
4
|
+
|
5
|
+
To avoid confusion, using [basic resolver queries](../README.md#merged-type-resolver-queries) is recommended unless you specifically need to interact with a service built for an Apollo ecosystem. Even then, be wary that it does not exceed the supported spec by [using features that will not work](#federation-features-that-will-most-definitly-break).
|
6
|
+
|
7
|
+
### Supported spec
|
8
|
+
|
9
|
+
The following subset of the federation spec is supported:
|
10
|
+
|
11
|
+
- `@key(fields: "id")` (repeatable) specifies a key field for an object type. The key `fields` argument may only contain one field selection.
|
12
|
+
- `_Entity` is a union type that must contain all types that implement a `@key`.
|
13
|
+
- `_Any` is a scalar that recieves raw JSON objects; each object representation contains a `__typename` and the type's key field.
|
14
|
+
- `_entities(representations: [_Any!]!): [_Entity]!` is a root query for local entity types.
|
15
|
+
|
16
|
+
The composer will automatcially detect and stitch schemas with an `_entities` query, for example:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
products_schema = <<~GRAPHQL
|
20
|
+
directive @key(fields: String!) repeatable on OBJECT
|
21
|
+
|
22
|
+
type Product @key(fields: "id") {
|
23
|
+
id: ID!
|
24
|
+
name: String!
|
25
|
+
}
|
26
|
+
|
27
|
+
union _Entity = Product
|
28
|
+
scalar _Any
|
29
|
+
|
30
|
+
type Query {
|
31
|
+
user(id: ID!): User
|
32
|
+
_entities(representations: [_Any!]!): [_Entity]!
|
33
|
+
}
|
34
|
+
GRAPHQL
|
35
|
+
|
36
|
+
catalog_schema = <<~GRAPHQL
|
37
|
+
directive @key(fields: String!) repeatable on OBJECT
|
38
|
+
|
39
|
+
type Product @key(fields: "id") {
|
40
|
+
id: ID!
|
41
|
+
price: Float!
|
42
|
+
}
|
43
|
+
|
44
|
+
union _Entity = Product
|
45
|
+
scalar _Any
|
46
|
+
|
47
|
+
type Query {
|
48
|
+
_entities(representations: [_Any!]!): [_Entity]!
|
49
|
+
}
|
50
|
+
GRAPHQL
|
51
|
+
|
52
|
+
client = GraphQL::Stitching::Client.new(locations: {
|
53
|
+
products: {
|
54
|
+
schema: GraphQL::Schema.from_definition(products_schema),
|
55
|
+
executable: ...,
|
56
|
+
},
|
57
|
+
catalog: {
|
58
|
+
schema: GraphQL::Schema.from_definition(catalog_schema),
|
59
|
+
executable: ...,
|
60
|
+
},
|
61
|
+
})
|
62
|
+
```
|
63
|
+
|
64
|
+
It's perfectly fine to mix and match schemas that implement an `_entities` query with schemas that implement `@stitch` directives; the protocols achieve the same result.
|
65
|
+
|
66
|
+
### Federation features that will most definitly break
|
67
|
+
|
68
|
+
- `@external` fields will confuse the stitching query planner.
|
69
|
+
- `@requires` fields will not be sent any dependencies.
|
70
|
+
- No support for Apollo composition directives.
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL::Stitching
|
4
|
+
class Composer
|
5
|
+
class BoundaryConfig
|
6
|
+
ENTITY_TYPENAME = "_Entity"
|
7
|
+
ENTITIES_QUERY = "_entities"
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def extract_directive_assignments(schema, location, assignments)
|
11
|
+
return EMPTY_OBJECT unless assignments && assignments.any?
|
12
|
+
|
13
|
+
assignments.each_with_object({}) do |kwargs, memo|
|
14
|
+
type = kwargs[:parent_type_name] ? schema.get_type(kwargs[:parent_type_name]) : schema.query
|
15
|
+
raise ComposerError, "Invalid stitch directive type `#{kwargs[:parent_type_name]}`" unless type
|
16
|
+
|
17
|
+
field = type.get_field(kwargs[:field_name])
|
18
|
+
raise ComposerError, "Invalid stitch directive field `#{kwargs[:field_name]}`" unless field
|
19
|
+
|
20
|
+
field_path = "#{location}.#{field.name}"
|
21
|
+
memo[field_path] ||= []
|
22
|
+
memo[field_path] << from_kwargs(kwargs)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def extract_federation_entities(schema, location)
|
27
|
+
return EMPTY_OBJECT unless federation_entities_schema?(schema)
|
28
|
+
|
29
|
+
schema.possible_types(schema.get_type(ENTITY_TYPENAME)).each_with_object({}) do |entity_type, memo|
|
30
|
+
entity_type.directives.each do |directive|
|
31
|
+
next unless directive.graphql_name == "key"
|
32
|
+
|
33
|
+
key = directive.arguments.keyword_arguments.fetch(:fields).strip
|
34
|
+
raise ComposerError, "Composite federation keys are not supported." unless /^\w+$/.match?(key)
|
35
|
+
|
36
|
+
field_path = "#{location}._entities"
|
37
|
+
memo[field_path] ||= []
|
38
|
+
memo[field_path] << new(
|
39
|
+
key: key,
|
40
|
+
type_name: entity_type.graphql_name,
|
41
|
+
federation: true,
|
42
|
+
)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def from_kwargs(kwargs)
|
48
|
+
new(
|
49
|
+
key: kwargs[:key],
|
50
|
+
type_name: kwargs[:type_name] || kwargs[:typeName],
|
51
|
+
federation: kwargs[:federation] || false,
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def federation_entities_schema?(schema)
|
58
|
+
entity_type = schema.get_type(ENTITY_TYPENAME)
|
59
|
+
entities_query = schema.query.get_field(ENTITIES_QUERY)
|
60
|
+
entity_type && entity_type.kind.union? && entities_query && entities_query.type.unwrap == entity_type
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
attr_reader :key, :type_name, :federation
|
65
|
+
|
66
|
+
def initialize(key:, type_name:, federation: false)
|
67
|
+
@key = key
|
68
|
+
@type_name = type_name
|
69
|
+
@federation = federation
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -3,6 +3,7 @@
|
|
3
3
|
require_relative "./composer/base_validator"
|
4
4
|
require_relative "./composer/validate_interfaces"
|
5
5
|
require_relative "./composer/validate_boundaries"
|
6
|
+
require_relative "./composer/boundary_config"
|
6
7
|
|
7
8
|
module GraphQL
|
8
9
|
module Stitching
|
@@ -61,7 +62,7 @@ module GraphQL
|
|
61
62
|
@default_value_merger = default_value_merger || BASIC_VALUE_MERGER
|
62
63
|
@directive_kwarg_merger = directive_kwarg_merger || BASIC_VALUE_MERGER
|
63
64
|
@root_field_location_selector = root_field_location_selector || BASIC_ROOT_FIELD_LOCATION_SELECTOR
|
64
|
-
@
|
65
|
+
@boundary_configs = {}
|
65
66
|
|
66
67
|
@field_map = nil
|
67
68
|
@boundary_map = nil
|
@@ -145,7 +146,8 @@ module GraphQL
|
|
145
146
|
|
146
147
|
builder = self
|
147
148
|
schema = Class.new(GraphQL::Schema) do
|
148
|
-
|
149
|
+
add_type_and_traverse(schema_types.values, root: false)
|
150
|
+
orphan_types(schema_types.values.select { |t| t.respond_to?(:kind) && t.kind.object? })
|
149
151
|
query schema_types[builder.query_name]
|
150
152
|
mutation schema_types[builder.mutation_name]
|
151
153
|
directives builder.schema_directives.values
|
@@ -186,37 +188,8 @@ module GraphQL
|
|
186
188
|
raise ComposerError, "The schema for `#{location}` location must be a GraphQL::Schema class."
|
187
189
|
end
|
188
190
|
|
189
|
-
|
190
|
-
|
191
|
-
raise ComposerError, "Invalid stitch directive type `#{dir[:parent_type_name]}`" unless type
|
192
|
-
|
193
|
-
field = type.fields[dir[:field_name]]
|
194
|
-
raise ComposerError, "Invalid stitch directive field `#{dir[:field_name]}`" unless field
|
195
|
-
|
196
|
-
field_path = "#{location}.#{field.name}"
|
197
|
-
@stitch_directives[field_path] ||= []
|
198
|
-
@stitch_directives[field_path] << dir.slice(:key, :type_name)
|
199
|
-
end
|
200
|
-
|
201
|
-
federation_entity_type = schema.types["_Entity"]
|
202
|
-
if federation_entity_type && federation_entity_type.kind.union? && schema.query.fields["_entities"]&.type&.unwrap == federation_entity_type
|
203
|
-
schema.possible_types(federation_entity_type).each do |entity_type|
|
204
|
-
entity_type.directives.each do |directive|
|
205
|
-
next unless directive.graphql_name == "key"
|
206
|
-
|
207
|
-
key = directive.arguments.keyword_arguments.fetch(:fields).strip
|
208
|
-
raise ComposerError, "Composite federation keys are not supported." unless /^\w+$/.match?(key)
|
209
|
-
|
210
|
-
field_path = "#{location}._entities"
|
211
|
-
@stitch_directives[field_path] ||= []
|
212
|
-
@stitch_directives[field_path] << {
|
213
|
-
key: key,
|
214
|
-
type_name: entity_type.graphql_name,
|
215
|
-
federation: true,
|
216
|
-
}
|
217
|
-
end
|
218
|
-
end
|
219
|
-
end
|
191
|
+
@boundary_configs.merge!(BoundaryConfig.extract_directive_assignments(schema, location, input[:stitch]))
|
192
|
+
@boundary_configs.merge!(BoundaryConfig.extract_federation_entities(schema, location))
|
220
193
|
|
221
194
|
schemas[location.to_s] = schema
|
222
195
|
executables[location.to_s] = input[:executable] || schema
|
@@ -556,19 +529,17 @@ module GraphQL
|
|
556
529
|
def extract_boundaries(type_name, types_by_location)
|
557
530
|
types_by_location.each do |location, type_candidate|
|
558
531
|
type_candidate.fields.each do |field_name, field_candidate|
|
559
|
-
|
532
|
+
boundary_type = field_candidate.type.unwrap
|
560
533
|
boundary_structure = Util.flatten_type_structure(field_candidate.type)
|
561
|
-
|
534
|
+
boundary_configs = @boundary_configs.fetch("#{location}.#{field_name}", [])
|
562
535
|
|
563
536
|
field_candidate.directives.each do |directive|
|
564
537
|
next unless directive.graphql_name == GraphQL::Stitching.stitch_directive
|
565
|
-
|
538
|
+
boundary_configs << BoundaryConfig.from_kwargs(directive.arguments.keyword_arguments)
|
566
539
|
end
|
567
540
|
|
568
|
-
|
569
|
-
|
570
|
-
impl_type_name = kwargs.fetch(:type_name, boundary_type_name)
|
571
|
-
key_selections = GraphQL.parse("{ #{key} }").definitions[0].selections
|
541
|
+
boundary_configs.each do |config|
|
542
|
+
key_selections = GraphQL.parse("{ #{config.key} }").definitions[0].selections
|
572
543
|
|
573
544
|
if key_selections.length != 1
|
574
545
|
raise ComposerError, "Boundary key at #{type_name}.#{field_name} must specify exactly one key."
|
@@ -577,6 +548,8 @@ module GraphQL
|
|
577
548
|
argument_name = key_selections[0].alias
|
578
549
|
argument_name ||= if field_candidate.arguments.size == 1
|
579
550
|
field_candidate.arguments.keys.first
|
551
|
+
elsif field_candidate.arguments[config.key]
|
552
|
+
config.key
|
580
553
|
end
|
581
554
|
|
582
555
|
argument = field_candidate.arguments[argument_name]
|
@@ -590,15 +563,26 @@ module GraphQL
|
|
590
563
|
raise ComposerError, "Mismatched input/output for #{type_name}.#{field_name}.#{argument_name} boundary. Arguments must map directly to results."
|
591
564
|
end
|
592
565
|
|
593
|
-
|
594
|
-
|
566
|
+
boundary_type_name = if config.type_name
|
567
|
+
if !boundary_type.kind.abstract?
|
568
|
+
raise ComposerError, "Resolver config may only specify a type name for abstract resolvers."
|
569
|
+
elsif !boundary_type.possible_types.find { _1.graphql_name == config.type_name }
|
570
|
+
raise ComposerError, "Type `#{config.type_name}` is not a possible return type for query `#{field_name}`."
|
571
|
+
end
|
572
|
+
config.type_name
|
573
|
+
else
|
574
|
+
boundary_type.graphql_name
|
575
|
+
end
|
576
|
+
|
577
|
+
@boundary_map[boundary_type_name] ||= []
|
578
|
+
@boundary_map[boundary_type_name] << Boundary.new(
|
595
579
|
location: location,
|
596
|
-
type_name:
|
580
|
+
type_name: boundary_type_name,
|
597
581
|
key: key_selections[0].name,
|
598
582
|
field: field_candidate.name,
|
599
583
|
arg: argument_name,
|
600
584
|
list: boundary_structure.first.list?,
|
601
|
-
federation:
|
585
|
+
federation: config.federation,
|
602
586
|
)
|
603
587
|
end
|
604
588
|
end
|
@@ -5,6 +5,7 @@ module GraphQL::Stitching
|
|
5
5
|
class ResolverDirective < GraphQL::Schema::Directive
|
6
6
|
graphql_name "resolver"
|
7
7
|
locations OBJECT, INTERFACE, UNION
|
8
|
+
argument :type_name, String, required: false
|
8
9
|
argument :location, String, required: true
|
9
10
|
argument :key, String, required: true
|
10
11
|
argument :field, String, required: true
|
@@ -31,7 +31,7 @@ module GraphQL
|
|
31
31
|
kwargs = directive.arguments.keyword_arguments
|
32
32
|
boundary_map[type_name] ||= []
|
33
33
|
boundary_map[type_name] << Boundary.new(
|
34
|
-
type_name: type_name,
|
34
|
+
type_name: kwargs.fetch(:type_name, type_name),
|
35
35
|
location: kwargs[:location],
|
36
36
|
key: kwargs[:key],
|
37
37
|
field: kwargs[:field],
|
@@ -134,6 +134,7 @@ module GraphQL
|
|
134
134
|
end
|
135
135
|
|
136
136
|
type.directive(ResolverDirective, **{
|
137
|
+
type_name: (boundary.type_name if boundary.type_name != type_name),
|
137
138
|
location: boundary.location,
|
138
139
|
key: boundary.key,
|
139
140
|
field: boundary.field,
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: graphql-stitching
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.2.
|
4
|
+
version: 1.2.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Greg MacWilliam
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-04-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|
@@ -82,6 +82,7 @@ files:
|
|
82
82
|
- docs/README.md
|
83
83
|
- docs/client.md
|
84
84
|
- docs/composer.md
|
85
|
+
- docs/federation_entities.md
|
85
86
|
- docs/http_executable.md
|
86
87
|
- docs/images/library.png
|
87
88
|
- docs/images/merging.png
|
@@ -104,12 +105,16 @@ files:
|
|
104
105
|
- examples/merged_types/remote1.rb
|
105
106
|
- examples/merged_types/remote2.rb
|
106
107
|
- gemfiles/graphql_1.13.9.gemfile
|
108
|
+
- gemfiles/graphql_2.0.0.gemfile
|
109
|
+
- gemfiles/graphql_2.1.0.gemfile
|
110
|
+
- gemfiles/graphql_2.2.0.gemfile
|
107
111
|
- graphql-stitching.gemspec
|
108
112
|
- lib/graphql/stitching.rb
|
109
113
|
- lib/graphql/stitching/boundary.rb
|
110
114
|
- lib/graphql/stitching/client.rb
|
111
115
|
- lib/graphql/stitching/composer.rb
|
112
116
|
- lib/graphql/stitching/composer/base_validator.rb
|
117
|
+
- lib/graphql/stitching/composer/boundary_config.rb
|
113
118
|
- lib/graphql/stitching/composer/validate_boundaries.rb
|
114
119
|
- lib/graphql/stitching/composer/validate_interfaces.rb
|
115
120
|
- lib/graphql/stitching/executor.rb
|