graphql-stitching 0.0.1 → 0.2.1
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/.gitignore +2 -0
- data/README.md +15 -16
- data/docs/README.md +1 -1
- data/docs/composer.md +45 -4
- data/docs/executor.md +17 -6
- data/docs/images/library.png +0 -0
- data/docs/planner.md +7 -7
- data/docs/request.md +47 -0
- data/docs/supergraph.md +1 -1
- data/graphql-stitching.gemspec +2 -2
- data/lib/graphql/stitching/composer.rb +89 -7
- data/lib/graphql/stitching/executor.rb +49 -28
- data/lib/graphql/stitching/gateway.rb +18 -13
- data/lib/graphql/stitching/planner.rb +17 -11
- data/lib/graphql/stitching/remote_client.rb +4 -4
- data/lib/graphql/stitching/request.rb +133 -0
- data/lib/graphql/stitching/shaper.rb +60 -56
- data/lib/graphql/stitching/supergraph.rb +7 -6
- data/lib/graphql/stitching/util.rb +9 -1
- data/lib/graphql/stitching/version.rb +1 -1
- data/lib/graphql/stitching.rb +5 -1
- metadata +6 -9
- data/.ruby-version +0 -1
- data/Gemfile.lock +0 -49
- data/docs/document.md +0 -15
- data/docs/shaper.md +0 -20
- 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: 05c82abfcbec2db513d097c21424916bea46ebb0e4b434db0dc10a92ddfc2f9b
|
4
|
+
data.tar.gz: 79e4184f4ed237e2132f67389bb838fbbf18a8141b1d2ab30f8c5b710f47cb8f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e6c6ccb67c95df19b01bbeea1c0ed731a7153c3e4a184dab7dad6c2c52a22e22183075d123bf2dc78f431e73ff1fb4d31f0a21100072adb4f05bff2813ff9294
|
7
|
+
data.tar.gz: 2b9257ed86bdac5cb2403e527fc90893c961637d14bda2c4e5a16be7c8dc23206f860bdb596cc26f35ea704156b7443c5ef013efcc4c8cde251c9a865777e3ad
|
data/.github/workflows/ci.yml
CHANGED
@@ -11,14 +11,20 @@ jobs:
|
|
11
11
|
runs-on: ubuntu-latest
|
12
12
|
strategy:
|
13
13
|
matrix:
|
14
|
-
|
14
|
+
include:
|
15
|
+
- gemfile: Gemfile
|
16
|
+
ruby: 3.2
|
17
|
+
- gemfile: Gemfile
|
18
|
+
ruby: 3.1
|
19
|
+
- gemfile: Gemfile
|
20
|
+
ruby: 2.7
|
15
21
|
|
16
22
|
steps:
|
17
23
|
- uses: actions/checkout@v2
|
18
24
|
- name: Setup Ruby
|
19
25
|
uses: ruby/setup-ruby@v1
|
20
26
|
with:
|
21
|
-
ruby-version: ${{ matrix.ruby
|
27
|
+
ruby-version: ${{ matrix.ruby }}
|
22
28
|
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
23
29
|
- name: Run tests
|
24
30
|
run: |
|
data/.gitignore
CHANGED
@@ -10,6 +10,7 @@
|
|
10
10
|
/test/version_tmp/
|
11
11
|
/tmp/
|
12
12
|
.envrc
|
13
|
+
Gemfile.lock
|
13
14
|
|
14
15
|
# Used by dotenv library to load environment variables.
|
15
16
|
# .env
|
@@ -17,6 +18,7 @@
|
|
17
18
|
|
18
19
|
# Ignore Byebug command history file.
|
19
20
|
.byebug_history
|
21
|
+
.ruby-version
|
20
22
|
.DS_Store
|
21
23
|
|
22
24
|
## Specific to RubyMotion:
|
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
|
|
@@ -46,7 +46,7 @@ showtimes_schema = <<~GRAPHQL
|
|
46
46
|
GRAPHQL
|
47
47
|
|
48
48
|
gateway = GraphQL::Stitching::Gateway.new(locations: {
|
49
|
-
|
49
|
+
movies: {
|
50
50
|
schema: GraphQL::Schema.from_definition(movies_schema),
|
51
51
|
executable: GraphQL::Stitching::RemoteClient.new(url: "http://localhost:3000"),
|
52
52
|
},
|
@@ -70,16 +70,15 @@ 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
|
-
- [Shaper](./docs/shaper.md) - takes the raw output of the executor and prepares it for delivery.
|
83
82
|
|
84
83
|
## Merged types
|
85
84
|
|
@@ -122,7 +121,7 @@ shipping_schema = <<~GRAPHQL
|
|
122
121
|
}
|
123
122
|
GRAPHQL
|
124
123
|
|
125
|
-
supergraph = GraphQL::Stitching::Composer.new({
|
124
|
+
supergraph = GraphQL::Stitching::Composer.new(schemas: {
|
126
125
|
"products" => GraphQL::Schema.from_definition(products_schema),
|
127
126
|
"shipping" => GraphQL::Schema.from_definition(shipping_schema),
|
128
127
|
})
|
@@ -147,7 +146,7 @@ type Query {
|
|
147
146
|
}
|
148
147
|
```
|
149
148
|
|
150
|
-
* 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.
|
151
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).
|
152
151
|
|
153
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:
|
@@ -262,29 +261,29 @@ GraphQL::Stitching.stitch_directive = "merge"
|
|
262
261
|
|
263
262
|
## Executables
|
264
263
|
|
265
|
-
|
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...
|
266
265
|
|
267
266
|
```ruby
|
268
267
|
class MyExecutable
|
269
|
-
def call(location, query_string, variables)
|
268
|
+
def call(location, query_string, variables, context)
|
270
269
|
# process a GraphQL request...
|
271
270
|
end
|
272
271
|
end
|
273
272
|
```
|
274
273
|
|
275
|
-
|
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`:
|
276
275
|
|
277
276
|
```ruby
|
278
277
|
supergraph = GraphQL::Stitching::Composer.new(...)
|
279
278
|
|
280
279
|
supergraph.assign_executable("location1", MyExecutable.new)
|
281
|
-
supergraph.assign_executable("location2", ->(loc, query, vars) { ... })
|
282
|
-
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|
|
283
282
|
# ...
|
284
283
|
end
|
285
284
|
```
|
286
285
|
|
287
|
-
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)).
|
288
287
|
|
289
288
|
## Concurrency
|
290
289
|
|
@@ -292,7 +291,7 @@ The [Executor](./docs/executor.md) component builds atop the Ruby fiber-based im
|
|
292
291
|
|
293
292
|
## Example
|
294
293
|
|
295
|
-
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:
|
296
295
|
|
297
296
|
```shell
|
298
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,18 +12,29 @@ query = <<~GRAPHQL
|
|
12
12
|
}
|
13
13
|
GRAPHQL
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
document = GraphQL::Stitching::Document.new(query, operation_name: "MyQuery")
|
15
|
+
request = GraphQL::Stitching::Request.new(query, variables: { "id" => "123" }, operation_name: "MyQuery")
|
18
16
|
|
19
17
|
plan = GraphQL::Stitching::Planner.new(
|
20
18
|
supergraph: supergraph,
|
21
|
-
|
19
|
+
request: request,
|
22
20
|
).perform
|
23
21
|
|
24
|
-
|
22
|
+
result = GraphQL::Stitching::Executor.new(
|
25
23
|
supergraph: supergraph,
|
26
24
|
plan: plan.to_h,
|
27
|
-
|
25
|
+
request: request,
|
28
26
|
).perform
|
29
27
|
```
|
28
|
+
|
29
|
+
### Raw results
|
30
|
+
|
31
|
+
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:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
# get the raw result without shaping
|
35
|
+
raw_result = GraphQL::Stitching::Executor.new(
|
36
|
+
supergraph: supergraph,
|
37
|
+
plan: plan.to_h,
|
38
|
+
request: request,
|
39
|
+
).perform(raw: true)
|
40
|
+
```
|
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,11 @@ request = <<~GRAPHQL
|
|
12
12
|
}
|
13
13
|
GRAPHQL
|
14
14
|
|
15
|
-
|
15
|
+
request = GraphQL::Stitching::Request.new(document, operation_name: "MyQuery").prepare!
|
16
16
|
|
17
17
|
plan = GraphQL::Stitching::Planner.new(
|
18
18
|
supergraph: supergraph,
|
19
|
-
|
19
|
+
request: request,
|
20
20
|
).perform
|
21
21
|
```
|
22
22
|
|
@@ -25,17 +25,17 @@ plan = GraphQL::Stitching::Planner.new(
|
|
25
25
|
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
26
|
|
27
27
|
```ruby
|
28
|
-
cached_plan = $redis.get(
|
28
|
+
cached_plan = $redis.get(request.digest)
|
29
29
|
|
30
30
|
plan = if cached_plan
|
31
31
|
JSON.parse(cached_plan)
|
32
32
|
else
|
33
33
|
plan_hash = GraphQL::Stitching::Planner.new(
|
34
34
|
supergraph: supergraph,
|
35
|
-
|
35
|
+
request: request,
|
36
36
|
).perform.to_h
|
37
37
|
|
38
|
-
$redis.set(
|
38
|
+
$redis.set(request.digest, JSON.generate(plan_hash))
|
39
39
|
plan_hash
|
40
40
|
end
|
41
41
|
|
data/docs/request.md
ADDED
@@ -0,0 +1,47 @@
|
|
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 using the `prepare!` method before using it:
|
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
|
+
request.prepare!
|
42
|
+
```
|
43
|
+
|
44
|
+
Preparing a request will apply several destructive transformations:
|
45
|
+
|
46
|
+
- Default values from variable definitions will be added to request variables.
|
47
|
+
- 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
@@ -8,11 +8,11 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.version = GraphQL::Stitching::VERSION
|
9
9
|
spec.authors = ['Greg MacWilliam']
|
10
10
|
spec.summary = 'GraphQL schema stitching for Ruby'
|
11
|
-
spec.description =
|
11
|
+
spec.description = 'Combine GraphQL services into one unified graph'
|
12
12
|
spec.homepage = 'https://github.com/gmac/graphql-stitching-ruby'
|
13
13
|
spec.license = 'MIT'
|
14
14
|
|
15
|
-
spec.required_ruby_version = '>=
|
15
|
+
spec.required_ruby_version = '>= 2.7.0'
|
16
16
|
|
17
17
|
spec.metadata = {
|
18
18
|
'homepage_uri' => 'https://github.com/gmac/graphql-stitching-ruby',
|
@@ -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,6 +317,49 @@ 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
|
|