graphql-stitching 1.2.5 → 1.4.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 +67 -17
- data/docs/README.md +2 -1
- data/docs/mechanics.md +2 -1
- data/docs/resolver.md +101 -0
- data/lib/graphql/stitching/client.rb +5 -1
- data/lib/graphql/stitching/composer/{boundary_config.rb → resolver_config.rb} +18 -13
- data/lib/graphql/stitching/composer/validate_interfaces.rb +4 -4
- data/lib/graphql/stitching/composer/validate_resolvers.rb +97 -0
- data/lib/graphql/stitching/composer.rb +107 -112
- data/lib/graphql/stitching/executor/{boundary_source.rb → resolver_source.rb} +40 -32
- data/lib/graphql/stitching/executor.rb +3 -3
- data/lib/graphql/stitching/plan.rb +3 -4
- data/lib/graphql/stitching/planner.rb +30 -41
- data/lib/graphql/stitching/planner_step.rb +6 -6
- data/lib/graphql/stitching/resolver/arguments.rb +284 -0
- data/lib/graphql/stitching/resolver/keys.rb +206 -0
- data/lib/graphql/stitching/resolver.rb +70 -0
- data/lib/graphql/stitching/shaper.rb +3 -3
- data/lib/graphql/stitching/skip_include.rb +1 -1
- data/lib/graphql/stitching/supergraph/key_directive.rb +13 -0
- data/lib/graphql/stitching/supergraph/resolver_directive.rb +4 -4
- data/lib/graphql/stitching/supergraph/to_definition.rb +165 -0
- data/lib/graphql/stitching/supergraph.rb +31 -144
- data/lib/graphql/stitching/util.rb +28 -0
- data/lib/graphql/stitching/version.rb +1 -1
- data/lib/graphql/stitching.rb +3 -2
- metadata +11 -7
- data/lib/graphql/stitching/boundary.rb +0 -29
- data/lib/graphql/stitching/composer/validate_boundaries.rb +0 -96
- data/lib/graphql/stitching/export_selection.rb +0 -42
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./resolver/arguments"
|
4
|
+
require_relative "./resolver/keys"
|
5
|
+
|
6
|
+
module GraphQL
|
7
|
+
module Stitching
|
8
|
+
# Defines a root resolver query that provides direct access to an entity type.
|
9
|
+
class Resolver
|
10
|
+
extend ArgumentsParser
|
11
|
+
extend KeysParser
|
12
|
+
|
13
|
+
# location name providing the resolver query.
|
14
|
+
attr_reader :location
|
15
|
+
|
16
|
+
# name of merged type fulfilled through this resolver.
|
17
|
+
attr_reader :type_name
|
18
|
+
|
19
|
+
# name of the root field to query.
|
20
|
+
attr_reader :field
|
21
|
+
|
22
|
+
# a key field to select from prior locations, sent as resolver argument.
|
23
|
+
attr_reader :key
|
24
|
+
|
25
|
+
# parsed resolver Argument structures.
|
26
|
+
attr_reader :arguments
|
27
|
+
|
28
|
+
def initialize(
|
29
|
+
location:,
|
30
|
+
type_name: nil,
|
31
|
+
list: false,
|
32
|
+
field: nil,
|
33
|
+
key: nil,
|
34
|
+
arguments: nil
|
35
|
+
)
|
36
|
+
@location = location
|
37
|
+
@type_name = type_name
|
38
|
+
@list = list
|
39
|
+
@field = field
|
40
|
+
@key = key
|
41
|
+
@arguments = arguments
|
42
|
+
end
|
43
|
+
|
44
|
+
# specifies when the resolver is a list query.
|
45
|
+
def list?
|
46
|
+
@list
|
47
|
+
end
|
48
|
+
|
49
|
+
def version
|
50
|
+
@version ||= Digest::SHA2.hexdigest(as_json.to_json)
|
51
|
+
end
|
52
|
+
|
53
|
+
def ==(other)
|
54
|
+
self.class == other.class && self.as_json == other.as_json
|
55
|
+
end
|
56
|
+
|
57
|
+
def as_json
|
58
|
+
{
|
59
|
+
location: location,
|
60
|
+
type_name: type_name,
|
61
|
+
list: list?,
|
62
|
+
field: field,
|
63
|
+
key: key.to_definition,
|
64
|
+
arguments: arguments.map(&:to_definition).join(", "),
|
65
|
+
argument_types: arguments.map(&:to_type_definition).join(", "),
|
66
|
+
}.tap(&:compact!)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -23,8 +23,8 @@ module GraphQL
|
|
23
23
|
def resolve_object_scope(raw_object, parent_type, selections, typename = nil)
|
24
24
|
return nil if raw_object.nil?
|
25
25
|
|
26
|
-
typename ||= raw_object[
|
27
|
-
raw_object.reject! { |key, _v|
|
26
|
+
typename ||= raw_object[Resolver::TYPENAME_EXPORT_NODE.alias]
|
27
|
+
raw_object.reject! { |key, _v| Resolver.export_key?(key) }
|
28
28
|
|
29
29
|
selections.each do |node|
|
30
30
|
case node
|
@@ -64,7 +64,7 @@ module GraphQL
|
|
64
64
|
return nil if result.nil?
|
65
65
|
|
66
66
|
else
|
67
|
-
raise "Unexpected node of type #{node.class.name} in selection set."
|
67
|
+
raise StitchingError, "Unexpected node of type #{node.class.name} in selection set."
|
68
68
|
end
|
69
69
|
end
|
70
70
|
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL::Stitching
|
4
|
+
class Supergraph
|
5
|
+
class KeyDirective < GraphQL::Schema::Directive
|
6
|
+
graphql_name "key"
|
7
|
+
locations OBJECT, INTERFACE, UNION
|
8
|
+
argument :key, String, required: true
|
9
|
+
argument :location, String, required: true
|
10
|
+
repeatable true
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -5,13 +5,13 @@ module GraphQL::Stitching
|
|
5
5
|
class ResolverDirective < GraphQL::Schema::Directive
|
6
6
|
graphql_name "resolver"
|
7
7
|
locations OBJECT, INTERFACE, UNION
|
8
|
-
argument :type_name, String, required: false
|
9
8
|
argument :location, String, required: true
|
9
|
+
argument :list, Boolean, required: false
|
10
10
|
argument :key, String, required: true
|
11
11
|
argument :field, String, required: true
|
12
|
-
argument :
|
13
|
-
argument :
|
14
|
-
argument :
|
12
|
+
argument :arguments, String, required: true
|
13
|
+
argument :argument_types, String, required: true
|
14
|
+
argument :type_name, String, required: false
|
15
15
|
repeatable true
|
16
16
|
end
|
17
17
|
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative "./key_directive"
|
3
|
+
require_relative "./resolver_directive"
|
4
|
+
require_relative "./source_directive"
|
5
|
+
|
6
|
+
module GraphQL::Stitching
|
7
|
+
class Supergraph
|
8
|
+
class << self
|
9
|
+
def validate_executable!(location, executable)
|
10
|
+
return true if executable.is_a?(Class) && executable <= GraphQL::Schema
|
11
|
+
return true if executable && executable.respond_to?(:call)
|
12
|
+
raise StitchingError, "Invalid executable provided for location `#{location}`."
|
13
|
+
end
|
14
|
+
|
15
|
+
def from_definition(schema, executables:)
|
16
|
+
schema = GraphQL::Schema.from_definition(schema) if schema.is_a?(String)
|
17
|
+
field_map = {}
|
18
|
+
resolver_map = {}
|
19
|
+
possible_locations = {}
|
20
|
+
introspection_types = schema.introspection_system.types.keys
|
21
|
+
|
22
|
+
schema.types.each do |type_name, type|
|
23
|
+
next if introspection_types.include?(type_name)
|
24
|
+
|
25
|
+
# Collect/build key definitions for each type
|
26
|
+
locations_by_key = type.directives.each_with_object({}) do |directive, memo|
|
27
|
+
next unless directive.graphql_name == KeyDirective.graphql_name
|
28
|
+
|
29
|
+
kwargs = directive.arguments.keyword_arguments
|
30
|
+
memo[kwargs[:key]] ||= []
|
31
|
+
memo[kwargs[:key]] << kwargs[:location]
|
32
|
+
end
|
33
|
+
|
34
|
+
key_definitions = locations_by_key.each_with_object({}) do |(key, locations), memo|
|
35
|
+
memo[key] = Resolver.parse_key(key, locations)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Collect/build resolver definitions for each type
|
39
|
+
type.directives.each do |directive|
|
40
|
+
next unless directive.graphql_name == ResolverDirective.graphql_name
|
41
|
+
|
42
|
+
kwargs = directive.arguments.keyword_arguments
|
43
|
+
resolver_map[type_name] ||= []
|
44
|
+
resolver_map[type_name] << Resolver.new(
|
45
|
+
location: kwargs[:location],
|
46
|
+
type_name: kwargs.fetch(:type_name, type_name),
|
47
|
+
field: kwargs[:field],
|
48
|
+
list: kwargs[:list] || false,
|
49
|
+
key: key_definitions[kwargs[:key]],
|
50
|
+
arguments: Resolver.parse_arguments_with_type_defs(kwargs[:arguments], kwargs[:argument_types]),
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
next unless type.kind.fields?
|
55
|
+
|
56
|
+
type.fields.each do |field_name, field|
|
57
|
+
# Collection locations for each field definition
|
58
|
+
field.directives.each do |d|
|
59
|
+
next unless d.graphql_name == SourceDirective.graphql_name
|
60
|
+
|
61
|
+
location = d.arguments.keyword_arguments[:location]
|
62
|
+
field_map[type_name] ||= {}
|
63
|
+
field_map[type_name][field_name] ||= []
|
64
|
+
field_map[type_name][field_name] << location
|
65
|
+
possible_locations[location] = true
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
executables = possible_locations.keys.each_with_object({}) do |location, memo|
|
71
|
+
executable = executables[location] || executables[location.to_sym]
|
72
|
+
if validate_executable!(location, executable)
|
73
|
+
memo[location] = executable
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
new(
|
78
|
+
schema: schema,
|
79
|
+
fields: field_map,
|
80
|
+
resolvers: resolver_map,
|
81
|
+
executables: executables,
|
82
|
+
)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def to_definition
|
87
|
+
if @schema.directives[KeyDirective.graphql_name].nil?
|
88
|
+
@schema.directive(KeyDirective)
|
89
|
+
end
|
90
|
+
if @schema.directives[ResolverDirective.graphql_name].nil?
|
91
|
+
@schema.directive(ResolverDirective)
|
92
|
+
end
|
93
|
+
if @schema.directives[SourceDirective.graphql_name].nil?
|
94
|
+
@schema.directive(SourceDirective)
|
95
|
+
end
|
96
|
+
|
97
|
+
@schema.types.each do |type_name, type|
|
98
|
+
if resolvers_for_type = @resolvers.dig(type_name)
|
99
|
+
# Apply key directives for each unique type/key/location
|
100
|
+
# (this allows keys to be composite selections and/or omitted from the supergraph schema)
|
101
|
+
keys_for_type = resolvers_for_type.each_with_object({}) do |resolver, memo|
|
102
|
+
memo[resolver.key.to_definition] ||= Set.new
|
103
|
+
memo[resolver.key.to_definition].merge(resolver.key.locations)
|
104
|
+
end
|
105
|
+
|
106
|
+
keys_for_type.each do |key, locations|
|
107
|
+
locations.each do |location|
|
108
|
+
params = { key: key, location: location }
|
109
|
+
|
110
|
+
unless has_directive?(type, KeyDirective.graphql_name, params)
|
111
|
+
type.directive(KeyDirective, **params)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Apply resolver directives for each unique query resolver
|
117
|
+
resolvers_for_type.each do |resolver|
|
118
|
+
params = {
|
119
|
+
location: resolver.location,
|
120
|
+
field: resolver.field,
|
121
|
+
list: resolver.list? || nil,
|
122
|
+
key: resolver.key.to_definition,
|
123
|
+
arguments: resolver.arguments.map(&:to_definition).join(", "),
|
124
|
+
argument_types: resolver.arguments.map(&:to_type_definition).join(", "),
|
125
|
+
type_name: (resolver.type_name if resolver.type_name != type_name),
|
126
|
+
}
|
127
|
+
|
128
|
+
unless has_directive?(type, ResolverDirective.graphql_name, params)
|
129
|
+
type.directive(ResolverDirective, **params.tap(&:compact!))
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
next unless type.kind.fields?
|
135
|
+
|
136
|
+
type.fields.each do |field_name, field|
|
137
|
+
locations_for_field = @locations_by_type_and_field.dig(type_name, field_name)
|
138
|
+
next if locations_for_field.nil?
|
139
|
+
|
140
|
+
# Apply source directives to annotate the possible locations of each field
|
141
|
+
locations_for_field.each do |location|
|
142
|
+
params = { location: location }
|
143
|
+
|
144
|
+
unless has_directive?(field, SourceDirective.graphql_name, params)
|
145
|
+
field.directive(SourceDirective, **params)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
@schema.to_definition
|
152
|
+
end
|
153
|
+
|
154
|
+
private
|
155
|
+
|
156
|
+
def has_directive?(element, directive_name, params)
|
157
|
+
existing = element.directives.find do |d|
|
158
|
+
kwargs = d.arguments.keyword_arguments
|
159
|
+
d.graphql_name == directive_name && params.all? { |k, v| kwargs[k] == v }
|
160
|
+
end
|
161
|
+
|
162
|
+
!existing.nil?
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -1,90 +1,26 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "./supergraph/
|
4
|
-
require_relative "./supergraph/source_directive"
|
3
|
+
require_relative "./supergraph/to_definition"
|
5
4
|
|
6
5
|
module GraphQL
|
7
6
|
module Stitching
|
8
7
|
class Supergraph
|
9
8
|
SUPERGRAPH_LOCATION = "__super"
|
10
9
|
|
11
|
-
class << self
|
12
|
-
def validate_executable!(location, executable)
|
13
|
-
return true if executable.is_a?(Class) && executable <= GraphQL::Schema
|
14
|
-
return true if executable && executable.respond_to?(:call)
|
15
|
-
raise StitchingError, "Invalid executable provided for location `#{location}`."
|
16
|
-
end
|
17
|
-
|
18
|
-
def from_definition(schema, executables:)
|
19
|
-
schema = GraphQL::Schema.from_definition(schema) if schema.is_a?(String)
|
20
|
-
field_map = {}
|
21
|
-
boundary_map = {}
|
22
|
-
possible_locations = {}
|
23
|
-
introspection_types = schema.introspection_system.types.keys
|
24
|
-
|
25
|
-
schema.types.each do |type_name, type|
|
26
|
-
next if introspection_types.include?(type_name)
|
27
|
-
|
28
|
-
type.directives.each do |directive|
|
29
|
-
next unless directive.graphql_name == ResolverDirective.graphql_name
|
30
|
-
|
31
|
-
kwargs = directive.arguments.keyword_arguments
|
32
|
-
boundary_map[type_name] ||= []
|
33
|
-
boundary_map[type_name] << Boundary.new(
|
34
|
-
type_name: kwargs.fetch(:type_name, type_name),
|
35
|
-
location: kwargs[:location],
|
36
|
-
key: kwargs[:key],
|
37
|
-
field: kwargs[:field],
|
38
|
-
arg: kwargs[:arg],
|
39
|
-
list: kwargs[:list] || false,
|
40
|
-
federation: kwargs[:federation] || false,
|
41
|
-
)
|
42
|
-
end
|
43
|
-
|
44
|
-
next unless type.kind.fields?
|
45
|
-
|
46
|
-
type.fields.each do |field_name, field|
|
47
|
-
field.directives.each do |d|
|
48
|
-
next unless d.graphql_name == SourceDirective.graphql_name
|
49
|
-
|
50
|
-
location = d.arguments.keyword_arguments[:location]
|
51
|
-
field_map[type_name] ||= {}
|
52
|
-
field_map[type_name][field_name] ||= []
|
53
|
-
field_map[type_name][field_name] << location
|
54
|
-
possible_locations[location] = true
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
executables = possible_locations.keys.each_with_object({}) do |location, memo|
|
60
|
-
executable = executables[location] || executables[location.to_sym]
|
61
|
-
if validate_executable!(location, executable)
|
62
|
-
memo[location] = executable
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
new(
|
67
|
-
schema: schema,
|
68
|
-
fields: field_map,
|
69
|
-
boundaries: boundary_map,
|
70
|
-
executables: executables,
|
71
|
-
)
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
10
|
# @return [GraphQL::Schema] the composed schema for the supergraph.
|
76
11
|
attr_reader :schema
|
77
12
|
|
78
13
|
# @return [Hash<String, Executable>] a map of executable resources by location.
|
79
14
|
attr_reader :executables
|
80
15
|
|
81
|
-
attr_reader :
|
16
|
+
attr_reader :resolvers, :locations_by_type_and_field
|
82
17
|
|
83
|
-
def initialize(schema:, fields: {},
|
18
|
+
def initialize(schema:, fields: {}, resolvers: {}, executables: {})
|
84
19
|
@schema = schema
|
85
20
|
@schema.use(GraphQL::Schema::AlwaysVisible)
|
86
21
|
|
87
|
-
@
|
22
|
+
@resolvers = resolvers
|
23
|
+
@resolvers_by_version = nil
|
88
24
|
@fields_by_type_and_location = nil
|
89
25
|
@locations_by_type = nil
|
90
26
|
@memoized_introspection_types = nil
|
@@ -111,65 +47,17 @@ module GraphQL
|
|
111
47
|
end.freeze
|
112
48
|
end
|
113
49
|
|
114
|
-
def to_definition
|
115
|
-
if @schema.directives[ResolverDirective.graphql_name].nil?
|
116
|
-
@schema.directive(ResolverDirective)
|
117
|
-
end
|
118
|
-
if @schema.directives[SourceDirective.graphql_name].nil?
|
119
|
-
@schema.directive(SourceDirective)
|
120
|
-
end
|
121
|
-
|
122
|
-
@schema.types.each do |type_name, type|
|
123
|
-
if boundaries_for_type = @boundaries.dig(type_name)
|
124
|
-
boundaries_for_type.each do |boundary|
|
125
|
-
existing = type.directives.find do |d|
|
126
|
-
kwargs = d.arguments.keyword_arguments
|
127
|
-
d.graphql_name == ResolverDirective.graphql_name &&
|
128
|
-
kwargs[:location] == boundary.location &&
|
129
|
-
kwargs[:key] == boundary.key &&
|
130
|
-
kwargs[:field] == boundary.field &&
|
131
|
-
kwargs[:arg] == boundary.arg &&
|
132
|
-
kwargs.fetch(:list, false) == boundary.list &&
|
133
|
-
kwargs.fetch(:federation, false) == boundary.federation
|
134
|
-
end
|
135
|
-
|
136
|
-
type.directive(ResolverDirective, **{
|
137
|
-
type_name: (boundary.type_name if boundary.type_name != type_name),
|
138
|
-
location: boundary.location,
|
139
|
-
key: boundary.key,
|
140
|
-
field: boundary.field,
|
141
|
-
arg: boundary.arg,
|
142
|
-
list: boundary.list || nil,
|
143
|
-
federation: boundary.federation || nil,
|
144
|
-
}.tap(&:compact!)) if existing.nil?
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
next unless type.kind.fields?
|
149
|
-
|
150
|
-
type.fields.each do |field_name, field|
|
151
|
-
locations_for_field = @locations_by_type_and_field.dig(type_name, field_name)
|
152
|
-
next if locations_for_field.nil?
|
153
|
-
|
154
|
-
locations_for_field.each do |location|
|
155
|
-
existing = field.directives.find do |d|
|
156
|
-
d.graphql_name == SourceDirective.graphql_name &&
|
157
|
-
d.arguments.keyword_arguments[:location] == location
|
158
|
-
end
|
159
|
-
|
160
|
-
field.directive(SourceDirective, location: location) if existing.nil?
|
161
|
-
end
|
162
|
-
end
|
163
|
-
end
|
164
|
-
|
165
|
-
@schema.to_definition
|
166
|
-
end
|
167
|
-
|
168
50
|
# @return [GraphQL::StaticValidation::Validator] static validator for the supergraph schema.
|
169
51
|
def static_validator
|
170
52
|
@static_validator ||= @schema.static_validator
|
171
53
|
end
|
172
54
|
|
55
|
+
def resolvers_by_version
|
56
|
+
@resolvers_by_version ||= resolvers.values.tap(&:flatten!).each_with_object({}) do |resolver, memo|
|
57
|
+
memo[resolver.version] = resolver
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
173
61
|
def fields
|
174
62
|
@locations_by_type_and_field.reject { |k, _v| memoized_introspection_types[k] }
|
175
63
|
end
|
@@ -242,37 +130,36 @@ module GraphQL
|
|
242
130
|
end
|
243
131
|
end
|
244
132
|
|
245
|
-
# collects all possible
|
246
|
-
# ("Type") => ["id", ...]
|
133
|
+
# collects all possible resolver keys for a given type
|
134
|
+
# ("Type") => [Key("id"), ...]
|
247
135
|
def possible_keys_for_type(type_name)
|
248
136
|
@possible_keys_by_type[type_name] ||= begin
|
249
137
|
if type_name == @schema.query.graphql_name
|
250
138
|
GraphQL::Stitching::EMPTY_ARRAY
|
251
139
|
else
|
252
|
-
@
|
140
|
+
@resolvers[type_name].map(&:key).uniq(&:to_definition)
|
253
141
|
end
|
254
142
|
end
|
255
143
|
end
|
256
144
|
|
257
|
-
# collects possible
|
258
|
-
# ("Type", "location") => ["id", ...]
|
145
|
+
# collects possible resolver keys for a given type and location
|
146
|
+
# ("Type", "location") => [Key("id"), ...]
|
259
147
|
def possible_keys_for_type_and_location(type_name, location)
|
260
148
|
possible_keys_by_type = @possible_keys_by_type_and_location[type_name] ||= {}
|
261
|
-
possible_keys_by_type[location] ||=
|
262
|
-
|
263
|
-
location_fields & possible_keys_for_type(type_name)
|
149
|
+
possible_keys_by_type[location] ||= possible_keys_for_type(type_name).select do |key|
|
150
|
+
key.locations.include?(location)
|
264
151
|
end
|
265
152
|
end
|
266
153
|
|
267
154
|
# For a given type, route from one origin location to one or more remote locations
|
268
|
-
# used to connect a partial type across locations via
|
155
|
+
# used to connect a partial type across locations via resolver queries
|
269
156
|
def route_type_to_locations(type_name, start_location, goal_locations)
|
270
157
|
key_count = possible_keys_for_type(type_name).length
|
271
158
|
|
272
159
|
if key_count.zero?
|
273
|
-
# nested root scopes have no
|
160
|
+
# nested root scopes have no resolver keys and just return a location
|
274
161
|
goal_locations.each_with_object({}) do |goal_location, memo|
|
275
|
-
memo[goal_location] = [
|
162
|
+
memo[goal_location] = [Resolver.new(location: goal_location)]
|
276
163
|
end
|
277
164
|
|
278
165
|
elsif key_count > 1
|
@@ -281,10 +168,10 @@ module GraphQL
|
|
281
168
|
|
282
169
|
else
|
283
170
|
# types with a single key attribute must all be within a single hop of each other,
|
284
|
-
# so can use a simple match to collect
|
285
|
-
@
|
286
|
-
if goal_locations.include?(
|
287
|
-
memo[
|
171
|
+
# so can use a simple match to collect resolvers for the goal locations.
|
172
|
+
@resolvers[type_name].each_with_object({}) do |resolver, memo|
|
173
|
+
if goal_locations.include?(resolver.location)
|
174
|
+
memo[resolver.location] = [resolver]
|
288
175
|
end
|
289
176
|
end
|
290
177
|
end
|
@@ -292,7 +179,7 @@ module GraphQL
|
|
292
179
|
|
293
180
|
private
|
294
181
|
|
295
|
-
PathNode = Struct.new(:location, :key, :cost, :
|
182
|
+
PathNode = Struct.new(:location, :key, :cost, :resolver, keyword_init: true)
|
296
183
|
|
297
184
|
# tunes A* search to favor paths with fewest joining locations, ie:
|
298
185
|
# favor longer paths through target locations over shorter paths with additional locations.
|
@@ -310,9 +197,9 @@ module GraphQL
|
|
310
197
|
current_key = path.last.key
|
311
198
|
current_cost = path.last.cost
|
312
199
|
|
313
|
-
@
|
314
|
-
forward_location =
|
315
|
-
next if current_key !=
|
200
|
+
@resolvers[type_name].each do |resolver|
|
201
|
+
forward_location = resolver.location
|
202
|
+
next if current_key != resolver.key
|
316
203
|
next if path.any? { _1.location == forward_location }
|
317
204
|
|
318
205
|
best_cost = costs[forward_location] || Float::INFINITY
|
@@ -323,13 +210,13 @@ module GraphQL
|
|
323
210
|
location: current_location,
|
324
211
|
key: current_key,
|
325
212
|
cost: current_cost,
|
326
|
-
|
213
|
+
resolver: resolver,
|
327
214
|
)
|
328
215
|
|
329
216
|
if goal_locations.include?(forward_location)
|
330
217
|
current_result = results[forward_location]
|
331
218
|
if current_result.nil? || current_cost < best_cost || (current_cost == best_cost && path.length < current_result.length)
|
332
|
-
results[forward_location] = path.map(&:
|
219
|
+
results[forward_location] = path.map(&:resolver)
|
333
220
|
end
|
334
221
|
else
|
335
222
|
path.last.cost += 1
|
@@ -47,6 +47,34 @@ module GraphQL
|
|
47
47
|
structure
|
48
48
|
end
|
49
49
|
|
50
|
+
# builds a single-dimensional representation of a wrapped type structure from AST
|
51
|
+
def flatten_ast_type_structure(ast, structure: [])
|
52
|
+
null = true
|
53
|
+
|
54
|
+
while ast.is_a?(GraphQL::Language::Nodes::NonNullType)
|
55
|
+
ast = ast.of_type
|
56
|
+
null = false
|
57
|
+
end
|
58
|
+
|
59
|
+
if ast.is_a?(GraphQL::Language::Nodes::ListType)
|
60
|
+
structure << TypeStructure.new(
|
61
|
+
list: true,
|
62
|
+
null: null,
|
63
|
+
name: nil,
|
64
|
+
)
|
65
|
+
|
66
|
+
flatten_ast_type_structure(ast.of_type, structure: structure)
|
67
|
+
else
|
68
|
+
structure << TypeStructure.new(
|
69
|
+
list: false,
|
70
|
+
null: null,
|
71
|
+
name: ast.name,
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
structure
|
76
|
+
end
|
77
|
+
|
50
78
|
# expands interfaces and unions to an array of their memberships
|
51
79
|
# like `schema.possible_types`, but includes child interfaces
|
52
80
|
def expand_abstract_type(schema, parent_type)
|
data/lib/graphql/stitching.rb
CHANGED
@@ -8,6 +8,8 @@ module GraphQL
|
|
8
8
|
EMPTY_ARRAY = [].freeze
|
9
9
|
|
10
10
|
class StitchingError < StandardError; end
|
11
|
+
class CompositionError < StitchingError; end
|
12
|
+
class ValidationError < CompositionError; end
|
11
13
|
|
12
14
|
class << self
|
13
15
|
def stitch_directive
|
@@ -24,11 +26,10 @@ module GraphQL
|
|
24
26
|
end
|
25
27
|
|
26
28
|
require_relative "stitching/supergraph"
|
27
|
-
require_relative "stitching/
|
29
|
+
require_relative "stitching/resolver"
|
28
30
|
require_relative "stitching/client"
|
29
31
|
require_relative "stitching/composer"
|
30
32
|
require_relative "stitching/executor"
|
31
|
-
require_relative "stitching/export_selection"
|
32
33
|
require_relative "stitching/http_executable"
|
33
34
|
require_relative "stitching/plan"
|
34
35
|
require_relative "stitching/planner_step"
|