graphql-stitching 1.0.5 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/docs/client.md +11 -13
- 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 +9 -8
- data/lib/graphql/stitching/executor/boundary_source.rb +2 -1
- data/lib/graphql/stitching/planner.rb +1 -1
- data/lib/graphql/stitching/request.rb +18 -14
- data/lib/graphql/stitching/skip_include.rb +5 -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 {
|
@@ -78,7 +76,7 @@ query {
|
|
78
76
|
}
|
79
77
|
```
|
80
78
|
|
81
|
-
Instead,
|
79
|
+
Instead, leverage query variables so that the document body remains consistent across requests:
|
82
80
|
|
83
81
|
```graphql
|
84
82
|
query($id: ID!) {
|
@@ -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
|
@@ -53,7 +53,7 @@ module GraphQL
|
|
53
53
|
end
|
54
54
|
end
|
55
55
|
|
56
|
-
# "
|
56
|
+
# "directive_name" => merged_directive
|
57
57
|
@schema_directives = @candidate_directives_by_name_and_location.each_with_object({}) do |(directive_name, directives_by_location), memo|
|
58
58
|
memo[directive_name] = build_directive(directive_name, directives_by_location)
|
59
59
|
end
|
@@ -88,19 +88,20 @@ module GraphQL
|
|
88
88
|
|
89
89
|
# "Typename" => merged_type
|
90
90
|
schema_types = @candidate_types_by_name_and_location.each_with_object({}) do |(type_name, types_by_location), memo|
|
91
|
-
kinds = types_by_location.values.map { _1.kind.name }.uniq
|
91
|
+
kinds = types_by_location.values.map { _1.kind.name }.tap(&:uniq!)
|
92
92
|
|
93
93
|
if kinds.length > 1
|
94
94
|
raise ComposerError, "Cannot merge different kinds for `#{type_name}`. Found: #{kinds.join(", ")}."
|
95
95
|
end
|
96
96
|
|
97
|
+
extract_boundaries(type_name, types_by_location) if type_name == @query_name
|
98
|
+
|
97
99
|
memo[type_name] = case kinds.first
|
98
100
|
when "SCALAR"
|
99
101
|
build_scalar_type(type_name, types_by_location)
|
100
102
|
when "ENUM"
|
101
103
|
build_enum_type(type_name, types_by_location, enum_usage)
|
102
104
|
when "OBJECT"
|
103
|
-
extract_boundaries(type_name, types_by_location) if type_name == @query_name
|
104
105
|
build_object_type(type_name, types_by_location)
|
105
106
|
when "INTERFACE"
|
106
107
|
build_interface_type(type_name, types_by_location)
|
@@ -200,7 +201,7 @@ module GraphQL
|
|
200
201
|
graphql_name(directive_name)
|
201
202
|
description(builder.merge_descriptions(directive_name, directives_by_location))
|
202
203
|
repeatable(directives_by_location.values.any?(&:repeatable?))
|
203
|
-
locations(*directives_by_location.values.flat_map(&:locations).uniq)
|
204
|
+
locations(*directives_by_location.values.flat_map(&:locations).tap(&:uniq!))
|
204
205
|
builder.build_merged_arguments(directive_name, directives_by_location, self, directive_name: directive_name)
|
205
206
|
end
|
206
207
|
end
|
@@ -262,7 +263,7 @@ module GraphQL
|
|
262
263
|
description(builder.merge_descriptions(type_name, types_by_location))
|
263
264
|
|
264
265
|
interface_names = types_by_location.values.flat_map { _1.interfaces.map(&:graphql_name) }
|
265
|
-
interface_names.uniq.each do |interface_name|
|
266
|
+
interface_names.tap(&:uniq!).each do |interface_name|
|
266
267
|
implements(builder.build_type_binding(interface_name))
|
267
268
|
end
|
268
269
|
|
@@ -280,7 +281,7 @@ module GraphQL
|
|
280
281
|
description(builder.merge_descriptions(type_name, types_by_location))
|
281
282
|
|
282
283
|
interface_names = types_by_location.values.flat_map { _1.interfaces.map(&:graphql_name) }
|
283
|
-
interface_names.uniq.each do |interface_name|
|
284
|
+
interface_names.tap(&:uniq!).each do |interface_name|
|
284
285
|
implements(builder.build_type_binding(interface_name))
|
285
286
|
end
|
286
287
|
|
@@ -296,7 +297,7 @@ module GraphQL
|
|
296
297
|
graphql_name(type_name)
|
297
298
|
description(builder.merge_descriptions(type_name, types_by_location))
|
298
299
|
|
299
|
-
possible_names = types_by_location.values.flat_map { _1.possible_types.map(&:graphql_name) }.uniq
|
300
|
+
possible_names = types_by_location.values.flat_map { _1.possible_types.map(&:graphql_name) }.tap(&:uniq!)
|
300
301
|
possible_types(*possible_names.map { builder.build_type_binding(_1) })
|
301
302
|
builder.build_merged_directives(type_name, types_by_location, self)
|
302
303
|
end
|
@@ -540,7 +541,7 @@ module GraphQL
|
|
540
541
|
field: field_candidate.name,
|
541
542
|
arg: argument_name,
|
542
543
|
list: boundary_structure.first.list?,
|
543
|
-
federation: kwargs[:federation],
|
544
|
+
federation: kwargs[:federation] || false,
|
544
545
|
)
|
545
546
|
end
|
546
547
|
end
|
@@ -4,14 +4,13 @@ module GraphQL
|
|
4
4
|
module Stitching
|
5
5
|
class Request
|
6
6
|
SUPPORTED_OPERATIONS = ["query", "mutation"].freeze
|
7
|
+
SKIP_INCLUDE_DIRECTIVE = /@(?:skip|include)/
|
7
8
|
|
8
9
|
attr_reader :document, :variables, :operation_name, :context
|
9
10
|
|
10
11
|
def initialize(document, operation_name: nil, variables: nil, context: nil)
|
11
|
-
@may_contain_runtime_directives = true
|
12
|
-
|
13
12
|
@document = if document.is_a?(String)
|
14
|
-
@
|
13
|
+
@string = document
|
15
14
|
GraphQL.parse(document)
|
16
15
|
else
|
17
16
|
document
|
@@ -23,13 +22,21 @@ module GraphQL
|
|
23
22
|
end
|
24
23
|
|
25
24
|
def string
|
26
|
-
@string
|
25
|
+
@string || normalized_string
|
26
|
+
end
|
27
|
+
|
28
|
+
def normalized_string
|
29
|
+
@normalized_string ||= @document.to_query_string
|
27
30
|
end
|
28
31
|
|
29
32
|
def digest
|
30
33
|
@digest ||= Digest::SHA2.hexdigest(string)
|
31
34
|
end
|
32
35
|
|
36
|
+
def normalized_digest
|
37
|
+
@normalized_digest ||= Digest::SHA2.hexdigest(normalized_string)
|
38
|
+
end
|
39
|
+
|
33
40
|
def operation
|
34
41
|
@operation ||= begin
|
35
42
|
operation_defs = @document.definitions.select do |d|
|
@@ -69,18 +76,15 @@ module GraphQL
|
|
69
76
|
|
70
77
|
def prepare!
|
71
78
|
operation.variables.each do |v|
|
72
|
-
@variables[v.name] = v.default_value if @variables[v.name].nil?
|
79
|
+
@variables[v.name] = v.default_value if @variables[v.name].nil? && !v.default_value.nil?
|
73
80
|
end
|
74
81
|
|
75
|
-
if @
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
@
|
80
|
-
@
|
81
|
-
@operation = nil
|
82
|
-
@variable_definitions = nil
|
83
|
-
@fragment_definitions = nil
|
82
|
+
if @string.nil? || @string.match?(SKIP_INCLUDE_DIRECTIVE)
|
83
|
+
SkipInclude.render(@document, @variables) do |modified_ast|
|
84
|
+
@document = modified_ast
|
85
|
+
@string = @normalized_string = nil
|
86
|
+
@digest = @normalized_digest = nil
|
87
|
+
@operation = @operation_directives = @variable_definitions = nil
|
84
88
|
end
|
85
89
|
end
|
86
90
|
|
@@ -15,7 +15,11 @@ module GraphQL
|
|
15
15
|
definition
|
16
16
|
end
|
17
17
|
|
18
|
-
return document
|
18
|
+
return document unless changed
|
19
|
+
|
20
|
+
document = document.merge(definitions: definitions)
|
21
|
+
yield(document) if block_given?
|
22
|
+
document
|
19
23
|
end
|
20
24
|
|
21
25
|
private
|
@@ -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
|