graphql-stitching 1.0.6 → 1.1.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/docs/client.md +10 -12
- data/docs/request.md +14 -10
- data/docs/supergraph.md +7 -11
- data/lib/graphql/stitching/client.rb +3 -3
- data/lib/graphql/stitching/composer.rb +1 -1
- data/lib/graphql/stitching/planner.rb +1 -1
- data/lib/graphql/stitching/supergraph.rb +132 -32
- data/lib/graphql/stitching/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 66c85693da3a07eb7b3ab81fe76b69ce095aaffa8b9cab68a1f34b86e20963ba
|
4
|
+
data.tar.gz: f1e8bbbe3ffdaf23189f219cad6fe604203527e12911a0609fb1711375effe58
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a86ac6f20dda8dc5ae68d548d85d9c4482856c56e86a07cee9ff5689ba95cf73f19dadd80d6f711a55a0b9e25796522b1b1feb4c7d70f7fa499a0f38beb0d24a
|
7
|
+
data.tar.gz: 61b2d6f21dc8001fab5e6d8c27d3cc04582fc506daa17f45354475cb9ef6812a355ec5819257d9d582d7f4a312b85b10b539322e3fa14b73a255376a75687658
|
data/docs/client.md
CHANGED
@@ -25,11 +25,9 @@ client = GraphQL::Stitching::Client.new(locations: {
|
|
25
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
26
|
|
27
27
|
```ruby
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
schema: exported_schema,
|
32
|
-
delegation_map: exported_mapping,
|
28
|
+
supergraph_sdl = File.read("precomposed_schema.graphql")
|
29
|
+
supergraph = GraphQL::Stitching::Supergraph.from_definition(
|
30
|
+
supergraph_sdl,
|
33
31
|
executables: { ... },
|
34
32
|
)
|
35
33
|
|
@@ -61,16 +59,16 @@ Arguments for the `execute` method include:
|
|
61
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.
|
62
60
|
|
63
61
|
```ruby
|
64
|
-
client.on_cache_read do |
|
65
|
-
$redis.get(
|
62
|
+
client.on_cache_read do |request|
|
63
|
+
$redis.get(request.digest) # << 3P code
|
66
64
|
end
|
67
65
|
|
68
|
-
client.on_cache_write do |
|
69
|
-
$redis.set(
|
66
|
+
client.on_cache_write do |request, payload|
|
67
|
+
$redis.set(request.digest, payload) # << 3P code
|
70
68
|
end
|
71
69
|
```
|
72
70
|
|
73
|
-
Note that inlined input data works against caching, so you should _avoid_
|
71
|
+
Note that inlined input data works against caching, so you should _avoid_ these input literals when possible:
|
74
72
|
|
75
73
|
```graphql
|
76
74
|
query {
|
@@ -93,11 +91,11 @@ query($id: ID!) {
|
|
93
91
|
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.
|
94
92
|
|
95
93
|
```ruby
|
96
|
-
client.on_error do |
|
94
|
+
client.on_error do |request, err|
|
97
95
|
# log the error
|
98
96
|
Bugsnag.notify(err)
|
99
97
|
|
100
98
|
# return a formatted message for the public response
|
101
|
-
"Whoops, please contact support abount request '#{context[:request_id]}'"
|
99
|
+
"Whoops, please contact support abount request '#{request.context[:request_id]}'"
|
102
100
|
end
|
103
101
|
```
|
data/docs/request.md
CHANGED
@@ -3,18 +3,22 @@
|
|
3
3
|
A `Request` contains a parsed GraphQL document and variables, and handles the logistics of extracting the appropriate operation, variable definitions, and fragments. A `Request` should be built once per server request and passed through to other stitching components that utilize request information.
|
4
4
|
|
5
5
|
```ruby
|
6
|
-
|
7
|
-
request = GraphQL::Stitching::Request.new(
|
8
|
-
|
9
|
-
request.document # parsed AST via GraphQL.parse
|
10
|
-
request.variables # user-submitted variables
|
11
|
-
request.string # normalized printed document string
|
12
|
-
request.digest # SHA digest of the normalized document string
|
13
|
-
|
14
|
-
request.variable_definitions # a mapping of variable names to their type definitions
|
15
|
-
request.fragment_definitions # a mapping of fragment names to their fragment definitions
|
6
|
+
source = "query FetchMovie($id: ID!) { movie(id:$id) { id genre } }"
|
7
|
+
request = GraphQL::Stitching::Request.new(source, variables: { "id" => "1" }, operation_name: "FetchMovie")
|
16
8
|
```
|
17
9
|
|
10
|
+
A `Request` provides the following information:
|
11
|
+
|
12
|
+
- `req.document`: parsed AST of the GraphQL source
|
13
|
+
- `req.variables`: a hash of user-submitted variables
|
14
|
+
- `req.string`: the original GraphQL source string, or printed document
|
15
|
+
- `req.digest`: a SHA2 of the request string
|
16
|
+
- `req.normalized_string`: printed document string with consistent whitespace
|
17
|
+
- `req.normalized_digest`: a SHA2 of the normalized string
|
18
|
+
- `req.operation`: the operation definition selected for the request
|
19
|
+
- `req.variable_definitions`: a mapping of variable names to their type definitions
|
20
|
+
- `req.fragment_definitions`: a mapping of fragment names to their fragment definitions
|
21
|
+
|
18
22
|
### Preparing requests
|
19
23
|
|
20
24
|
A request should be prepared for stitching using the `prepare!` method _after_ validations have been run:
|
data/docs/supergraph.md
CHANGED
@@ -4,29 +4,25 @@ A `Supergraph` is the singuar representation of a stitched graph. `Supergraph` i
|
|
4
4
|
|
5
5
|
### Export and caching
|
6
6
|
|
7
|
-
A Supergraph is designed to be composed, cached, and restored. Calling
|
7
|
+
A Supergraph is designed to be composed, cached, and restored. Calling `to_definition` will return an SDL (Schema Definition Language) print of the combined graph schema with delegation mapping directives. This pre-composed schema can be persisted in any raw format that suits your stack:
|
8
8
|
|
9
9
|
```ruby
|
10
|
-
supergraph_sdl
|
10
|
+
supergraph_sdl = supergraph.to_definition
|
11
11
|
|
12
|
-
# stash
|
12
|
+
# stash this composed schema in a cache...
|
13
13
|
$redis.set("cached_supergraph_sdl", supergraph_sdl)
|
14
|
-
$redis.set("cached_delegation_map", JSON.generate(delegation_map))
|
15
14
|
|
16
|
-
# or, write the
|
15
|
+
# or, write the composed schema as a file into your repo...
|
17
16
|
File.write("supergraph/schema.graphql", supergraph_sdl)
|
18
|
-
File.write("supergraph/delegation_map.json", JSON.generate(delegation_map))
|
19
17
|
```
|
20
18
|
|
21
|
-
To restore a Supergraph, call `
|
19
|
+
To restore a Supergraph, call `from_definition` providing the cached SDL string and a hash of executables keyed by their location names:
|
22
20
|
|
23
21
|
```ruby
|
24
22
|
supergraph_sdl = $redis.get("cached_supergraph_sdl")
|
25
|
-
delegation_map = JSON.parse($redis.get("cached_delegation_map"))
|
26
23
|
|
27
|
-
supergraph = GraphQL::Stitching::Supergraph.
|
28
|
-
|
29
|
-
delegation_map: delegation_map,
|
24
|
+
supergraph = GraphQL::Stitching::Supergraph.from_definition(
|
25
|
+
supergraph_sdl,
|
30
26
|
executables: {
|
31
27
|
my_remote: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3000"),
|
32
28
|
my_local: MyLocalSchema,
|
@@ -52,7 +52,7 @@ module GraphQL
|
|
52
52
|
rescue GraphQL::ParseError, GraphQL::ExecutionError => e
|
53
53
|
error_result([e])
|
54
54
|
rescue StandardError => e
|
55
|
-
custom_message = @on_error.call(
|
55
|
+
custom_message = @on_error.call(request, e) if @on_error
|
56
56
|
error_result([{ "message" => custom_message || "An unexpected error occured." }])
|
57
57
|
end
|
58
58
|
|
@@ -75,14 +75,14 @@ module GraphQL
|
|
75
75
|
|
76
76
|
def fetch_plan(request)
|
77
77
|
if @on_cache_read
|
78
|
-
cached_plan = @on_cache_read.call(request
|
78
|
+
cached_plan = @on_cache_read.call(request)
|
79
79
|
return GraphQL::Stitching::Plan.from_json(JSON.parse(cached_plan)) if cached_plan
|
80
80
|
end
|
81
81
|
|
82
82
|
plan = yield
|
83
83
|
|
84
84
|
if @on_cache_write
|
85
|
-
@on_cache_write.call(request
|
85
|
+
@on_cache_write.call(request, JSON.generate(plan.as_json))
|
86
86
|
end
|
87
87
|
|
88
88
|
plan
|
@@ -3,34 +3,89 @@
|
|
3
3
|
module GraphQL
|
4
4
|
module Stitching
|
5
5
|
class Supergraph
|
6
|
-
|
6
|
+
SUPERGRAPH_LOCATION = "__super"
|
7
|
+
|
8
|
+
class ResolverDirective < GraphQL::Schema::Directive
|
9
|
+
graphql_name "resolver"
|
10
|
+
locations OBJECT, INTERFACE, UNION
|
11
|
+
argument :location, String, required: true
|
12
|
+
argument :key, String, required: true
|
13
|
+
argument :field, String, required: true
|
14
|
+
argument :arg, String, required: true
|
15
|
+
argument :list, Boolean, required: false
|
16
|
+
argument :federation, Boolean, required: false
|
17
|
+
repeatable true
|
18
|
+
end
|
7
19
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
20
|
+
class SourceDirective < GraphQL::Schema::Directive
|
21
|
+
graphql_name "source"
|
22
|
+
locations FIELD_DEFINITION
|
23
|
+
argument :location, String, required: true
|
24
|
+
repeatable true
|
12
25
|
end
|
13
26
|
|
14
|
-
|
15
|
-
|
27
|
+
class << self
|
28
|
+
def validate_executable!(location, executable)
|
29
|
+
return true if executable.is_a?(Class) && executable <= GraphQL::Schema
|
30
|
+
return true if executable && executable.respond_to?(:call)
|
31
|
+
raise StitchingError, "Invalid executable provided for location `#{location}`."
|
32
|
+
end
|
33
|
+
|
34
|
+
def from_definition(schema, executables:)
|
35
|
+
schema = GraphQL::Schema.from_definition(schema) if schema.is_a?(String)
|
36
|
+
field_map = {}
|
37
|
+
boundary_map = {}
|
38
|
+
possible_locations = {}
|
39
|
+
introspection_types = schema.introspection_system.types.keys
|
40
|
+
|
41
|
+
schema.types.each do |type_name, type|
|
42
|
+
next if introspection_types.include?(type_name)
|
43
|
+
|
44
|
+
type.directives.each do |directive|
|
45
|
+
next unless directive.graphql_name == ResolverDirective.graphql_name
|
46
|
+
|
47
|
+
kwargs = directive.arguments.keyword_arguments
|
48
|
+
boundary_map[type_name] ||= []
|
49
|
+
boundary_map[type_name] << Boundary.new(
|
50
|
+
type_name: type_name,
|
51
|
+
location: kwargs[:location],
|
52
|
+
key: kwargs[:key],
|
53
|
+
field: kwargs[:field],
|
54
|
+
arg: kwargs[:arg],
|
55
|
+
list: kwargs[:list] || false,
|
56
|
+
federation: kwargs[:federation] || false,
|
57
|
+
)
|
58
|
+
end
|
59
|
+
|
60
|
+
next unless type.kind.fields?
|
16
61
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
62
|
+
type.fields.each do |field_name, field|
|
63
|
+
field.directives.each do |d|
|
64
|
+
next unless d.graphql_name == SourceDirective.graphql_name
|
65
|
+
|
66
|
+
location = d.arguments.keyword_arguments[:location]
|
67
|
+
field_map[type_name] ||= {}
|
68
|
+
field_map[type_name][field_name] ||= []
|
69
|
+
field_map[type_name][field_name] << location
|
70
|
+
possible_locations[location] = true
|
71
|
+
end
|
72
|
+
end
|
21
73
|
end
|
22
|
-
end
|
23
74
|
|
24
|
-
|
25
|
-
|
26
|
-
|
75
|
+
executables = possible_locations.keys.each_with_object({}) do |location, memo|
|
76
|
+
executable = executables[location] || executables[location.to_sym]
|
77
|
+
if validate_executable!(location, executable)
|
78
|
+
memo[location] = executable
|
79
|
+
end
|
80
|
+
end
|
27
81
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
82
|
+
new(
|
83
|
+
schema: schema,
|
84
|
+
fields: field_map,
|
85
|
+
boundaries: boundary_map,
|
86
|
+
executables: executables,
|
87
|
+
)
|
88
|
+
end
|
34
89
|
end
|
35
90
|
|
36
91
|
attr_reader :schema, :boundaries, :locations_by_type_and_field, :executables
|
@@ -48,32 +103,77 @@ module GraphQL
|
|
48
103
|
next unless type.kind.fields?
|
49
104
|
|
50
105
|
memo[type_name] = type.fields.keys.each_with_object({}) do |field_name, m|
|
51
|
-
m[field_name] = [
|
106
|
+
m[field_name] = [SUPERGRAPH_LOCATION]
|
52
107
|
end
|
53
108
|
end.freeze
|
54
109
|
|
55
110
|
# validate and normalize executable references
|
56
|
-
@executables = executables.each_with_object({
|
111
|
+
@executables = executables.each_with_object({ SUPERGRAPH_LOCATION => @schema }) do |(location, executable), memo|
|
57
112
|
if self.class.validate_executable!(location, executable)
|
58
113
|
memo[location.to_s] = executable
|
59
114
|
end
|
60
115
|
end.freeze
|
61
116
|
end
|
62
117
|
|
118
|
+
def to_definition
|
119
|
+
if @schema.directives[ResolverDirective.graphql_name].nil?
|
120
|
+
@schema.directive(ResolverDirective)
|
121
|
+
end
|
122
|
+
if @schema.directives[SourceDirective.graphql_name].nil?
|
123
|
+
@schema.directive(SourceDirective)
|
124
|
+
end
|
125
|
+
|
126
|
+
@schema.types.each do |type_name, type|
|
127
|
+
if boundaries_for_type = @boundaries.dig(type_name)
|
128
|
+
boundaries_for_type.each do |boundary|
|
129
|
+
existing = type.directives.find do |d|
|
130
|
+
kwargs = d.arguments.keyword_arguments
|
131
|
+
d.graphql_name == ResolverDirective.graphql_name &&
|
132
|
+
kwargs[:location] == boundary.location &&
|
133
|
+
kwargs[:key] == boundary.key &&
|
134
|
+
kwargs[:field] == boundary.field &&
|
135
|
+
kwargs[:arg] == boundary.arg &&
|
136
|
+
kwargs.fetch(:list, false) == boundary.list &&
|
137
|
+
kwargs.fetch(:federation, false) == boundary.federation
|
138
|
+
end
|
139
|
+
|
140
|
+
type.directive(ResolverDirective, **{
|
141
|
+
location: boundary.location,
|
142
|
+
key: boundary.key,
|
143
|
+
field: boundary.field,
|
144
|
+
arg: boundary.arg,
|
145
|
+
list: boundary.list || nil,
|
146
|
+
federation: boundary.federation || nil,
|
147
|
+
}.tap(&:compact!)) if existing.nil?
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
next unless type.kind.fields?
|
152
|
+
|
153
|
+
type.fields.each do |field_name, field|
|
154
|
+
locations_for_field = @locations_by_type_and_field.dig(type_name, field_name)
|
155
|
+
next if locations_for_field.nil?
|
156
|
+
|
157
|
+
locations_for_field.each do |location|
|
158
|
+
existing = field.directives.find do |d|
|
159
|
+
d.graphql_name == SourceDirective.graphql_name &&
|
160
|
+
d.arguments.keyword_arguments[:location] == location
|
161
|
+
end
|
162
|
+
|
163
|
+
field.directive(SourceDirective, location: location) if existing.nil?
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
@schema.to_definition
|
169
|
+
end
|
170
|
+
|
63
171
|
def fields
|
64
172
|
@locations_by_type_and_field.reject { |k, _v| memoized_introspection_types[k] }
|
65
173
|
end
|
66
174
|
|
67
175
|
def locations
|
68
|
-
@executables.keys.reject { _1 ==
|
69
|
-
end
|
70
|
-
|
71
|
-
def export
|
72
|
-
return GraphQL::Schema::Printer.print_schema(@schema), {
|
73
|
-
"locations" => locations,
|
74
|
-
"fields" => fields,
|
75
|
-
"boundaries" => @boundaries.map { |k, b| [k, b.map(&:as_json)] }.to_h,
|
76
|
-
}
|
176
|
+
@executables.keys.reject { _1 == SUPERGRAPH_LOCATION }
|
77
177
|
end
|
78
178
|
|
79
179
|
def memoized_introspection_types
|
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.0
|
4
|
+
version: 1.1.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-12-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|