graphql-stitching 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +27 -0
  3. data/.gitignore +59 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +11 -0
  6. data/Gemfile.lock +49 -0
  7. data/LICENSE +21 -0
  8. data/Procfile +3 -0
  9. data/README.md +329 -0
  10. data/Rakefile +12 -0
  11. data/docs/README.md +14 -0
  12. data/docs/composer.md +69 -0
  13. data/docs/document.md +15 -0
  14. data/docs/executor.md +29 -0
  15. data/docs/gateway.md +106 -0
  16. data/docs/images/library.png +0 -0
  17. data/docs/images/merging.png +0 -0
  18. data/docs/images/stitching.png +0 -0
  19. data/docs/planner.md +43 -0
  20. data/docs/shaper.md +20 -0
  21. data/docs/supergraph.md +65 -0
  22. data/example/gateway.rb +50 -0
  23. data/example/graphiql.html +153 -0
  24. data/example/remote1.rb +26 -0
  25. data/example/remote2.rb +26 -0
  26. data/graphql-stitching.gemspec +34 -0
  27. data/lib/graphql/stitching/composer/base_validator.rb +11 -0
  28. data/lib/graphql/stitching/composer/validate_boundaries.rb +80 -0
  29. data/lib/graphql/stitching/composer/validate_interfaces.rb +24 -0
  30. data/lib/graphql/stitching/composer.rb +442 -0
  31. data/lib/graphql/stitching/document.rb +59 -0
  32. data/lib/graphql/stitching/executor.rb +254 -0
  33. data/lib/graphql/stitching/gateway.rb +120 -0
  34. data/lib/graphql/stitching/planner.rb +323 -0
  35. data/lib/graphql/stitching/planner_operation.rb +59 -0
  36. data/lib/graphql/stitching/remote_client.rb +25 -0
  37. data/lib/graphql/stitching/shaper.rb +92 -0
  38. data/lib/graphql/stitching/supergraph.rb +171 -0
  39. data/lib/graphql/stitching/util.rb +63 -0
  40. data/lib/graphql/stitching/version.rb +7 -0
  41. data/lib/graphql/stitching.rb +30 -0
  42. 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
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+ gemspec
5
+
6
+ gem 'rack'
7
+ gem 'rackup'
8
+ gem 'foreman'
9
+ gem 'pry'
10
+ gem 'pry-byebug'
11
+ gem 'warning'
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
@@ -0,0 +1,3 @@
1
+ gateway: bundle exec ruby example/gateway.rb
2
+ remote1: bundle exec ruby example/remote1.rb
3
+ remote2: bundle exec ruby example/remote2.rb
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
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new(:test) do |t, args|
6
+ puts args
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList['test/**/*_test.rb']
10
+ end
11
+
12
+ task :default => :test
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
+ ```