graphql-stitching 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +27 -0
- data/.gitignore +59 -0
- data/.ruby-version +1 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +49 -0
- data/LICENSE +21 -0
- data/Procfile +3 -0
- data/README.md +329 -0
- data/Rakefile +12 -0
- data/docs/README.md +14 -0
- data/docs/composer.md +69 -0
- data/docs/document.md +15 -0
- data/docs/executor.md +29 -0
- data/docs/gateway.md +106 -0
- data/docs/images/library.png +0 -0
- data/docs/images/merging.png +0 -0
- data/docs/images/stitching.png +0 -0
- data/docs/planner.md +43 -0
- data/docs/shaper.md +20 -0
- data/docs/supergraph.md +65 -0
- data/example/gateway.rb +50 -0
- data/example/graphiql.html +153 -0
- data/example/remote1.rb +26 -0
- data/example/remote2.rb +26 -0
- data/graphql-stitching.gemspec +34 -0
- data/lib/graphql/stitching/composer/base_validator.rb +11 -0
- data/lib/graphql/stitching/composer/validate_boundaries.rb +80 -0
- data/lib/graphql/stitching/composer/validate_interfaces.rb +24 -0
- data/lib/graphql/stitching/composer.rb +442 -0
- data/lib/graphql/stitching/document.rb +59 -0
- data/lib/graphql/stitching/executor.rb +254 -0
- data/lib/graphql/stitching/gateway.rb +120 -0
- data/lib/graphql/stitching/planner.rb +323 -0
- data/lib/graphql/stitching/planner_operation.rb +59 -0
- data/lib/graphql/stitching/remote_client.rb +25 -0
- data/lib/graphql/stitching/shaper.rb +92 -0
- data/lib/graphql/stitching/supergraph.rb +171 -0
- data/lib/graphql/stitching/util.rb +63 -0
- data/lib/graphql/stitching/version.rb +7 -0
- data/lib/graphql/stitching.rb +30 -0
- metadata +142 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a228b0fe5779d625285182f885a7d65b5f66541f275a5313602e68a4c9eb209d
|
4
|
+
data.tar.gz: f6efcaeafd7c417db07f007641f84c800c0ebb32c99ad19c5004aa2bb9ac8685
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1d484e39db0ac919e68477e8733fa6f0ca31ffde74b2bfebc4e13ba6b55d7973dc793c6df4251ae7b912c450bdc38ab4478e300c350d7be67555a635687b04a9
|
7
|
+
data.tar.gz: 6354381e960a922453cd38ae5c513b60f2d2ae4585ec2a53a1ab7b405c2da6ca6d01bce9999794f9c8b6df7fe0d49176a4f944ab0efabfcb8ff252393a60f88e
|
@@ -0,0 +1,27 @@
|
|
1
|
+
name: CI
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches: [ main ]
|
6
|
+
pull_request:
|
7
|
+
branches: [ main ]
|
8
|
+
|
9
|
+
jobs:
|
10
|
+
test:
|
11
|
+
runs-on: ubuntu-latest
|
12
|
+
strategy:
|
13
|
+
matrix:
|
14
|
+
ruby-version: ['3.1.1']
|
15
|
+
|
16
|
+
steps:
|
17
|
+
- uses: actions/checkout@v2
|
18
|
+
- name: Setup Ruby
|
19
|
+
uses: ruby/setup-ruby@v1
|
20
|
+
with:
|
21
|
+
ruby-version: ${{ matrix.ruby-version }}
|
22
|
+
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
23
|
+
- name: Run tests
|
24
|
+
run: |
|
25
|
+
gem install bundler
|
26
|
+
bundle install --jobs 4 --retry 3
|
27
|
+
bundle exec rake test
|
data/.gitignore
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
/.config
|
4
|
+
/coverage/
|
5
|
+
/InstalledFiles
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/spec/examples.txt
|
9
|
+
/test/tmp/
|
10
|
+
/test/version_tmp/
|
11
|
+
/tmp/
|
12
|
+
.envrc
|
13
|
+
|
14
|
+
# Used by dotenv library to load environment variables.
|
15
|
+
# .env
|
16
|
+
/example/env.json
|
17
|
+
|
18
|
+
# Ignore Byebug command history file.
|
19
|
+
.byebug_history
|
20
|
+
.DS_Store
|
21
|
+
|
22
|
+
## Specific to RubyMotion:
|
23
|
+
.dat*
|
24
|
+
.repl_history
|
25
|
+
build/
|
26
|
+
*.bridgesupport
|
27
|
+
build-iPhoneOS/
|
28
|
+
build-iPhoneSimulator/
|
29
|
+
|
30
|
+
## Specific to RubyMotion (use of CocoaPods):
|
31
|
+
#
|
32
|
+
# We recommend against adding the Pods directory to your .gitignore. However
|
33
|
+
# you should judge for yourself, the pros and cons are mentioned at:
|
34
|
+
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
35
|
+
#
|
36
|
+
# vendor/Pods/
|
37
|
+
|
38
|
+
## Documentation cache and generated files:
|
39
|
+
/.yardoc/
|
40
|
+
/_yardoc/
|
41
|
+
/doc/
|
42
|
+
/rdoc/
|
43
|
+
|
44
|
+
## Environment normalization:
|
45
|
+
/.bundle/
|
46
|
+
/vendor/bundle
|
47
|
+
/lib/bundler/man/
|
48
|
+
|
49
|
+
# for a library or gem, you might want to ignore these files since the code is
|
50
|
+
# intended to run in multiple environments; otherwise, check them in:
|
51
|
+
# Gemfile.lock
|
52
|
+
# .ruby-version
|
53
|
+
# .ruby-gemset
|
54
|
+
|
55
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
56
|
+
.rvmrc
|
57
|
+
|
58
|
+
# Used by RuboCop. Remote config files pulled in from inherit_from directive.
|
59
|
+
# .rubocop-https?--*
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.1.1
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
graphql-stitching (0.0.1)
|
5
|
+
graphql (~> 2.0.16)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
byebug (11.1.3)
|
11
|
+
coderay (1.1.3)
|
12
|
+
foreman (0.87.2)
|
13
|
+
graphql (2.0.16)
|
14
|
+
method_source (1.0.0)
|
15
|
+
minitest (5.17.0)
|
16
|
+
pry (0.14.2)
|
17
|
+
coderay (~> 1.1)
|
18
|
+
method_source (~> 1.0)
|
19
|
+
pry-byebug (3.10.1)
|
20
|
+
byebug (~> 11.0)
|
21
|
+
pry (>= 0.13, < 0.15)
|
22
|
+
rack (3.0.4.1)
|
23
|
+
rackup (2.1.0)
|
24
|
+
rack (>= 3)
|
25
|
+
webrick (~> 1.8)
|
26
|
+
rake (12.3.3)
|
27
|
+
warning (1.3.0)
|
28
|
+
webrick (1.8.1)
|
29
|
+
|
30
|
+
PLATFORMS
|
31
|
+
arm64-darwin-21
|
32
|
+
ruby
|
33
|
+
x86_64-darwin-21
|
34
|
+
x86_64-linux
|
35
|
+
|
36
|
+
DEPENDENCIES
|
37
|
+
bundler (~> 2.0)
|
38
|
+
foreman
|
39
|
+
graphql-stitching!
|
40
|
+
minitest (~> 5.12)
|
41
|
+
pry
|
42
|
+
pry-byebug
|
43
|
+
rack
|
44
|
+
rackup
|
45
|
+
rake (~> 12.0)
|
46
|
+
warning
|
47
|
+
|
48
|
+
BUNDLED WITH
|
49
|
+
2.4.1
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2023 Greg MacWilliam
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/Procfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,329 @@
|
|
1
|
+
## GraphQL Stitching for Ruby
|
2
|
+
|
3
|
+
GraphQL stitching composes a single schema from multiple underlying GraphQL resources, then smartly delegates portions of incoming requests to their respective service locations in dependency order and returns the merged results. This allows an entire location graph to be queried through one combined GraphQL surface area.
|
4
|
+
|
5
|
+
![Stitched graph](./docs/images/stitching.png)
|
6
|
+
|
7
|
+
**Supports:**
|
8
|
+
- Merged object and interface types.
|
9
|
+
- Multiple keys per merged type.
|
10
|
+
- Shared objects, enums, and inputs across locations.
|
11
|
+
- Combining local and remote schemas.
|
12
|
+
|
13
|
+
**NOT Supported:**
|
14
|
+
- Computed fields (ie: federation-style `@requires`)
|
15
|
+
- Subscriptions
|
16
|
+
|
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
|
+
|
19
|
+
## Getting started
|
20
|
+
|
21
|
+
Add to your Gemfile:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
gem "graphql-stitching"
|
25
|
+
```
|
26
|
+
|
27
|
+
Run `bundle install`, then require unless running an autoloading framework (Rails, etc):
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
require "graphql/stitching"
|
31
|
+
```
|
32
|
+
|
33
|
+
## Usage
|
34
|
+
|
35
|
+
The quickest way to start is to use the provided [`Gateway`](./docs/gateway.md) component that assembles a stitched graph ready to execute requests:
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
movies_schema = <<~GRAPHQL
|
39
|
+
type Movie { id: ID! name: String! }
|
40
|
+
type Query { movie(id: ID!): Movie }
|
41
|
+
GRAPHQL
|
42
|
+
|
43
|
+
showtimes_schema = <<~GRAPHQL
|
44
|
+
type Showtime { id: ID! time: String! }
|
45
|
+
type Query { showtime(id: ID!): Showtime }
|
46
|
+
GRAPHQL
|
47
|
+
|
48
|
+
gateway = GraphQL::Stitching::Gateway.new(locations: {
|
49
|
+
products: {
|
50
|
+
schema: GraphQL::Schema.from_definition(movies_schema),
|
51
|
+
executable: GraphQL::Stitching::RemoteClient.new(url: "http://localhost:3000"),
|
52
|
+
},
|
53
|
+
showtimes: {
|
54
|
+
schema: GraphQL::Schema.from_definition(showtimes_schema),
|
55
|
+
executable: GraphQL::Stitching::RemoteClient.new(url: "http://localhost:3001"),
|
56
|
+
},
|
57
|
+
my_local: {
|
58
|
+
schema: MyLocal::GraphQL::Schema,
|
59
|
+
},
|
60
|
+
})
|
61
|
+
|
62
|
+
result = gateway.execute(
|
63
|
+
query: "query FetchFromAll($movieId:ID!, $showtimeId:ID!){
|
64
|
+
movie(id:$movieId) { name }
|
65
|
+
showtime(id:$showtimeId): { time }
|
66
|
+
myLocalField
|
67
|
+
}",
|
68
|
+
variables: { "movieId" => "1", "showtimeId" => "2" },
|
69
|
+
operation_name: "FetchFromAll"
|
70
|
+
)
|
71
|
+
```
|
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.
|
74
|
+
|
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
|
+
|
77
|
+
- [Composer](./docs/composer.md) - merges and validates many schemas into one graph.
|
78
|
+
- [Supergraph](./docs/supergraph.md) - manages the combined schema and location routing maps. Can be exported, cached, and rehydrated.
|
79
|
+
- [Document](./docs/document.md) - manages a parsed GraphQL request document.
|
80
|
+
- [Planner](./docs/planner.md) - builds a cacheable query plan for a request document.
|
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
|
+
|
84
|
+
## Merged types
|
85
|
+
|
86
|
+
`Object` and `Interface` types may exist with different fields in different graph locations, and will get merged together in the combined schema.
|
87
|
+
|
88
|
+
![Merging types](./docs/images/merging.png)
|
89
|
+
|
90
|
+
To facilitate this merging of types, stitching must know how to cross-reference and fetch each variant of a type from its source location. This is done using the `@stitch` directive:
|
91
|
+
|
92
|
+
```graphql
|
93
|
+
directive @stitch(key: String!) repeatable on FIELD_DEFINITION
|
94
|
+
```
|
95
|
+
|
96
|
+
This directive is applied to root queries where a merged type may be accessed in each location, and a `key` argument specifies a field needed from other locations to be used as a query argument.
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
products_schema = <<~GRAPHQL
|
100
|
+
directive @stitch(key: String!) repeatable on FIELD_DEFINITION
|
101
|
+
|
102
|
+
type Product {
|
103
|
+
id: ID!
|
104
|
+
name: String!
|
105
|
+
}
|
106
|
+
|
107
|
+
type Query {
|
108
|
+
product(id: ID!): Product @stitch(key: "id")
|
109
|
+
}
|
110
|
+
GRAPHQL
|
111
|
+
|
112
|
+
shipping_schema = <<~GRAPHQL
|
113
|
+
directive @stitch(key: String!) repeatable on FIELD_DEFINITION
|
114
|
+
|
115
|
+
type Product {
|
116
|
+
id: ID!
|
117
|
+
weight: Float!
|
118
|
+
}
|
119
|
+
|
120
|
+
type Query {
|
121
|
+
products(ids: [ID!]!): [Product]! @stitch(key: "id")
|
122
|
+
}
|
123
|
+
GRAPHQL
|
124
|
+
|
125
|
+
supergraph = GraphQL::Stitching::Composer.new({
|
126
|
+
"products" => GraphQL::Schema.from_definition(products_schema),
|
127
|
+
"shipping" => GraphQL::Schema.from_definition(shipping_schema),
|
128
|
+
})
|
129
|
+
|
130
|
+
supergraph.assign_executable("products",
|
131
|
+
GraphQL::Stitching::RemoteClient.new(url: "http://localhost:3001")
|
132
|
+
)
|
133
|
+
supergraph.assign_executable("shipping",
|
134
|
+
GraphQL::Stitching::RemoteClient.new(url: "http://localhost:3002")
|
135
|
+
)
|
136
|
+
```
|
137
|
+
|
138
|
+
Focusing on the `@stitch` directive usage:
|
139
|
+
|
140
|
+
```graphql
|
141
|
+
type Product {
|
142
|
+
id: ID!
|
143
|
+
name: String!
|
144
|
+
}
|
145
|
+
type Query {
|
146
|
+
product(id: ID!): Product @stitch(key: "id")
|
147
|
+
}
|
148
|
+
```
|
149
|
+
|
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.
|
151
|
+
* 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
|
+
|
153
|
+
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:
|
154
|
+
|
155
|
+
```graphql
|
156
|
+
type Product {
|
157
|
+
id: ID!
|
158
|
+
}
|
159
|
+
```
|
160
|
+
|
161
|
+
The above representation of a `Product` type provides no unique data beyond a key that is available in other locations. Thus, this representation will never require an inbound request to fetch it, and its stitching query may be omitted. This pattern of providing key-only types is very common in stitching: it allows a foreign key to be represented as an object stub that may be enriched by data collected from other locations.
|
162
|
+
|
163
|
+
#### List queries
|
164
|
+
|
165
|
+
It's okay ([even preferable](https://www.youtube.com/watch?v=VmK0KBHTcWs) in many 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.
|
166
|
+
|
167
|
+
```graphql
|
168
|
+
type Query {
|
169
|
+
products(ids: [ID!]!): [Product]! @stitch(key: "id")
|
170
|
+
}
|
171
|
+
```
|
172
|
+
|
173
|
+
#### Abstract queries
|
174
|
+
|
175
|
+
It's okay for stitching queries to be implemented through abstract types. An abstract query will provide access to all of its possible types. For interfaces, the key selection should match a field within the interface. For unions, all possible types must implement the key selection individually.
|
176
|
+
|
177
|
+
```graphql
|
178
|
+
interface Node {
|
179
|
+
id: ID!
|
180
|
+
}
|
181
|
+
type Product implements Node {
|
182
|
+
id: ID!
|
183
|
+
name: String!
|
184
|
+
}
|
185
|
+
type Query {
|
186
|
+
nodes(ids: [ID!]!): [Node]! @stitch(key: "id")
|
187
|
+
}
|
188
|
+
```
|
189
|
+
|
190
|
+
#### Multiple query arguments
|
191
|
+
|
192
|
+
Stitching infers which argument to use for queries with a single argument. For queries that accept multiple arguments, the key must provide an argument mapping specified as `"<arg>:<key>"`. Note the `"id:id"` key:
|
193
|
+
|
194
|
+
```graphql
|
195
|
+
type Query {
|
196
|
+
product(id: ID, upc: ID): Product @stitch(key: "id:id")
|
197
|
+
}
|
198
|
+
```
|
199
|
+
|
200
|
+
#### Multiple type keys
|
201
|
+
|
202
|
+
A type may exist in multiple locations across the graph using different keys, for example:
|
203
|
+
|
204
|
+
```graphql
|
205
|
+
type Product { id:ID! } # storefronts location
|
206
|
+
type Product { id:ID! upc:ID! } # products location
|
207
|
+
type Product { upc:ID! } # catelog location
|
208
|
+
```
|
209
|
+
|
210
|
+
In the above graph, the `storefronts` and `catelog` locations have different keys that join through an intermediary. This pattern is perfectly valid and resolvable as long as the intermediary provides stitching queries for each possible key:
|
211
|
+
|
212
|
+
```graphql
|
213
|
+
type Product {
|
214
|
+
id: ID!
|
215
|
+
upc: ID!
|
216
|
+
}
|
217
|
+
type Query {
|
218
|
+
productById(id: ID): Product @stitch(key: "id")
|
219
|
+
productByUpc(upc: ID): Product @stitch(key: "upc")
|
220
|
+
}
|
221
|
+
```
|
222
|
+
|
223
|
+
The `@stitch` directive is also repeatable, allowing a single query to associate with multiple keys:
|
224
|
+
|
225
|
+
```graphql
|
226
|
+
type Product {
|
227
|
+
id: ID!
|
228
|
+
upc: ID!
|
229
|
+
}
|
230
|
+
type Query {
|
231
|
+
product(id: ID, upc: ID): Product @stitch(key: "id:id") @stitch(key: "upc:upc")
|
232
|
+
}
|
233
|
+
```
|
234
|
+
|
235
|
+
#### Class-based schemas
|
236
|
+
|
237
|
+
The `@stitch` directive can be added to class-based schemas with a directive class:
|
238
|
+
|
239
|
+
```ruby
|
240
|
+
class StitchField < GraphQL::Schema::Directive
|
241
|
+
graphql_name "stitch"
|
242
|
+
locations FIELD_DEFINITION
|
243
|
+
repeatable true
|
244
|
+
argument :key, String, required: true
|
245
|
+
end
|
246
|
+
|
247
|
+
class Query < GraphQL::Schema::Object
|
248
|
+
field :product, Product, null: false do
|
249
|
+
directive StitchField, key: "id"
|
250
|
+
argument :id, ID, required: true
|
251
|
+
end
|
252
|
+
end
|
253
|
+
```
|
254
|
+
|
255
|
+
#### Custom directive names
|
256
|
+
|
257
|
+
The library is configured to use a `@stitch` directive by default. You may customize this by setting a new name during initialization:
|
258
|
+
|
259
|
+
```ruby
|
260
|
+
GraphQL::Stitching.stitch_directive = "merge"
|
261
|
+
```
|
262
|
+
|
263
|
+
## Executables
|
264
|
+
|
265
|
+
A [Supergraph](./docs/supergraph.md) will delegate requests to the individual `GraphQL::Schema` classes that composed it. You may change this behavior by assigning new executables: these may be `GraphQL::Schema` classes, or objects that respond to `.call` with the following arguments...
|
266
|
+
|
267
|
+
```ruby
|
268
|
+
class MyExecutable
|
269
|
+
def call(location, query_string, variables)
|
270
|
+
# process a GraphQL request...
|
271
|
+
end
|
272
|
+
end
|
273
|
+
```
|
274
|
+
|
275
|
+
Executables are assigned to a supergraph using `assign_executable`:
|
276
|
+
|
277
|
+
```ruby
|
278
|
+
supergraph = GraphQL::Stitching::Composer.new(...)
|
279
|
+
|
280
|
+
supergraph.assign_executable("location1", MyExecutable.new)
|
281
|
+
supergraph.assign_executable("location2", ->(loc, query, vars) { ... })
|
282
|
+
supergraph.assign_executable("location3") do |loc, query vars|
|
283
|
+
# ...
|
284
|
+
end
|
285
|
+
```
|
286
|
+
|
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 _must_ manually assign all executables to a `Supergraph` instance when rehydrating it from cache ([see docs](./docs/supergraph.md)).
|
288
|
+
|
289
|
+
## Concurrency
|
290
|
+
|
291
|
+
The [Executor](./docs/executor.md) component builds atop the Ruby fiber-based implementation of `GraphQL::Dataloader`. Non-blocking concurrency requires setting a fiber scheduler via `Fiber.set_scheduler`, see [graphql-ruby docs](https://graphql-ruby.org/dataloader/nonblocking.html). You may also need to build your own remote clients using corresponding HTTP libraries.
|
292
|
+
|
293
|
+
## Example
|
294
|
+
|
295
|
+
This repo includes a working example of several stitched schemas running across Rack servers. Try running it:
|
296
|
+
|
297
|
+
```shell
|
298
|
+
bundle install
|
299
|
+
foreman start
|
300
|
+
```
|
301
|
+
|
302
|
+
Then visit the gateway service at `http://localhost:3000` and try this query:
|
303
|
+
|
304
|
+
```graphql
|
305
|
+
query {
|
306
|
+
storefront(id: "1") {
|
307
|
+
id
|
308
|
+
products {
|
309
|
+
upc
|
310
|
+
name
|
311
|
+
price
|
312
|
+
manufacturer {
|
313
|
+
name
|
314
|
+
address
|
315
|
+
products { upc name }
|
316
|
+
}
|
317
|
+
}
|
318
|
+
}
|
319
|
+
}
|
320
|
+
```
|
321
|
+
|
322
|
+
The above query collects data from all locations, two of which are remote schemas and the third a local schema. The combined graph schema is also stitched in to provide introspection capabilities.
|
323
|
+
|
324
|
+
## Tests
|
325
|
+
|
326
|
+
```shell
|
327
|
+
bundle install
|
328
|
+
bundle exec rake test [TEST=path/to/test.rb]
|
329
|
+
```
|
data/Rakefile
ADDED
data/docs/README.md
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
## GraphQL::Stitching
|
2
|
+
|
3
|
+
This module provides a collection of components that may be composed into a stitched schema.
|
4
|
+
|
5
|
+
![Library flow](./images/library.png)
|
6
|
+
|
7
|
+
Major components include:
|
8
|
+
|
9
|
+
- [Gateway](./gateway.md) - an out-of-the-box stitching configuration.
|
10
|
+
- [Composer](./composer.md) - merges and validates many schemas into one graph.
|
11
|
+
- [Supergraph](./supergraph.md) - manages the combined schema and location routing maps. Can be exported, cached, and rehydrated.
|
12
|
+
- [Document](./document.md) - manages a parsed GraphQL request document.
|
13
|
+
- [Planner](./planner.md) - builds a cacheable query plan for a request document.
|
14
|
+
- [Executor](./executor.md) - executes a query plan with given request variables.
|
data/docs/composer.md
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
## GraphQL::Stitching::Composer
|
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 context provides a combined GraphQL schema and delegation maps used to route incoming requests:
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
storefronts_sdl = <<~GRAPHQL
|
7
|
+
type Storefront {
|
8
|
+
id:ID!
|
9
|
+
name: String!
|
10
|
+
products: [Product]
|
11
|
+
}
|
12
|
+
|
13
|
+
type Product {
|
14
|
+
id:ID!
|
15
|
+
}
|
16
|
+
|
17
|
+
type Query {
|
18
|
+
storefront(id: ID!): Storefront
|
19
|
+
}
|
20
|
+
GRAPHQL
|
21
|
+
|
22
|
+
products_sdl = <<~GRAPHQL
|
23
|
+
directive @stitch(key: String!) repeatable on FIELD_DEFINITION
|
24
|
+
|
25
|
+
type Product {
|
26
|
+
id:ID!
|
27
|
+
name: String
|
28
|
+
price: Int
|
29
|
+
}
|
30
|
+
|
31
|
+
type Query {
|
32
|
+
product(id: ID!): Product @stitch(key: "id")
|
33
|
+
}
|
34
|
+
GRAPHQL
|
35
|
+
|
36
|
+
supergraph = GraphQL::Stitching::Composer.new({
|
37
|
+
"storefronts" => GraphQL::Schema.from_definition(storefronts_sdl),
|
38
|
+
"products" => GraphQL::Schema.from_definition(products_sdl),
|
39
|
+
}).perform
|
40
|
+
|
41
|
+
combined_schema = supergraph.schema
|
42
|
+
```
|
43
|
+
|
44
|
+
The individual schemas provided to the composer are assigned a location name based on their input key. These source schemas may be built from SDL (Schema Definition Language) strings using `GraphQL::Schema.from_definition`, or may be structured Ruby classes that inherit from `GraphQL::Schema`. The source schemas are used exclusively for type reference and do NOT need any real data resolvers. Likewise, the resulting combined schema is only used for type reference and resolving introspections.
|
45
|
+
|
46
|
+
### Merge patterns
|
47
|
+
|
48
|
+
The strategy used to merge source schemas into the combined schema is based on each element type:
|
49
|
+
|
50
|
+
- `Object` and `Interface` types merge their fields together:
|
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`.
|
53
|
+
- Objects with unique fields across locations must implement [`@stitch` accessors](../README.md#merged-types).
|
54
|
+
- Shared object types without `@stitch` accessors must contain identical fields.
|
55
|
+
- Merged interfaces must remain compatible with all underlying implementations.
|
56
|
+
|
57
|
+
- `InputObject` types intersect arguments from across locations (arguments must appear in all locations):
|
58
|
+
- Arguments must share a value type, and the strictest nullability across locations is used.
|
59
|
+
- Composition fails if argument intersection would eliminate a non-null argument.
|
60
|
+
|
61
|
+
- `Enum` types merge their values based on how the enum is used:
|
62
|
+
- Enums used anywhere as an argument will intersect their values (common values across all locations).
|
63
|
+
- Enums used exclusively in read contexts will provide a union of values (all values across all locations).
|
64
|
+
|
65
|
+
- `Union` types merge all possible types from across all locations.
|
66
|
+
|
67
|
+
- `Scalar` types are added for all scalar names across all locations.
|
68
|
+
|
69
|
+
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.
|
data/docs/document.md
ADDED
@@ -0,0 +1,15 @@
|
|
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
|
+
```
|
data/docs/executor.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
## GraphQL::Stitching::Executor
|
2
|
+
|
3
|
+
An `Executor` accepts a [`Supergraph`](./supergraph.md), a [query plan hash](./planner.md), and optional request variables. It handles executing requests and merging results collected from across graph locations.
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
query = <<~GRAPHQL
|
7
|
+
query MyQuery($id: ID!) {
|
8
|
+
product(id:$id) {
|
9
|
+
title
|
10
|
+
brands { name }
|
11
|
+
}
|
12
|
+
}
|
13
|
+
GRAPHQL
|
14
|
+
|
15
|
+
variables = { "id" => "123" }
|
16
|
+
|
17
|
+
document = GraphQL::Stitching::Document.new(query, operation_name: "MyQuery")
|
18
|
+
|
19
|
+
plan = GraphQL::Stitching::Planner.new(
|
20
|
+
supergraph: supergraph,
|
21
|
+
document: document,
|
22
|
+
).perform
|
23
|
+
|
24
|
+
raw_result = GraphQL::Stitching::Executor.new(
|
25
|
+
supergraph: supergraph,
|
26
|
+
plan: plan.to_h,
|
27
|
+
variables: variables,
|
28
|
+
).perform
|
29
|
+
```
|