graphql-stitching 0.1.0 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +17 -17
- data/docs/README.md +1 -1
- data/docs/composer.md +45 -4
- data/docs/executor.md +31 -12
- data/docs/images/library.png +0 -0
- data/docs/planner.md +11 -7
- data/docs/request.md +50 -0
- data/docs/supergraph.md +1 -1
- data/graphql-stitching.gemspec +1 -1
- data/lib/graphql/stitching/composer.rb +98 -16
- data/lib/graphql/stitching/executor.rb +88 -45
- data/lib/graphql/stitching/gateway.rb +18 -13
- data/lib/graphql/stitching/planner.rb +204 -151
- data/lib/graphql/stitching/remote_client.rb +4 -4
- data/lib/graphql/stitching/request.rb +133 -0
- data/lib/graphql/stitching/shaper.rb +7 -7
- data/lib/graphql/stitching/supergraph.rb +44 -10
- data/lib/graphql/stitching/util.rb +28 -35
- data/lib/graphql/stitching/version.rb +1 -1
- data/lib/graphql/stitching.rb +7 -1
- metadata +6 -6
- data/docs/document.md +0 -15
- data/lib/graphql/stitching/document.rb +0 -59
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f67a4b892fba612e2e38552ae0a0f681ed0fdc136c04ecef4d2c01692eed9eb8
|
4
|
+
data.tar.gz: 3db164c53dc67d64b7364ba5a17186e3eb612074993b21df5f9a8b059fca9f89
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d687747a19a25a69b1c998910265a183bdb22338fec5e9787412ca5c1cdfc5e0b838bb49ce4942ce41dec0e000e0ddc35bdf07368a7a772751ffc5342b7ee48
|
7
|
+
data.tar.gz: f4419aa525964b37db62ce2343d11914ac56677825c8f1450c5187465b2f28e3a740acf6779309bdb0a5fbd41829054101aed4c9d0eb764882759c50bbf1b641
|
data/README.md
CHANGED
@@ -7,12 +7,12 @@ GraphQL stitching composes a single schema from multiple underlying GraphQL reso
|
|
7
7
|
**Supports:**
|
8
8
|
- Merged object and interface types.
|
9
9
|
- Multiple keys per merged type.
|
10
|
-
- Shared objects, enums, and inputs across locations.
|
10
|
+
- Shared objects, fields, enums, and inputs across locations.
|
11
11
|
- Combining local and remote schemas.
|
12
12
|
|
13
13
|
**NOT Supported:**
|
14
|
-
- Computed fields (ie: federation-style `@requires`)
|
15
|
-
- Subscriptions
|
14
|
+
- Computed fields (ie: federation-style `@requires`).
|
15
|
+
- Subscriptions, defer/stream.
|
16
16
|
|
17
17
|
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. While Ruby is not the fastest language for a high-throughput API gateway, the opportunity here is for a Ruby application to stitch its local schema onto a remote schema (making itself a superset of the remote) without requiring an additional gateway service.
|
18
18
|
|
@@ -70,13 +70,13 @@ result = gateway.execute(
|
|
70
70
|
)
|
71
71
|
```
|
72
72
|
|
73
|
-
Schemas provided to the `Gateway` constructor 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.
|
73
|
+
Schemas provided to the `Gateway` constructor 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) for more information on how schemas get merged.
|
74
74
|
|
75
75
|
While the [`Gateway`](./docs/gateway.md) constructor is an easy quick start, the library also has several discrete components that can be assembled into custom workflows:
|
76
76
|
|
77
77
|
- [Composer](./docs/composer.md) - merges and validates many schemas into one graph.
|
78
78
|
- [Supergraph](./docs/supergraph.md) - manages the combined schema and location routing maps. Can be exported, cached, and rehydrated.
|
79
|
-
- [
|
79
|
+
- [Request](./docs/request.md) - prepares a requested GraphQL document and variables for stitching.
|
80
80
|
- [Planner](./docs/planner.md) - builds a cacheable query plan for a request document.
|
81
81
|
- [Executor](./docs/executor.md) - executes a query plan with given request variables.
|
82
82
|
|
@@ -121,7 +121,7 @@ shipping_schema = <<~GRAPHQL
|
|
121
121
|
}
|
122
122
|
GRAPHQL
|
123
123
|
|
124
|
-
supergraph = GraphQL::Stitching::Composer.new({
|
124
|
+
supergraph = GraphQL::Stitching::Composer.new(schemas: {
|
125
125
|
"products" => GraphQL::Schema.from_definition(products_schema),
|
126
126
|
"shipping" => GraphQL::Schema.from_definition(shipping_schema),
|
127
127
|
})
|
@@ -146,7 +146,7 @@ type Query {
|
|
146
146
|
}
|
147
147
|
```
|
148
148
|
|
149
|
-
* The `@stitch` directive is applied to a root query where the merged type may be accessed. The merged type is inferred from the field return.
|
149
|
+
* The `@stitch` directive is applied to a root query where the merged type may be accessed. The merged type identity is inferred from the field return.
|
150
150
|
* The `key: "id"` parameter indicates that an `{ id }` must be selected from prior locations so it may be submitted as an argument to this query. The query argument used to send the key is inferred when possible (more on arguments later).
|
151
151
|
|
152
152
|
Each location that provides a unique variant of a type must provide _exactly one_ stitching query per possible key (more on multiple keys later). The exception to this requirement are types that contain only a single key field:
|
@@ -214,12 +214,12 @@ type Product {
|
|
214
214
|
upc: ID!
|
215
215
|
}
|
216
216
|
type Query {
|
217
|
-
productById(id: ID): Product @stitch(key: "id")
|
218
|
-
productByUpc(upc: ID): Product @stitch(key: "upc")
|
217
|
+
productById(id: ID!): Product @stitch(key: "id")
|
218
|
+
productByUpc(upc: ID!): Product @stitch(key: "upc")
|
219
219
|
}
|
220
220
|
```
|
221
221
|
|
222
|
-
The `@stitch` directive is also repeatable, allowing a single query to associate with multiple keys:
|
222
|
+
The `@stitch` directive is also repeatable (_requires graphql-ruby v2.0.15_), allowing a single query to associate with multiple keys:
|
223
223
|
|
224
224
|
```graphql
|
225
225
|
type Product {
|
@@ -261,29 +261,29 @@ GraphQL::Stitching.stitch_directive = "merge"
|
|
261
261
|
|
262
262
|
## Executables
|
263
263
|
|
264
|
-
|
264
|
+
An executable resource performs location-specific GraphQL requests. Executables may be `GraphQL::Schema` classes, or objects that respond to `.call` with the following arguments...
|
265
265
|
|
266
266
|
```ruby
|
267
267
|
class MyExecutable
|
268
|
-
def call(location, query_string, variables)
|
268
|
+
def call(location, query_string, variables, context)
|
269
269
|
# process a GraphQL request...
|
270
270
|
end
|
271
271
|
end
|
272
272
|
```
|
273
273
|
|
274
|
-
|
274
|
+
By default, a [Supergraph](./docs/supergraph.md) will use the individual `GraphQL::Schema` classes that composed it as executable resources for each location. You may assign new executables using `assign_executable`:
|
275
275
|
|
276
276
|
```ruby
|
277
277
|
supergraph = GraphQL::Stitching::Composer.new(...)
|
278
278
|
|
279
279
|
supergraph.assign_executable("location1", MyExecutable.new)
|
280
|
-
supergraph.assign_executable("location2", ->(loc, query, vars) { ... })
|
281
|
-
supergraph.assign_executable("location3") do |loc, query vars|
|
280
|
+
supergraph.assign_executable("location2", ->(loc, query, vars, ctx) { ... })
|
281
|
+
supergraph.assign_executable("location3") do |loc, query vars, ctx|
|
282
282
|
# ...
|
283
283
|
end
|
284
284
|
```
|
285
285
|
|
286
|
-
The `GraphQL::Stitching::RemoteClient` 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
|
286
|
+
The `GraphQL::Stitching::RemoteClient` 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)).
|
287
287
|
|
288
288
|
## Concurrency
|
289
289
|
|
@@ -291,7 +291,7 @@ The [Executor](./docs/executor.md) component builds atop the Ruby fiber-based im
|
|
291
291
|
|
292
292
|
## Example
|
293
293
|
|
294
|
-
This repo includes a working example of several stitched schemas running across Rack servers. Try running it:
|
294
|
+
This repo includes a working example of several stitched schemas running across small Rack servers. Try running it:
|
295
295
|
|
296
296
|
```shell
|
297
297
|
bundle install
|
data/docs/README.md
CHANGED
@@ -9,6 +9,6 @@ Major components include:
|
|
9
9
|
- [Gateway](./gateway.md) - an out-of-the-box stitching configuration.
|
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.
|
13
13
|
- [Planner](./planner.md) - builds a cacheable query plan for a request document.
|
14
14
|
- [Executor](./executor.md) - executes a query plan with given request variables.
|
data/docs/composer.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
## GraphQL::Stitching::Composer
|
2
2
|
|
3
|
-
The `Composer` receives many individual `GraphQL:Schema` instances for various graph locations and _composes_ them into a combined [`Supergraph`](./supergraph.md) that is validated for integrity. The resulting
|
3
|
+
The `Composer` receives many individual `GraphQL:Schema` instances for various graph locations and _composes_ them into a combined [`Supergraph`](./supergraph.md) that is validated for integrity. The resulting supergraph provides a combined GraphQL schema and delegation maps used to route incoming requests:
|
4
4
|
|
5
5
|
```ruby
|
6
6
|
storefronts_sdl = <<~GRAPHQL
|
@@ -33,7 +33,7 @@ products_sdl = <<~GRAPHQL
|
|
33
33
|
}
|
34
34
|
GRAPHQL
|
35
35
|
|
36
|
-
supergraph = GraphQL::Stitching::Composer.new({
|
36
|
+
supergraph = GraphQL::Stitching::Composer.new(schemas: {
|
37
37
|
"storefronts" => GraphQL::Schema.from_definition(storefronts_sdl),
|
38
38
|
"products" => GraphQL::Schema.from_definition(products_sdl),
|
39
39
|
}).perform
|
@@ -47,9 +47,9 @@ The individual schemas provided to the composer are assigned a location name bas
|
|
47
47
|
|
48
48
|
The strategy used to merge source schemas into the combined schema is based on each element type:
|
49
49
|
|
50
|
-
- `Object` and `Interface` types merge their fields together:
|
50
|
+
- `Object` and `Interface` types merge their fields and directives together:
|
51
51
|
- Common fields across locations must share a value type, and the weakest nullability is used.
|
52
|
-
- Field arguments merge using the same rules as `InputObject`.
|
52
|
+
- Field and directive arguments merge using the same rules as `InputObject`.
|
53
53
|
- Objects with unique fields across locations must implement [`@stitch` accessors](../README.md#merged-types).
|
54
54
|
- Shared object types without `@stitch` accessors must contain identical fields.
|
55
55
|
- Merged interfaces must remain compatible with all underlying implementations.
|
@@ -66,4 +66,45 @@ The strategy used to merge source schemas into the combined schema is based on e
|
|
66
66
|
|
67
67
|
- `Scalar` types are added for all scalar names across all locations.
|
68
68
|
|
69
|
+
- `Directive` definitions are added for all distinct names across locations:
|
70
|
+
- Arguments merge using the same rules as `InputObject`.
|
71
|
+
- Stitching directives (both definitions and assignments) are omitted.
|
72
|
+
|
69
73
|
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.
|
74
|
+
|
75
|
+
### Value merger functions
|
76
|
+
|
77
|
+
The composer has no way of intelligently merging static data values that are embedded into a schema. These include:
|
78
|
+
|
79
|
+
- Element descriptions
|
80
|
+
- Element deprecations
|
81
|
+
- Directive keyword argument values
|
82
|
+
|
83
|
+
By default, the first non-null value encountered across locations is used to fill these data slots. You may customize this aggregation process by providing value merger functions:
|
84
|
+
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
supergraph = GraphQL::Stitching::Composer.new(
|
88
|
+
schemas: { ... },
|
89
|
+
description_merger: ->(values_by_location, info) { values_by_location.values.last },
|
90
|
+
deprecation_merger: ->(values_by_location, info) { values_by_location.values.last },
|
91
|
+
directive_kwarg_merger: ->(values_by_location, info) { values_by_location.values.last },
|
92
|
+
).perform
|
93
|
+
```
|
94
|
+
|
95
|
+
Each merger accepts a `values_by_location` and an `info` argument; these provide the values found across locations and info about where in schema they were encountered:
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
values_by_location = {
|
99
|
+
"storefronts" => "A fabulous data type.",
|
100
|
+
"products" => "An excellent data type.",
|
101
|
+
}
|
102
|
+
|
103
|
+
info = {
|
104
|
+
type_name: "Product",
|
105
|
+
# field_name: ...,
|
106
|
+
# argument_name: ...,
|
107
|
+
}
|
108
|
+
```
|
109
|
+
|
110
|
+
The function should then select a value (or compute a new one) and return that for use in the combined schema.
|
data/docs/executor.md
CHANGED
@@ -12,28 +12,47 @@ query = <<~GRAPHQL
|
|
12
12
|
}
|
13
13
|
GRAPHQL
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
request = GraphQL::Stitching::Request.new(
|
16
|
+
query,
|
17
|
+
variables: { "id" => "123" },
|
18
|
+
operation_name: "MyQuery",
|
19
|
+
)
|
18
20
|
|
19
21
|
plan = GraphQL::Stitching::Planner.new(
|
20
22
|
supergraph: supergraph,
|
21
|
-
|
23
|
+
request: request,
|
22
24
|
).perform
|
23
25
|
|
24
|
-
|
25
|
-
raw_result = GraphQL::Stitching::Executor.new(
|
26
|
+
result = GraphQL::Stitching::Executor.new(
|
26
27
|
supergraph: supergraph,
|
28
|
+
request: request,
|
27
29
|
plan: plan.to_h,
|
28
|
-
variables: variables,
|
29
30
|
).perform
|
31
|
+
```
|
30
32
|
|
31
|
-
|
32
|
-
|
33
|
+
### Raw results
|
34
|
+
|
35
|
+
By default, execution results are always returned with document shaping (stitching additions removed, missing fields added, null bubbling applied). You may access the raw execution result by calling the `perform` method with a `raw: true` argument:
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
# get the raw result without shaping
|
39
|
+
raw_result = GraphQL::Stitching::Executor.new(
|
33
40
|
supergraph: supergraph,
|
41
|
+
request: request,
|
34
42
|
plan: plan.to_h,
|
35
|
-
|
36
|
-
|
43
|
+
).perform(raw: true)
|
44
|
+
```
|
45
|
+
|
46
|
+
### Batching
|
47
|
+
|
48
|
+
The Executor batches together as many requests as possible to a given location at a given time. Batched queries are written with the operation name suffixed by all operation keys in the batch, and root stitching fields are each prefixed by their batch index and collection index (for non-list fields):
|
49
|
+
|
50
|
+
```graphql
|
51
|
+
query MyOperation_2_3($lang:String!,$currency:Currency!){
|
52
|
+
_0_result: storefronts(ids:["7","8"]) { name(lang:$lang) }
|
53
|
+
_1_0_result: product(upc:"abc") { price(currency:$currency) }
|
54
|
+
_1_1_result: product(upc:"xyz") { price(currency:$currency) }
|
55
|
+
}
|
37
56
|
```
|
38
57
|
|
39
|
-
|
58
|
+
All told, the executor will make one request per location per generation of data. Generations started on separate forks of the resolution tree will be resolved independently.
|
data/docs/images/library.png
CHANGED
Binary file
|
data/docs/planner.md
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
## GraphQL::Stitching::Planner
|
2
2
|
|
3
|
-
A `Planner` generates a query plan for a given [`Supergraph`](./supergraph.md) and
|
3
|
+
A `Planner` generates a query plan for a given [`Supergraph`](./supergraph.md) and [`Request`](./request.md). The generated plan breaks down all the discrete GraphQL operations that must be delegated across locations and their sequencing.
|
4
4
|
|
5
5
|
```ruby
|
6
|
-
|
6
|
+
document = <<~GRAPHQL
|
7
7
|
query MyQuery($id: ID!) {
|
8
8
|
product(id:$id) {
|
9
9
|
title
|
@@ -12,11 +12,15 @@ request = <<~GRAPHQL
|
|
12
12
|
}
|
13
13
|
GRAPHQL
|
14
14
|
|
15
|
-
|
15
|
+
request = GraphQL::Stitching::Request.new(
|
16
|
+
document,
|
17
|
+
variables: { "id" => "1" },
|
18
|
+
operation_name: "MyQuery",
|
19
|
+
).prepare!
|
16
20
|
|
17
21
|
plan = GraphQL::Stitching::Planner.new(
|
18
22
|
supergraph: supergraph,
|
19
|
-
|
23
|
+
request: request,
|
20
24
|
).perform
|
21
25
|
```
|
22
26
|
|
@@ -25,17 +29,17 @@ plan = GraphQL::Stitching::Planner.new(
|
|
25
29
|
Plans are designed to be cacheable. This is very useful for redundant GraphQL documents (commonly sent by frontend clients) where there's no sense in planning every request individually. It's far more efficient to generate a plan once and cache it, then simply retreive the plan and execute it for future requests.
|
26
30
|
|
27
31
|
```ruby
|
28
|
-
cached_plan = $redis.get(
|
32
|
+
cached_plan = $redis.get(request.digest)
|
29
33
|
|
30
34
|
plan = if cached_plan
|
31
35
|
JSON.parse(cached_plan)
|
32
36
|
else
|
33
37
|
plan_hash = GraphQL::Stitching::Planner.new(
|
34
38
|
supergraph: supergraph,
|
35
|
-
|
39
|
+
request: request,
|
36
40
|
).perform.to_h
|
37
41
|
|
38
|
-
$redis.set(
|
42
|
+
$redis.set(request.digest, JSON.generate(plan_hash))
|
39
43
|
plan_hash
|
40
44
|
end
|
41
45
|
|
data/docs/request.md
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
## GraphQL::Stitching::Request
|
2
|
+
|
3
|
+
A `Request` contains a parsed GraphQL document and variables, and handles the logistics of extracting the appropriate operation, variable definitions, and fragments. A `Request` should be built once per server request and passed through to other stitching components that utilize request information.
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
document = "query FetchMovie($id: ID!) { movie(id:$id) { id genre } }"
|
7
|
+
request = GraphQL::Stitching::Request.new(document, variables: { "id" => "1" }, operation_name: "FetchMovie")
|
8
|
+
|
9
|
+
request.document # parsed AST via GraphQL.parse
|
10
|
+
request.variables # user-submitted variables
|
11
|
+
request.string # normalized printed document string
|
12
|
+
request.digest # SHA digest of the normalized document string
|
13
|
+
|
14
|
+
request.variable_definitions # a mapping of variable names to their type definitions
|
15
|
+
request.fragment_definitions # a mapping of fragment names to their fragment definitions
|
16
|
+
```
|
17
|
+
|
18
|
+
### Preparing requests
|
19
|
+
|
20
|
+
A request should be prepared for stitching using the `prepare!` method _after_ validations have been run:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
document = <<~GRAPHQL
|
24
|
+
query FetchMovie($id: ID!, $lang: String = "en", $withShowtimes: Boolean = true) {
|
25
|
+
movie(id:$id) {
|
26
|
+
id
|
27
|
+
title(lang: $lang)
|
28
|
+
showtimes @include(if: $withShowtimes) {
|
29
|
+
time
|
30
|
+
}
|
31
|
+
}
|
32
|
+
}
|
33
|
+
GRAPHQL
|
34
|
+
|
35
|
+
request = GraphQL::Stitching::Request.new(
|
36
|
+
document,
|
37
|
+
variables: { "id" => "1" },
|
38
|
+
operation_name: "FetchMovie",
|
39
|
+
)
|
40
|
+
|
41
|
+
errors = MySchema.validate(request.document)
|
42
|
+
# return early with any static validation errors...
|
43
|
+
|
44
|
+
request.prepare!
|
45
|
+
```
|
46
|
+
|
47
|
+
Preparing a request will apply several destructive transformations:
|
48
|
+
|
49
|
+
- Default values from variable definitions will be added to request variables.
|
50
|
+
- The document will be pre-shaped based on `@skip` and `@include` directives.
|
data/docs/supergraph.md
CHANGED
@@ -6,7 +6,7 @@ A `Supergraph` is the singuar representation of a stitched graph. `Supergraph` i
|
|
6
6
|
storefronts_sdl = "type Query { storefront(id: ID!): Storefront } ..."
|
7
7
|
products_sdl = "type Query { product(id: ID!): Product } ..."
|
8
8
|
|
9
|
-
supergraph = GraphQL::Stitching::Composer.new({
|
9
|
+
supergraph = GraphQL::Stitching::Composer.new(schemas: {
|
10
10
|
"storefronts" => GraphQL::Schema.from_definition(storefronts_sdl),
|
11
11
|
"products" => GraphQL::Schema.from_definition(products_sdl),
|
12
12
|
}).perform
|
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', '~> 2.0.
|
29
|
+
spec.add_runtime_dependency 'graphql', '~> 2.0.3'
|
30
30
|
|
31
31
|
spec.add_development_dependency 'bundler', '~> 2.0'
|
32
32
|
spec.add_development_dependency 'rake', '~> 12.0'
|
@@ -6,9 +6,9 @@ module GraphQL
|
|
6
6
|
class ComposerError < StitchingError; end
|
7
7
|
class ValidationError < ComposerError; end
|
8
8
|
|
9
|
-
attr_reader :query_name, :mutation_name, :subschema_types_by_name_and_location
|
9
|
+
attr_reader :query_name, :mutation_name, :subschema_types_by_name_and_location, :schema_directives
|
10
10
|
|
11
|
-
|
11
|
+
DEFAULT_VALUE_MERGER = ->(values_by_location, _info) { values_by_location.values.find { !_1.nil? } }
|
12
12
|
|
13
13
|
VALIDATORS = [
|
14
14
|
"ValidateInterfaces",
|
@@ -20,7 +20,8 @@ module GraphQL
|
|
20
20
|
query_name: "Query",
|
21
21
|
mutation_name: "Mutation",
|
22
22
|
description_merger: nil,
|
23
|
-
deprecation_merger: nil
|
23
|
+
deprecation_merger: nil,
|
24
|
+
directive_kwarg_merger: nil
|
24
25
|
)
|
25
26
|
@schemas = schemas
|
26
27
|
@query_name = query_name
|
@@ -29,11 +30,27 @@ module GraphQL
|
|
29
30
|
@boundary_map = {}
|
30
31
|
@mapped_type_names = {}
|
31
32
|
|
32
|
-
@description_merger = description_merger ||
|
33
|
-
@deprecation_merger = deprecation_merger ||
|
33
|
+
@description_merger = description_merger || DEFAULT_VALUE_MERGER
|
34
|
+
@deprecation_merger = deprecation_merger || DEFAULT_VALUE_MERGER
|
35
|
+
@directive_kwarg_merger = directive_kwarg_merger || DEFAULT_VALUE_MERGER
|
34
36
|
end
|
35
37
|
|
36
38
|
def perform
|
39
|
+
# "directive_name" => "location" => candidate_directive
|
40
|
+
@subschema_directives_by_name_and_location = @schemas.each_with_object({}) do |(location, schema), memo|
|
41
|
+
(schema.directives.keys - schema.default_directives.keys - GraphQL::Stitching.stitching_directive_names).each do |directive_name|
|
42
|
+
memo[directive_name] ||= {}
|
43
|
+
memo[directive_name][location] = schema.directives[directive_name]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# "Typename" => merged_directive
|
48
|
+
@schema_directives = @subschema_directives_by_name_and_location.each_with_object({}) do |(directive_name, directives_by_location), memo|
|
49
|
+
memo[directive_name] = build_directive(directive_name, directives_by_location)
|
50
|
+
end
|
51
|
+
|
52
|
+
@schema_directives.merge!(GraphQL::Schema.default_directives)
|
53
|
+
|
37
54
|
# "Typename" => "location" => candidate_type
|
38
55
|
@subschema_types_by_name_and_location = @schemas.each_with_object({}) do |(location, schema), memo|
|
39
56
|
raise ComposerError, "Location keys must be strings" unless location.is_a?(String)
|
@@ -91,6 +108,7 @@ module GraphQL
|
|
91
108
|
orphan_types schema_types.values
|
92
109
|
query schema_types[builder.query_name]
|
93
110
|
mutation schema_types[builder.mutation_name]
|
111
|
+
directives builder.schema_directives.values
|
94
112
|
|
95
113
|
own_orphan_types.clear
|
96
114
|
end
|
@@ -112,6 +130,18 @@ module GraphQL
|
|
112
130
|
supergraph
|
113
131
|
end
|
114
132
|
|
133
|
+
def build_directive(directive_name, directives_by_location)
|
134
|
+
builder = self
|
135
|
+
|
136
|
+
Class.new(GraphQL::Schema::Directive) do
|
137
|
+
graphql_name(directive_name)
|
138
|
+
description(builder.merge_descriptions(directive_name, directives_by_location))
|
139
|
+
repeatable(directives_by_location.values.any?(&:repeatable?))
|
140
|
+
locations(*directives_by_location.values.flat_map(&:locations).uniq)
|
141
|
+
builder.build_merged_arguments(directive_name, directives_by_location, self)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
115
145
|
def build_scalar_type(type_name, types_by_location)
|
116
146
|
built_in_type = GraphQL::Schema::BUILT_IN_TYPES[type_name]
|
117
147
|
return built_in_type if built_in_type
|
@@ -121,6 +151,7 @@ module GraphQL
|
|
121
151
|
Class.new(GraphQL::Schema::Scalar) do
|
122
152
|
graphql_name(type_name)
|
123
153
|
description(builder.merge_descriptions(type_name, types_by_location))
|
154
|
+
builder.build_merged_directives(type_name, types_by_location, self)
|
124
155
|
end
|
125
156
|
end
|
126
157
|
|
@@ -146,13 +177,16 @@ module GraphQL
|
|
146
177
|
Class.new(GraphQL::Schema::Enum) do
|
147
178
|
graphql_name(type_name)
|
148
179
|
description(builder.merge_descriptions(type_name, types_by_location))
|
180
|
+
builder.build_merged_directives(type_name, types_by_location, self)
|
149
181
|
|
150
182
|
enum_values_by_value_location.each do |value, enum_values_by_location|
|
151
|
-
value(value,
|
183
|
+
enum_value = value(value,
|
152
184
|
value: value,
|
153
185
|
description: builder.merge_descriptions(type_name, enum_values_by_location, enum_value: value),
|
154
186
|
deprecation_reason: builder.merge_deprecations(type_name, enum_values_by_location, enum_value: value),
|
155
187
|
)
|
188
|
+
|
189
|
+
builder.build_merged_directives(type_name, enum_values_by_location, enum_value, enum_value: value)
|
156
190
|
end
|
157
191
|
end
|
158
192
|
end
|
@@ -170,6 +204,7 @@ module GraphQL
|
|
170
204
|
end
|
171
205
|
|
172
206
|
builder.build_merged_fields(type_name, types_by_location, self)
|
207
|
+
builder.build_merged_directives(type_name, types_by_location, self)
|
173
208
|
end
|
174
209
|
end
|
175
210
|
|
@@ -187,6 +222,7 @@ module GraphQL
|
|
187
222
|
end
|
188
223
|
|
189
224
|
builder.build_merged_fields(type_name, types_by_location, self)
|
225
|
+
builder.build_merged_directives(type_name, types_by_location, self)
|
190
226
|
end
|
191
227
|
end
|
192
228
|
|
@@ -199,6 +235,7 @@ module GraphQL
|
|
199
235
|
|
200
236
|
possible_names = types_by_location.values.flat_map { _1.possible_types.map(&:graphql_name) }.uniq
|
201
237
|
possible_types(*possible_names.map { builder.build_type_binding(_1) })
|
238
|
+
builder.build_merged_directives(type_name, types_by_location, self)
|
202
239
|
end
|
203
240
|
end
|
204
241
|
|
@@ -209,6 +246,7 @@ module GraphQL
|
|
209
246
|
graphql_name(type_name)
|
210
247
|
description(builder.merge_descriptions(type_name, types_by_location))
|
211
248
|
builder.build_merged_arguments(type_name, types_by_location, self)
|
249
|
+
builder.build_merged_directives(type_name, types_by_location, self)
|
212
250
|
end
|
213
251
|
end
|
214
252
|
|
@@ -243,6 +281,7 @@ module GraphQL
|
|
243
281
|
)
|
244
282
|
|
245
283
|
build_merged_arguments(type_name, fields_by_location, schema_field, field_name: field_name)
|
284
|
+
build_merged_directives(type_name, fields_by_location, schema_field, field_name: field_name)
|
246
285
|
end
|
247
286
|
end
|
248
287
|
|
@@ -270,7 +309,7 @@ module GraphQL
|
|
270
309
|
# Getting double args sometimes... why?
|
271
310
|
return if owner.arguments.any? { _1.first == argument_name }
|
272
311
|
|
273
|
-
owner.argument(
|
312
|
+
schema_argument = owner.argument(
|
274
313
|
argument_name,
|
275
314
|
description: merge_descriptions(type_name, arguments_by_location, argument_name: argument_name, field_name: field_name),
|
276
315
|
deprecation_reason: merge_deprecations(type_name, arguments_by_location, argument_name: argument_name, field_name: field_name),
|
@@ -278,12 +317,55 @@ module GraphQL
|
|
278
317
|
required: value_types.any?(&:non_null?),
|
279
318
|
camelize: false,
|
280
319
|
)
|
320
|
+
|
321
|
+
build_merged_directives(type_name, arguments_by_location, schema_argument, field_name: field_name, argument_name: argument_name)
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
def build_merged_directives(type_name, members_by_location, owner, field_name: nil, argument_name: nil, enum_value: nil)
|
326
|
+
directives_by_name_location = members_by_location.each_with_object({}) do |(location, member_candidate), memo|
|
327
|
+
member_candidate.directives.each do |directive|
|
328
|
+
memo[directive.graphql_name] ||= {}
|
329
|
+
memo[directive.graphql_name][location] ||= {}
|
330
|
+
memo[directive.graphql_name][location] = directive
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
directives_by_name_location.each do |directive_name, directives_by_location|
|
335
|
+
directive_class = @schema_directives[directive_name]
|
336
|
+
next unless directive_class
|
337
|
+
|
338
|
+
# handled by deprecation_reason merger...
|
339
|
+
next if directive_class.graphql_name == "deprecated"
|
340
|
+
|
341
|
+
kwarg_values_by_name_location = directives_by_location.each_with_object({}) do |(location, directive), memo|
|
342
|
+
directive.arguments.keyword_arguments.each do |key, value|
|
343
|
+
key = key.to_s
|
344
|
+
next unless directive_class.arguments[key]
|
345
|
+
|
346
|
+
memo[key] ||= {}
|
347
|
+
memo[key][location] = value
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
kwargs = kwarg_values_by_name_location.each_with_object({}) do |(kwarg_name, kwarg_values_by_location), memo|
|
352
|
+
memo[kwarg_name.to_sym] = @directive_kwarg_merger.call(kwarg_values_by_location, {
|
353
|
+
type_name: type_name,
|
354
|
+
field_name: field_name,
|
355
|
+
argument_name: argument_name,
|
356
|
+
enum_value: enum_value,
|
357
|
+
directive_name: directive_name,
|
358
|
+
kwarg_name: kwarg_name,
|
359
|
+
}.compact!)
|
360
|
+
end
|
361
|
+
|
362
|
+
owner.directive(directive_class, **kwargs)
|
281
363
|
end
|
282
364
|
end
|
283
365
|
|
284
366
|
def merge_value_types(type_name, type_candidates, field_name: nil, argument_name: nil)
|
285
367
|
path = [type_name, field_name, argument_name].compact.join(".")
|
286
|
-
named_types = type_candidates.map {
|
368
|
+
named_types = type_candidates.map { _1.unwrap.graphql_name }.uniq
|
287
369
|
|
288
370
|
unless named_types.all? { _1 == named_types.first }
|
289
371
|
raise ComposerError, "Cannot compose mixed types at `#{path}`. Found: #{named_types.join(", ")}."
|
@@ -340,7 +422,7 @@ module GraphQL
|
|
340
422
|
def extract_boundaries(type_name, types_by_location)
|
341
423
|
types_by_location.each do |location, type_candidate|
|
342
424
|
type_candidate.fields.each do |field_name, field_candidate|
|
343
|
-
boundary_type_name =
|
425
|
+
boundary_type_name = field_candidate.type.unwrap.graphql_name
|
344
426
|
boundary_list = Util.get_list_structure(field_candidate.type)
|
345
427
|
|
346
428
|
field_candidate.directives.each do |directive|
|
@@ -388,10 +470,10 @@ module GraphQL
|
|
388
470
|
boundary_type = schema.types[type_name]
|
389
471
|
next unless boundary_type.kind.abstract?
|
390
472
|
|
391
|
-
|
392
|
-
|
393
|
-
@boundary_map[
|
394
|
-
@boundary_map[
|
473
|
+
expanded_types = Util.expand_abstract_type(schema, boundary_type)
|
474
|
+
expanded_types.select { @subschema_types_by_name_and_location[_1.graphql_name].length > 1 }.each do |expanded_type|
|
475
|
+
@boundary_map[expanded_type.graphql_name] ||= []
|
476
|
+
@boundary_map[expanded_type.graphql_name].push(*@boundary_map[type_name])
|
395
477
|
end
|
396
478
|
end
|
397
479
|
end
|
@@ -406,18 +488,18 @@ module GraphQL
|
|
406
488
|
|
407
489
|
if type.kind.object? || type.kind.interface?
|
408
490
|
type.fields.values.each do |field|
|
409
|
-
field_type =
|
491
|
+
field_type = field.type.unwrap
|
410
492
|
reads << field_type.graphql_name if field_type.kind.enum?
|
411
493
|
|
412
494
|
field.arguments.values.each do |argument|
|
413
|
-
argument_type =
|
495
|
+
argument_type = argument.type.unwrap
|
414
496
|
writes << argument_type.graphql_name if argument_type.kind.enum?
|
415
497
|
end
|
416
498
|
end
|
417
499
|
|
418
500
|
elsif type.kind.input_object?
|
419
501
|
type.arguments.values.each do |argument|
|
420
|
-
argument_type =
|
502
|
+
argument_type = argument.type.unwrap
|
421
503
|
writes << argument_type.graphql_name if argument_type.kind.enum?
|
422
504
|
end
|
423
505
|
end
|