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.
- checksums.yaml +4 -4
- data/README.md +58 -424
- data/docs/composing_a_supergraph.md +215 -0
- data/docs/error_handling.md +69 -0
- data/docs/executables.md +112 -0
- data/docs/introduction.md +17 -0
- data/docs/merged_types.md +457 -0
- data/docs/{federation_entities.md → merged_types_apollo.md} +1 -1
- data/docs/performance.md +71 -0
- data/docs/query_planning.md +102 -0
- data/docs/serving_a_supergraph.md +152 -0
- data/docs/subscriptions.md +1 -1
- data/docs/visibility.md +21 -11
- data/lib/graphql/stitching/client.rb +6 -0
- data/lib/graphql/stitching/composer.rb +18 -10
- data/lib/graphql/stitching/request.rb +4 -0
- data/lib/graphql/stitching/supergraph.rb +4 -2
- data/lib/graphql/stitching/version.rb +1 -1
- metadata +11 -11
- data/docs/README.md +0 -19
- data/docs/client.md +0 -107
- data/docs/composer.md +0 -125
- data/docs/http_executable.md +0 -51
- data/docs/mechanics.md +0 -306
- data/docs/request.md +0 -34
- data/docs/supergraph.md +0 -31
- data/docs/type_resolver.md +0 -101
@@ -0,0 +1,152 @@
|
|
1
|
+
## Serving a supergraph
|
2
|
+
|
3
|
+
Serving a stitched schema should be optimized by environment. In `production` we favor speed and stability over flexibility, while in `development` we favor the reverse. Among the simplest ways to deploy a stitched schema is to compose it locally, write the composed schema as a `.graphql` file in your repo, and then load the pre-composed schema into a stitching client at runtime. This assures that composition always happens before deployment where failures can be detected.
|
4
|
+
|
5
|
+
### Exporting a production schema
|
6
|
+
|
7
|
+
1. Make a helper class for building your supergraph and exporting it as an SDL string:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
class SupergraphHelper
|
11
|
+
def self.export
|
12
|
+
client = GraphQL::Stitching::Client.new({
|
13
|
+
remote: {
|
14
|
+
schema: GraphQL::Schema.from_definition(File.read("db/schema/remote.graphql"))
|
15
|
+
},
|
16
|
+
local: {
|
17
|
+
schema: MyLocalSchema
|
18
|
+
}
|
19
|
+
})
|
20
|
+
|
21
|
+
client.supergraph.to_definition
|
22
|
+
end
|
23
|
+
end
|
24
|
+
```
|
25
|
+
|
26
|
+
2. Setup a `rake` task for writing the export to a repo file:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
task :compose_supergraph do
|
30
|
+
File.write("db/schema/supergraph.graphql", SupergraphHelper.export)
|
31
|
+
puts "Schema composition was successful."
|
32
|
+
end
|
33
|
+
|
34
|
+
# bundle exec rake compose-supergraph
|
35
|
+
```
|
36
|
+
|
37
|
+
3. Also as part of the export Rake task, it's advisable to run a [schema comparator](https://github.com/xuorig/graphql-schema_comparator) across the `main` version and the current compilation to catch breaking change regressions that may arise [during composition](./composing_a_supergraph.md#schema-merge-patterns):
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
task :compose_supergraph do
|
41
|
+
# ...
|
42
|
+
|
43
|
+
supergraph_file = "db/schema/supergraph.graphql"
|
44
|
+
head_commit = %x(git merge-base HEAD origin/main).strip!
|
45
|
+
head_source = %x(git show #{head_commit}:#{supergraph_file})
|
46
|
+
|
47
|
+
old_schema = GraphQL::Schema.from_definition(head_source)
|
48
|
+
new_schema = GraphQL::Schema.from_definition(File.read(supergraph_file))
|
49
|
+
diff = GraphQL::SchemaComparator.compare(old_schema, new_schema)
|
50
|
+
raise "Breaking changes found:\n-#{diff.breaking_changes.join("\n-")}" if diff.breaking?
|
51
|
+
|
52
|
+
# ...
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
4. As a CI safeguard, be sure to write a test that compares the supergraph export against the current repo file. This assures the latest schema is always expored before deploying:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
test "supergraph export is up to date." do
|
60
|
+
assert_equal SupergraphHelper.export, File.read("db/schema/supergraph.graphql")
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
### Supergraph controller
|
65
|
+
|
66
|
+
Then at runtime, execute requests using a client built for the environment. The `production` client should load the pre-composed export schema, while the `development` client can live reload using runtime composition. Be sure to memoize any static schemas that the development client uses to minimize reloading overhead:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
class SupergraphController < ApplicationController
|
70
|
+
protect_from_forgery with: :null_session, prepend: true
|
71
|
+
|
72
|
+
def execute
|
73
|
+
# see visibility docs...
|
74
|
+
visibility_profile = select_visibility_profile_for_audience(current_user)
|
75
|
+
|
76
|
+
client.execute(
|
77
|
+
query: params[:query],
|
78
|
+
variables: params[:variables],
|
79
|
+
operation_name: params[:operation_name],
|
80
|
+
context: { visibility_profile: visibility_profile },
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
# select which client to use based on the environment...
|
87
|
+
def client
|
88
|
+
Rails.env.production? ? production_client : development_client
|
89
|
+
end
|
90
|
+
|
91
|
+
# production uses a pre-composed supergraph read from the repo...
|
92
|
+
def production_client
|
93
|
+
@production_client ||= begin
|
94
|
+
supergraph_sdl = File.read("db/schema/supergraph.graphql")
|
95
|
+
|
96
|
+
GraphQL::Stitching::Client.from_definition(supergraph_sdl, executables: {
|
97
|
+
remote: GraphQL::Stitching::HttpExecutable.new("https://api.remote.com/graphql"),
|
98
|
+
local: MyLocalSchema,
|
99
|
+
}).tap do |client|
|
100
|
+
# see performance and error handling docs...
|
101
|
+
client.on_cache_read { ... }
|
102
|
+
client.on_cache_write { ... }
|
103
|
+
client.on_error { ... }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# development uses a supergraph composed on the fly...
|
109
|
+
def development_client
|
110
|
+
GraphQL::Stitching::Client.new(locations: {
|
111
|
+
remote: {
|
112
|
+
schema: remote_schema,
|
113
|
+
executable: GraphQL::Stitching::HttpExecutable.new("https://localhost:3001/graphql"),
|
114
|
+
},
|
115
|
+
local: {
|
116
|
+
schema: MyLocalSchema,
|
117
|
+
},
|
118
|
+
})
|
119
|
+
end
|
120
|
+
|
121
|
+
# other flat schemas used in development should be
|
122
|
+
# cached in memory to avoid as much runtime overhead as possible
|
123
|
+
def remote_schema
|
124
|
+
@remote_schema ||= GraphQL::Schema.from_definition(File.read("db/schema/remote.graphql"))
|
125
|
+
end
|
126
|
+
end
|
127
|
+
```
|
128
|
+
|
129
|
+
### Client execution
|
130
|
+
|
131
|
+
The `Client.execute` method provides a mostly drop-in replacement for [`GraphQL::Schema.execute`](https://graphql-ruby.org/queries/executing_queries):
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
client.execute(
|
135
|
+
query: params[:query],
|
136
|
+
variables: params[:variables],
|
137
|
+
operation_name: params[:operation_name],
|
138
|
+
context: { visibility_profile: visibility_profile },
|
139
|
+
)
|
140
|
+
```
|
141
|
+
|
142
|
+
It provides a subset of the standard `execute` arguments:
|
143
|
+
|
144
|
+
* `query`: a query (or mutation) as a string or parsed AST.
|
145
|
+
* `variables`: a hash of variables for the request.
|
146
|
+
* `operation_name`: the name of the operation to execute (when multiple are provided).
|
147
|
+
* `validate`: true if static validation should run on the supergraph schema before execution.
|
148
|
+
* `context`: an object passed through to executable calls and client hooks.
|
149
|
+
|
150
|
+
### Production reloading
|
151
|
+
|
152
|
+
It is possible to "hot" reload a production supergraph (ie: update the graph without a server deployment) using a background process to poll a remote supergraph file for changes and then build it into a new client for the controller at runtime. This works fine as long as locations and their executables don't change. If locations will change, the runtime _must_ be prepared to dynamically generate appropraite location executables.
|
data/docs/subscriptions.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
##
|
1
|
+
## Subscriptions
|
2
2
|
|
3
3
|
Stitching is an interesting prospect for subscriptions because socket-based interactions can be isolated to their own schema/server with very little implementation beyond resolving entity keys. Then, entity data can be stitched onto subscription payloads from other locations.
|
4
4
|
|
data/docs/visibility.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# Visibility
|
2
2
|
|
3
|
-
Visibility controls can hide parts of a supergraph from select audiences without compromising stitching operations. Restricted schema elements are hidden from introspection and validate as though they do not exist (which is different from traditional authorization where an element is acknowledged as restricted). Visibility is useful for managing multiple distributions of a schema for different audiences.
|
3
|
+
Visibility controls can hide parts of a supergraph from select audiences without compromising stitching operations. Restricted schema elements are hidden from introspection and validate as though they do not exist (which is different from traditional authorization where an element is acknowledged as restricted). Visibility is useful for managing multiple distributions of a schema for different audiences, and provides a flexible analog to Apollo Federation's `@inaccessible` rule.
|
4
4
|
|
5
|
-
Under the hood, this system wraps `GraphQL::Schema::Visibility`
|
5
|
+
Under the hood, this system wraps [GraphQL visibility](https://graphql-ruby.org/authorization/visibility) (specifically, the newer `GraphQL::Schema::Visibility` with nil profile support) and requires at least GraphQL Ruby v2.5.3.
|
6
6
|
|
7
7
|
## Example
|
8
8
|
|
@@ -41,7 +41,7 @@ type Query {
|
|
41
41
|
}
|
42
42
|
```
|
43
43
|
|
44
|
-
When composing a stitching client, the names of all possible visibility profiles that the supergraph
|
44
|
+
When composing a stitching client, the names of all possible visibility profiles that the supergraph should respond to are specified in composer options:
|
45
45
|
|
46
46
|
```ruby
|
47
47
|
client = GraphQL::Stitching::Client.new(
|
@@ -61,20 +61,20 @@ client = GraphQL::Stitching::Client.new(
|
|
61
61
|
)
|
62
62
|
```
|
63
63
|
|
64
|
-
The client can then execute requests with a `visibility_profile` parameter in context that specifies
|
64
|
+
The client can then execute requests with a `visibility_profile` parameter in context that specifies one of these names:
|
65
65
|
|
66
66
|
```ruby
|
67
67
|
query = %|{
|
68
68
|
featuredProduct {
|
69
69
|
title # always visible
|
70
70
|
price # always visible
|
71
|
-
msrp # only visible to
|
72
|
-
id # only visible
|
71
|
+
msrp # only visible to "private" or without profile
|
72
|
+
id # only visible without profile
|
73
73
|
}
|
74
74
|
}|
|
75
75
|
|
76
76
|
result = client.execute(query, context: {
|
77
|
-
visibility_profile: "public", # << or private
|
77
|
+
visibility_profile: "public", # << or "private"
|
78
78
|
})
|
79
79
|
```
|
80
80
|
|
@@ -82,9 +82,9 @@ The `visibility_profile` parameter will select which visibility distribution to
|
|
82
82
|
|
83
83
|
- Using `visibility_profile: "public"` will say the `msrp` field does not exist (because it is restricted to "private").
|
84
84
|
- Using `visibility_profile: "private"` will accesses the `msrp` field as usual.
|
85
|
-
-
|
85
|
+
- Providing no profile parameter (or `visibility_profile: nil`) will access the entire graph without any visibility constraints.
|
86
86
|
|
87
|
-
The full potential of visibility comes when hiding stitching implementation details, such as the `id` field (which is the stitching key for the Product type). While the `id` field is hidden from all named profiles, it remains operational for the stitching implementation.
|
87
|
+
The full potential of visibility comes when hiding stitching implementation details, such as the `id` field (which is the stitching key for the Product type). While the `id` field is hidden from all named profiles, it remains operational for use by the stitching implementation.
|
88
88
|
|
89
89
|
## Adding visibility directives
|
90
90
|
|
@@ -105,7 +105,7 @@ end
|
|
105
105
|
|
106
106
|
## Merging visibilities
|
107
107
|
|
108
|
-
Visibility directives merge across schemas into the narrowest constraint possible.
|
108
|
+
Visibility directives merge across schemas into the narrowest constraint possible. Profiles for an element will intersect into its merged supergraph constraint:
|
109
109
|
|
110
110
|
```graphql
|
111
111
|
# location 1
|
@@ -165,4 +165,14 @@ type Query {
|
|
165
165
|
}
|
166
166
|
```
|
167
167
|
|
168
|
-
In this example, hiding the `Widget` type will also hide the `Query.widget` field that returns it.
|
168
|
+
In this example, hiding the `Widget` type will also hide the `Query.widget` field that returns it. You can review materialized visibility profiles by printing their respective schemas:
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
public_schema = client.supergraph.to_definition(visibility_profile: "public")
|
172
|
+
File.write("schemas/supergraph_public.graphql", public_schema)
|
173
|
+
|
174
|
+
private_schema = client.supergraph.to_definition(visibility_profile: "private")
|
175
|
+
File.write("schemas/supergraph_private.graphql", private_schema)
|
176
|
+
```
|
177
|
+
|
178
|
+
It's helpful to commit these outputs to your repo where you can monitor their diffs during the PR process.
|
@@ -7,6 +7,12 @@ module GraphQL
|
|
7
7
|
# Client is an out-of-the-box helper that assembles all
|
8
8
|
# stitching components into a workflow that executes requests.
|
9
9
|
class Client
|
10
|
+
class << self
|
11
|
+
def from_definition(schema, executables:)
|
12
|
+
new(supergraph: Supergraph.from_definition(schema, executables: executables))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
10
16
|
# @return [Supergraph] composed supergraph that services incoming requests.
|
11
17
|
attr_reader :supergraph
|
12
18
|
|
@@ -26,9 +26,6 @@ module GraphQL
|
|
26
26
|
# @api private
|
27
27
|
VISIBILITY_PROFILES_MERGER = ->(values_by_location, _info) { values_by_location.values.reduce(:&) }
|
28
28
|
|
29
|
-
# @api private
|
30
|
-
BASIC_ROOT_FIELD_LOCATION_SELECTOR = ->(locations, _info) { locations.last }
|
31
|
-
|
32
29
|
# @api private
|
33
30
|
COMPOSITION_VALIDATORS = [
|
34
31
|
ValidateInterfaces,
|
@@ -59,7 +56,8 @@ module GraphQL
|
|
59
56
|
deprecation_merger: nil,
|
60
57
|
default_value_merger: nil,
|
61
58
|
directive_kwarg_merger: nil,
|
62
|
-
root_field_location_selector: nil
|
59
|
+
root_field_location_selector: nil,
|
60
|
+
root_entrypoints: nil
|
63
61
|
)
|
64
62
|
@query_name = query_name
|
65
63
|
@mutation_name = mutation_name
|
@@ -68,7 +66,8 @@ module GraphQL
|
|
68
66
|
@deprecation_merger = deprecation_merger || BASIC_VALUE_MERGER
|
69
67
|
@default_value_merger = default_value_merger || BASIC_VALUE_MERGER
|
70
68
|
@directive_kwarg_merger = directive_kwarg_merger || BASIC_VALUE_MERGER
|
71
|
-
@root_field_location_selector = root_field_location_selector
|
69
|
+
@root_field_location_selector = root_field_location_selector
|
70
|
+
@root_entrypoints = root_entrypoints || {}
|
72
71
|
|
73
72
|
@field_map = {}
|
74
73
|
@resolver_map = {}
|
@@ -631,11 +630,20 @@ module GraphQL
|
|
631
630
|
root_field_locations = @field_map[root_type.graphql_name][root_field_name]
|
632
631
|
next unless root_field_locations.length > 1
|
633
632
|
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
633
|
+
root_field_path = "#{root_type.graphql_name}.#{root_field_name}"
|
634
|
+
target_location = if @root_field_location_selector && @root_entrypoints.empty?
|
635
|
+
Warning.warn("Composer option `root_field_location_selector` is deprecated and will be removed.")
|
636
|
+
@root_field_location_selector.call(root_field_locations, {
|
637
|
+
type_name: root_type.graphql_name,
|
638
|
+
field_name: root_field_name,
|
639
|
+
})
|
640
|
+
else
|
641
|
+
@root_entrypoints[root_field_path] || root_field_locations.last
|
642
|
+
end
|
643
|
+
|
644
|
+
unless root_field_locations.include?(target_location)
|
645
|
+
raise CompositionError, "Invalid `root_entrypoints` configuration: `#{root_field_path}` has no `#{target_location}` location."
|
646
|
+
end
|
639
647
|
|
640
648
|
root_field_locations.reject! { _1 == target_location }
|
641
649
|
root_field_locations.unshift(target_location)
|
@@ -57,6 +57,10 @@ module GraphQL
|
|
57
57
|
@context[:request] = self
|
58
58
|
end
|
59
59
|
|
60
|
+
def original_document
|
61
|
+
@query.document
|
62
|
+
end
|
63
|
+
|
60
64
|
# @return [String] the original document string, or a print of the parsed AST document.
|
61
65
|
def string
|
62
66
|
with_prepared_document { @string || normalized_string }
|
@@ -58,8 +58,10 @@ module GraphQL
|
|
58
58
|
end
|
59
59
|
end
|
60
60
|
|
61
|
-
def to_definition
|
62
|
-
@schema.to_definition
|
61
|
+
def to_definition(visibility_profile: nil)
|
62
|
+
@schema.to_definition(context: {
|
63
|
+
visibility_profile: visibility_profile,
|
64
|
+
}.tap(&:compact!))
|
63
65
|
end
|
64
66
|
|
65
67
|
def resolvers_by_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: 1.7.
|
4
|
+
version: 1.7.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: 2025-05-
|
11
|
+
date: 2025-05-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|
@@ -79,19 +79,19 @@ files:
|
|
79
79
|
- LICENSE
|
80
80
|
- README.md
|
81
81
|
- Rakefile
|
82
|
-
- docs/
|
83
|
-
- docs/
|
84
|
-
- docs/
|
85
|
-
- docs/federation_entities.md
|
86
|
-
- docs/http_executable.md
|
82
|
+
- docs/composing_a_supergraph.md
|
83
|
+
- docs/error_handling.md
|
84
|
+
- docs/executables.md
|
87
85
|
- docs/images/library.png
|
88
86
|
- docs/images/merging.png
|
89
87
|
- docs/images/stitching.png
|
90
|
-
- docs/
|
91
|
-
- docs/
|
88
|
+
- docs/introduction.md
|
89
|
+
- docs/merged_types.md
|
90
|
+
- docs/merged_types_apollo.md
|
91
|
+
- docs/performance.md
|
92
|
+
- docs/query_planning.md
|
93
|
+
- docs/serving_a_supergraph.md
|
92
94
|
- docs/subscriptions.md
|
93
|
-
- docs/supergraph.md
|
94
|
-
- docs/type_resolver.md
|
95
95
|
- docs/visibility.md
|
96
96
|
- examples/file_uploads/Gemfile
|
97
97
|
- examples/file_uploads/Procfile
|
data/docs/README.md
DELETED
@@ -1,19 +0,0 @@
|
|
1
|
-
## GraphQL::Stitching
|
2
|
-
|
3
|
-
This module provides a collection of components that may be composed into a stitched schema.
|
4
|
-
|
5
|
-

|
6
|
-
|
7
|
-
Major components include:
|
8
|
-
|
9
|
-
- [Client](./client.md) - an out-of-the-box setup for performing stitched requests.
|
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
|
-
- [Request](./request.md) - prepares a requested GraphQL document and variables for stitching.
|
13
|
-
- [HttpExecutable](./http_executable.md) - proxies requests to remotes with multipart file upload support.
|
14
|
-
|
15
|
-
Additional topics:
|
16
|
-
|
17
|
-
- [Stitching mechanics](./mechanics.md) - more about building for stitching and how it operates.
|
18
|
-
- [Subscriptions](./subscriptions.md) - explore how to stitch realtime event subscriptions.
|
19
|
-
- [Federation entities](./federation_entities.md) - more about Apollo Federation compatibility.
|
data/docs/client.md
DELETED
@@ -1,107 +0,0 @@
|
|
1
|
-
## GraphQL::Stitching::Client
|
2
|
-
|
3
|
-
The `Client` is an out-of-the-box convenience with all stitching components assembled into a default workflow. A client is designed to work for most common needs, though you're welcome to assemble the component parts into your own configuration (see the [client source](../lib/graphql/stitching/client.rb) for an example). A client is constructed with the same [location settings](./composer.md#performing-composition) used to perform supergraph composition:
|
4
|
-
|
5
|
-
```ruby
|
6
|
-
movies_schema = "type Query { ..."
|
7
|
-
showtimes_schema = "type Query { ..."
|
8
|
-
|
9
|
-
client = GraphQL::Stitching::Client.new(locations: {
|
10
|
-
products: {
|
11
|
-
schema: GraphQL::Schema.from_definition(movies_schema),
|
12
|
-
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3000"),
|
13
|
-
stitch: [{ field_name: "products", key: "id" }],
|
14
|
-
},
|
15
|
-
showtimes: {
|
16
|
-
schema: GraphQL::Schema.from_definition(showtimes_schema),
|
17
|
-
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3001"),
|
18
|
-
},
|
19
|
-
my_local: {
|
20
|
-
schema: MyLocal::GraphQL::Schema,
|
21
|
-
},
|
22
|
-
})
|
23
|
-
```
|
24
|
-
|
25
|
-
Alternatively, you may pass a prebuilt `Supergraph` instance to the `Client` constructor. This is useful when [exporting and rehydrating](./supergraph.md#export-and-caching) supergraph instances, which bypasses the need for runtime composition:
|
26
|
-
|
27
|
-
```ruby
|
28
|
-
supergraph_sdl = File.read("precomposed_schema.graphql")
|
29
|
-
supergraph = GraphQL::Stitching::Supergraph.from_definition(
|
30
|
-
supergraph_sdl,
|
31
|
-
executables: { ... },
|
32
|
-
)
|
33
|
-
|
34
|
-
client = GraphQL::Stitching::Client.new(supergraph: supergraph)
|
35
|
-
```
|
36
|
-
|
37
|
-
### Execution
|
38
|
-
|
39
|
-
A client provides an `execute` method with a subset of arguments provided by [`GraphQL::Schema.execute`](https://graphql-ruby.org/queries/executing_queries). Executing requests on a stitching client becomes mostly a drop-in replacement to executing on a `GraphQL::Schema` instance:
|
40
|
-
|
41
|
-
```ruby
|
42
|
-
result = client.execute(
|
43
|
-
query: "query MyProduct($id: ID!) { product(id: $id) { name } }",
|
44
|
-
variables: { "id" => "1" },
|
45
|
-
operation_name: "MyProduct",
|
46
|
-
)
|
47
|
-
```
|
48
|
-
|
49
|
-
Arguments for the `execute` method include:
|
50
|
-
|
51
|
-
* `query`: a query (or mutation) as a string or parsed AST.
|
52
|
-
* `variables`: a hash of variables for the request.
|
53
|
-
* `operation_name`: the name of the operation to execute (when multiple are provided).
|
54
|
-
* `validate`: true if static validation should run on the supergraph schema before execution.
|
55
|
-
* `context`: an object passed through to executable calls and client hooks.
|
56
|
-
|
57
|
-
### Cache hooks
|
58
|
-
|
59
|
-
The client provides cache hooks to enable caching query plans across requests. Without caching, every request made to the client will be planned individually. With caching, a query may be planned once, cached, and then executed from cache for subsequent requests. Cache keys are a normalized digest of each query string.
|
60
|
-
|
61
|
-
```ruby
|
62
|
-
client.on_cache_read do |request|
|
63
|
-
$cache.get(request.digest) # << 3P code
|
64
|
-
end
|
65
|
-
|
66
|
-
client.on_cache_write do |request, payload|
|
67
|
-
$cache.set(request.digest, payload) # << 3P code
|
68
|
-
end
|
69
|
-
```
|
70
|
-
|
71
|
-
All request digests use SHA2 by default. You can swap in [a faster algorithm](https://github.com/Shopify/blake3-rb) and/or add base scoping by reconfiguring the stitching library:
|
72
|
-
|
73
|
-
```ruby
|
74
|
-
GraphQL::Stitching.digest { |str| Digest::MD5.hexdigest("v2/#{str}") }
|
75
|
-
```
|
76
|
-
|
77
|
-
Note that inlined input data works against caching, so you should _avoid_ these input literals when possible:
|
78
|
-
|
79
|
-
```graphql
|
80
|
-
query {
|
81
|
-
product(id: "1") { name }
|
82
|
-
}
|
83
|
-
```
|
84
|
-
|
85
|
-
Instead, leverage query variables so that the document body remains consistent across requests:
|
86
|
-
|
87
|
-
```graphql
|
88
|
-
query($id: ID!) {
|
89
|
-
product(id: $id) { name }
|
90
|
-
}
|
91
|
-
|
92
|
-
# variables: { "id" => "1" }
|
93
|
-
```
|
94
|
-
|
95
|
-
### Error hooks
|
96
|
-
|
97
|
-
The client also provides an error hook. Any program errors rescued during execution will be passed to the `on_error` handler, which can report on the error as needed and return a formatted error message for the client to add to the [GraphQL errors](https://spec.graphql.org/June2018/#sec-Errors) result.
|
98
|
-
|
99
|
-
```ruby
|
100
|
-
client.on_error do |request, err|
|
101
|
-
# log the error
|
102
|
-
Bugsnag.notify(err)
|
103
|
-
|
104
|
-
# return a formatted message for the public response
|
105
|
-
"Whoops, please contact support abount request '#{request.context[:request_id]}'"
|
106
|
-
end
|
107
|
-
```
|
data/docs/composer.md
DELETED
@@ -1,125 +0,0 @@
|
|
1
|
-
## GraphQL::Stitching::Composer
|
2
|
-
|
3
|
-
A `Composer` receives many individual `GraphQL::Schema` instances representing various graph locations and _composes_ them into one combined [`Supergraph`](./supergraph.md) that is validated for integrity.
|
4
|
-
|
5
|
-
### Configuring composition
|
6
|
-
|
7
|
-
A `Composer` may be constructed with optional settings that tune how it builds a schema:
|
8
|
-
|
9
|
-
```ruby
|
10
|
-
composer = GraphQL::Stitching::Composer.new(
|
11
|
-
query_name: "Query",
|
12
|
-
mutation_name: "Mutation",
|
13
|
-
subscription_name: "Subscription",
|
14
|
-
description_merger: ->(values_by_location, info) { values_by_location.values.join("\n") },
|
15
|
-
deprecation_merger: ->(values_by_location, info) { values_by_location.values.first },
|
16
|
-
default_value_merger: ->(values_by_location, info) { values_by_location.values.first },
|
17
|
-
directive_kwarg_merger: ->(values_by_location, info) { values_by_location.values.last },
|
18
|
-
root_field_location_selector: ->(locations, info) { locations.last },
|
19
|
-
)
|
20
|
-
```
|
21
|
-
|
22
|
-
Constructor arguments:
|
23
|
-
|
24
|
-
- **`query_name:`** _optional_, 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.
|
25
|
-
|
26
|
-
- **`mutation_name:`** _optional_, 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.
|
27
|
-
|
28
|
-
- **`subscription_name:`** _optional_, 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.
|
29
|
-
|
30
|
-
- **`description_merger:`** _optional_, a [value merger function](#value-merger-functions) for merging element description strings from across locations.
|
31
|
-
|
32
|
-
- **`deprecation_merger:`** _optional_, a [value merger function](#value-merger-functions) for merging element deprecation strings from across locations.
|
33
|
-
|
34
|
-
- **`default_value_merger:`** _optional_, a [value merger function](#value-merger-functions) for merging argument default values from across locations.
|
35
|
-
|
36
|
-
- **`directive_kwarg_merger:`** _optional_, a [value merger function](#value-merger-functions) for merging directive keyword arguments from across locations.
|
37
|
-
|
38
|
-
- **`root_field_location_selector:`** _optional_, selects a default routing location for root fields with multiple locations. Use this to prioritize sending root fields to their primary data sources (only applies while routing the root operation scope). This handler receives an array of possible locations and an info object with field information, and should return the prioritized location. The last location is used by default.
|
39
|
-
|
40
|
-
#### Value merger functions
|
41
|
-
|
42
|
-
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:
|
43
|
-
|
44
|
-
```ruby
|
45
|
-
composer = GraphQL::Stitching::Composer.new(
|
46
|
-
description_merger: ->(values_by_location, info) { values_by_location.values.compact.join("\n") },
|
47
|
-
)
|
48
|
-
```
|
49
|
-
|
50
|
-
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:
|
51
|
-
|
52
|
-
```ruby
|
53
|
-
values_by_location = {
|
54
|
-
"storefronts" => "A fabulous data type.",
|
55
|
-
"products" => "An excellent data type.",
|
56
|
-
}
|
57
|
-
|
58
|
-
info = {
|
59
|
-
type_name: "Product",
|
60
|
-
# field_name: ...,
|
61
|
-
# argument_name: ...,
|
62
|
-
# directive_name: ...,
|
63
|
-
}
|
64
|
-
```
|
65
|
-
|
66
|
-
### Performing composition
|
67
|
-
|
68
|
-
Construct a `Composer` and call its `perform` method with location settings to compose a supergraph:
|
69
|
-
|
70
|
-
```ruby
|
71
|
-
storefronts_sdl = "type Query { ..."
|
72
|
-
products_sdl = "type Query { ..."
|
73
|
-
|
74
|
-
supergraph = GraphQL::Stitching::Composer.new.perform({
|
75
|
-
storefronts: {
|
76
|
-
schema: GraphQL::Schema.from_definition(storefronts_sdl),
|
77
|
-
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3001"),
|
78
|
-
stitch: [{ field_name: "storefront", key: "id" }],
|
79
|
-
},
|
80
|
-
products: {
|
81
|
-
schema: GraphQL::Schema.from_definition(products_sdl),
|
82
|
-
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3002"),
|
83
|
-
},
|
84
|
-
my_local: {
|
85
|
-
schema: MyLocalSchema,
|
86
|
-
},
|
87
|
-
})
|
88
|
-
|
89
|
-
combined_schema = supergraph.schema
|
90
|
-
```
|
91
|
-
|
92
|
-
Location settings have top-level keys that specify arbitrary location names, each of which provide:
|
93
|
-
|
94
|
-
- **`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 is also used as the location's executable, see below).
|
95
|
-
|
96
|
-
- **`executable:`** _optional_, provides an executable resource to be called when delegating a request to this location. Executables are `GraphQL::Schema` classes or any object with a `.call(request, source, variables)` method that returns a GraphQL response. Omitting the executable option will use the location's provided `schema` as the executable resource.
|
97
|
-
|
98
|
-
- **`stitch:`** _optional_, an array of configs used to dynamically apply `@stitch` directives to select root fields prior to composing. This is useful when you can't easily render stitching directives into a location's source schema.
|
99
|
-
|
100
|
-
### Merge patterns
|
101
|
-
|
102
|
-
The strategy used to merge source schemas into the combined schema is based on each element type:
|
103
|
-
|
104
|
-
- Arguments of fields, directives, and `InputObject` types intersect for each parent element across locations (an element's arguments must appear in all locations):
|
105
|
-
- Arguments must share a value type, and the strictest nullability across locations is used.
|
106
|
-
- Composition fails if argument intersection would eliminate a non-null argument.
|
107
|
-
|
108
|
-
- `Object` and `Interface` types merge their fields and directives together:
|
109
|
-
- Common fields across locations must share a value type, and the weakest nullability is used.
|
110
|
-
- Objects with unique fields across locations must implement [`@stitch` accessors](../README.md#merged-types).
|
111
|
-
- Shared object types without `@stitch` accessors must contain identical fields.
|
112
|
-
- Merged interfaces must remain compatible with all underlying implementations.
|
113
|
-
|
114
|
-
- `Enum` types merge their values based on how the enum is used:
|
115
|
-
- Enums used anywhere as an argument will intersect their values (common values across all locations).
|
116
|
-
- Enums used exclusively in read contexts will provide a union of values (all values across all locations).
|
117
|
-
|
118
|
-
- `Union` types merge all possible types from across all locations.
|
119
|
-
|
120
|
-
- `Scalar` types are added for all scalar names across all locations.
|
121
|
-
|
122
|
-
- `Directive` definitions are added for all distinct names across locations:
|
123
|
-
- Stitching directives (both definitions and assignments) are omitted.
|
124
|
-
|
125
|
-
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.
|