graphql-stitching 1.3.0 → 1.4.1
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 +57 -7
- data/docs/resolver.md +101 -0
- data/lib/graphql/stitching/client.rb +5 -1
- data/lib/graphql/stitching/composer/resolver_config.rb +17 -12
- data/lib/graphql/stitching/composer/validate_interfaces.rb +4 -4
- data/lib/graphql/stitching/composer/validate_resolvers.rb +23 -22
- data/lib/graphql/stitching/composer.rb +77 -83
- data/lib/graphql/stitching/executor/resolver_source.rb +25 -26
- data/lib/graphql/stitching/plan.rb +2 -3
- data/lib/graphql/stitching/planner.rb +11 -22
- data/lib/graphql/stitching/planner_step.rb +1 -1
- data/lib/graphql/stitching/resolver/arguments.rb +284 -0
- data/lib/graphql/stitching/resolver/keys.rb +206 -0
- data/lib/graphql/stitching/resolver.rb +44 -23
- 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 -5
- data/lib/graphql/stitching/supergraph/to_definition.rb +165 -0
- data/lib/graphql/stitching/supergraph.rb +13 -128
- data/lib/graphql/stitching/util.rb +28 -0
- data/lib/graphql/stitching/version.rb +1 -1
- data/lib/graphql/stitching.rb +2 -1
- metadata +7 -3
- data/lib/graphql/stitching/export_selection.rb +0 -42
@@ -0,0 +1,284 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL::Stitching
|
4
|
+
class Resolver
|
5
|
+
# Defines a single resolver argument structure
|
6
|
+
# @api private
|
7
|
+
class Argument
|
8
|
+
attr_reader :name
|
9
|
+
attr_reader :value
|
10
|
+
attr_reader :type_name
|
11
|
+
|
12
|
+
def initialize(name:, value:, list: false, type_name: nil)
|
13
|
+
@name = name
|
14
|
+
@value = value
|
15
|
+
@list = list
|
16
|
+
@type_name = type_name
|
17
|
+
end
|
18
|
+
|
19
|
+
def list?
|
20
|
+
@list
|
21
|
+
end
|
22
|
+
|
23
|
+
def key?
|
24
|
+
value.key?
|
25
|
+
end
|
26
|
+
|
27
|
+
def verify_key(key)
|
28
|
+
if key?
|
29
|
+
value.verify_key(self, key)
|
30
|
+
true
|
31
|
+
else
|
32
|
+
false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def ==(other)
|
37
|
+
self.class == other.class &&
|
38
|
+
@name == other.name &&
|
39
|
+
@value == other.value &&
|
40
|
+
@type_name == other.type_name &&
|
41
|
+
@list == other.list?
|
42
|
+
end
|
43
|
+
|
44
|
+
def build(origin_obj)
|
45
|
+
value.build(origin_obj)
|
46
|
+
end
|
47
|
+
|
48
|
+
def print
|
49
|
+
"#{name}: #{value.print}"
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_definition
|
53
|
+
print.gsub(%|"|, "'")
|
54
|
+
end
|
55
|
+
|
56
|
+
alias_method :to_s, :to_definition
|
57
|
+
|
58
|
+
def to_type_definition
|
59
|
+
"#{name}: #{to_type_signature}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_type_signature
|
63
|
+
# need to derive nullability...
|
64
|
+
list? ? "[#{@type_name}!]!" : "#{@type_name}!"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# An abstract argument input value
|
69
|
+
# @api private
|
70
|
+
class ArgumentValue
|
71
|
+
attr_reader :value
|
72
|
+
|
73
|
+
def initialize(value)
|
74
|
+
@value = value
|
75
|
+
end
|
76
|
+
|
77
|
+
def key?
|
78
|
+
false
|
79
|
+
end
|
80
|
+
|
81
|
+
def verify_key(arg, key)
|
82
|
+
nil
|
83
|
+
end
|
84
|
+
|
85
|
+
def ==(other)
|
86
|
+
self.class == other.class && value == other.value
|
87
|
+
end
|
88
|
+
|
89
|
+
def build(origin_obj)
|
90
|
+
value
|
91
|
+
end
|
92
|
+
|
93
|
+
def print
|
94
|
+
value
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# An object input value
|
99
|
+
# @api private
|
100
|
+
class ObjectArgumentValue < ArgumentValue
|
101
|
+
def key?
|
102
|
+
value.any?(&:key?)
|
103
|
+
end
|
104
|
+
|
105
|
+
def verify_key(arg, key)
|
106
|
+
value.each { _1.verify_key(key) }
|
107
|
+
end
|
108
|
+
|
109
|
+
def build(origin_obj)
|
110
|
+
value.each_with_object({}) do |arg, memo|
|
111
|
+
memo[arg.name] = arg.build(origin_obj)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def print
|
116
|
+
"{#{value.map(&:print).join(", ")}}"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# A key input value
|
121
|
+
# @api private
|
122
|
+
class KeyArgumentValue < ArgumentValue
|
123
|
+
def initialize(value)
|
124
|
+
super(Array(value))
|
125
|
+
end
|
126
|
+
|
127
|
+
def key?
|
128
|
+
true
|
129
|
+
end
|
130
|
+
|
131
|
+
def verify_key(arg, key)
|
132
|
+
key_field = value.reduce(Resolver::KeyField.new("", inner: key)) do |field, ns|
|
133
|
+
if ns == Resolver::TYPE_NAME
|
134
|
+
Resolver::KeyField.new(Resolver::TYPE_NAME)
|
135
|
+
elsif field
|
136
|
+
field.inner.find { _1.name == ns }
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# still not capturing enough type information to accurately compare key/arg types...
|
141
|
+
# best we can do for now is to verify the argument insertion matches a key path.
|
142
|
+
if key_field.nil?
|
143
|
+
raise CompositionError, "Argument `#{arg.name}: #{print}` cannot insert key `#{key.to_definition}`."
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def build(origin_obj)
|
148
|
+
value.each_with_index.reduce(origin_obj) do |obj, (ns, idx)|
|
149
|
+
obj[idx.zero? ? Resolver.export_key(ns) : ns]
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def print
|
154
|
+
"$.#{value.join(".")}"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# A typed enum input value
|
159
|
+
# @api private
|
160
|
+
class EnumArgumentValue < ArgumentValue
|
161
|
+
end
|
162
|
+
|
163
|
+
# A primitive input value literal
|
164
|
+
# @api private
|
165
|
+
class LiteralArgumentValue < ArgumentValue
|
166
|
+
def print
|
167
|
+
JSON.generate(value)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# Parser for building argument templates into resolver structures
|
172
|
+
# @api private
|
173
|
+
module ArgumentsParser
|
174
|
+
# Parses an argument template string into resolver arguments via schema casting.
|
175
|
+
# @param template [String] the template string to parse.
|
176
|
+
# @param field_def [GraphQL::Schema::FieldDefinition] a field definition providing arguments schema.
|
177
|
+
# @return [[GraphQL::Stitching::Resolver::Argument]] an array of resolver arguments.
|
178
|
+
def parse_arguments_with_field(template, field_def)
|
179
|
+
ast = parse_arg_defs(template)
|
180
|
+
args = build_argument_set(ast, field_def.arguments)
|
181
|
+
args.each do |arg|
|
182
|
+
next unless arg.key?
|
183
|
+
|
184
|
+
if field_def.type.list? && !arg.list?
|
185
|
+
raise CompositionError, "Cannot use repeatable key for `#{field_def.owner.graphql_name}.#{field_def.graphql_name}` " \
|
186
|
+
"in non-list argument `#{arg.name}`."
|
187
|
+
elsif !field_def.type.list? && arg.list?
|
188
|
+
raise CompositionError, "Cannot use non-repeatable key for `#{field_def.owner.graphql_name}.#{field_def.graphql_name}` " \
|
189
|
+
"in list argument `#{arg.name}`."
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
args
|
194
|
+
end
|
195
|
+
|
196
|
+
# Parses an argument template string into resolver arguments via SDL casting.
|
197
|
+
# @param template [String] the template string to parse.
|
198
|
+
# @param type_defs [String] the type definition string declaring argument types.
|
199
|
+
# @return [[GraphQL::Stitching::Resolver::Argument]] an array of resolver arguments.
|
200
|
+
def parse_arguments_with_type_defs(template, type_defs)
|
201
|
+
type_map = parse_type_defs(type_defs)
|
202
|
+
parse_arg_defs(template).map { build_argument(_1, type_struct: type_map[_1.name]) }
|
203
|
+
end
|
204
|
+
|
205
|
+
private
|
206
|
+
|
207
|
+
def parse_arg_defs(template)
|
208
|
+
template = template
|
209
|
+
.gsub("'", %|"|) # 'sfoo' -> "sfoo"
|
210
|
+
.gsub(/(\$[\w\.]+)/) { %|"#{_1}"| } # $.key -> "$.key"
|
211
|
+
.tap(&:strip!)
|
212
|
+
|
213
|
+
template = template[1..-2] if template.start_with?("(") && template.end_with?(")")
|
214
|
+
|
215
|
+
GraphQL.parse("{ f(#{template}) }")
|
216
|
+
.definitions.first
|
217
|
+
.selections.first
|
218
|
+
.arguments
|
219
|
+
end
|
220
|
+
|
221
|
+
def parse_type_defs(template)
|
222
|
+
GraphQL.parse("type T { #{template} }")
|
223
|
+
.definitions.first
|
224
|
+
.fields.each_with_object({}) do |node, memo|
|
225
|
+
memo[node.name] = GraphQL::Stitching::Util.flatten_ast_type_structure(node.type)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def build_argument_set(nodes, argument_defs)
|
230
|
+
if argument_defs
|
231
|
+
argument_defs.each_value do |argument_def|
|
232
|
+
if argument_def.type.non_null? && !nodes.find { _1.name == argument_def.graphql_name }
|
233
|
+
raise CompositionError, "Required argument `#{argument_def.graphql_name}` has no input."
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
nodes.map do |node|
|
239
|
+
argument_def = if argument_defs
|
240
|
+
arg = argument_defs[node.name]
|
241
|
+
raise CompositionError, "Input `#{node.name}` is not a valid argument." unless arg
|
242
|
+
arg
|
243
|
+
end
|
244
|
+
|
245
|
+
build_argument(node, argument_def: argument_def)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def build_argument(node, argument_def: nil, type_struct: nil)
|
250
|
+
value = if node.value.is_a?(GraphQL::Language::Nodes::InputObject)
|
251
|
+
build_object_value(node.value, argument_def ? argument_def.type.unwrap : nil)
|
252
|
+
elsif node.value.is_a?(GraphQL::Language::Nodes::Enum)
|
253
|
+
EnumArgumentValue.new(node.value.name)
|
254
|
+
elsif node.value.is_a?(String) && node.value.start_with?("$.")
|
255
|
+
KeyArgumentValue.new(node.value.sub(/^\$\./, "").split("."))
|
256
|
+
else
|
257
|
+
LiteralArgumentValue.new(node.value)
|
258
|
+
end
|
259
|
+
|
260
|
+
Argument.new(
|
261
|
+
name: node.name,
|
262
|
+
value: value,
|
263
|
+
# doesn't support nested lists...?
|
264
|
+
list: argument_def ? argument_def.type.list? : (type_struct&.first&.list? || false),
|
265
|
+
type_name: argument_def ? argument_def.type.unwrap.graphql_name : type_struct&.last&.name,
|
266
|
+
)
|
267
|
+
end
|
268
|
+
|
269
|
+
def build_object_value(node, object_def)
|
270
|
+
if object_def
|
271
|
+
if !object_def.kind.input_object? && !object_def.kind.scalar?
|
272
|
+
raise CompositionError, "Objects can only be built into input object and scalar positions."
|
273
|
+
elsif object_def.kind.scalar? && GraphQL::Schema::BUILT_IN_TYPES[object_def.graphql_name]
|
274
|
+
raise CompositionError, "Objects can only be built into custom scalar types."
|
275
|
+
elsif object_def.kind.scalar?
|
276
|
+
object_def = nil
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
ObjectArgumentValue.new(build_argument_set(node.arguments, object_def&.arguments))
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
@@ -0,0 +1,206 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL::Stitching
|
4
|
+
class Resolver
|
5
|
+
EXPORT_PREFIX = "_export_"
|
6
|
+
TYPE_NAME = "__typename"
|
7
|
+
|
8
|
+
class FieldNode
|
9
|
+
# GraphQL Ruby changed the argument assigning Field.alias from
|
10
|
+
# a generic `alias` hash key to a structured `field_alias` kwarg
|
11
|
+
# in https://github.com/rmosolgo/graphql-ruby/pull/4718.
|
12
|
+
# This adapts to the library implementation present.
|
13
|
+
GRAPHQL_RUBY_FIELD_ALIAS_KWARG = !GraphQL::Language::Nodes::Field.new(field_alias: "a").alias.nil?
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def build(field_name:, field_alias: nil, selections: GraphQL::Stitching::EMPTY_ARRAY)
|
17
|
+
if GRAPHQL_RUBY_FIELD_ALIAS_KWARG
|
18
|
+
GraphQL::Language::Nodes::Field.new(
|
19
|
+
field_alias: field_alias,
|
20
|
+
name: field_name,
|
21
|
+
selections: selections,
|
22
|
+
)
|
23
|
+
else
|
24
|
+
GraphQL::Language::Nodes::Field.new(
|
25
|
+
alias: field_alias,
|
26
|
+
name: field_name,
|
27
|
+
selections: selections,
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class KeyFieldSet < Array
|
35
|
+
def initialize(fields)
|
36
|
+
super(fields.sort_by(&:name))
|
37
|
+
@to_definition = nil
|
38
|
+
@export_nodes = nil
|
39
|
+
end
|
40
|
+
|
41
|
+
def ==(other)
|
42
|
+
to_definition == other.to_definition
|
43
|
+
end
|
44
|
+
|
45
|
+
def default_argument_name
|
46
|
+
length == 1 ? first.name : nil
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_definition
|
50
|
+
@to_definition ||= map(&:to_definition).join(" ").freeze
|
51
|
+
end
|
52
|
+
|
53
|
+
alias_method :to_s, :to_definition
|
54
|
+
|
55
|
+
def export_nodes
|
56
|
+
@export_nodes ||= map(&:export_node)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
EMPTY_FIELD_SET = KeyFieldSet.new(GraphQL::Stitching::EMPTY_ARRAY)
|
61
|
+
TYPENAME_EXPORT_NODE = FieldNode.build(
|
62
|
+
field_alias: "#{EXPORT_PREFIX}#{TYPE_NAME}",
|
63
|
+
field_name: TYPE_NAME,
|
64
|
+
)
|
65
|
+
|
66
|
+
class Key < KeyFieldSet
|
67
|
+
attr_reader :locations
|
68
|
+
|
69
|
+
def initialize(fields, locations: GraphQL::Stitching::EMPTY_ARRAY)
|
70
|
+
super(fields)
|
71
|
+
@locations = locations
|
72
|
+
to_definition
|
73
|
+
export_nodes
|
74
|
+
freeze
|
75
|
+
end
|
76
|
+
|
77
|
+
def export_nodes
|
78
|
+
@export_nodes ||= begin
|
79
|
+
nodes = map(&:export_node)
|
80
|
+
nodes << TYPENAME_EXPORT_NODE
|
81
|
+
nodes
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
class KeyField
|
87
|
+
# name of the key, may be a field alias
|
88
|
+
attr_reader :name
|
89
|
+
|
90
|
+
# inner key selections
|
91
|
+
attr_reader :inner
|
92
|
+
|
93
|
+
# optional information about location and typing, used during composition
|
94
|
+
attr_accessor :type_name
|
95
|
+
attr_accessor :list
|
96
|
+
alias_method :list?, :list
|
97
|
+
|
98
|
+
def initialize(name, root: false, inner: EMPTY_FIELD_SET)
|
99
|
+
@name = name
|
100
|
+
@inner = inner
|
101
|
+
@root = root
|
102
|
+
end
|
103
|
+
|
104
|
+
def to_definition
|
105
|
+
@inner.empty? ? @name : "#{@name} { #{@inner.to_definition} }"
|
106
|
+
end
|
107
|
+
|
108
|
+
def export_node
|
109
|
+
FieldNode.build(
|
110
|
+
field_alias: @root ? "#{EXPORT_PREFIX}#{@name}" : nil,
|
111
|
+
field_name: @name,
|
112
|
+
selections: @inner.export_nodes,
|
113
|
+
)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
module KeysParser
|
118
|
+
def export_key(name)
|
119
|
+
"#{EXPORT_PREFIX}#{name}"
|
120
|
+
end
|
121
|
+
|
122
|
+
def export_key?(name)
|
123
|
+
return false unless name
|
124
|
+
|
125
|
+
name.start_with?(EXPORT_PREFIX)
|
126
|
+
end
|
127
|
+
|
128
|
+
def parse_key(template, locations = GraphQL::Stitching::EMPTY_ARRAY)
|
129
|
+
Key.new(parse_field_set(template), locations: locations)
|
130
|
+
end
|
131
|
+
|
132
|
+
def parse_key_with_types(template, subgraph_types_by_location)
|
133
|
+
field_set = parse_field_set(template)
|
134
|
+
locations = subgraph_types_by_location.filter_map do |location, subgraph_type|
|
135
|
+
location if field_set_matches_type?(field_set, subgraph_type)
|
136
|
+
end
|
137
|
+
|
138
|
+
if locations.none?
|
139
|
+
message = "Key `#{field_set.to_definition}` does not exist in any location."
|
140
|
+
message += " Composite key selections may not be distributed." if field_set.length > 1
|
141
|
+
raise CompositionError, message
|
142
|
+
end
|
143
|
+
|
144
|
+
assign_field_set_info!(field_set, subgraph_types_by_location[locations.first])
|
145
|
+
Key.new(field_set, locations: locations)
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
def parse_field_set(template)
|
151
|
+
template = template.strip
|
152
|
+
template = template[1..-2] if template.start_with?("{") && template.end_with?("}")
|
153
|
+
|
154
|
+
ast = GraphQL.parse("{ #{template} }").definitions.first.selections
|
155
|
+
build_field_set(ast, root: true)
|
156
|
+
end
|
157
|
+
|
158
|
+
def build_field_set(selections, root: false)
|
159
|
+
return EMPTY_FIELD_SET if selections.empty?
|
160
|
+
|
161
|
+
fields = selections.map do |node|
|
162
|
+
raise CompositionError, "Key selections must be fields." unless node.is_a?(GraphQL::Language::Nodes::Field)
|
163
|
+
raise CompositionError, "Key fields may not specify aliases." unless node.alias.nil?
|
164
|
+
|
165
|
+
KeyField.new(node.name, inner: build_field_set(node.selections), root: root)
|
166
|
+
end
|
167
|
+
|
168
|
+
KeyFieldSet.new(fields)
|
169
|
+
end
|
170
|
+
|
171
|
+
def field_set_matches_type?(field_set, subgraph_type)
|
172
|
+
subgraph_type = subgraph_type.unwrap
|
173
|
+
field_set.all? do |field|
|
174
|
+
# fixme: union doesn't have fields, but may support these selections...
|
175
|
+
next true if subgraph_type.kind.union?
|
176
|
+
field_matches_type?(field, subgraph_type.get_field(field.name)&.type&.unwrap)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def field_matches_type?(field, subgraph_type)
|
181
|
+
return false if subgraph_type.nil?
|
182
|
+
|
183
|
+
if field.inner.empty? && subgraph_type.kind.composite?
|
184
|
+
raise CompositionError, "Composite key fields must contain nested selections."
|
185
|
+
end
|
186
|
+
|
187
|
+
field.inner.empty? || field_set_matches_type?(field.inner, subgraph_type)
|
188
|
+
end
|
189
|
+
|
190
|
+
def assign_field_set_info!(field_set, subgraph_type)
|
191
|
+
subgraph_type = subgraph_type.unwrap
|
192
|
+
field_set.each do |field|
|
193
|
+
# fixme: union doesn't have fields, but may support these selections...
|
194
|
+
next if subgraph_type.kind.union?
|
195
|
+
assign_field_info!(field, subgraph_type.get_field(field.name).type)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def assign_field_info!(field, subgraph_type)
|
200
|
+
field.list = subgraph_type.list?
|
201
|
+
field.type_name = subgraph_type.unwrap.graphql_name
|
202
|
+
assign_field_set_info!(field.inner, subgraph_type)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
@@ -1,47 +1,68 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "./resolver/arguments"
|
4
|
+
require_relative "./resolver/keys"
|
5
|
+
|
3
6
|
module GraphQL
|
4
7
|
module Stitching
|
5
8
|
# Defines a root resolver query that provides direct access to an entity type.
|
6
|
-
Resolver
|
9
|
+
class Resolver
|
10
|
+
extend ArgumentsParser
|
11
|
+
extend KeysParser
|
12
|
+
|
7
13
|
# location name providing the resolver query.
|
8
|
-
:location
|
14
|
+
attr_reader :location
|
9
15
|
|
10
16
|
# name of merged type fulfilled through this resolver.
|
11
|
-
:type_name
|
17
|
+
attr_reader :type_name
|
18
|
+
|
19
|
+
# name of the root field to query.
|
20
|
+
attr_reader :field
|
12
21
|
|
13
22
|
# a key field to select from prior locations, sent as resolver argument.
|
14
|
-
:key
|
23
|
+
attr_reader :key
|
15
24
|
|
16
|
-
#
|
17
|
-
:
|
25
|
+
# parsed resolver Argument structures.
|
26
|
+
attr_reader :arguments
|
18
27
|
|
19
|
-
|
20
|
-
|
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
|
21
43
|
|
22
|
-
#
|
23
|
-
|
44
|
+
# specifies when the resolver is a list query.
|
45
|
+
def list?
|
46
|
+
@list
|
47
|
+
end
|
24
48
|
|
25
|
-
|
26
|
-
|
49
|
+
def version
|
50
|
+
@version ||= Digest::SHA2.hexdigest(as_json.to_json)
|
51
|
+
end
|
27
52
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
) do
|
32
|
-
alias_method :list?, :list
|
33
|
-
alias_method :representations?, :representations
|
53
|
+
def ==(other)
|
54
|
+
self.class == other.class && self.as_json == other.as_json
|
55
|
+
end
|
34
56
|
|
35
57
|
def as_json
|
36
58
|
{
|
37
59
|
location: location,
|
38
60
|
type_name: type_name,
|
39
|
-
|
61
|
+
list: list?,
|
40
62
|
field: field,
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
representations: representations,
|
63
|
+
key: key.to_definition,
|
64
|
+
arguments: arguments.map(&:to_definition).join(", "),
|
65
|
+
argument_types: arguments.map(&:to_type_definition).join(", "),
|
45
66
|
}.tap(&:compact!)
|
46
67
|
end
|
47
68
|
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,14 +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 :
|
15
|
-
argument :representations, Boolean, required: false
|
12
|
+
argument :arguments, String, required: true
|
13
|
+
argument :argument_types, String, required: true
|
14
|
+
argument :type_name, String, required: false
|
16
15
|
repeatable true
|
17
16
|
end
|
18
17
|
end
|