graphql-stitching 1.1.0 → 1.1.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 +2 -2
- data/README.md +19 -19
- data/docs/executor.md +3 -2
- data/docs/mechanics.md +54 -0
- data/docs/planner.md +5 -5
- data/docs/request.md +6 -1
- data/lib/graphql/stitching/client.rb +4 -0
- data/lib/graphql/stitching/composer.rb +6 -0
- data/lib/graphql/stitching/export_selection.rb +7 -1
- data/lib/graphql/stitching/request.rb +9 -0
- data/lib/graphql/stitching/shaper.rb +1 -0
- data/lib/graphql/stitching/supergraph.rb +4 -0
- data/lib/graphql/stitching/util.rb +9 -0
- data/lib/graphql/stitching/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9cdcb3361e391ba9b6dd78aa38c308c08ddb6fde2720d15208e8dfb96c3006ea
|
4
|
+
data.tar.gz: 9f64b024bda3987d1c523cf52b49f0ca2dfd688ac4c89bbf18de30c88c098156
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b51bcfeaa20e9cdea6c2d4b4e472e8c23e13ec8af36e1b23852603d0329045af064e3ae738467a0823be0c9f545105acba001b708c207eb7f08d51ebf0791e1f
|
7
|
+
data.tar.gz: c529e74c88fc7193d7823d8c92a488b3e459b5ba9f88e8d62355ae2bd5e9eb0ed2b0d6f7fd14b6b8ed13ab1bb53a93bc5da49dcecd5deb92ee180fa70604c80b
|
data/.github/workflows/ci.yml
CHANGED
@@ -16,7 +16,7 @@ jobs:
|
|
16
16
|
ruby: 3.2
|
17
17
|
- gemfile: Gemfile
|
18
18
|
ruby: 3.1
|
19
|
-
- gemfile: gemfiles/graphql_1.13.gemfile
|
19
|
+
- gemfile: gemfiles/graphql_1.13.9.gemfile
|
20
20
|
ruby: 3.1
|
21
21
|
- gemfile: Gemfile
|
22
22
|
ruby: 2.7
|
@@ -30,6 +30,6 @@ jobs:
|
|
30
30
|
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
31
31
|
- name: Run tests
|
32
32
|
run: |
|
33
|
-
gem install bundler
|
33
|
+
gem install bundler -v 2.4.22
|
34
34
|
bundle install --jobs 4 --retry 3
|
35
35
|
bundle exec rake test
|
data/README.md
CHANGED
@@ -113,12 +113,12 @@ products_schema = <<~GRAPHQL
|
|
113
113
|
}
|
114
114
|
GRAPHQL
|
115
115
|
|
116
|
-
|
116
|
+
catalog_schema = <<~GRAPHQL
|
117
117
|
directive @stitch(key: String!) repeatable on FIELD_DEFINITION
|
118
118
|
|
119
119
|
type Product {
|
120
120
|
id: ID!
|
121
|
-
|
121
|
+
price: Float!
|
122
122
|
}
|
123
123
|
|
124
124
|
type Query {
|
@@ -131,7 +131,7 @@ client = GraphQL::Stitching::Client.new(locations: {
|
|
131
131
|
schema: GraphQL::Schema.from_definition(products_schema),
|
132
132
|
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3001"),
|
133
133
|
},
|
134
|
-
|
134
|
+
catalog: {
|
135
135
|
schema: GraphQL::Schema.from_definition(shipping_schema),
|
136
136
|
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3002"),
|
137
137
|
},
|
@@ -151,9 +151,9 @@ type Query {
|
|
151
151
|
```
|
152
152
|
|
153
153
|
* 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.
|
154
|
-
* 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).
|
154
|
+
* 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](#multiple-query-arguments) later).
|
155
155
|
|
156
|
-
Each location that provides a unique variant of a type must provide one stitching query
|
156
|
+
Each location that provides a unique variant of a type must provide at least one stitching query. The exception to this requirement are types that contain only a single key field:
|
157
157
|
|
158
158
|
```graphql
|
159
159
|
type Product {
|
@@ -165,7 +165,7 @@ The above representation of a `Product` type provides no unique data beyond a ke
|
|
165
165
|
|
166
166
|
#### List queries
|
167
167
|
|
168
|
-
It's okay ([even preferable](
|
168
|
+
It's okay ([even preferable](#batching) in most circumstances) to provide a list accessor as a stitching query. The only requirement is that both the field argument and return type must be lists, and the query results are expected to be a mapped set with `null` holding the position of missing results.
|
169
169
|
|
170
170
|
```graphql
|
171
171
|
type Query {
|
@@ -228,7 +228,7 @@ type Query {
|
|
228
228
|
}
|
229
229
|
```
|
230
230
|
|
231
|
-
The `@stitch` directive is also repeatable
|
231
|
+
The `@stitch` directive is also repeatable, allowing a single query to associate with multiple keys:
|
232
232
|
|
233
233
|
```graphql
|
234
234
|
type Product {
|
@@ -311,16 +311,15 @@ The [Apollo Federation specification](https://www.apollographql.com/docs/federat
|
|
311
311
|
The composer will automatcially detect and stitch schemas with an `_entities` query, for example:
|
312
312
|
|
313
313
|
```ruby
|
314
|
-
|
314
|
+
products_schema = <<~GRAPHQL
|
315
315
|
directive @key(fields: String!) repeatable on OBJECT
|
316
316
|
|
317
|
-
type
|
317
|
+
type Product @key(fields: "id") {
|
318
318
|
id: ID!
|
319
319
|
name: String!
|
320
|
-
address: String!
|
321
320
|
}
|
322
321
|
|
323
|
-
union _Entity =
|
322
|
+
union _Entity = Product
|
324
323
|
scalar _Any
|
325
324
|
|
326
325
|
type Query {
|
@@ -329,15 +328,15 @@ accounts_schema = <<~GRAPHQL
|
|
329
328
|
}
|
330
329
|
GRAPHQL
|
331
330
|
|
332
|
-
|
331
|
+
catalog_schema = <<~GRAPHQL
|
333
332
|
directive @key(fields: String!) repeatable on OBJECT
|
334
333
|
|
335
|
-
type
|
334
|
+
type Product @key(fields: "id") {
|
336
335
|
id: ID!
|
337
|
-
|
336
|
+
price: Float!
|
338
337
|
}
|
339
338
|
|
340
|
-
union _Entity =
|
339
|
+
union _Entity = Product
|
341
340
|
scalar _Any
|
342
341
|
|
343
342
|
type Query {
|
@@ -346,12 +345,12 @@ comments_schema = <<~GRAPHQL
|
|
346
345
|
GRAPHQL
|
347
346
|
|
348
347
|
client = GraphQL::Stitching::Client.new(locations: {
|
349
|
-
|
350
|
-
schema: GraphQL::Schema.from_definition(
|
348
|
+
products: {
|
349
|
+
schema: GraphQL::Schema.from_definition(products_schema),
|
351
350
|
executable: ...,
|
352
351
|
},
|
353
|
-
|
354
|
-
schema: GraphQL::Schema.from_definition(
|
352
|
+
catalog: {
|
353
|
+
schema: GraphQL::Schema.from_definition(catalog_schema),
|
355
354
|
executable: ...,
|
356
355
|
},
|
357
356
|
})
|
@@ -426,6 +425,7 @@ The [Executor](./docs/executor.md) component builds atop the Ruby fiber-based im
|
|
426
425
|
|
427
426
|
## Additional topics
|
428
427
|
|
428
|
+
- [Deploying a stitched schema](./docs/mechanics.md#deploying-a-stitched-schema)
|
429
429
|
- [Field selection routing](./docs/mechanics.md#field-selection-routing)
|
430
430
|
- [Root selection routing](./docs/mechanics.md#root-selection-routing)
|
431
431
|
- [Stitched errors](./docs/mechanics.md#stitched-errors)
|
data/docs/executor.md
CHANGED
@@ -16,6 +16,7 @@ request = GraphQL::Stitching::Request.new(
|
|
16
16
|
query,
|
17
17
|
variables: { "id" => "123" },
|
18
18
|
operation_name: "MyQuery",
|
19
|
+
context: { ... },
|
19
20
|
)
|
20
21
|
|
21
22
|
plan = GraphQL::Stitching::Planner.new(
|
@@ -26,7 +27,7 @@ plan = GraphQL::Stitching::Planner.new(
|
|
26
27
|
result = GraphQL::Stitching::Executor.new(
|
27
28
|
supergraph: supergraph,
|
28
29
|
request: request,
|
29
|
-
plan: plan
|
30
|
+
plan: plan,
|
30
31
|
).perform
|
31
32
|
```
|
32
33
|
|
@@ -39,7 +40,7 @@ By default, execution results are always returned with document shaping (stitchi
|
|
39
40
|
raw_result = GraphQL::Stitching::Executor.new(
|
40
41
|
supergraph: supergraph,
|
41
42
|
request: request,
|
42
|
-
plan: plan
|
43
|
+
plan: plan,
|
43
44
|
).perform(raw: true)
|
44
45
|
```
|
45
46
|
|
data/docs/mechanics.md
CHANGED
@@ -1,5 +1,59 @@
|
|
1
1
|
## Schema Stitching, mechanics
|
2
2
|
|
3
|
+
### Deploying a stitched schema
|
4
|
+
|
5
|
+
Among the simplest and most effective ways to manage a stitched schema is to compose it locally, write the composed SDL as a `.graphql` file in your repo, and then load the composed schema into a stitching client at runtime. For example, setup a `rake` task that loads/fetches subgraph schemas, composes them, and then writes the composed schema definition as a file committed to the repo:
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
task :compose_graphql do
|
9
|
+
schema1_sdl = ... # load schema 1
|
10
|
+
schema2_sdl = ... # load schema 2
|
11
|
+
|
12
|
+
supergraph = GraphQL::Stitching::Composer.new.perform({
|
13
|
+
schema1: {
|
14
|
+
schema: GraphQL::Schema.from_definition(schema1_sdl)
|
15
|
+
},
|
16
|
+
schema2: {
|
17
|
+
schema: GraphQL::Schema.from_definition(schema2_sdl)
|
18
|
+
}
|
19
|
+
})
|
20
|
+
|
21
|
+
File.write("schema/supergraph.graphql", supergraph.to_definition)
|
22
|
+
puts "Schema composition was successful."
|
23
|
+
end
|
24
|
+
|
25
|
+
# bundle exec rake compose-graphql
|
26
|
+
```
|
27
|
+
|
28
|
+
Then at runtime, load the composed schema into a stitching client:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
class GraphQlController
|
32
|
+
class < self
|
33
|
+
def client
|
34
|
+
@client ||= begin
|
35
|
+
supergraph_sdl = File.read("schema/supergraph.graphql")
|
36
|
+
supergraph = GraphQL::Stitching::Supergraph.from_definition(supergraph_sdl, executables: {
|
37
|
+
schema1: GraphQL::Stitching::HttpExecutable.new("http://localhost:3001/graphql"),
|
38
|
+
schema2: GraphQL::Stitching::HttpExecutable.new("http://localhost:3002/graphql"),
|
39
|
+
})
|
40
|
+
GraphQL::Stitching::Client.new(supergraph: supergraph)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def exec
|
46
|
+
self.class.client.execute(
|
47
|
+
params[:query],
|
48
|
+
variables: params[:variables],
|
49
|
+
operation_name: params[:operation_name]
|
50
|
+
)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
This process assures that composition always happens before deployment where failures can be detected. Hot reloading of the supergraph can also be accommodated by uploading the composed schema to a sync location (cloud storage, etc) that is polled by the application runtime. When the schema changes, load it into a new stitching client and swap that into the application.
|
56
|
+
|
3
57
|
### Field selection routing
|
4
58
|
|
5
59
|
Fields of a merged type may exist in multiple locations. For example, the `title` field below is provided by both locations:
|
data/docs/planner.md
CHANGED
@@ -32,15 +32,15 @@ Plans are designed to be cacheable. This is very useful for redundant GraphQL do
|
|
32
32
|
cached_plan = $redis.get(request.digest)
|
33
33
|
|
34
34
|
plan = if cached_plan
|
35
|
-
JSON.parse(cached_plan)
|
35
|
+
GraphQL::Stitching::Plan.from_json(JSON.parse(cached_plan))
|
36
36
|
else
|
37
|
-
|
37
|
+
plan = GraphQL::Stitching::Planner.new(
|
38
38
|
supergraph: supergraph,
|
39
39
|
request: request,
|
40
|
-
).perform
|
40
|
+
).perform
|
41
41
|
|
42
|
-
$redis.set(request.digest, JSON.generate(
|
43
|
-
|
42
|
+
$redis.set(request.digest, JSON.generate(plan.as_json))
|
43
|
+
plan
|
44
44
|
end
|
45
45
|
|
46
46
|
# execute the plan...
|
data/docs/request.md
CHANGED
@@ -4,7 +4,12 @@ A `Request` contains a parsed GraphQL document and variables, and handles the lo
|
|
4
4
|
|
5
5
|
```ruby
|
6
6
|
source = "query FetchMovie($id: ID!) { movie(id:$id) { id genre } }"
|
7
|
-
request = GraphQL::Stitching::Request.new(
|
7
|
+
request = GraphQL::Stitching::Request.new(
|
8
|
+
source,
|
9
|
+
variables: { "id" => "1" },
|
10
|
+
operation_name: "FetchMovie",
|
11
|
+
context: { ... },
|
12
|
+
)
|
8
13
|
```
|
9
14
|
|
10
15
|
A `Request` provides the following information:
|
@@ -20,6 +20,10 @@ module GraphQL
|
|
20
20
|
composer ||= GraphQL::Stitching::Composer.new
|
21
21
|
composer.perform(locations)
|
22
22
|
end
|
23
|
+
|
24
|
+
@on_cache_read = nil
|
25
|
+
@on_cache_write = nil
|
26
|
+
@on_error = nil
|
23
27
|
end
|
24
28
|
|
25
29
|
def execute(query:, variables: nil, operation_name: nil, context: nil, validate: true)
|
@@ -39,6 +39,12 @@ module GraphQL
|
|
39
39
|
@directive_kwarg_merger = directive_kwarg_merger || BASIC_VALUE_MERGER
|
40
40
|
@root_field_location_selector = root_field_location_selector || BASIC_ROOT_FIELD_LOCATION_SELECTOR
|
41
41
|
@stitch_directives = {}
|
42
|
+
|
43
|
+
@field_map = nil
|
44
|
+
@boundary_map = nil
|
45
|
+
@mapped_type_names = nil
|
46
|
+
@candidate_directives_by_name_and_location = nil
|
47
|
+
@schema_directives = nil
|
42
48
|
end
|
43
49
|
|
44
50
|
def perform(locations_input)
|
@@ -8,6 +8,8 @@ module GraphQL
|
|
8
8
|
EXPORT_PREFIX = "_export_"
|
9
9
|
|
10
10
|
class << self
|
11
|
+
@typename_node = nil
|
12
|
+
|
11
13
|
def key?(name)
|
12
14
|
return false unless name
|
13
15
|
|
@@ -19,7 +21,11 @@ module GraphQL
|
|
19
21
|
end
|
20
22
|
|
21
23
|
def key_node(field_name)
|
22
|
-
|
24
|
+
if Util.graphql_version?(2, 2)
|
25
|
+
GraphQL::Language::Nodes::Field.new(field_alias: key(field_name), name: field_name)
|
26
|
+
else
|
27
|
+
GraphQL::Language::Nodes::Field.new(alias: key(field_name), name: field_name)
|
28
|
+
end
|
23
29
|
end
|
24
30
|
|
25
31
|
def typename_node
|
@@ -9,6 +9,15 @@ module GraphQL
|
|
9
9
|
attr_reader :document, :variables, :operation_name, :context
|
10
10
|
|
11
11
|
def initialize(document, operation_name: nil, variables: nil, context: nil)
|
12
|
+
@string = nil
|
13
|
+
@digest = nil
|
14
|
+
@normalized_string = nil
|
15
|
+
@normalized_digest = nil
|
16
|
+
@operation = nil
|
17
|
+
@operation_directives = nil
|
18
|
+
@variable_definitions = nil
|
19
|
+
@fragment_definitions = nil
|
20
|
+
|
12
21
|
@document = if document.is_a?(String)
|
13
22
|
@string = document
|
14
23
|
GraphQL.parse(document)
|
@@ -97,6 +97,10 @@ module GraphQL
|
|
97
97
|
@possible_keys_by_type_and_location = {}
|
98
98
|
@memoized_schema_possible_types = {}
|
99
99
|
@memoized_schema_fields = {}
|
100
|
+
@memoized_introspection_types = nil
|
101
|
+
@memoized_schema_types = nil
|
102
|
+
@fields_by_type_and_location = nil
|
103
|
+
@locations_by_type = nil
|
100
104
|
|
101
105
|
# add introspection types into the fields mapping
|
102
106
|
@locations_by_type_and_field = memoized_introspection_types.each_with_object(fields) do |(type_name, type), memo|
|
@@ -12,7 +12,16 @@ module GraphQL
|
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
|
+
GRAPHQL_VERSION = GraphQL::VERSION.split(".").map(&:to_i).freeze
|
16
|
+
|
15
17
|
class << self
|
18
|
+
def graphql_version?(major, minor = nil, patch = nil)
|
19
|
+
result = GRAPHQL_VERSION[0] >= major
|
20
|
+
result &&= GRAPHQL_VERSION[1] >= minor if minor
|
21
|
+
result &&= GRAPHQL_VERSION[2] >= patch if patch
|
22
|
+
result
|
23
|
+
end
|
24
|
+
|
16
25
|
# specifies if a type is a primitive leaf value
|
17
26
|
def is_leaf_type?(type)
|
18
27
|
type.kind.scalar? || type.kind.enum?
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: graphql-stitching
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
4
|
+
version: 1.1.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-12-
|
11
|
+
date: 2023-12-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|