graphql-stitching 0.3.4 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +79 -14
- data/docs/README.md +1 -1
- data/docs/client.md +103 -0
- data/docs/composer.md +2 -2
- data/docs/supergraph.md +1 -1
- data/example/gateway.rb +4 -4
- data/lib/graphql/stitching/{gateway.rb → client.rb} +7 -7
- data/lib/graphql/stitching/composer/validate_boundaries.rb +4 -4
- data/lib/graphql/stitching/composer.rb +44 -22
- data/lib/graphql/stitching/executor/boundary_source.rb +199 -0
- data/lib/graphql/stitching/executor/root_source.rb +48 -0
- data/lib/graphql/stitching/executor.rb +7 -231
- data/lib/graphql/stitching/{remote_client.rb → http_executable.rb} +1 -1
- data/lib/graphql/stitching/planner.rb +264 -189
- data/lib/graphql/stitching/planner_operation.rb +18 -16
- data/lib/graphql/stitching/shaper.rb +1 -1
- data/lib/graphql/stitching/supergraph.rb +21 -28
- data/lib/graphql/stitching/version.rb +1 -1
- data/lib/graphql/stitching.rb +3 -2
- metadata +7 -5
- data/docs/gateway.md +0 -103
@@ -4,16 +4,6 @@ module GraphQL
|
|
4
4
|
module Stitching
|
5
5
|
class Supergraph
|
6
6
|
LOCATION = "__super"
|
7
|
-
INTROSPECTION_TYPES = [
|
8
|
-
"__Schema",
|
9
|
-
"__Type",
|
10
|
-
"__Field",
|
11
|
-
"__Directive",
|
12
|
-
"__EnumValue",
|
13
|
-
"__InputValue",
|
14
|
-
"__TypeKind",
|
15
|
-
"__DirectiveLocation",
|
16
|
-
].freeze
|
17
7
|
|
18
8
|
def self.validate_executable!(location, executable)
|
19
9
|
return true if executable.is_a?(Class) && executable <= GraphQL::Schema
|
@@ -50,11 +40,10 @@ module GraphQL
|
|
50
40
|
@memoized_schema_fields = {}
|
51
41
|
|
52
42
|
# add introspection types into the fields mapping
|
53
|
-
@locations_by_type_and_field =
|
54
|
-
|
55
|
-
next unless introspection_type.kind.fields?
|
43
|
+
@locations_by_type_and_field = memoized_introspection_types.each_with_object(fields) do |(type_name, type), memo|
|
44
|
+
next unless type.kind.fields?
|
56
45
|
|
57
|
-
memo[type_name] =
|
46
|
+
memo[type_name] = type.fields.keys.each_with_object({}) do |field_name, m|
|
58
47
|
m[field_name] = [LOCATION]
|
59
48
|
end
|
60
49
|
end.freeze
|
@@ -68,7 +57,7 @@ module GraphQL
|
|
68
57
|
end
|
69
58
|
|
70
59
|
def fields
|
71
|
-
@locations_by_type_and_field.reject { |k, _v|
|
60
|
+
@locations_by_type_and_field.reject { |k, _v| memoized_introspection_types[k] }
|
72
61
|
end
|
73
62
|
|
74
63
|
def locations
|
@@ -83,6 +72,10 @@ module GraphQL
|
|
83
72
|
}
|
84
73
|
end
|
85
74
|
|
75
|
+
def memoized_introspection_types
|
76
|
+
@memoized_introspection_types ||= schema.introspection_system.types
|
77
|
+
end
|
78
|
+
|
86
79
|
def memoized_schema_types
|
87
80
|
@memoized_schema_types ||= @schema.types
|
88
81
|
end
|
@@ -94,11 +87,14 @@ module GraphQL
|
|
94
87
|
def memoized_schema_fields(type_name)
|
95
88
|
@memoized_schema_fields[type_name] ||= begin
|
96
89
|
fields = memoized_schema_types[type_name].fields
|
97
|
-
|
90
|
+
@schema.introspection_system.dynamic_fields.each do |field|
|
91
|
+
fields[field.name] ||= field # adds __typename
|
92
|
+
end
|
98
93
|
|
99
94
|
if type_name == @schema.query.graphql_name
|
100
|
-
|
101
|
-
|
95
|
+
@schema.introspection_system.entry_points.each do |field|
|
96
|
+
fields[field.name] ||= field # adds __schema, __type
|
97
|
+
end
|
102
98
|
end
|
103
99
|
|
104
100
|
fields
|
@@ -148,9 +144,7 @@ module GraphQL
|
|
148
144
|
# ("Type") => ["id", ...]
|
149
145
|
def possible_keys_for_type(type_name)
|
150
146
|
@possible_keys_by_type[type_name] ||= begin
|
151
|
-
|
152
|
-
keys.uniq!
|
153
|
-
keys
|
147
|
+
@boundaries[type_name].map { _1["key"] }.tap(&:uniq!)
|
154
148
|
end
|
155
149
|
end
|
156
150
|
|
@@ -190,18 +184,18 @@ module GraphQL
|
|
190
184
|
costs = {}
|
191
185
|
|
192
186
|
paths = possible_keys_for_type_and_location(type_name, start_location).map do |possible_key|
|
193
|
-
[{ location: start_location,
|
187
|
+
[{ location: start_location, key: possible_key, cost: 0 }]
|
194
188
|
end
|
195
189
|
|
196
190
|
while paths.any?
|
197
191
|
path = paths.pop
|
198
192
|
current_location = path.last[:location]
|
199
|
-
|
193
|
+
current_key = path.last[:key]
|
200
194
|
current_cost = path.last[:cost]
|
201
195
|
|
202
196
|
@boundaries[type_name].each do |boundary|
|
203
197
|
forward_location = boundary["location"]
|
204
|
-
next if
|
198
|
+
next if current_key != boundary["key"]
|
205
199
|
next if path.any? { _1[:location] == forward_location }
|
206
200
|
|
207
201
|
best_cost = costs[forward_location] || Float::INFINITY
|
@@ -210,7 +204,7 @@ module GraphQL
|
|
210
204
|
path.pop
|
211
205
|
path << {
|
212
206
|
location: current_location,
|
213
|
-
|
207
|
+
key: current_key,
|
214
208
|
cost: current_cost,
|
215
209
|
boundary: boundary,
|
216
210
|
}
|
@@ -228,14 +222,13 @@ module GraphQL
|
|
228
222
|
costs[forward_location] = forward_cost if forward_cost < best_cost
|
229
223
|
|
230
224
|
possible_keys_for_type_and_location(type_name, forward_location).each do |possible_key|
|
231
|
-
paths << [*path, { location: forward_location,
|
225
|
+
paths << [*path, { location: forward_location, key: possible_key, cost: forward_cost }]
|
232
226
|
end
|
233
227
|
end
|
234
228
|
|
235
229
|
paths.sort! do |a, b|
|
236
230
|
cost_diff = a.last[:cost] - b.last[:cost]
|
237
|
-
|
238
|
-
a.length - b.length
|
231
|
+
cost_diff.zero? ? a.length - b.length : cost_diff
|
239
232
|
end.reverse!
|
240
233
|
end
|
241
234
|
|
data/lib/graphql/stitching.rb
CHANGED
@@ -5,6 +5,7 @@ require "graphql"
|
|
5
5
|
module GraphQL
|
6
6
|
module Stitching
|
7
7
|
EMPTY_OBJECT = {}.freeze
|
8
|
+
EMPTY_ARRAY = [].freeze
|
8
9
|
|
9
10
|
class StitchingError < StandardError; end
|
10
11
|
|
@@ -23,13 +24,13 @@ module GraphQL
|
|
23
24
|
end
|
24
25
|
end
|
25
26
|
|
26
|
-
require_relative "stitching/gateway"
|
27
27
|
require_relative "stitching/supergraph"
|
28
|
+
require_relative "stitching/client"
|
28
29
|
require_relative "stitching/composer"
|
29
30
|
require_relative "stitching/executor"
|
31
|
+
require_relative "stitching/http_executable"
|
30
32
|
require_relative "stitching/planner_operation"
|
31
33
|
require_relative "stitching/planner"
|
32
|
-
require_relative "stitching/remote_client"
|
33
34
|
require_relative "stitching/request"
|
34
35
|
require_relative "stitching/shaper"
|
35
36
|
require_relative "stitching/util"
|
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: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Greg MacWilliam
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-08-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|
@@ -80,9 +80,9 @@ files:
|
|
80
80
|
- README.md
|
81
81
|
- Rakefile
|
82
82
|
- docs/README.md
|
83
|
+
- docs/client.md
|
83
84
|
- docs/composer.md
|
84
85
|
- docs/executor.md
|
85
|
-
- docs/gateway.md
|
86
86
|
- docs/images/library.png
|
87
87
|
- docs/images/merging.png
|
88
88
|
- docs/images/stitching.png
|
@@ -97,15 +97,17 @@ files:
|
|
97
97
|
- gemfiles/graphql_1.13.9.gemfile
|
98
98
|
- graphql-stitching.gemspec
|
99
99
|
- lib/graphql/stitching.rb
|
100
|
+
- lib/graphql/stitching/client.rb
|
100
101
|
- lib/graphql/stitching/composer.rb
|
101
102
|
- lib/graphql/stitching/composer/base_validator.rb
|
102
103
|
- lib/graphql/stitching/composer/validate_boundaries.rb
|
103
104
|
- lib/graphql/stitching/composer/validate_interfaces.rb
|
104
105
|
- lib/graphql/stitching/executor.rb
|
105
|
-
- lib/graphql/stitching/
|
106
|
+
- lib/graphql/stitching/executor/boundary_source.rb
|
107
|
+
- lib/graphql/stitching/executor/root_source.rb
|
108
|
+
- lib/graphql/stitching/http_executable.rb
|
106
109
|
- lib/graphql/stitching/planner.rb
|
107
110
|
- lib/graphql/stitching/planner_operation.rb
|
108
|
-
- lib/graphql/stitching/remote_client.rb
|
109
111
|
- lib/graphql/stitching/request.rb
|
110
112
|
- lib/graphql/stitching/shaper.rb
|
111
113
|
- lib/graphql/stitching/supergraph.rb
|
data/docs/gateway.md
DELETED
@@ -1,103 +0,0 @@
|
|
1
|
-
## GraphQL::Stitching::Gateway
|
2
|
-
|
3
|
-
The `Gateway` is an out-of-the-box convenience with all stitching components assembled into a default workflow. A gateway is designed to work for most common needs, though you're welcome to assemble the component parts into your own configuration. A Gateway 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
|
-
gateway = GraphQL::Stitching::Gateway.new(locations: {
|
10
|
-
products: {
|
11
|
-
schema: GraphQL::Schema.from_definition(movies_schema),
|
12
|
-
executable: GraphQL::Stitching::RemoteClient.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::RemoteClient.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 Gateway 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
|
-
exported_schema = "type Query { ..."
|
29
|
-
exported_mapping = JSON.parse("{ ... }")
|
30
|
-
supergraph = GraphQL::Stitching::Supergraph.from_export(
|
31
|
-
schema: exported_schema,
|
32
|
-
delegation_map: exported_mapping,
|
33
|
-
executables: { ... },
|
34
|
-
)
|
35
|
-
|
36
|
-
gateway = GraphQL::Stitching::Gateway.new(supergraph: supergraph)
|
37
|
-
```
|
38
|
-
|
39
|
-
### Execution
|
40
|
-
|
41
|
-
A gateway 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 stitched gateway becomes mostly a drop-in replacement to executing on a `GraphQL::Schema` instance:
|
42
|
-
|
43
|
-
```ruby
|
44
|
-
result = gateway.execute(
|
45
|
-
query: "query MyProduct($id: ID!) { product(id: $id) { name } }",
|
46
|
-
variables: { "id" => "1" },
|
47
|
-
operation_name: "MyProduct",
|
48
|
-
)
|
49
|
-
```
|
50
|
-
|
51
|
-
Arguments for the `execute` method include:
|
52
|
-
|
53
|
-
* `query`: a query (or mutation) as a string or parsed AST.
|
54
|
-
* `variables`: a hash of variables for the request.
|
55
|
-
* `operation_name`: the name of the operation to execute (when multiple are provided).
|
56
|
-
* `validate`: true if static validation should run on the supergraph schema before execution.
|
57
|
-
* `context`: an object passed through to executable calls and gateway hooks.
|
58
|
-
|
59
|
-
### Cache hooks
|
60
|
-
|
61
|
-
The gateway provides cache hooks to enable caching query plans across requests. Without caching, every request made the the gateway 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.
|
62
|
-
|
63
|
-
```ruby
|
64
|
-
gateway.on_cache_read do |key, _context|
|
65
|
-
$redis.get(key) # << 3P code
|
66
|
-
end
|
67
|
-
|
68
|
-
gateway.on_cache_write do |key, payload, _context|
|
69
|
-
$redis.set(key, payload) # << 3P code
|
70
|
-
end
|
71
|
-
```
|
72
|
-
|
73
|
-
Note that inlined input data works against caching, so you should _avoid_ this:
|
74
|
-
|
75
|
-
```graphql
|
76
|
-
query {
|
77
|
-
product(id: "1") { name }
|
78
|
-
}
|
79
|
-
```
|
80
|
-
|
81
|
-
Instead, always leverage variables in queries so that the document body remains consistent across requests:
|
82
|
-
|
83
|
-
```graphql
|
84
|
-
query($id: ID!) {
|
85
|
-
product(id: $id) { name }
|
86
|
-
}
|
87
|
-
|
88
|
-
# variables: { "id" => "1" }
|
89
|
-
```
|
90
|
-
|
91
|
-
### Error hooks
|
92
|
-
|
93
|
-
The gateway 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 gateway to add to the [GraphQL errors](https://spec.graphql.org/June2018/#sec-Errors) result.
|
94
|
-
|
95
|
-
```ruby
|
96
|
-
gateway.on_error do |err, context|
|
97
|
-
# log the error
|
98
|
-
Bugsnag.notify(err)
|
99
|
-
|
100
|
-
# return a formatted message for the public response
|
101
|
-
"Whoops, please contact support abount request '#{context[:request_id]}'"
|
102
|
-
end
|
103
|
-
```
|