graphql-stitching 0.0.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 +7 -0
- data/.github/workflows/ci.yml +27 -0
- data/.gitignore +59 -0
- data/.ruby-version +1 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +49 -0
- data/LICENSE +21 -0
- data/Procfile +3 -0
- data/README.md +329 -0
- data/Rakefile +12 -0
- data/docs/README.md +14 -0
- data/docs/composer.md +69 -0
- data/docs/document.md +15 -0
- data/docs/executor.md +29 -0
- data/docs/gateway.md +106 -0
- data/docs/images/library.png +0 -0
- data/docs/images/merging.png +0 -0
- data/docs/images/stitching.png +0 -0
- data/docs/planner.md +43 -0
- data/docs/shaper.md +20 -0
- data/docs/supergraph.md +65 -0
- data/example/gateway.rb +50 -0
- data/example/graphiql.html +153 -0
- data/example/remote1.rb +26 -0
- data/example/remote2.rb +26 -0
- data/graphql-stitching.gemspec +34 -0
- data/lib/graphql/stitching/composer/base_validator.rb +11 -0
- data/lib/graphql/stitching/composer/validate_boundaries.rb +80 -0
- data/lib/graphql/stitching/composer/validate_interfaces.rb +24 -0
- data/lib/graphql/stitching/composer.rb +442 -0
- data/lib/graphql/stitching/document.rb +59 -0
- data/lib/graphql/stitching/executor.rb +254 -0
- data/lib/graphql/stitching/gateway.rb +120 -0
- data/lib/graphql/stitching/planner.rb +323 -0
- data/lib/graphql/stitching/planner_operation.rb +59 -0
- data/lib/graphql/stitching/remote_client.rb +25 -0
- data/lib/graphql/stitching/shaper.rb +92 -0
- data/lib/graphql/stitching/supergraph.rb +171 -0
- data/lib/graphql/stitching/util.rb +63 -0
- data/lib/graphql/stitching/version.rb +7 -0
- data/lib/graphql/stitching.rb +30 -0
- metadata +142 -0
@@ -0,0 +1,442 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Stitching
|
5
|
+
class Composer
|
6
|
+
class ComposerError < StitchingError; end
|
7
|
+
class ValidationError < ComposerError; end
|
8
|
+
|
9
|
+
attr_reader :query_name, :mutation_name, :subschema_types_by_name_and_location
|
10
|
+
|
11
|
+
DEFAULT_STRING_MERGER = ->(str_by_location, _info) { str_by_location.values.find { !_1.nil? } }
|
12
|
+
|
13
|
+
VALIDATORS = [
|
14
|
+
"ValidateInterfaces",
|
15
|
+
"ValidateBoundaries",
|
16
|
+
].freeze
|
17
|
+
|
18
|
+
def initialize(
|
19
|
+
schemas:,
|
20
|
+
query_name: "Query",
|
21
|
+
mutation_name: "Mutation",
|
22
|
+
description_merger: nil,
|
23
|
+
deprecation_merger: nil
|
24
|
+
)
|
25
|
+
@schemas = schemas
|
26
|
+
@query_name = query_name
|
27
|
+
@mutation_name = mutation_name
|
28
|
+
@field_map = {}
|
29
|
+
@boundary_map = {}
|
30
|
+
@mapped_type_names = {}
|
31
|
+
|
32
|
+
@description_merger = description_merger || DEFAULT_STRING_MERGER
|
33
|
+
@deprecation_merger = deprecation_merger || DEFAULT_STRING_MERGER
|
34
|
+
end
|
35
|
+
|
36
|
+
def perform
|
37
|
+
# "Typename" => "location" => candidate_type
|
38
|
+
@subschema_types_by_name_and_location = @schemas.each_with_object({}) do |(location, schema), memo|
|
39
|
+
raise ComposerError, "Location keys must be strings" unless location.is_a?(String)
|
40
|
+
raise ComposerError, "The subscription operation is not supported." if schema.subscription
|
41
|
+
|
42
|
+
schema.types.each do |type_name, type_candidate|
|
43
|
+
next if Supergraph::INTROSPECTION_TYPES.include?(type_name)
|
44
|
+
|
45
|
+
if type_name == @query_name && type_candidate != schema.query
|
46
|
+
raise ComposerError, "Query name \"#{@query_name}\" is used by non-query type in #{location} schema."
|
47
|
+
elsif type_name == @mutation_name && type_candidate != schema.mutation
|
48
|
+
raise ComposerError, "Mutation name \"#{@mutation_name}\" is used by non-mutation type in #{location} schema."
|
49
|
+
end
|
50
|
+
|
51
|
+
type_name = @query_name if type_candidate == schema.query
|
52
|
+
type_name = @mutation_name if type_candidate == schema.mutation
|
53
|
+
@mapped_type_names[type_candidate.graphql_name] = type_name if type_candidate.graphql_name != type_name
|
54
|
+
|
55
|
+
memo[type_name] ||= {}
|
56
|
+
memo[type_name][location] = type_candidate
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
enum_usage = build_enum_usage_map(@schemas.values)
|
61
|
+
|
62
|
+
# "Typename" => merged_type
|
63
|
+
schema_types = @subschema_types_by_name_and_location.each_with_object({}) do |(type_name, types_by_location), memo|
|
64
|
+
kinds = types_by_location.values.map { _1.kind.name }.uniq
|
65
|
+
|
66
|
+
unless kinds.all? { _1 == kinds.first }
|
67
|
+
raise ComposerError, "Cannot merge different kinds for `#{type_name}`. Found: #{kinds.join(", ")}."
|
68
|
+
end
|
69
|
+
|
70
|
+
memo[type_name] = case kinds.first
|
71
|
+
when "SCALAR"
|
72
|
+
build_scalar_type(type_name, types_by_location)
|
73
|
+
when "ENUM"
|
74
|
+
build_enum_type(type_name, types_by_location, enum_usage)
|
75
|
+
when "OBJECT"
|
76
|
+
extract_boundaries(type_name, types_by_location)
|
77
|
+
build_object_type(type_name, types_by_location)
|
78
|
+
when "INTERFACE"
|
79
|
+
build_interface_type(type_name, types_by_location)
|
80
|
+
when "UNION"
|
81
|
+
build_union_type(type_name, types_by_location)
|
82
|
+
when "INPUT_OBJECT"
|
83
|
+
build_input_object_type(type_name, types_by_location)
|
84
|
+
else
|
85
|
+
raise ComposerError, "Unexpected kind encountered for `#{type_name}`. Found: #{kind}."
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
builder = self
|
90
|
+
schema = Class.new(GraphQL::Schema) do
|
91
|
+
orphan_types schema_types.values
|
92
|
+
query schema_types[builder.query_name]
|
93
|
+
mutation schema_types[builder.mutation_name]
|
94
|
+
|
95
|
+
own_orphan_types.clear
|
96
|
+
end
|
97
|
+
|
98
|
+
expand_abstract_boundaries(schema)
|
99
|
+
|
100
|
+
supergraph = Supergraph.new(
|
101
|
+
schema: schema,
|
102
|
+
fields: @field_map,
|
103
|
+
boundaries: @boundary_map,
|
104
|
+
executables: @schemas,
|
105
|
+
)
|
106
|
+
|
107
|
+
VALIDATORS.each do |validator|
|
108
|
+
klass = Object.const_get("GraphQL::Stitching::Composer::#{validator}")
|
109
|
+
klass.new.perform(supergraph, self)
|
110
|
+
end
|
111
|
+
|
112
|
+
supergraph
|
113
|
+
end
|
114
|
+
|
115
|
+
def build_scalar_type(type_name, types_by_location)
|
116
|
+
built_in_type = GraphQL::Schema::BUILT_IN_TYPES[type_name]
|
117
|
+
return built_in_type if built_in_type
|
118
|
+
|
119
|
+
builder = self
|
120
|
+
|
121
|
+
Class.new(GraphQL::Schema::Scalar) do
|
122
|
+
graphql_name(type_name)
|
123
|
+
description(builder.merge_descriptions(type_name, types_by_location))
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def build_enum_type(type_name, types_by_location, enum_usage)
|
128
|
+
builder = self
|
129
|
+
|
130
|
+
# "value" => "location" => enum_value
|
131
|
+
enum_values_by_value_location = types_by_location.each_with_object({}) do |(location, type_candidate), memo|
|
132
|
+
type_candidate.enum_values.each do |enum_value_candidate|
|
133
|
+
memo[enum_value_candidate.value] ||= {}
|
134
|
+
memo[enum_value_candidate.value][location] ||= {}
|
135
|
+
memo[enum_value_candidate.value][location] = enum_value_candidate
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# intersect input enum types
|
140
|
+
if enum_usage.fetch(type_name, []).include?(:write)
|
141
|
+
enum_values_by_value_location.reject! do |value, enum_values_by_location|
|
142
|
+
types_by_location.keys.length != enum_values_by_location.keys.length
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
Class.new(GraphQL::Schema::Enum) do
|
147
|
+
graphql_name(type_name)
|
148
|
+
description(builder.merge_descriptions(type_name, types_by_location))
|
149
|
+
|
150
|
+
enum_values_by_value_location.each do |value, enum_values_by_location|
|
151
|
+
value(value,
|
152
|
+
value: value,
|
153
|
+
description: builder.merge_descriptions(type_name, enum_values_by_location, enum_value: value),
|
154
|
+
deprecation_reason: builder.merge_deprecations(type_name, enum_values_by_location, enum_value: value),
|
155
|
+
)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def build_object_type(type_name, types_by_location)
|
161
|
+
builder = self
|
162
|
+
|
163
|
+
Class.new(GraphQL::Schema::Object) do
|
164
|
+
graphql_name(type_name)
|
165
|
+
description(builder.merge_descriptions(type_name, types_by_location))
|
166
|
+
|
167
|
+
interface_names = types_by_location.values.flat_map { _1.interfaces.map(&:graphql_name) }
|
168
|
+
interface_names.uniq.each do |interface_name|
|
169
|
+
implements(builder.build_type_binding(interface_name))
|
170
|
+
end
|
171
|
+
|
172
|
+
builder.build_merged_fields(type_name, types_by_location, self)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def build_interface_type(type_name, types_by_location)
|
177
|
+
builder = self
|
178
|
+
|
179
|
+
Module.new do
|
180
|
+
include GraphQL::Schema::Interface
|
181
|
+
graphql_name(type_name)
|
182
|
+
description(builder.merge_descriptions(type_name, types_by_location))
|
183
|
+
|
184
|
+
interface_names = types_by_location.values.flat_map { _1.interfaces.map(&:graphql_name) }
|
185
|
+
interface_names.uniq.each do |interface_name|
|
186
|
+
implements(builder.build_type_binding(interface_name))
|
187
|
+
end
|
188
|
+
|
189
|
+
builder.build_merged_fields(type_name, types_by_location, self)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def build_union_type(type_name, types_by_location)
|
194
|
+
builder = self
|
195
|
+
|
196
|
+
Class.new(GraphQL::Schema::Union) do
|
197
|
+
graphql_name(type_name)
|
198
|
+
description(builder.merge_descriptions(type_name, types_by_location))
|
199
|
+
|
200
|
+
possible_names = types_by_location.values.flat_map { _1.possible_types.map(&:graphql_name) }.uniq
|
201
|
+
possible_types(*possible_names.map { builder.build_type_binding(_1) })
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def build_input_object_type(type_name, types_by_location)
|
206
|
+
builder = self
|
207
|
+
|
208
|
+
Class.new(GraphQL::Schema::InputObject) do
|
209
|
+
graphql_name(type_name)
|
210
|
+
description(builder.merge_descriptions(type_name, types_by_location))
|
211
|
+
builder.build_merged_arguments(type_name, types_by_location, self)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def build_type_binding(type_name)
|
216
|
+
GraphQL::Schema::LateBoundType.new(@mapped_type_names.fetch(type_name, type_name))
|
217
|
+
end
|
218
|
+
|
219
|
+
def build_merged_fields(type_name, types_by_location, owner)
|
220
|
+
# "field_name" => "location" => field
|
221
|
+
fields_by_name_location = types_by_location.each_with_object({}) do |(location, type_candidate), memo|
|
222
|
+
@field_map[type_name] ||= {}
|
223
|
+
type_candidate.fields.each do |field_name, field_candidate|
|
224
|
+
@field_map[type_name][field_candidate.name] ||= []
|
225
|
+
@field_map[type_name][field_candidate.name] << location
|
226
|
+
|
227
|
+
memo[field_name] ||= {}
|
228
|
+
memo[field_name][location] ||= {}
|
229
|
+
memo[field_name][location] = field_candidate
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
fields_by_name_location.each do |field_name, fields_by_location|
|
234
|
+
value_types = fields_by_location.values.map(&:type)
|
235
|
+
|
236
|
+
schema_field = owner.field(
|
237
|
+
field_name,
|
238
|
+
description: merge_descriptions(type_name, fields_by_location, field_name: field_name),
|
239
|
+
deprecation_reason: merge_deprecations(type_name, fields_by_location, field_name: field_name),
|
240
|
+
type: merge_value_types(type_name, value_types, field_name: field_name),
|
241
|
+
null: !value_types.all?(&:non_null?),
|
242
|
+
camelize: false,
|
243
|
+
)
|
244
|
+
|
245
|
+
build_merged_arguments(type_name, fields_by_location, schema_field, field_name: field_name)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def build_merged_arguments(type_name, members_by_location, owner, field_name: nil)
|
250
|
+
# "argument_name" => "location" => argument
|
251
|
+
args_by_name_location = members_by_location.each_with_object({}) do |(location, member_candidate), memo|
|
252
|
+
member_candidate.arguments.each do |argument_name, argument|
|
253
|
+
memo[argument_name] ||= {}
|
254
|
+
memo[argument_name][location] ||= {}
|
255
|
+
memo[argument_name][location] = argument
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
args_by_name_location.each do |argument_name, arguments_by_location|
|
260
|
+
value_types = arguments_by_location.values.map(&:type)
|
261
|
+
|
262
|
+
if arguments_by_location.length != members_by_location.length
|
263
|
+
if value_types.any?(&:non_null?)
|
264
|
+
path = [type_name, field_name, argument_name].compact.join(".")
|
265
|
+
raise ComposerError, "Required argument `#{path}` must be defined in all locations." # ...or hidden?
|
266
|
+
end
|
267
|
+
next
|
268
|
+
end
|
269
|
+
|
270
|
+
# Getting double args sometimes... why?
|
271
|
+
return if owner.arguments.any? { _1.first == argument_name }
|
272
|
+
|
273
|
+
owner.argument(
|
274
|
+
argument_name,
|
275
|
+
description: merge_descriptions(type_name, arguments_by_location, argument_name: argument_name, field_name: field_name),
|
276
|
+
deprecation_reason: merge_deprecations(type_name, arguments_by_location, argument_name: argument_name, field_name: field_name),
|
277
|
+
type: merge_value_types(type_name, value_types, argument_name: argument_name, field_name: field_name),
|
278
|
+
required: value_types.any?(&:non_null?),
|
279
|
+
camelize: false,
|
280
|
+
)
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
def merge_value_types(type_name, type_candidates, field_name: nil, argument_name: nil)
|
285
|
+
path = [type_name, field_name, argument_name].compact.join(".")
|
286
|
+
named_types = type_candidates.map { Util.get_named_type(_1).graphql_name }.uniq
|
287
|
+
|
288
|
+
unless named_types.all? { _1 == named_types.first }
|
289
|
+
raise ComposerError, "Cannot compose mixed types at `#{path}`. Found: #{named_types.join(", ")}."
|
290
|
+
end
|
291
|
+
|
292
|
+
type = GraphQL::Schema::BUILT_IN_TYPES.fetch(named_types.first, build_type_binding(named_types.first))
|
293
|
+
list_structures = type_candidates.map { Util.get_list_structure(_1) }
|
294
|
+
|
295
|
+
if list_structures.any?(&:any?)
|
296
|
+
if list_structures.any? { _1.length != list_structures.first.length }
|
297
|
+
raise ComposerError, "Cannot compose mixed list structures at `#{path}`."
|
298
|
+
end
|
299
|
+
|
300
|
+
list_structures.each(&:reverse!)
|
301
|
+
list_structures.first.each_with_index do |current, index|
|
302
|
+
# input arguments use strongest nullability, readonly fields use weakest
|
303
|
+
non_null = list_structures.public_send(argument_name ? :any? : :all?) do |list_structure|
|
304
|
+
list_structure[index].start_with?("non_null")
|
305
|
+
end
|
306
|
+
|
307
|
+
case current
|
308
|
+
when "list", "non_null_list"
|
309
|
+
type = type.to_list_type
|
310
|
+
type = type.to_non_null_type if non_null
|
311
|
+
when "element", "non_null_element"
|
312
|
+
type = type.to_non_null_type if non_null
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
type
|
318
|
+
end
|
319
|
+
|
320
|
+
def merge_descriptions(type_name, members_by_location, field_name: nil, argument_name: nil, enum_value: nil)
|
321
|
+
strings_by_location = members_by_location.each_with_object({}) { |(l, m), memo| memo[l] = m.description }
|
322
|
+
@description_merger.call(strings_by_location, {
|
323
|
+
type_name: type_name,
|
324
|
+
field_name: field_name,
|
325
|
+
argument_name: argument_name,
|
326
|
+
enum_value: enum_value,
|
327
|
+
}.compact!)
|
328
|
+
end
|
329
|
+
|
330
|
+
def merge_deprecations(type_name, members_by_location, field_name: nil, argument_name: nil, enum_value: nil)
|
331
|
+
strings_by_location = members_by_location.each_with_object({}) { |(l, m), memo| memo[l] = m.deprecation_reason }
|
332
|
+
@deprecation_merger.call(strings_by_location, {
|
333
|
+
type_name: type_name,
|
334
|
+
field_name: field_name,
|
335
|
+
argument_name: argument_name,
|
336
|
+
enum_value: enum_value,
|
337
|
+
}.compact!)
|
338
|
+
end
|
339
|
+
|
340
|
+
def extract_boundaries(type_name, types_by_location)
|
341
|
+
types_by_location.each do |location, type_candidate|
|
342
|
+
type_candidate.fields.each do |field_name, field_candidate|
|
343
|
+
boundary_type_name = Util.get_named_type(field_candidate.type).graphql_name
|
344
|
+
boundary_list = Util.get_list_structure(field_candidate.type)
|
345
|
+
|
346
|
+
field_candidate.directives.each do |directive|
|
347
|
+
next unless directive.graphql_name == GraphQL::Stitching.stitch_directive
|
348
|
+
|
349
|
+
key = directive.arguments.keyword_arguments.fetch(:key)
|
350
|
+
key_selections = GraphQL.parse("{ #{key} }").definitions[0].selections
|
351
|
+
|
352
|
+
if key_selections.length != 1
|
353
|
+
raise ComposerError, "Boundary key at #{type_name}.#{field_name} must specify exactly one key."
|
354
|
+
end
|
355
|
+
|
356
|
+
argument_name = key_selections[0].alias
|
357
|
+
argument_name ||= if field_candidate.arguments.size == 1
|
358
|
+
field_candidate.arguments.keys.first
|
359
|
+
end
|
360
|
+
|
361
|
+
argument = field_candidate.arguments[argument_name]
|
362
|
+
unless argument
|
363
|
+
# contextualize this... "boundaries with multiple args need mapping aliases."
|
364
|
+
raise ComposerError, "Invalid boundary argument `#{argument_name}` for #{type_name}.#{field_name}."
|
365
|
+
end
|
366
|
+
|
367
|
+
argument_list = Util.get_list_structure(argument.type)
|
368
|
+
if argument_list.length != boundary_list.length
|
369
|
+
raise ComposerError, "Mismatched input/output for #{type_name}.#{field_name}.#{argument_name} boundary. Arguments must map directly to results."
|
370
|
+
end
|
371
|
+
|
372
|
+
@boundary_map[boundary_type_name] ||= []
|
373
|
+
@boundary_map[boundary_type_name] << {
|
374
|
+
"location" => location,
|
375
|
+
"selection" => key_selections[0].name,
|
376
|
+
"field" => field_candidate.name,
|
377
|
+
"arg" => argument_name,
|
378
|
+
"list" => boundary_list.any?,
|
379
|
+
"type_name" => boundary_type_name,
|
380
|
+
}
|
381
|
+
end
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
def expand_abstract_boundaries(schema)
|
387
|
+
@boundary_map.keys.each do |type_name|
|
388
|
+
boundary_type = schema.types[type_name]
|
389
|
+
next unless boundary_type.kind.abstract?
|
390
|
+
|
391
|
+
possible_types = Util.get_possible_types(schema, boundary_type)
|
392
|
+
possible_types.select { @subschema_types_by_name_and_location[_1.graphql_name].length > 1 }.each do |possible_type|
|
393
|
+
@boundary_map[possible_type.graphql_name] ||= []
|
394
|
+
@boundary_map[possible_type.graphql_name].push(*@boundary_map[type_name])
|
395
|
+
end
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
def build_enum_usage_map(schemas)
|
400
|
+
reads = []
|
401
|
+
writes = []
|
402
|
+
|
403
|
+
schemas.each do |schema|
|
404
|
+
schema.types.values.each do |type|
|
405
|
+
next if Supergraph::INTROSPECTION_TYPES.include?(type.graphql_name)
|
406
|
+
|
407
|
+
if type.kind.object? || type.kind.interface?
|
408
|
+
type.fields.values.each do |field|
|
409
|
+
field_type = Util.get_named_type(field.type)
|
410
|
+
reads << field_type.graphql_name if field_type.kind.enum?
|
411
|
+
|
412
|
+
field.arguments.values.each do |argument|
|
413
|
+
argument_type = Util.get_named_type(argument.type)
|
414
|
+
writes << argument_type.graphql_name if argument_type.kind.enum?
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
elsif type.kind.input_object?
|
419
|
+
type.arguments.values.each do |argument|
|
420
|
+
argument_type = Util.get_named_type(argument.type)
|
421
|
+
writes << argument_type.graphql_name if argument_type.kind.enum?
|
422
|
+
end
|
423
|
+
end
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
usage = reads.uniq.each_with_object({}) do |enum_name, memo|
|
428
|
+
memo[enum_name] ||= []
|
429
|
+
memo[enum_name] << :read
|
430
|
+
end
|
431
|
+
writes.uniq.each_with_object(usage) do |enum_name, memo|
|
432
|
+
memo[enum_name] ||= []
|
433
|
+
memo[enum_name] << :write
|
434
|
+
end
|
435
|
+
end
|
436
|
+
end
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
require_relative "./composer/base_validator"
|
441
|
+
require_relative "./composer/validate_interfaces"
|
442
|
+
require_relative "./composer/validate_boundaries"
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Stitching
|
5
|
+
class Document
|
6
|
+
SUPPORTED_OPERATIONS = ["query", "mutation"].freeze
|
7
|
+
|
8
|
+
attr_reader :ast, :operation_name
|
9
|
+
|
10
|
+
def initialize(string_or_ast, operation_name: nil)
|
11
|
+
@ast = if string_or_ast.is_a?(String)
|
12
|
+
GraphQL.parse(string_or_ast)
|
13
|
+
else
|
14
|
+
string_or_ast
|
15
|
+
end
|
16
|
+
|
17
|
+
@operation_name = operation_name
|
18
|
+
end
|
19
|
+
|
20
|
+
def string
|
21
|
+
@string ||= GraphQL::Language::Printer.new.print(@ast)
|
22
|
+
end
|
23
|
+
|
24
|
+
def digest
|
25
|
+
@digest ||= Digest::SHA2.hexdigest(string)
|
26
|
+
end
|
27
|
+
|
28
|
+
def operation
|
29
|
+
@operation ||= begin
|
30
|
+
operation_defs = @ast.definitions.select do |d|
|
31
|
+
next unless d.is_a?(GraphQL::Language::Nodes::OperationDefinition)
|
32
|
+
next unless SUPPORTED_OPERATIONS.include?(d.operation_type)
|
33
|
+
@operation_name ? d.name == @operation_name : true
|
34
|
+
end
|
35
|
+
|
36
|
+
if operation_defs.length < 1
|
37
|
+
raise GraphQL::ExecutionError, "Invalid root operation."
|
38
|
+
elsif operation_defs.length > 1
|
39
|
+
raise GraphQL::ExecutionError, "An operation name is required when sending multiple operations."
|
40
|
+
end
|
41
|
+
|
42
|
+
operation_defs.first
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def variable_definitions
|
47
|
+
@variable_definitions ||= operation.variables.each_with_object({}) do |v, memo|
|
48
|
+
memo[v.name] = v.type
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def fragment_definitions
|
53
|
+
@fragment_definitions ||= @ast.definitions.each_with_object({}) do |d, memo|
|
54
|
+
memo[d.name] = d if d.is_a?(GraphQL::Language::Nodes::FragmentDefinition)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|