graphql-stitching 1.7.0 → 1.7.2
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/executor/root_source.rb +1 -1
- data/lib/graphql/stitching/executor/shaper.rb +1 -1
- data/lib/graphql/stitching/executor/type_resolver_source.rb +1 -1
- data/lib/graphql/stitching/planner.rb +2 -2
- data/lib/graphql/stitching/request.rb +9 -0
- data/lib/graphql/stitching/supergraph/from_definition.rb +3 -6
- data/lib/graphql/stitching/supergraph.rb +7 -5
- 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)
|
@@ -16,7 +16,7 @@ module GraphQL::Stitching
|
|
16
16
|
@executor.request.operation_name,
|
17
17
|
@executor.request.operation_directives,
|
18
18
|
)
|
19
|
-
query_variables = @executor.request.variables.slice(*op.variables.
|
19
|
+
query_variables = @executor.request.variables.slice(*op.variables.each_key)
|
20
20
|
result = @executor.request.supergraph.execute_at_location(op.location, query_document, query_variables, @executor.request)
|
21
21
|
@executor.query_count += 1
|
22
22
|
|
@@ -32,7 +32,7 @@ module GraphQL::Stitching
|
|
32
32
|
field_name = node.alias || node.name
|
33
33
|
|
34
34
|
if @request.query.get_field(parent_type, node.name).introspection?
|
35
|
-
if node.name == TYPENAME && parent_type == @root_type
|
35
|
+
if node.name == TYPENAME && parent_type == @root_type && node != TypeResolver::TYPENAME_EXPORT_NODE
|
36
36
|
raw_object[field_name] = @root_type.graphql_name
|
37
37
|
end
|
38
38
|
next
|
@@ -165,7 +165,7 @@ module GraphQL::Stitching
|
|
165
165
|
if pathed_errors_by_op_index_and_object_id.any?
|
166
166
|
pathed_errors_by_op_index_and_object_id.each do |op_index, pathed_errors_by_object_id|
|
167
167
|
repath_errors!(pathed_errors_by_object_id, ops.dig(op_index, "path"))
|
168
|
-
errors_result.
|
168
|
+
errors_result.push(*pathed_errors_by_object_id.each_value)
|
169
169
|
end
|
170
170
|
end
|
171
171
|
|
@@ -213,7 +213,7 @@ module GraphQL
|
|
213
213
|
input_selections.each do |node|
|
214
214
|
case node
|
215
215
|
when GraphQL::Language::Nodes::Field
|
216
|
-
if node.alias&.start_with?(TypeResolver::EXPORT_PREFIX)
|
216
|
+
if node.alias&.start_with?(TypeResolver::EXPORT_PREFIX) && node != TypeResolver::TYPENAME_EXPORT_NODE
|
217
217
|
raise StitchingError, %(Alias "#{node.alias}" is not allowed because "#{TypeResolver::EXPORT_PREFIX}" is a reserved prefix.)
|
218
218
|
elsif node.name == TYPENAME
|
219
219
|
locale_selections << node
|
@@ -284,7 +284,7 @@ module GraphQL
|
|
284
284
|
remote_selections_by_location = delegate_remote_selections(parent_type, remote_selections)
|
285
285
|
|
286
286
|
# D) Create paths routing to new entrypoint locations via resolver queries.
|
287
|
-
routes = @supergraph.route_type_to_locations(parent_type.graphql_name, current_location, remote_selections_by_location.
|
287
|
+
routes = @supergraph.route_type_to_locations(parent_type.graphql_name, current_location, remote_selections_by_location.each_key)
|
288
288
|
|
289
289
|
# E) Translate resolver pathways into new entrypoints.
|
290
290
|
routes.each_value do |route|
|
@@ -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 }
|
@@ -143,6 +147,11 @@ module GraphQL
|
|
143
147
|
@query.static_errors
|
144
148
|
end
|
145
149
|
|
150
|
+
# @return [Boolean] is the request valid?
|
151
|
+
def valid?
|
152
|
+
validate.empty?
|
153
|
+
end
|
154
|
+
|
146
155
|
# Gets and sets the query plan for the request. Assigned query plans may pull from a cache,
|
147
156
|
# which is useful for redundant GraphQL documents (commonly sent by frontend clients).
|
148
157
|
# ```ruby
|
@@ -21,11 +21,8 @@ module GraphQL::Stitching
|
|
21
21
|
field_map = {}
|
22
22
|
resolver_map = {}
|
23
23
|
possible_locations = {}
|
24
|
-
|
25
|
-
|
26
|
-
else
|
27
|
-
[]
|
28
|
-
end
|
24
|
+
visibility_definition = schema.directives[GraphQL::Stitching.visibility_directive]
|
25
|
+
visibility_profiles = visibility_definition&.get_argument("profiles")&.default_value || EMPTY_ARRAY
|
29
26
|
|
30
27
|
schema.types.each do |type_name, type|
|
31
28
|
next if type.introspection?
|
@@ -75,7 +72,7 @@ module GraphQL::Stitching
|
|
75
72
|
end
|
76
73
|
end
|
77
74
|
|
78
|
-
executables = possible_locations.
|
75
|
+
executables = possible_locations.each_key.each_with_object({}) do |location, memo|
|
79
76
|
executable = executables[location] || executables[location.to_sym]
|
80
77
|
if validate_executable!(location, executable)
|
81
78
|
memo[location] = executable
|
@@ -38,7 +38,7 @@ module GraphQL
|
|
38
38
|
@locations_by_type_and_field = @memoized_introspection_types.each_with_object(fields) do |(type_name, type), memo|
|
39
39
|
next unless type.kind.fields?
|
40
40
|
|
41
|
-
memo[type_name] = type.fields.
|
41
|
+
memo[type_name] = type.fields.each_key.each_with_object({}) do |field_name, m|
|
42
42
|
m[field_name] = [SUPERGRAPH_LOCATION]
|
43
43
|
end
|
44
44
|
end.freeze
|
@@ -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
|
@@ -73,7 +75,7 @@ module GraphQL
|
|
73
75
|
end
|
74
76
|
|
75
77
|
def locations
|
76
|
-
@executables.
|
78
|
+
@executables.each_key.reject { _1 == SUPERGRAPH_LOCATION }
|
77
79
|
end
|
78
80
|
|
79
81
|
def memoized_schema_fields(type_name)
|
@@ -128,7 +130,7 @@ module GraphQL
|
|
128
130
|
# "Type" => ["location1", "location2", ...]
|
129
131
|
def locations_by_type
|
130
132
|
@locations_by_type ||= @locations_by_type_and_field.each_with_object({}) do |(type_name, fields), memo|
|
131
|
-
memo[type_name] = fields.values.flatten.uniq
|
133
|
+
memo[type_name] = fields.values.tap(&:flatten!).tap(&:uniq!)
|
132
134
|
end
|
133
135
|
end
|
134
136
|
|
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.2
|
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-19 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
|
-
```
|