graphql-stitching 0.3.6 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +77 -12
- data/docs/README.md +1 -1
- data/docs/client.md +103 -0
- data/docs/composer.md +2 -2
- data/docs/supergraph.md +1 -1
- data/example/gateway.rb +4 -4
- data/lib/graphql/stitching/boundary.rb +28 -0
- data/lib/graphql/stitching/{gateway.rb → client.rb} +12 -12
- data/lib/graphql/stitching/composer/validate_boundaries.rb +6 -6
- data/lib/graphql/stitching/composer/validate_interfaces.rb +2 -2
- data/lib/graphql/stitching/composer.rb +49 -29
- data/lib/graphql/stitching/executor/boundary_source.rb +199 -0
- data/lib/graphql/stitching/executor/root_source.rb +48 -0
- data/lib/graphql/stitching/executor.rb +6 -229
- data/lib/graphql/stitching/{remote_client.rb → http_executable.rb} +1 -1
- data/lib/graphql/stitching/plan.rb +65 -0
- data/lib/graphql/stitching/planner.rb +70 -89
- data/lib/graphql/stitching/planner_step.rb +63 -0
- data/lib/graphql/stitching/request.rb +2 -51
- data/lib/graphql/stitching/selection_hint.rb +29 -0
- data/lib/graphql/stitching/shaper.rb +2 -2
- data/lib/graphql/stitching/skip_include.rb +81 -0
- data/lib/graphql/stitching/supergraph.rb +29 -23
- data/lib/graphql/stitching/util.rb +49 -38
- data/lib/graphql/stitching/version.rb +1 -1
- data/lib/graphql/stitching.rb +8 -4
- metadata +12 -6
- data/docs/gateway.md +0 -103
- data/lib/graphql/stitching/planner_operation.rb +0 -63
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4ce8bb1075d536f01aa63487df3e1686bc0c2899b58ca14e95383caef38812e2
|
4
|
+
data.tar.gz: ae39e685bdb31cbf3962216cfce263b2970aa2aeac6be1901a7397cee6eb1656
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e718527102969773accf28b2323cbde1d2c467339c44bb876144ae5387a7aaec64143b35b6dfff21401fb087c64ed8742f79cda78e743518ba045871c320f51f
|
7
|
+
data.tar.gz: 74bc97f475b61ea51dd8ce9e42a1ff2783d0c1717a38ff812e256962d64b97ef0ccc092de11e529a22e274d8ad5cf77af9fd7ed2fd680e20e0c058534942a5d6
|
data/README.md
CHANGED
@@ -9,6 +9,7 @@ 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.
|
12
13
|
|
13
14
|
**NOT Supported:**
|
14
15
|
- Computed fields (ie: federation-style `@requires`).
|
@@ -32,7 +33,7 @@ require "graphql/stitching"
|
|
32
33
|
|
33
34
|
## Usage
|
34
35
|
|
35
|
-
The quickest way to start is to use the provided [`
|
36
|
+
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 [caching hooks](./docs/client.md#cache-hooks):
|
36
37
|
|
37
38
|
```ruby
|
38
39
|
movies_schema = <<~GRAPHQL
|
@@ -45,21 +46,21 @@ showtimes_schema = <<~GRAPHQL
|
|
45
46
|
type Query { showtime(id: ID!): Showtime }
|
46
47
|
GRAPHQL
|
47
48
|
|
48
|
-
|
49
|
+
client = GraphQL::Stitching::Client.new(locations: {
|
49
50
|
movies: {
|
50
51
|
schema: GraphQL::Schema.from_definition(movies_schema),
|
51
|
-
executable: GraphQL::Stitching::
|
52
|
+
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3000"),
|
52
53
|
},
|
53
54
|
showtimes: {
|
54
55
|
schema: GraphQL::Schema.from_definition(showtimes_schema),
|
55
|
-
executable: GraphQL::Stitching::
|
56
|
+
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3001"),
|
56
57
|
},
|
57
58
|
my_local: {
|
58
59
|
schema: MyLocal::GraphQL::Schema,
|
59
60
|
},
|
60
61
|
})
|
61
62
|
|
62
|
-
result =
|
63
|
+
result = client.execute(
|
63
64
|
query: "query FetchFromAll($movieId:ID!, $showtimeId:ID!){
|
64
65
|
movie(id:$movieId) { name }
|
65
66
|
showtime(id:$showtimeId): { time }
|
@@ -72,7 +73,7 @@ result = gateway.execute(
|
|
72
73
|
|
73
74
|
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. See [composer docs](./docs/composer.md#merge-patterns) for more information on how schemas get merged.
|
74
75
|
|
75
|
-
While the `
|
76
|
+
While the `Client` constructor is an easy quick start, the library also has several discrete components that can be assembled into custom workflows:
|
76
77
|
|
77
78
|
- [Composer](./docs/composer.md) - merges and validates many schemas into one supergraph.
|
78
79
|
- [Supergraph](./docs/supergraph.md) - manages the combined schema, location routing maps, and executable resources. Can be exported, cached, and rehydrated.
|
@@ -86,7 +87,11 @@ While the `Gateway` constructor is an easy quick start, the library also has sev
|
|
86
87
|
|
87
88
|
![Merging types](./docs/images/merging.png)
|
88
89
|
|
89
|
-
To facilitate this merging of types, stitching must know how to cross-reference and fetch each variant of a type from its source location. This
|
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. This can be done using [arbitrary queries](#merged-types-via-arbitrary-queries) or [federation entities](#merged-types-via-federation-entities).
|
91
|
+
|
92
|
+
### Merged types via arbitrary queries
|
93
|
+
|
94
|
+
Types can merge through arbitrary queries using the `@stitch` directive:
|
90
95
|
|
91
96
|
```graphql
|
92
97
|
directive @stitch(key: String!) repeatable on FIELD_DEFINITION
|
@@ -121,14 +126,14 @@ shipping_schema = <<~GRAPHQL
|
|
121
126
|
}
|
122
127
|
GRAPHQL
|
123
128
|
|
124
|
-
|
129
|
+
client = GraphQL::Stitching::Client.new(locations: {
|
125
130
|
products: {
|
126
131
|
schema: GraphQL::Schema.from_definition(products_schema),
|
127
|
-
executable: GraphQL::Stitching::
|
132
|
+
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3001"),
|
128
133
|
},
|
129
134
|
shipping: {
|
130
135
|
schema: GraphQL::Schema.from_definition(shipping_schema),
|
131
|
-
executable: GraphQL::Stitching::
|
136
|
+
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3002"),
|
132
137
|
},
|
133
138
|
})
|
134
139
|
```
|
@@ -294,6 +299,66 @@ The library is configured to use a `@stitch` directive by default. You may custo
|
|
294
299
|
GraphQL::Stitching.stitch_directive = "merge"
|
295
300
|
```
|
296
301
|
|
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
|
+
accounts_schema = <<~GRAPHQL
|
315
|
+
directive @key(fields: String!) repeatable on OBJECT
|
316
|
+
|
317
|
+
type User @key(fields: "id") {
|
318
|
+
id: ID!
|
319
|
+
name: String!
|
320
|
+
address: String!
|
321
|
+
}
|
322
|
+
|
323
|
+
union _Entity = User
|
324
|
+
scalar _Any
|
325
|
+
|
326
|
+
type Query {
|
327
|
+
user(id: ID!): User
|
328
|
+
_entities(representations: [_Any!]!): [_Entity]!
|
329
|
+
}
|
330
|
+
GRAPHQL
|
331
|
+
|
332
|
+
comments_schema = <<~GRAPHQL
|
333
|
+
directive @key(fields: String!) repeatable on OBJECT
|
334
|
+
|
335
|
+
type User @key(fields: "id") {
|
336
|
+
id: ID!
|
337
|
+
comments: [String!]!
|
338
|
+
}
|
339
|
+
|
340
|
+
union _Entity = User
|
341
|
+
scalar _Any
|
342
|
+
|
343
|
+
type Query {
|
344
|
+
_entities(representations: [_Any!]!): [_Entity]!
|
345
|
+
}
|
346
|
+
GRAPHQL
|
347
|
+
|
348
|
+
client = GraphQL::Stitching::Client.new(locations: {
|
349
|
+
accounts: {
|
350
|
+
schema: GraphQL::Schema.from_definition(accounts_schema),
|
351
|
+
executable: ...,
|
352
|
+
},
|
353
|
+
comments: {
|
354
|
+
schema: GraphQL::Schema.from_definition(comments_schema),
|
355
|
+
executable: ...,
|
356
|
+
},
|
357
|
+
})
|
358
|
+
```
|
359
|
+
|
360
|
+
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.
|
361
|
+
|
297
362
|
## Executables
|
298
363
|
|
299
364
|
An executable resource performs location-specific GraphQL requests. Executables may be `GraphQL::Schema` classes, or any object that responds to `.call(location, source, variables, context)` and returns a raw GraphQL response:
|
@@ -320,7 +385,7 @@ supergraph = GraphQL::Stitching::Composer.new.perform({
|
|
320
385
|
},
|
321
386
|
second: {
|
322
387
|
schema: SecondSchema,
|
323
|
-
executable: GraphQL::Stitching::
|
388
|
+
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3001", headers: { ... }),
|
324
389
|
},
|
325
390
|
third: {
|
326
391
|
schema: ThirdSchema,
|
@@ -333,7 +398,7 @@ supergraph = GraphQL::Stitching::Composer.new.perform({
|
|
333
398
|
})
|
334
399
|
```
|
335
400
|
|
336
|
-
The `GraphQL::Stitching::
|
401
|
+
The `GraphQL::Stitching::HttpExecutable` class is provided as a simple executable wrapper around `Net::HTTP.post`. You should build your own executables to leverage your existing libraries and to add instrumentation. Note that you must manually assign all executables to a `Supergraph` when rehydrating it from cache ([see docs](./docs/supergraph.md)).
|
337
402
|
|
338
403
|
## Concurrency
|
339
404
|
|
data/docs/README.md
CHANGED
@@ -6,7 +6,7 @@ This module provides a collection of components that may be composed into a stit
|
|
6
6
|
|
7
7
|
Major components include:
|
8
8
|
|
9
|
-
- [
|
9
|
+
- [Client](./client.md) - an out-of-the-box setup for performing stitched requests.
|
10
10
|
- [Composer](./composer.md) - merges and validates many schemas into one graph.
|
11
11
|
- [Supergraph](./supergraph.md) - manages the combined schema and location routing maps. Can be exported, cached, and rehydrated.
|
12
12
|
- [Request](./request.md) - prepares a requested GraphQL document and variables for stitching.
|
data/docs/client.md
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
## GraphQL::Stitching::Client
|
2
|
+
|
3
|
+
The `Client` is an out-of-the-box convenience with all stitching components assembled into a default workflow. A client is designed to work for most common needs, though you're welcome to assemble the component parts into your own configuration (see the [client source](../lib/graphql/stitching/client.rb) for an example). A client is constructed with the same [location settings](./composer.md#performing-composition) used to perform supergraph composition:
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
movies_schema = "type Query { ..."
|
7
|
+
showtimes_schema = "type Query { ..."
|
8
|
+
|
9
|
+
client = GraphQL::Stitching::Client.new(locations: {
|
10
|
+
products: {
|
11
|
+
schema: GraphQL::Schema.from_definition(movies_schema),
|
12
|
+
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3000"),
|
13
|
+
stitch: [{ field_name: "products", key: "id" }],
|
14
|
+
},
|
15
|
+
showtimes: {
|
16
|
+
schema: GraphQL::Schema.from_definition(showtimes_schema),
|
17
|
+
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3001"),
|
18
|
+
},
|
19
|
+
my_local: {
|
20
|
+
schema: MyLocal::GraphQL::Schema,
|
21
|
+
},
|
22
|
+
})
|
23
|
+
```
|
24
|
+
|
25
|
+
Alternatively, you may pass a prebuilt `Supergraph` instance to the `Client` constructor. This is useful when [exporting and rehydrating](./supergraph.md#export-and-caching) supergraph instances, which bypasses the need for runtime composition:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
exported_schema = "type Query { ..."
|
29
|
+
exported_mapping = JSON.parse("{ ... }")
|
30
|
+
supergraph = GraphQL::Stitching::Supergraph.from_export(
|
31
|
+
schema: exported_schema,
|
32
|
+
delegation_map: exported_mapping,
|
33
|
+
executables: { ... },
|
34
|
+
)
|
35
|
+
|
36
|
+
client = GraphQL::Stitching::Client.new(supergraph: supergraph)
|
37
|
+
```
|
38
|
+
|
39
|
+
### Execution
|
40
|
+
|
41
|
+
A client provides an `execute` method with a subset of arguments provided by [`GraphQL::Schema.execute`](https://graphql-ruby.org/queries/executing_queries). Executing requests on a stitching client becomes mostly a drop-in replacement to executing on a `GraphQL::Schema` instance:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
result = client.execute(
|
45
|
+
query: "query MyProduct($id: ID!) { product(id: $id) { name } }",
|
46
|
+
variables: { "id" => "1" },
|
47
|
+
operation_name: "MyProduct",
|
48
|
+
)
|
49
|
+
```
|
50
|
+
|
51
|
+
Arguments for the `execute` method include:
|
52
|
+
|
53
|
+
* `query`: a query (or mutation) as a string or parsed AST.
|
54
|
+
* `variables`: a hash of variables for the request.
|
55
|
+
* `operation_name`: the name of the operation to execute (when multiple are provided).
|
56
|
+
* `validate`: true if static validation should run on the supergraph schema before execution.
|
57
|
+
* `context`: an object passed through to executable calls and client hooks.
|
58
|
+
|
59
|
+
### Cache hooks
|
60
|
+
|
61
|
+
The client provides cache hooks to enable caching query plans across requests. Without caching, every request made to the client will be planned individually. With caching, a query may be planned once, cached, and then executed from cache for subsequent requests. Cache keys are a normalized digest of each query string.
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
client.on_cache_read do |key, _context|
|
65
|
+
$redis.get(key) # << 3P code
|
66
|
+
end
|
67
|
+
|
68
|
+
client.on_cache_write do |key, payload, _context|
|
69
|
+
$redis.set(key, payload) # << 3P code
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
Note that inlined input data works against caching, so you should _avoid_ this:
|
74
|
+
|
75
|
+
```graphql
|
76
|
+
query {
|
77
|
+
product(id: "1") { name }
|
78
|
+
}
|
79
|
+
```
|
80
|
+
|
81
|
+
Instead, always leverage variables in queries so that the document body remains consistent across requests:
|
82
|
+
|
83
|
+
```graphql
|
84
|
+
query($id: ID!) {
|
85
|
+
product(id: $id) { name }
|
86
|
+
}
|
87
|
+
|
88
|
+
# variables: { "id" => "1" }
|
89
|
+
```
|
90
|
+
|
91
|
+
### Error hooks
|
92
|
+
|
93
|
+
The client also provides an error hook. Any program errors rescued during execution will be passed to the `on_error` handler, which can report on the error as needed and return a formatted error message for the client to add to the [GraphQL errors](https://spec.graphql.org/June2018/#sec-Errors) result.
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
client.on_error do |err, context|
|
97
|
+
# log the error
|
98
|
+
Bugsnag.notify(err)
|
99
|
+
|
100
|
+
# return a formatted message for the public response
|
101
|
+
"Whoops, please contact support abount request '#{context[:request_id]}'"
|
102
|
+
end
|
103
|
+
```
|
data/docs/composer.md
CHANGED
@@ -68,12 +68,12 @@ products_sdl = "type Query { ..."
|
|
68
68
|
supergraph = GraphQL::Stitching::Composer.new.perform({
|
69
69
|
storefronts: {
|
70
70
|
schema: GraphQL::Schema.from_definition(storefronts_sdl),
|
71
|
-
executable: GraphQL::Stitching::
|
71
|
+
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3001"),
|
72
72
|
stitch: [{ field_name: "storefront", key: "id" }],
|
73
73
|
},
|
74
74
|
products: {
|
75
75
|
schema: GraphQL::Schema.from_definition(products_sdl),
|
76
|
-
executable: GraphQL::Stitching::
|
76
|
+
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3002"),
|
77
77
|
},
|
78
78
|
my_local: {
|
79
79
|
schema: MyLocalSchema,
|
data/docs/supergraph.md
CHANGED
@@ -28,7 +28,7 @@ supergraph = GraphQL::Stitching::Supergraph.from_export(
|
|
28
28
|
schema: supergraph_sdl,
|
29
29
|
delegation_map: delegation_map,
|
30
30
|
executables: {
|
31
|
-
my_remote: GraphQL::Stitching::
|
31
|
+
my_remote: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3000"),
|
32
32
|
my_local: MyLocalSchema,
|
33
33
|
}
|
34
34
|
)
|
data/example/gateway.rb
CHANGED
@@ -13,17 +13,17 @@ class StitchedApp
|
|
13
13
|
@graphiql = file.read
|
14
14
|
file.close
|
15
15
|
|
16
|
-
@
|
16
|
+
@client = GraphQL::Stitching::Client.new(locations: {
|
17
17
|
products: {
|
18
18
|
schema: Schemas::Example::Products,
|
19
19
|
},
|
20
20
|
storefronts: {
|
21
21
|
schema: Schemas::Example::Storefronts,
|
22
|
-
executable: GraphQL::Stitching::
|
22
|
+
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3001/graphql"),
|
23
23
|
},
|
24
24
|
manufacturers: {
|
25
25
|
schema: Schemas::Example::Manufacturers,
|
26
|
-
executable: GraphQL::Stitching::
|
26
|
+
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3002/graphql"),
|
27
27
|
}
|
28
28
|
})
|
29
29
|
end
|
@@ -34,7 +34,7 @@ class StitchedApp
|
|
34
34
|
when /graphql/
|
35
35
|
params = JSON.parse(req.body.read)
|
36
36
|
|
37
|
-
result = @
|
37
|
+
result = @client.execute(
|
38
38
|
query: params["query"],
|
39
39
|
variables: params["variables"],
|
40
40
|
operation_name: params["operationName"],
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Stitching
|
5
|
+
Boundary = Struct.new(
|
6
|
+
:location,
|
7
|
+
:type_name,
|
8
|
+
:key,
|
9
|
+
:field,
|
10
|
+
:arg,
|
11
|
+
:list,
|
12
|
+
:federation,
|
13
|
+
keyword_init: true
|
14
|
+
) do
|
15
|
+
def as_json
|
16
|
+
{
|
17
|
+
location: location,
|
18
|
+
type_name: type_name,
|
19
|
+
key: key,
|
20
|
+
field: field,
|
21
|
+
arg: arg,
|
22
|
+
list: list,
|
23
|
+
federation: federation,
|
24
|
+
}.tap(&:compact!)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -4,16 +4,16 @@ require "json"
|
|
4
4
|
|
5
5
|
module GraphQL
|
6
6
|
module Stitching
|
7
|
-
class
|
8
|
-
class
|
7
|
+
class Client
|
8
|
+
class ClientError < StitchingError; end
|
9
9
|
|
10
10
|
attr_reader :supergraph
|
11
11
|
|
12
12
|
def initialize(locations: nil, supergraph: nil, composer: nil)
|
13
13
|
@supergraph = if locations && supergraph
|
14
|
-
raise
|
14
|
+
raise ClientError, "Cannot provide both locations and a supergraph."
|
15
15
|
elsif supergraph && !supergraph.is_a?(GraphQL::Stitching::Supergraph)
|
16
|
-
raise
|
16
|
+
raise ClientError, "Provided supergraph must be a GraphQL::Stitching::Supergraph instance."
|
17
17
|
elsif supergraph
|
18
18
|
supergraph
|
19
19
|
else
|
@@ -41,7 +41,7 @@ module GraphQL
|
|
41
41
|
GraphQL::Stitching::Planner.new(
|
42
42
|
supergraph: @supergraph,
|
43
43
|
request: request,
|
44
|
-
).perform
|
44
|
+
).perform
|
45
45
|
end
|
46
46
|
|
47
47
|
GraphQL::Stitching::Executor.new(
|
@@ -57,17 +57,17 @@ module GraphQL
|
|
57
57
|
end
|
58
58
|
|
59
59
|
def on_cache_read(&block)
|
60
|
-
raise
|
60
|
+
raise ClientError, "A cache read block is required." unless block_given?
|
61
61
|
@on_cache_read = block
|
62
62
|
end
|
63
63
|
|
64
64
|
def on_cache_write(&block)
|
65
|
-
raise
|
65
|
+
raise ClientError, "A cache write block is required." unless block_given?
|
66
66
|
@on_cache_write = block
|
67
67
|
end
|
68
68
|
|
69
69
|
def on_error(&block)
|
70
|
-
raise
|
70
|
+
raise ClientError, "An error handler block is required." unless block_given?
|
71
71
|
@on_error = block
|
72
72
|
end
|
73
73
|
|
@@ -76,16 +76,16 @@ module GraphQL
|
|
76
76
|
def fetch_plan(request)
|
77
77
|
if @on_cache_read
|
78
78
|
cached_plan = @on_cache_read.call(request.digest, request.context)
|
79
|
-
return JSON.parse(cached_plan) if cached_plan
|
79
|
+
return GraphQL::Stitching::Plan.from_json(JSON.parse(cached_plan)) if cached_plan
|
80
80
|
end
|
81
81
|
|
82
|
-
|
82
|
+
plan = yield
|
83
83
|
|
84
84
|
if @on_cache_write
|
85
|
-
@on_cache_write.call(request.digest, JSON.generate(
|
85
|
+
@on_cache_write.call(request.digest, JSON.generate(plan.as_json), request.context)
|
86
86
|
end
|
87
87
|
|
88
|
-
|
88
|
+
plan
|
89
89
|
end
|
90
90
|
|
91
91
|
def error_result(errors)
|
@@ -32,16 +32,16 @@ module GraphQL
|
|
32
32
|
|
33
33
|
# only one boundary allowed per type/location/key
|
34
34
|
boundaries_by_location_and_key = boundaries.each_with_object({}) do |boundary, memo|
|
35
|
-
if memo.dig(boundary
|
36
|
-
raise Composer::ValidationError, "Multiple boundary queries for `#{type.graphql_name}.#{boundary
|
37
|
-
"found in #{boundary
|
35
|
+
if memo.dig(boundary.location, boundary.key)
|
36
|
+
raise Composer::ValidationError, "Multiple boundary queries for `#{type.graphql_name}.#{boundary.key}` "\
|
37
|
+
"found in #{boundary.location}. Limit one boundary query per type and key in each location. "\
|
38
38
|
"Abstract boundaries provide all possible types."
|
39
39
|
end
|
40
|
-
memo[boundary
|
41
|
-
memo[boundary
|
40
|
+
memo[boundary.location] ||= {}
|
41
|
+
memo[boundary.location][boundary.key] = boundary
|
42
42
|
end
|
43
43
|
|
44
|
-
boundary_keys = boundaries.map { _1
|
44
|
+
boundary_keys = boundaries.map { _1.key }.uniq
|
45
45
|
key_only_types_by_location = candidate_types_by_location.select do |location, subschema_type|
|
46
46
|
subschema_type.fields.keys.length == 1 && boundary_keys.include?(subschema_type.fields.keys.first)
|
47
47
|
end
|
@@ -32,12 +32,12 @@ module GraphQL
|
|
32
32
|
interface_type_structure.each_with_index do |interface_struct, index|
|
33
33
|
possible_struct = possible_type_structure[index]
|
34
34
|
|
35
|
-
if possible_struct
|
35
|
+
if possible_struct.name != interface_struct.name
|
36
36
|
raise Composer::ValidationError, "Incompatible named types between field #{possible_type.graphql_name}.#{field_name} of type "\
|
37
37
|
"#{intersecting_field.type.to_type_signature} and interface #{interface_type.graphql_name}.#{field_name} of type #{interface_field.type.to_type_signature}."
|
38
38
|
end
|
39
39
|
|
40
|
-
if possible_struct
|
40
|
+
if possible_struct.null? && interface_struct.non_null?
|
41
41
|
raise Composer::ValidationError, "Incompatible nullability between field #{possible_type.graphql_name}.#{field_name} of type "\
|
42
42
|
"#{intersecting_field.type.to_type_signature} and interface #{interface_type.graphql_name}.#{field_name} of type #{interface_field.type.to_type_signature}."
|
43
43
|
end
|
@@ -30,6 +30,7 @@ module GraphQL
|
|
30
30
|
@deprecation_merger = deprecation_merger || DEFAULT_VALUE_MERGER
|
31
31
|
@directive_kwarg_merger = directive_kwarg_merger || DEFAULT_VALUE_MERGER
|
32
32
|
@root_field_location_selector = root_field_location_selector || DEFAULT_ROOT_FIELD_LOCATION_SELECTOR
|
33
|
+
@stitch_directives = {}
|
33
34
|
end
|
34
35
|
|
35
36
|
def perform(locations_input)
|
@@ -91,7 +92,7 @@ module GraphQL
|
|
91
92
|
when "ENUM"
|
92
93
|
build_enum_type(type_name, types_by_location, enum_usage)
|
93
94
|
when "OBJECT"
|
94
|
-
extract_boundaries(type_name, types_by_location)
|
95
|
+
extract_boundaries(type_name, types_by_location) if type_name == @query_name
|
95
96
|
build_object_type(type_name, types_by_location)
|
96
97
|
when "INTERFACE"
|
97
98
|
build_interface_type(type_name, types_by_location)
|
@@ -145,22 +146,35 @@ module GraphQL
|
|
145
146
|
raise ComposerError, "The schema for `#{location}` location must be a GraphQL::Schema class."
|
146
147
|
end
|
147
148
|
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
locations :FIELD_DEFINITION
|
152
|
-
argument :key, String
|
153
|
-
repeatable true
|
154
|
-
end
|
149
|
+
input.fetch(:stitch, GraphQL::Stitching::EMPTY_ARRAY).each do |dir|
|
150
|
+
type = dir[:parent_type_name] ? schema.types[dir[:parent_type_name]] : schema.query
|
151
|
+
raise ComposerError, "Invalid stitch directive type `#{dir[:parent_type_name]}`" unless type
|
155
152
|
|
156
|
-
|
157
|
-
|
158
|
-
raise ComposerError, "Invalid stitch directive type `#{dir[:type_name]}`" unless type
|
153
|
+
field = type.fields[dir[:field_name]]
|
154
|
+
raise ComposerError, "Invalid stitch directive field `#{dir[:field_name]}`" unless field
|
159
155
|
|
160
|
-
|
161
|
-
|
156
|
+
field_path = "#{location}.#{field.name}"
|
157
|
+
@stitch_directives[field_path] ||= []
|
158
|
+
@stitch_directives[field_path] << dir.slice(:key, :type_name)
|
159
|
+
end
|
162
160
|
|
163
|
-
|
161
|
+
federation_entity_type = schema.types["_Entity"]
|
162
|
+
if federation_entity_type && federation_entity_type.kind.union? && schema.query.fields["_entities"]&.type&.unwrap == federation_entity_type
|
163
|
+
schema.possible_types(federation_entity_type).each do |entity_type|
|
164
|
+
entity_type.directives.each do |directive|
|
165
|
+
next unless directive.graphql_name == "key"
|
166
|
+
|
167
|
+
key = directive.arguments.keyword_arguments.fetch(:fields).strip
|
168
|
+
raise ComposerError, "Composite federation keys are not supported." unless /^\w+$/.match?(key)
|
169
|
+
|
170
|
+
field_path = "#{location}._entities"
|
171
|
+
@stitch_directives[field_path] ||= []
|
172
|
+
@stitch_directives[field_path] << {
|
173
|
+
key: key,
|
174
|
+
type_name: entity_type.graphql_name,
|
175
|
+
federation: true,
|
176
|
+
}
|
177
|
+
end
|
164
178
|
end
|
165
179
|
end
|
166
180
|
|
@@ -416,21 +430,21 @@ module GraphQL
|
|
416
430
|
raise ComposerError, "Cannot compose mixed list structures at `#{path}`."
|
417
431
|
end
|
418
432
|
|
419
|
-
if alt_structure.last
|
433
|
+
if alt_structure.last.name != basis_structure.last.name
|
420
434
|
raise ComposerError, "Cannot compose mixed types at `#{path}`."
|
421
435
|
end
|
422
436
|
end
|
423
437
|
|
424
438
|
type = GraphQL::Schema::BUILT_IN_TYPES.fetch(
|
425
|
-
basis_structure.last
|
426
|
-
build_type_binding(basis_structure.last
|
439
|
+
basis_structure.last.name,
|
440
|
+
build_type_binding(basis_structure.last.name)
|
427
441
|
)
|
428
442
|
|
429
443
|
basis_structure.reverse!.each_with_index do |basis, index|
|
430
444
|
rev_index = basis_structure.length - index - 1
|
431
|
-
non_null = alt_structures.each_with_object([
|
445
|
+
non_null = alt_structures.each_with_object([basis.non_null?]) { |s, m| m << s[rev_index].non_null? }
|
432
446
|
|
433
|
-
type = type.to_list_type if basis
|
447
|
+
type = type.to_list_type if basis.list?
|
434
448
|
type = type.to_non_null_type if argument_name ? non_null.any? : non_null.all?
|
435
449
|
end
|
436
450
|
|
@@ -462,11 +476,16 @@ module GraphQL
|
|
462
476
|
type_candidate.fields.each do |field_name, field_candidate|
|
463
477
|
boundary_type_name = field_candidate.type.unwrap.graphql_name
|
464
478
|
boundary_structure = Util.flatten_type_structure(field_candidate.type)
|
479
|
+
boundary_kwargs = @stitch_directives["#{location}.#{field_name}"] || []
|
465
480
|
|
466
481
|
field_candidate.directives.each do |directive|
|
467
482
|
next unless directive.graphql_name == GraphQL::Stitching.stitch_directive
|
483
|
+
boundary_kwargs << directive.arguments.keyword_arguments
|
484
|
+
end
|
468
485
|
|
469
|
-
|
486
|
+
boundary_kwargs.each do |kwargs|
|
487
|
+
key = kwargs.fetch(:key)
|
488
|
+
impl_type_name = kwargs.fetch(:type_name, boundary_type_name)
|
470
489
|
key_selections = GraphQL.parse("{ #{key} }").definitions[0].selections
|
471
490
|
|
472
491
|
if key_selections.length != 1
|
@@ -489,15 +508,16 @@ module GraphQL
|
|
489
508
|
raise ComposerError, "Mismatched input/output for #{type_name}.#{field_name}.#{argument_name} boundary. Arguments must map directly to results."
|
490
509
|
end
|
491
510
|
|
492
|
-
@boundary_map[
|
493
|
-
@boundary_map[
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
511
|
+
@boundary_map[impl_type_name] ||= []
|
512
|
+
@boundary_map[impl_type_name] << Boundary.new(
|
513
|
+
location: location,
|
514
|
+
type_name: impl_type_name,
|
515
|
+
key: key_selections[0].name,
|
516
|
+
field: field_candidate.name,
|
517
|
+
arg: argument_name,
|
518
|
+
list: boundary_structure.first.list?,
|
519
|
+
federation: kwargs[:federation],
|
520
|
+
)
|
501
521
|
end
|
502
522
|
end
|
503
523
|
end
|