graphql-stitching 0.1.0 → 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/README.md +14 -14
- data/docs/README.md +1 -1
- data/docs/composer.md +45 -4
- data/docs/executor.md +14 -13
- 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/lib/graphql/stitching/composer.rb +89 -7
- data/lib/graphql/stitching/executor.rb +48 -32
- 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 +5 -5
- data/lib/graphql/stitching/supergraph.rb +7 -6
- data/lib/graphql/stitching/version.rb +1 -1
- data/lib/graphql/stitching.rb +5 -1
- metadata +4 -4
- 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: 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/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:
|
@@ -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,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
|
-
|
25
|
-
raw_result = GraphQL::Stitching::Executor.new(
|
22
|
+
result = GraphQL::Stitching::Executor.new(
|
26
23
|
supergraph: supergraph,
|
27
24
|
plan: plan.to_h,
|
28
|
-
|
25
|
+
request: request,
|
29
26
|
).perform
|
27
|
+
```
|
28
|
+
|
29
|
+
### Raw results
|
30
30
|
|
31
|
-
|
32
|
-
|
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(
|
33
36
|
supergraph: supergraph,
|
34
37
|
plan: plan.to_h,
|
35
|
-
|
36
|
-
).perform(
|
38
|
+
request: request,
|
39
|
+
).perform(raw: true)
|
37
40
|
```
|
38
|
-
|
39
|
-
Note that an executor's `perform` method accepts a document argument. When provided, the raw execution result will be shaped for delivery to match the document. Without a document, the raw result will be returned with stitching inclusions and no null bubbling applied.
|
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
|
@@ -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
|
|
@@ -7,16 +7,17 @@ module GraphQL
|
|
7
7
|
class Executor
|
8
8
|
|
9
9
|
class RootSource < GraphQL::Dataloader::Source
|
10
|
-
def initialize(executor)
|
10
|
+
def initialize(executor, location)
|
11
11
|
@executor = executor
|
12
|
+
@location = location
|
12
13
|
end
|
13
14
|
|
14
15
|
def fetch(ops)
|
15
16
|
op = ops.first # There should only ever be one per location at a time
|
16
17
|
|
17
18
|
query_document = build_query(op)
|
18
|
-
query_variables = @executor.variables.slice(*op["variables"].keys)
|
19
|
-
result = @executor.supergraph.execute_at_location(op["location"], query_document, query_variables)
|
19
|
+
query_variables = @executor.request.variables.slice(*op["variables"].keys)
|
20
|
+
result = @executor.supergraph.execute_at_location(op["location"], query_document, query_variables, @executor.request.context)
|
20
21
|
@executor.query_count += 1
|
21
22
|
|
22
23
|
@executor.data.merge!(result["data"]) if result["data"]
|
@@ -24,7 +25,8 @@ module GraphQL
|
|
24
25
|
result["errors"].each { _1.delete("locations") }
|
25
26
|
@executor.errors.concat(result["errors"])
|
26
27
|
end
|
27
|
-
|
28
|
+
|
29
|
+
ops.map { op["key"] }
|
28
30
|
end
|
29
31
|
|
30
32
|
def build_query(op)
|
@@ -61,8 +63,8 @@ module GraphQL
|
|
61
63
|
|
62
64
|
if origin_sets_by_operation.any?
|
63
65
|
query_document, variable_names = build_query(origin_sets_by_operation)
|
64
|
-
variables = @executor.variables.slice(*variable_names)
|
65
|
-
raw_result = @executor.supergraph.execute_at_location(@location, query_document, variables)
|
66
|
+
variables = @executor.request.variables.slice(*variable_names)
|
67
|
+
raw_result = @executor.supergraph.execute_at_location(@location, query_document, variables, @executor.request.context)
|
66
68
|
@executor.query_count += 1
|
67
69
|
|
68
70
|
merge_results!(origin_sets_by_operation, raw_result.dig("data"))
|
@@ -165,23 +167,26 @@ module GraphQL
|
|
165
167
|
|
166
168
|
private
|
167
169
|
|
168
|
-
#
|
170
|
+
# traverse forward through origin data, expanding arrays to follow all paths
|
169
171
|
# any errors found for an origin object_id have their path prefixed by the object path
|
170
172
|
def repath_errors!(pathed_errors_by_object_id, forward_path, current_path=[], root=@executor.data)
|
171
|
-
current_path
|
172
|
-
forward_path = forward_path[1..-1]
|
173
|
+
current_path.push(forward_path.shift)
|
173
174
|
scope = root[current_path.last]
|
174
175
|
|
175
176
|
if forward_path.any? && scope.is_a?(Array)
|
176
177
|
scope.each_with_index do |element, index|
|
177
178
|
inner_elements = element.is_a?(Array) ? element.flatten : [element]
|
178
179
|
inner_elements.each do |inner_element|
|
179
|
-
|
180
|
+
current_path << index
|
181
|
+
repath_errors!(pathed_errors_by_object_id, forward_path, current_path, inner_element)
|
182
|
+
current_path.pop
|
180
183
|
end
|
181
184
|
end
|
182
185
|
|
183
186
|
elsif forward_path.any?
|
184
|
-
|
187
|
+
current_path << index
|
188
|
+
repath_errors!(pathed_errors_by_object_id, forward_path, current_path, scope)
|
189
|
+
current_path.pop
|
185
190
|
|
186
191
|
elsif scope.is_a?(Array)
|
187
192
|
scope.each_with_index do |element, index|
|
@@ -196,61 +201,72 @@ module GraphQL
|
|
196
201
|
errors = pathed_errors_by_object_id[scope.object_id]
|
197
202
|
errors.each { _1["path"] = [*current_path, *_1["path"]] } if errors
|
198
203
|
end
|
204
|
+
|
205
|
+
forward_path.unshift(current_path.pop)
|
199
206
|
end
|
200
207
|
end
|
201
208
|
|
202
|
-
attr_reader :supergraph, :
|
209
|
+
attr_reader :supergraph, :request, :data, :errors
|
203
210
|
attr_accessor :query_count
|
204
211
|
|
205
|
-
def initialize(supergraph:, plan:,
|
212
|
+
def initialize(supergraph:, request:, plan:, nonblocking: false)
|
206
213
|
@supergraph = supergraph
|
207
|
-
@
|
214
|
+
@request = request
|
208
215
|
@queue = plan["ops"]
|
209
216
|
@data = {}
|
210
217
|
@errors = []
|
211
218
|
@query_count = 0
|
219
|
+
@exec_cycles = 0
|
212
220
|
@dataloader = GraphQL::Dataloader.new(nonblocking: nonblocking)
|
213
221
|
end
|
214
222
|
|
215
|
-
def perform(
|
223
|
+
def perform(raw: false)
|
216
224
|
exec!
|
217
|
-
|
218
225
|
result = {}
|
219
|
-
result["data"] = @data if @data && @data.length > 0
|
220
|
-
result["errors"] = @errors if @errors.length > 0
|
221
226
|
|
222
|
-
if
|
223
|
-
GraphQL::Stitching::Shaper.new(
|
227
|
+
if @data && @data.length > 0
|
228
|
+
result["data"] = raw ? @data : GraphQL::Stitching::Shaper.new(
|
224
229
|
schema: @supergraph.schema,
|
225
|
-
|
226
|
-
).perform!(
|
227
|
-
else
|
228
|
-
result
|
230
|
+
request: @request,
|
231
|
+
).perform!(@data)
|
229
232
|
end
|
233
|
+
|
234
|
+
if @errors.length > 0
|
235
|
+
result["errors"] = @errors
|
236
|
+
end
|
237
|
+
|
238
|
+
result
|
230
239
|
end
|
231
240
|
|
232
241
|
private
|
233
242
|
|
234
243
|
def exec!(after_keys = [0])
|
244
|
+
if @exec_cycles > @queue.length
|
245
|
+
# sanity check... if we've exceeded queue size, then something went wrong.
|
246
|
+
raise StitchingError, "Too many execution requests attempted."
|
247
|
+
end
|
248
|
+
|
235
249
|
@dataloader.append_job do
|
236
|
-
|
250
|
+
tasks = @queue
|
237
251
|
.select { after_keys.include?(_1["after_key"]) }
|
238
|
-
.group_by { _1["location"] }
|
239
|
-
.map do |location, ops|
|
240
|
-
if
|
241
|
-
@dataloader.with(RootSource, self).request_all(ops)
|
252
|
+
.group_by { [_1["location"], _1["boundary"].nil?] }
|
253
|
+
.map do |(location, root_source), ops|
|
254
|
+
if root_source
|
255
|
+
@dataloader.with(RootSource, self, location).request_all(ops)
|
242
256
|
else
|
243
257
|
@dataloader.with(BoundarySource, self, location).request_all(ops)
|
244
258
|
end
|
245
259
|
end
|
246
260
|
|
247
|
-
|
261
|
+
tasks.each(&method(:exec_task))
|
248
262
|
end
|
263
|
+
|
264
|
+
@exec_cycles += 1
|
249
265
|
@dataloader.run
|
250
266
|
end
|
251
267
|
|
252
|
-
def
|
253
|
-
next_keys =
|
268
|
+
def exec_task(task)
|
269
|
+
next_keys = task.load
|
254
270
|
next_keys.compact!
|
255
271
|
exec!(next_keys) if next_keys.any?
|
256
272
|
end
|
@@ -7,8 +7,6 @@ module GraphQL
|
|
7
7
|
class Gateway
|
8
8
|
class GatewayError < StitchingError; end
|
9
9
|
|
10
|
-
EMPTY_CONTEXT = {}.freeze
|
11
|
-
|
12
10
|
attr_reader :supergraph
|
13
11
|
|
14
12
|
def initialize(locations: nil, supergraph: nil)
|
@@ -25,29 +23,36 @@ module GraphQL
|
|
25
23
|
end
|
26
24
|
end
|
27
25
|
|
28
|
-
def execute(query:, variables: nil, operation_name: nil, context:
|
29
|
-
|
26
|
+
def execute(query:, variables: nil, operation_name: nil, context: nil, validate: true)
|
27
|
+
request = GraphQL::Stitching::Request.new(
|
28
|
+
query,
|
29
|
+
operation_name: operation_name,
|
30
|
+
variables: variables,
|
31
|
+
context: context,
|
32
|
+
)
|
30
33
|
|
31
34
|
if validate
|
32
|
-
validation_errors = @supergraph.schema.validate(document
|
35
|
+
validation_errors = @supergraph.schema.validate(request.document)
|
33
36
|
return error_result(validation_errors) if validation_errors.any?
|
34
37
|
end
|
35
38
|
|
39
|
+
request.prepare!
|
40
|
+
|
36
41
|
begin
|
37
|
-
plan = fetch_plan(
|
42
|
+
plan = fetch_plan(request) do
|
38
43
|
GraphQL::Stitching::Planner.new(
|
39
44
|
supergraph: @supergraph,
|
40
|
-
|
45
|
+
request: request,
|
41
46
|
).perform.to_h
|
42
47
|
end
|
43
48
|
|
44
49
|
GraphQL::Stitching::Executor.new(
|
45
50
|
supergraph: @supergraph,
|
51
|
+
request: request,
|
46
52
|
plan: plan,
|
47
|
-
|
48
|
-
).perform(document)
|
53
|
+
).perform
|
49
54
|
rescue StandardError => e
|
50
|
-
custom_message = @on_error.call(e, context) if @on_error
|
55
|
+
custom_message = @on_error.call(e, request.context) if @on_error
|
51
56
|
error_result([{ "message" => custom_message || "An unexpected error occured." }])
|
52
57
|
end
|
53
58
|
end
|
@@ -91,16 +96,16 @@ module GraphQL
|
|
91
96
|
supergraph
|
92
97
|
end
|
93
98
|
|
94
|
-
def fetch_plan(
|
99
|
+
def fetch_plan(request)
|
95
100
|
if @on_cache_read
|
96
|
-
cached_plan = @on_cache_read.call(
|
101
|
+
cached_plan = @on_cache_read.call(request.digest, request.context)
|
97
102
|
return JSON.parse(cached_plan) if cached_plan
|
98
103
|
end
|
99
104
|
|
100
105
|
plan_json = yield
|
101
106
|
|
102
107
|
if @on_cache_write
|
103
|
-
@on_cache_write.call(
|
108
|
+
@on_cache_write.call(request.digest, JSON.generate(plan_json), request.context)
|
104
109
|
end
|
105
110
|
|
106
111
|
plan_json
|
@@ -6,9 +6,9 @@ module GraphQL
|
|
6
6
|
SUPERGRAPH_LOCATIONS = [Supergraph::LOCATION].freeze
|
7
7
|
TYPENAME_NODE = GraphQL::Language::Nodes::Field.new(alias: "_STITCH_typename", name: "__typename")
|
8
8
|
|
9
|
-
def initialize(supergraph:,
|
9
|
+
def initialize(supergraph:, request:)
|
10
10
|
@supergraph = supergraph
|
11
|
-
@
|
11
|
+
@request = request
|
12
12
|
@sequence_key = 0
|
13
13
|
@operations_by_grouping = {}
|
14
14
|
end
|
@@ -37,7 +37,11 @@ module GraphQL
|
|
37
37
|
extract_locale_selections(location, parent_type, selections, insertion_path, parent_key)
|
38
38
|
end
|
39
39
|
|
40
|
-
grouping =
|
40
|
+
grouping = String.new
|
41
|
+
grouping << after_key.to_s << "/" << location << "/" << parent_type.graphql_name
|
42
|
+
grouping = insertion_path.reduce(grouping) do |memo, segment|
|
43
|
+
memo << "/" << segment
|
44
|
+
end
|
41
45
|
|
42
46
|
if op = @operations_by_grouping[grouping]
|
43
47
|
op.selections += selection_set if selection_set
|
@@ -62,12 +66,12 @@ module GraphQL
|
|
62
66
|
end
|
63
67
|
|
64
68
|
def build_root_operations
|
65
|
-
case @
|
69
|
+
case @request.operation.operation_type
|
66
70
|
when "query"
|
67
71
|
# plan steps grouping all fields by location for async execution
|
68
72
|
parent_type = @supergraph.schema.query
|
69
73
|
|
70
|
-
selections_by_location = @
|
74
|
+
selections_by_location = @request.operation.selections.each_with_object({}) do |node, memo|
|
71
75
|
locations = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name] || SUPERGRAPH_LOCATIONS
|
72
76
|
memo[locations.last] ||= []
|
73
77
|
memo[locations.last] << node
|
@@ -82,7 +86,7 @@ module GraphQL
|
|
82
86
|
parent_type = @supergraph.schema.mutation
|
83
87
|
location_groups = []
|
84
88
|
|
85
|
-
@
|
89
|
+
@request.operation.selections.reduce(nil) do |last_location, node|
|
86
90
|
location = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name].last
|
87
91
|
if location != last_location
|
88
92
|
location_groups << {
|
@@ -161,8 +165,10 @@ module GraphQL
|
|
161
165
|
if Util.is_leaf_type?(field_type)
|
162
166
|
selections_result << node
|
163
167
|
else
|
164
|
-
|
165
|
-
selection_set, variables = extract_locale_selections(current_location, field_type, node.selections,
|
168
|
+
insertion_path.push(node.alias || node.name)
|
169
|
+
selection_set, variables = extract_locale_selections(current_location, field_type, node.selections, insertion_path, after_key)
|
170
|
+
insertion_path.pop
|
171
|
+
|
166
172
|
selections_result << node.merge(selections: selection_set)
|
167
173
|
variables_result.merge!(variables)
|
168
174
|
end
|
@@ -177,7 +183,7 @@ module GraphQL
|
|
177
183
|
implements_fragments = true
|
178
184
|
|
179
185
|
when GraphQL::Language::Nodes::FragmentSpread
|
180
|
-
fragment = @
|
186
|
+
fragment = @request.fragment_definitions[node.name]
|
181
187
|
next unless @supergraph.locations_by_type[fragment.type.name].include?(current_location)
|
182
188
|
|
183
189
|
fragment_type = @supergraph.schema.types[fragment.type.name]
|
@@ -260,7 +266,7 @@ module GraphQL
|
|
260
266
|
location: location,
|
261
267
|
selections: selections_by_location[location],
|
262
268
|
parent_type: parent_type,
|
263
|
-
insertion_path: insertion_path,
|
269
|
+
insertion_path: insertion_path.dup,
|
264
270
|
boundary: boundary,
|
265
271
|
after_key: after_key,
|
266
272
|
)
|
@@ -289,7 +295,7 @@ module GraphQL
|
|
289
295
|
when GraphQL::Language::Nodes::InputObject
|
290
296
|
extract_node_variables!(argument.value, memo)
|
291
297
|
when GraphQL::Language::Nodes::VariableIdentifier
|
292
|
-
memo[argument.value.name] ||= @
|
298
|
+
memo[argument.value.name] ||= @request.variable_definitions[argument.value.name]
|
293
299
|
end
|
294
300
|
end
|
295
301
|
end
|
@@ -9,14 +9,14 @@ module GraphQL
|
|
9
9
|
class RemoteClient
|
10
10
|
def initialize(url:, headers:{})
|
11
11
|
@url = url
|
12
|
-
@headers = headers
|
12
|
+
@headers = { "Content-Type" => "application/json" }.merge!(headers)
|
13
13
|
end
|
14
14
|
|
15
|
-
def call(
|
15
|
+
def call(_location, document, variables, _context)
|
16
16
|
response = Net::HTTP.post(
|
17
17
|
URI(@url),
|
18
|
-
{ "query" => document, "variables" => variables }
|
19
|
-
|
18
|
+
JSON.generate({ "query" => document, "variables" => variables }),
|
19
|
+
@headers,
|
20
20
|
)
|
21
21
|
JSON.parse(response.body)
|
22
22
|
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Stitching
|
5
|
+
class Request
|
6
|
+
SUPPORTED_OPERATIONS = ["query", "mutation"].freeze
|
7
|
+
EMPTY_CONTEXT = {}.freeze
|
8
|
+
|
9
|
+
class ApplyRuntimeDirectives < GraphQL::Language::Visitor
|
10
|
+
def initialize(document, variables)
|
11
|
+
@changed = false
|
12
|
+
@variables = variables
|
13
|
+
super(document)
|
14
|
+
end
|
15
|
+
|
16
|
+
def changed?
|
17
|
+
@changed
|
18
|
+
end
|
19
|
+
|
20
|
+
def on_field(node, parent)
|
21
|
+
delete_node = false
|
22
|
+
filtered_directives = if node.directives.any?
|
23
|
+
node.directives.select do |directive|
|
24
|
+
if directive.name == "skip"
|
25
|
+
delete_node = assess_argument_value(directive.arguments.first)
|
26
|
+
false
|
27
|
+
elsif directive.name == "include"
|
28
|
+
delete_node = !assess_argument_value(directive.arguments.first)
|
29
|
+
false
|
30
|
+
else
|
31
|
+
true
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
if delete_node
|
37
|
+
@changed = true
|
38
|
+
super(DELETE_NODE, parent)
|
39
|
+
elsif filtered_directives && filtered_directives.length != node.directives.length
|
40
|
+
@changed = true
|
41
|
+
super(node.merge(directives: filtered_directives), parent)
|
42
|
+
else
|
43
|
+
super
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def assess_argument_value(arg)
|
50
|
+
if arg.value.is_a?(GraphQL::Language::Nodes::VariableIdentifier)
|
51
|
+
return @variables[arg.value.name]
|
52
|
+
end
|
53
|
+
arg.value
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
attr_reader :document, :variables, :operation_name, :context
|
58
|
+
|
59
|
+
def initialize(document, operation_name: nil, variables: nil, context: nil)
|
60
|
+
@may_contain_runtime_directives = true
|
61
|
+
|
62
|
+
@document = if document.is_a?(String)
|
63
|
+
@may_contain_runtime_directives = document.include?("@")
|
64
|
+
GraphQL.parse(document)
|
65
|
+
else
|
66
|
+
document
|
67
|
+
end
|
68
|
+
|
69
|
+
@operation_name = operation_name
|
70
|
+
@variables = variables || {}
|
71
|
+
@context = context || EMPTY_CONTEXT
|
72
|
+
end
|
73
|
+
|
74
|
+
def string
|
75
|
+
@string ||= @document.to_query_string
|
76
|
+
end
|
77
|
+
|
78
|
+
def digest
|
79
|
+
@digest ||= Digest::SHA2.hexdigest(string)
|
80
|
+
end
|
81
|
+
|
82
|
+
def operation
|
83
|
+
@operation ||= begin
|
84
|
+
operation_defs = @document.definitions.select do |d|
|
85
|
+
next unless d.is_a?(GraphQL::Language::Nodes::OperationDefinition)
|
86
|
+
next unless SUPPORTED_OPERATIONS.include?(d.operation_type)
|
87
|
+
@operation_name ? d.name == @operation_name : true
|
88
|
+
end
|
89
|
+
|
90
|
+
if operation_defs.length < 1
|
91
|
+
raise GraphQL::ExecutionError, "Invalid root operation."
|
92
|
+
elsif operation_defs.length > 1
|
93
|
+
raise GraphQL::ExecutionError, "An operation name is required when sending multiple operations."
|
94
|
+
end
|
95
|
+
|
96
|
+
operation_defs.first
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def variable_definitions
|
101
|
+
@variable_definitions ||= operation.variables.each_with_object({}) do |v, memo|
|
102
|
+
memo[v.name] = v.type
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def fragment_definitions
|
107
|
+
@fragment_definitions ||= @document.definitions.each_with_object({}) do |d, memo|
|
108
|
+
memo[d.name] = d if d.is_a?(GraphQL::Language::Nodes::FragmentDefinition)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def prepare!
|
113
|
+
operation.variables.each do |v|
|
114
|
+
@variables[v.name] ||= v.default_value
|
115
|
+
end
|
116
|
+
|
117
|
+
return self unless @may_contain_runtime_directives
|
118
|
+
|
119
|
+
visitor = ApplyRuntimeDirectives.new(@document, @variables)
|
120
|
+
@document = visitor.visit
|
121
|
+
|
122
|
+
if visitor.changed?
|
123
|
+
@string = nil
|
124
|
+
@digest = nil
|
125
|
+
@operation = nil
|
126
|
+
@variable_definitions = nil
|
127
|
+
@fragment_definitions = nil
|
128
|
+
end
|
129
|
+
self
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -4,14 +4,14 @@
|
|
4
4
|
module GraphQL
|
5
5
|
module Stitching
|
6
6
|
class Shaper
|
7
|
-
def initialize(schema:,
|
7
|
+
def initialize(schema:, request:)
|
8
8
|
@schema = schema
|
9
|
-
@
|
9
|
+
@request = request
|
10
10
|
end
|
11
11
|
|
12
12
|
def perform!(raw)
|
13
|
-
|
14
|
-
raw
|
13
|
+
root_type = @schema.public_send(@request.operation.operation_type)
|
14
|
+
resolve_object_scope(raw, root_type, @request.operation.selections)
|
15
15
|
end
|
16
16
|
|
17
17
|
private
|
@@ -48,7 +48,7 @@ module GraphQL
|
|
48
48
|
return nil if result.nil?
|
49
49
|
|
50
50
|
when GraphQL::Language::Nodes::FragmentSpread
|
51
|
-
fragment = @
|
51
|
+
fragment = @request.fragment_definitions[node.name]
|
52
52
|
fragment_type = @schema.types[fragment.type.name]
|
53
53
|
next unless typename == fragment_type.graphql_name
|
54
54
|
|
@@ -57,27 +57,28 @@ module GraphQL
|
|
57
57
|
def assign_executable(location, executable = nil, &block)
|
58
58
|
executable ||= block
|
59
59
|
unless executable.is_a?(Class) && executable <= GraphQL::Schema
|
60
|
-
raise "A client or block handler must be provided." unless executable
|
61
|
-
raise "A client must be callable" unless executable.respond_to?(:call)
|
60
|
+
raise StitchingError, "A client or block handler must be provided." unless executable
|
61
|
+
raise StitchingError, "A client must be callable" unless executable.respond_to?(:call)
|
62
62
|
end
|
63
63
|
@executables[location] = executable
|
64
64
|
end
|
65
65
|
|
66
|
-
def execute_at_location(location, query, variables)
|
66
|
+
def execute_at_location(location, query, variables, context)
|
67
67
|
executable = executables[location]
|
68
68
|
|
69
69
|
if executable.nil?
|
70
|
-
raise "No executable assigned for #{location} location."
|
70
|
+
raise StitchingError, "No executable assigned for #{location} location."
|
71
71
|
elsif executable.is_a?(Class) && executable <= GraphQL::Schema
|
72
72
|
executable.execute(
|
73
73
|
query: query,
|
74
74
|
variables: variables,
|
75
|
+
context: context,
|
75
76
|
validate: false,
|
76
77
|
)
|
77
78
|
elsif executable.respond_to?(:call)
|
78
|
-
executable.call(location, query, variables)
|
79
|
+
executable.call(location, query, variables, context)
|
79
80
|
else
|
80
|
-
raise "Missing valid executable for #{location} location."
|
81
|
+
raise StitchingError, "Missing valid executable for #{location} location."
|
81
82
|
end
|
82
83
|
end
|
83
84
|
|
data/lib/graphql/stitching.rb
CHANGED
@@ -13,6 +13,10 @@ module GraphQL
|
|
13
13
|
end
|
14
14
|
|
15
15
|
attr_writer :stitch_directive
|
16
|
+
|
17
|
+
def stitching_directive_names
|
18
|
+
[stitch_directive]
|
19
|
+
end
|
16
20
|
end
|
17
21
|
end
|
18
22
|
end
|
@@ -20,11 +24,11 @@ end
|
|
20
24
|
require_relative "stitching/gateway"
|
21
25
|
require_relative "stitching/supergraph"
|
22
26
|
require_relative "stitching/composer"
|
23
|
-
require_relative "stitching/document"
|
24
27
|
require_relative "stitching/executor"
|
25
28
|
require_relative "stitching/planner_operation"
|
26
29
|
require_relative "stitching/planner"
|
27
30
|
require_relative "stitching/remote_client"
|
31
|
+
require_relative "stitching/request"
|
28
32
|
require_relative "stitching/shaper"
|
29
33
|
require_relative "stitching/util"
|
30
34
|
require_relative "stitching/version"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: graphql-stitching
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Greg MacWilliam
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-02-
|
11
|
+
date: 2023-02-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|
@@ -81,13 +81,13 @@ files:
|
|
81
81
|
- Rakefile
|
82
82
|
- docs/README.md
|
83
83
|
- docs/composer.md
|
84
|
-
- docs/document.md
|
85
84
|
- docs/executor.md
|
86
85
|
- docs/gateway.md
|
87
86
|
- docs/images/library.png
|
88
87
|
- docs/images/merging.png
|
89
88
|
- docs/images/stitching.png
|
90
89
|
- docs/planner.md
|
90
|
+
- docs/request.md
|
91
91
|
- docs/supergraph.md
|
92
92
|
- example/gateway.rb
|
93
93
|
- example/graphiql.html
|
@@ -99,12 +99,12 @@ files:
|
|
99
99
|
- lib/graphql/stitching/composer/base_validator.rb
|
100
100
|
- lib/graphql/stitching/composer/validate_boundaries.rb
|
101
101
|
- lib/graphql/stitching/composer/validate_interfaces.rb
|
102
|
-
- lib/graphql/stitching/document.rb
|
103
102
|
- lib/graphql/stitching/executor.rb
|
104
103
|
- lib/graphql/stitching/gateway.rb
|
105
104
|
- lib/graphql/stitching/planner.rb
|
106
105
|
- lib/graphql/stitching/planner_operation.rb
|
107
106
|
- lib/graphql/stitching/remote_client.rb
|
107
|
+
- lib/graphql/stitching/request.rb
|
108
108
|
- lib/graphql/stitching/shaper.rb
|
109
109
|
- lib/graphql/stitching/supergraph.rb
|
110
110
|
- lib/graphql/stitching/util.rb
|
data/docs/document.md
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
## GraphQL::Stitching::Document
|
2
|
-
|
3
|
-
A `Document` wraps a parsed GraphQL request, and handles the logistics of extracting its appropriate operation, variable definitions, and fragments. A `Document` should be built once for a request and passed through to other stitching components that utilize document information.
|
4
|
-
|
5
|
-
```ruby
|
6
|
-
query = "query FetchMovie($id: ID!) { movie(id:$id) { id genre } }"
|
7
|
-
document = GraphQL::Stitching::Document.new(query, operation_name: "FetchMovie")
|
8
|
-
|
9
|
-
document.ast # parsed AST via GraphQL.parse
|
10
|
-
document.string # normalized printed string
|
11
|
-
document.digest # SHA digest of the normalized string
|
12
|
-
|
13
|
-
document.variables # mapping of variable names to type definitions
|
14
|
-
document.fragments # mapping of fragment names to fragment definitions
|
15
|
-
```
|
@@ -1,59 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module GraphQL
|
4
|
-
module Stitching
|
5
|
-
class Document
|
6
|
-
SUPPORTED_OPERATIONS = ["query", "mutation"].freeze
|
7
|
-
|
8
|
-
attr_reader :ast, :operation_name
|
9
|
-
|
10
|
-
def initialize(string_or_ast, operation_name: nil)
|
11
|
-
@ast = if string_or_ast.is_a?(String)
|
12
|
-
GraphQL.parse(string_or_ast)
|
13
|
-
else
|
14
|
-
string_or_ast
|
15
|
-
end
|
16
|
-
|
17
|
-
@operation_name = operation_name
|
18
|
-
end
|
19
|
-
|
20
|
-
def string
|
21
|
-
@string ||= GraphQL::Language::Printer.new.print(@ast)
|
22
|
-
end
|
23
|
-
|
24
|
-
def digest
|
25
|
-
@digest ||= Digest::SHA2.hexdigest(string)
|
26
|
-
end
|
27
|
-
|
28
|
-
def operation
|
29
|
-
@operation ||= begin
|
30
|
-
operation_defs = @ast.definitions.select do |d|
|
31
|
-
next unless d.is_a?(GraphQL::Language::Nodes::OperationDefinition)
|
32
|
-
next unless SUPPORTED_OPERATIONS.include?(d.operation_type)
|
33
|
-
@operation_name ? d.name == @operation_name : true
|
34
|
-
end
|
35
|
-
|
36
|
-
if operation_defs.length < 1
|
37
|
-
raise GraphQL::ExecutionError, "Invalid root operation."
|
38
|
-
elsif operation_defs.length > 1
|
39
|
-
raise GraphQL::ExecutionError, "An operation name is required when sending multiple operations."
|
40
|
-
end
|
41
|
-
|
42
|
-
operation_defs.first
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def variable_definitions
|
47
|
-
@variable_definitions ||= operation.variables.each_with_object({}) do |v, memo|
|
48
|
-
memo[v.name] = v.type
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
def fragment_definitions
|
53
|
-
@fragment_definitions ||= @ast.definitions.each_with_object({}) do |d, memo|
|
54
|
-
memo[d.name] = d if d.is_a?(GraphQL::Language::Nodes::FragmentDefinition)
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
59
|
-
end
|