graphql-stitching 1.7.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,215 @@
1
+ ## Composing a Supergraph
2
+
3
+ A stitching client is constructed with many subgraph schemas, and must first _compose_ them into one unified schema that can introspect and validate supergraph requests. Composition only happens once upon initialization.
4
+
5
+ ### Location settings
6
+
7
+ When building a client, pass a `locations` hash with named definitions for each subgraph location:
8
+
9
+ ```ruby
10
+ client = GraphQL::Stitching::Client.new(locations: {
11
+ products: {
12
+ schema: GraphQL::Schema.from_definition(File.read("schemas/products.graphql")),
13
+ executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3001"),
14
+ },
15
+ users: {
16
+ schema: GraphQL::Schema.from_definition(File.read("schemas/users.graphql")),
17
+ executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3002"),
18
+ stitch: [{ field_name: "users", key: "id" }],
19
+ },
20
+ my_local: {
21
+ schema: MyLocalSchema,
22
+ },
23
+ })
24
+ ```
25
+
26
+ Location settings have top-level keys that specify arbitrary location name keywords, each of which provide:
27
+
28
+ - **`schema:`** _required_, provides a `GraphQL::Schema` class for the location. This may be a class-based schema that inherits from `GraphQL::Schema`, or built from SDL (Schema Definition Language) string using `GraphQL::Schema.from_definition` and mapped to a remote location. The provided schema is only used for type reference and does not require any real data resolvers (unless it's also used as the location's executable, see below).
29
+
30
+ - **`executable:`**, provides an executable resource to be called when delegating a request to this location, see [documentation](./executables.md). Omitting the executable option will use the location's provided `schema` as the executable resource.
31
+
32
+ - **`stitch:`**, an array of static configs used to dynamically apply [`@stitch` directives](./merged_types.md#merged-type-resolver-queries) to root fields while composing. Each config may specify `field_name`, `key`, `arguments`, and `type_name`.
33
+
34
+ ### Composer options
35
+
36
+ When building a client, you may pass `composer_options` to tune how it builds a supergraph. All settings are optional:
37
+
38
+ ```ruby
39
+ client = GraphQL::Stitching::Client.new(
40
+ composer_options: {
41
+ query_name: "Query",
42
+ mutation_name: "Mutation",
43
+ subscription_name: "Subscription",
44
+ visibility_profiles: nil, # ["public", "private", ...]
45
+ description_merger: ->(values_by_location, info) { values_by_location.values.join("\n") },
46
+ deprecation_merger: ->(values_by_location, info) { values_by_location.values.first },
47
+ default_value_merger: ->(values_by_location, info) { values_by_location.values.first },
48
+ directive_kwarg_merger: ->(values_by_location, info) { values_by_location.values.last },
49
+ root_entrypoints: {},
50
+ },
51
+ locations: {
52
+ # ...
53
+ }
54
+ )
55
+ ```
56
+
57
+ - **`query_name:`**, the name of the root query type in the composed schema; `Query` by default. The root query types from all location schemas will be merged into this type, regardless of their local names.
58
+
59
+ - **`mutation_name:`**, the name of the root mutation type in the composed schema; `Mutation` by default. The root mutation types from all location schemas will be merged into this type, regardless of their local names.
60
+
61
+ - **`subscription_name:`**, the name of the root subscription type in the composed schema; `Subscription` by default. The root subscription types from all location schemas will be merged into this type, regardless of their local names.
62
+
63
+ - **`visibility_profiles:`**, an array of [visibility profiles](./visibility.md) that the supergraph responds to.
64
+
65
+ - **`description_merger:`**, a [value merger function](#value-merger-functions) for merging element description strings from across locations.
66
+
67
+ - **`deprecation_merger:`**, a [value merger function](#value-merger-functions) for merging element deprecation strings from across locations.
68
+
69
+ - **`default_value_merger:`**, a [value merger function](#value-merger-functions) for merging argument default values from across locations.
70
+
71
+ - **`directive_kwarg_merger:`**, a [value merger function](#value-merger-functions) for merging directive keyword arguments from across locations.
72
+
73
+ - **`root_entrypoints:`**, a hash of root field names mapped to their entrypoint locations, see [overlapping root fields](#overlapping-root-fields) below.
74
+
75
+ #### Value merger functions
76
+
77
+ Static data values such as element descriptions and directive arguments must also merge across locations. By default, the first non-null value encountered for a given element attribute is used. A value merger function may customize this process by selecting a different value or computing a new one:
78
+
79
+ ```ruby
80
+ join_values_merger = ->(values_by_location, info) { values_by_location.values.compact.join("\n") }
81
+
82
+ client = GraphQL::Stitching::Client.new(
83
+ composer_options: {
84
+ description_merger: join_values_merger,
85
+ deprecation_merger: join_values_merger,
86
+ default_value_merger: join_values_merger,
87
+ directive_kwarg_merger: join_values_merger,
88
+ },
89
+ )
90
+ ```
91
+
92
+ A merger function receives `values_by_location` and `info` arguments; these provide possible values keyed by location and info about where in the schema these values were encountered:
93
+
94
+ ```ruby
95
+ values_by_location = {
96
+ "users" => "A fabulous data type.",
97
+ "products" => "An excellent data type.",
98
+ }
99
+
100
+ info = {
101
+ type_name: "Product",
102
+ # field_name: ...,
103
+ # argument_name: ...,
104
+ # directive_name: ...,
105
+ }
106
+ ```
107
+
108
+ ### Cached supergraphs
109
+
110
+ Composition is a nuanced process with a high potential for validation failures. While performing composition at runtime is fine in development mode, it becomes an unnecessary risk in production. It's much safer to compose your supergraph in development mode, cache the composition, and then rehydrate the supergraph from cache in production.
111
+
112
+ First, compose your supergraph in development mode and write it to file:
113
+
114
+ ```ruby
115
+ client = GraphQL::Stitching::Client.new(locations: {
116
+ products: {
117
+ schema: GraphQL::Schema.from_definition(File.read("schemas/products.graphql")),
118
+ executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3001"),
119
+ },
120
+ users: {
121
+ schema: GraphQL::Schema.from_definition(File.read("schemas/users.graphql")),
122
+ executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3002"),
123
+ },
124
+ my_local: {
125
+ schema: MyLocalSchema,
126
+ },
127
+ })
128
+
129
+ File.write("schemas/supergraph.graphql", client.supergraph.to_definition)
130
+ ```
131
+
132
+ Then in production, rehydrate the client using the cached supergraph and its production-appropriate executables:
133
+
134
+ ```ruby
135
+ client = GraphQL::Stitching::Client.from_definition(
136
+ File.read("schemas/supergraph.graphql"),
137
+ executables: {
138
+ products: GraphQL::Stitching::HttpExecutable.new(url: "https://products.myapp.com/graphql"),
139
+ users: GraphQL::Stitching::HttpExecutable.new(url: "http://users.myapp.com/graphql"),
140
+ my_local: MyLocalSchema,
141
+ }
142
+ )
143
+ ```
144
+
145
+ ### Overlapping root fields
146
+
147
+ Some subgraph schemas may have overlapping root fields, such as the `product` field below. You may specify a `root_entrypoints` composer option to map overlapping root fields to a preferred location:
148
+
149
+ ```ruby
150
+ infos_schema = %|
151
+ type Product {
152
+ id: ID!
153
+ title: String!
154
+ }
155
+ type Query {
156
+ product(id: ID!): Product @stitch(key: "id")
157
+ }
158
+ |
159
+
160
+ prices_schema = %|
161
+ type Product {
162
+ id: ID!
163
+ price: Float!
164
+ }
165
+ type Query {
166
+ product(id: ID!): Product @stitch(key: "id")
167
+ }
168
+ |
169
+
170
+ client = GraphQL::Stitching::Client.new(
171
+ composer_options: {
172
+ root_entrypoints: {
173
+ "Query.product" => "infos",
174
+ }
175
+ },
176
+ locations: {
177
+ infos: {
178
+ schema: GraphQL::Schema.from_definition(infos_schema),
179
+ executable: #... ,
180
+ },
181
+ prices: {
182
+ schema: GraphQL::Schema.from_definition(prices_schema),
183
+ executable: #... ,
184
+ },
185
+ }
186
+ )
187
+ ```
188
+
189
+ In the above, selecting the root `product` field will route to the "infos" schema by default. You should bias root fields to their most general-purpose location. This option _only_ applies to root fields where the query planner has no starting location bias (learn more about [query planning](./query_planning.md)). Note that [type resolver queries](./merged_types.md#type-resolver-queries) are unaffected by entrypoint bias; a type resolver will always be accessed directly for a location when needed.
190
+
191
+ ### Schema merge patterns
192
+
193
+ The strategy used to merge subgraph schemas into the combined supergraph schema is based on each element type:
194
+
195
+ - Arguments of fields, directives, and `InputObject` types intersect for each parent element across locations (an element's arguments must appear in all locations):
196
+ - Arguments must share a value type, and the strictest nullability across locations is used.
197
+ - Composition fails if argument intersection would eliminate a non-null argument.
198
+
199
+ - `Object` and `Interface` types merge their fields and directives together:
200
+ - Common fields across locations must share a value type, and the weakest nullability is used.
201
+ - Objects with unique fields across locations must implement [`@stitch` accessors](./merged_types.md).
202
+ - Shared object types without `@stitch` accessors must contain identical fields.
203
+ - Merged interfaces must remain compatible with all underlying implementations.
204
+
205
+ - `Enum` types merge their values based on how the enum is used:
206
+ - Enums used anywhere as an argument will intersect their values (common values across all locations).
207
+ - Enums used exclusively in read contexts will provide a union of values (all values across all locations).
208
+
209
+ - `Union` types merge all possible types from across all locations.
210
+
211
+ - `Scalar` types are added for all scalar names across all locations.
212
+
213
+ - `Directive` definitions are added for all distinct names across locations:
214
+ - `@visibility` directives intersect their profiles, see [documentation](./visibility.md).
215
+ - `@stitch` directives (both definitions and assignments) are omitted.
@@ -0,0 +1,69 @@
1
+ ## Error handling
2
+
3
+ Failed stitching requests can be tricky to debug because it's not always obvious where the actual error occured. Error handling helps surface issues and make them easier to locate.
4
+
5
+ ### Supergraph errors
6
+
7
+ When exceptions happen while executing requests within the stitching layer, they will be rescued by the stitching client and trigger an `on_error` hook. You should add your stack's error reporting here and return a formatted error message to appear in [GraphQL errors](https://spec.graphql.org/June2018/#sec-Errors) for the request.
8
+
9
+ ```ruby
10
+ client = GraphQL::Stitching::Client.new(locations: { ... })
11
+ client.on_error do |request, err|
12
+ # log the error
13
+ Bugsnag.notify(err)
14
+
15
+ # return a formatted message for the public response
16
+ "Whoops, please contact support abount request '#{request.context[:request_id]}'"
17
+ end
18
+
19
+ # Result:
20
+ # { "errors" => [{ "message" => "Whoops, please contact support abount request '12345'" }] }
21
+ ```
22
+
23
+ ### Subgraph errors
24
+
25
+ When subgraph resources produce errors, it's very important that each error provides a proper `path` indicating the field associated with the error. Most major GraphQL implementations, including GraphQL Ruby, [do this automatically](https://graphql-ruby.org/errors/overview.html):
26
+
27
+ ```json
28
+ {
29
+ "data": { "shop": { "product": null } },
30
+ "errors": [{
31
+ "message": "Record not found.",
32
+ "path": ["shop", "product"]
33
+ }]
34
+ }
35
+ ```
36
+
37
+ Be careful when resolving lists, particularly for merged type resolvers. Lists should only error out specific array positions rather than the entire array result whenever possible, for example:
38
+
39
+ ```ruby
40
+ def products
41
+ [
42
+ { id: "1" },
43
+ GraphQL::ExecutionError.new("Not found"),
44
+ { id: "3" },
45
+ ]
46
+ end
47
+ ```
48
+
49
+ These cases should report corresponding errors pathed down to the list index without affecting other successful results in the list:
50
+
51
+ ```json
52
+ {
53
+ "data": {
54
+ "products": [{ "id": "1" }, null, { "id": "3" }]
55
+ },
56
+ "errors": [{
57
+ "message": "Record not found.",
58
+ "path": ["products", 1]
59
+ }]
60
+ }
61
+ ```
62
+
63
+ ### Merging subgraph errors
64
+
65
+ All [spec GraphQL errors](https://spec.graphql.org/June2018/#sec-Errors) returned from subgraph queries will flow through the stitched request and into the final result. Formatting these errors follows one of two strategies:
66
+
67
+ 1. **Direct passthrough**, where subgraph errors are returned directly in the merged response without modification. This strategy is used for errors without a `path` (ie: "base" errors), and errors pathed to root fields.
68
+
69
+ 2. **Mapped passthrough**, where the `path` attribute of a subgraph error is remapped to an insertion point in the supergraph request. This strategy is used when a merged type resolver returns an error for an object in a lower-level position of the supergraph document.
@@ -0,0 +1,112 @@
1
+ ## Executables
2
+
3
+ An executable resource performs location-specific GraphQL requests. Executables may be `GraphQL::Schema` classes, or any object that responds to `.call(request, source, variables)` and returns a raw GraphQL response:
4
+
5
+ ```ruby
6
+ class MyExecutable
7
+ def call(request, source, variables)
8
+ # process a GraphQL request...
9
+ return {
10
+ "data" => { ... },
11
+ "errors" => [ ... ],
12
+ }
13
+ end
14
+ end
15
+ ```
16
+
17
+ A supergraph is composed with executable resources provided for each location. Any location that omits the `executable` option will use the provided `schema` as its default executable:
18
+
19
+ ```ruby
20
+ client = GraphQL::Stitching::Client.new(locations: {
21
+ first: {
22
+ schema: FirstSchema,
23
+ # executable:^^^^^^ delegates to FirstSchema,
24
+ },
25
+ second: {
26
+ schema: SecondSchema,
27
+ executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3001", headers: { ... }),
28
+ },
29
+ third: {
30
+ schema: ThirdSchema,
31
+ executable: MyExecutable.new,
32
+ },
33
+ fourth: {
34
+ schema: FourthSchema,
35
+ executable: ->(req, query, vars) { ... },
36
+ },
37
+ })
38
+ ```
39
+
40
+ ## HttpExecutable
41
+
42
+ The `GraphQL::Stitching` library provides one default executable: `HttpExecutable`. This is an out-of-the-box convenience for sending HTTP post requests to a remote location, or a base class for your own implementation with [file uploads](#file-uploads).
43
+
44
+ ```ruby
45
+ executable = GraphQL::Stitching::HttpExecutable.new(
46
+ url: "http://localhost:3001",
47
+ headers: { "Authorization" => "..." },
48
+ upload_types: #...
49
+ )
50
+ ```
51
+
52
+ - **`url:`**, the URL of an endpoint to post GraphQL requests to.
53
+ - **`headers:`**, a hash of headers to encode into post requests.
54
+ - **`upload_types:`**, an array of scalar names to process as [file uploads](#file-uploads), see below.
55
+
56
+ Extend this class to reimplement HTTP transmit behaviors using your own libraries. Specifically, override the following methods:
57
+
58
+ - **`send(request, document, variables)`**, transmits a basic HTTP request.
59
+ - **`send_multipart_form(request, form_data)`**, transmits multipart form data.
60
+
61
+ ### The `Stitching::Request` object
62
+
63
+ HttpExecutable methods recieve the supergraph's `request` object, which contains all information about the supergraph request being processed. This includes useful caching information:
64
+
65
+ - `req.variables`: a hash of user-submitted variables.
66
+ - `req.original_document`: the original validated request document, before skip/include.
67
+ - `req.string`: the prepared GraphQL source string being executed, after skip/include.
68
+ - `req.digest`: digest of the prepared string, hashed by the [`Stitching.digest`](./performance.md#digests) implementation.
69
+ - `req.normalized_string`: printed source string with consistent whitespace.
70
+ - `req.normalized_digest`: a digest of the normalized string, hashed by the [`Stitching.digest`](./performance.md#digests) implementation.
71
+ - `req.operation`: the operation definition selected for the request.
72
+ - `req.variable_definitions`: a mapping of variable names to their type definitions.
73
+ - `req.fragment_definitions`: a mapping of fragment names to their fragment definitions.
74
+
75
+ ## File uploads
76
+
77
+ The [GraphQL upload spec](https://github.com/jaydenseric/graphql-multipart-request-spec) defines a multipart form structure for submitting GraphQL requests with file upload attachments. These can proxy through a supergraph with the following steps:
78
+
79
+ ### 1. Input file uploads as Tempfile variables
80
+
81
+ ```ruby
82
+ client.execute(
83
+ "mutation($file: Upload) { upload(file: $file) }",
84
+ variables: { "file" => Tempfile.new(...) }
85
+ )
86
+ ```
87
+
88
+ File uploads must enter the supergraph as standard GraphQL variables with `Tempfile` values cast as a dedicated upload scalar type. The simplest way to recieve this input is to install [apollo_upload_server](https://github.com/jetruby/apollo_upload_server-ruby) into your app's middleware so that multipart form submissions automatically unpack into tempfile variables.
89
+
90
+ ### 2. Enable `HttpExecutable.upload_types`
91
+
92
+ ```ruby
93
+ client = GraphQL::Stitching::Client.new(locations: {
94
+ alpha: {
95
+ schema: GraphQL::Schema.from_definition(...),
96
+ executable: GraphQL::Stitching::HttpExecutable.new(
97
+ url: "http://localhost:3001",
98
+ upload_types: ["Upload"], # << extract `Upload` scalars into multipart forms
99
+ ),
100
+ },
101
+ bravo: {
102
+ schema: GraphQL::Schema.from_definition(...),
103
+ executable: GraphQL::Stitching::HttpExecutable.new(
104
+ url: "http://localhost:3002",
105
+ ),
106
+ },
107
+ })
108
+ ```
109
+
110
+ A location's `HttpExecutable` can then re-package `Tempfile` variables into multipart forms before sending them upstream. This is enabled with an `upload_types` parameter that specifies what scalar names require form extraction. Enabling `upload_types` adds some additional subgraph request processing, so it should only be enabled for locations that will actually recieve file uploads.
111
+
112
+ The upstream location will recieve a multipart form submission from the supergraph that can again be unpacked using [apollo_upload_server](https://github.com/jetruby/apollo_upload_server-ruby) or similar.
@@ -0,0 +1,17 @@
1
+ ## What is Stitching?
2
+
3
+ You've probably done this: fetch some objects from an endpoint; then map those into a collection of keys that gets sent to another endpoint for more data; then hook all the results together in your client. This is stitching. GraphQL stitching simply automates this process so that one large _supergraph_ schema can transparently query from many _subgraph_ locations.
4
+
5
+ ## Stitching vs. Federation
6
+
7
+ Stitching encompasses the raw mechanics of combining GraphQL schemas with cross-referenced objects, and is the underpinning mechanics for major federation frameworks such as [Apollo Federation](https://www.apollographql.com/federation) and [Wundergraph](https://wundergraph.com/). Stitching is generic library behavior that plugs into your server, while federation ecosystems _give you_ a server, a schema deployment pipeline, a control plane, and opinionated management workflows.
8
+
9
+ If you're scaling a large distributed architecture with heavy throughput demands, then you'll probably benefit from growing atop a major federation framework. However, if you just have an existing Ruby app and want to hook a few external schemas into its GraphQL API, then incorporating an entire federation framework is probably overkill. Stitching offers good middleground.
10
+
11
+ ## The `GraphQL::Stitching` library
12
+
13
+ This library contains the component parts for assembling a stitched schema, and rolls the entire workflow up into a `GraphQL::Stitching::Client` class.
14
+
15
+ ![Library flow](./images/library.png)
16
+
17
+ For the most part, this entire library can be driven using just a `Client`. First [compose a supergraph](./composing_a_supergraph.md) with [executables](./executables.md) for each location, configure its [merged types](./merged_types.md), and then [serve the client](./serving_a_supergraph.md) in your app's GraphQL controller.