graphql-stitching 0.3.4 → 1.0.0
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 +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
|
-
```
|