graphql-stitching 1.2.0 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 32918bd6b17708d712d3ac945f6007b0405299379372aabda8b0c3a5f0c246da
4
- data.tar.gz: 608562f6b35cfe912e2388c6cbfce8c774bedc6f6518d257eb0de8b45fdc8b52
3
+ metadata.gz: 4cb8c0d3b16cda5db8b82d0d0f9eba93a4537c3faa584b1b301dc84b34b5fd0e
4
+ data.tar.gz: 83f4b436479307ac8575f718ace5a82288024a6e5741f786151e55920d7ef61f
5
5
  SHA512:
6
- metadata.gz: c0f40d79e3b06c9477f5967f12f6e33c0534b916c3454669d2802357137ff78d7bb9bb7d99d7c82d43ea47f58fac1c4d0e5aa38243d4cbfcdec6ba192ef56c0e
7
- data.tar.gz: eed8c018a8efde4500bb0eb93de78803f555c77fad284718e945648160442eea249439d2a2cb5b4c0a5121fc148197f48b9ea0d50a21e7cfa291499fe4533912
6
+ metadata.gz: e247e539e223a1fbefcd398c1aeb3940fa91320b81c45caeaa21b3ddca50631ffb6ddbd3963cde07f11609cbaacc1f814a167fefebab8f888979afe929bcb93f
7
+ data.tar.gz: 66de3bacb73749bd90b4d8e7ade4bf35cc144e0ae60b3ff1c81a5f548b20d74e204454b6727875984da293b4b3853aa9bf6215283666f2b037a7dc148e8aa64b
data/.yardopts ADDED
@@ -0,0 +1,5 @@
1
+ --no-private
2
+ --markup=markdown
3
+ --readme=readme.md
4
+ --title='GraphQL Stitching Ruby API Documentation'
5
+ 'lib/**/*.rb' - '*.md'
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module GraphQL
4
- module Stitching
5
- class Composer::BaseValidator
3
+ module GraphQL::Stitching
4
+ class Composer
5
+ class BaseValidator
6
6
  def perform(ctx, composer)
7
7
  raise "not implemented"
8
8
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module GraphQL
4
- module Stitching
5
- class Composer::ValidateBoundaries < Composer::BaseValidator
3
+ module GraphQL::Stitching
4
+ class Composer
5
+ class ValidateBoundaries < BaseValidator
6
6
 
7
7
  def perform(ctx, composer)
8
8
  ctx.schema.types.each do |type_name, type|
@@ -1,9 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module GraphQL
4
- module Stitching
5
- class Composer::ValidateInterfaces < Composer::BaseValidator
6
-
3
+ module GraphQL::Stitching
4
+ class Composer
5
+ class ValidateInterfaces < BaseValidator
7
6
  # For each composed interface, check the interface against each possible type
8
7
  # to assure that intersecting fields have compatible types, structures, and nullability.
9
8
  # Verifies compatibility of types that inherit interface contracts through merging.
@@ -1,26 +1,49 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "./composer/base_validator"
4
+ require_relative "./composer/validate_interfaces"
5
+ require_relative "./composer/validate_boundaries"
6
+
3
7
  module GraphQL
4
8
  module Stitching
5
9
  class Composer
6
10
  class ComposerError < StitchingError; end
7
11
  class ValidationError < ComposerError; end
8
- class ReferenceType < GraphQL::Schema::Object
9
- field(:f, String) do
10
- argument(:a, String)
12
+
13
+ # @api private
14
+ NO_DEFAULT_VALUE = begin
15
+ class T < GraphQL::Schema::Object
16
+ field(:f, String) do
17
+ argument(:a, String)
18
+ end
11
19
  end
20
+
21
+ T.get_field("f").get_argument("a").default_value
12
22
  end
13
23
 
14
- NO_DEFAULT_VALUE = ReferenceType.get_field("f").get_argument("a").default_value
24
+ # @api private
15
25
  BASIC_VALUE_MERGER = ->(values_by_location, _info) { values_by_location.values.find { !_1.nil? } }
26
+
27
+ # @api private
16
28
  BASIC_ROOT_FIELD_LOCATION_SELECTOR = ->(locations, _info) { locations.last }
17
29
 
30
+ # @api private
18
31
  VALIDATORS = [
19
32
  "ValidateInterfaces",
20
33
  "ValidateBoundaries",
21
34
  ].freeze
22
35
 
23
- attr_reader :query_name, :mutation_name, :candidate_types_by_name_and_location, :schema_directives
36
+ # @return [String] name of the Query type in the composed schema.
37
+ attr_reader :query_name
38
+
39
+ # @return [String] name of the Mutation type in the composed schema.
40
+ attr_reader :mutation_name
41
+
42
+ # @api private
43
+ attr_reader :candidate_types_by_name_and_location
44
+
45
+ # @api private
46
+ attr_reader :schema_directives
24
47
 
25
48
  def initialize(
26
49
  query_name: "Query",
@@ -148,6 +171,8 @@ module GraphQL
148
171
  supergraph
149
172
  end
150
173
 
174
+ # @!scope class
175
+ # @!visibility private
151
176
  def prepare_locations_input(locations_input)
152
177
  schemas = {}
153
178
  executables = {}
@@ -200,6 +225,8 @@ module GraphQL
200
225
  return schemas, executables
201
226
  end
202
227
 
228
+ # @!scope class
229
+ # @!visibility private
203
230
  def build_directive(directive_name, directives_by_location)
204
231
  builder = self
205
232
 
@@ -212,6 +239,8 @@ module GraphQL
212
239
  end
213
240
  end
214
241
 
242
+ # @!scope class
243
+ # @!visibility private
215
244
  def build_scalar_type(type_name, types_by_location)
216
245
  built_in_type = GraphQL::Schema::BUILT_IN_TYPES[type_name]
217
246
  return built_in_type if built_in_type
@@ -225,6 +254,8 @@ module GraphQL
225
254
  end
226
255
  end
227
256
 
257
+ # @!scope class
258
+ # @!visibility private
228
259
  def build_enum_type(type_name, types_by_location, enum_usage)
229
260
  builder = self
230
261
 
@@ -261,6 +292,8 @@ module GraphQL
261
292
  end
262
293
  end
263
294
 
295
+ # @!scope class
296
+ # @!visibility private
264
297
  def build_object_type(type_name, types_by_location)
265
298
  builder = self
266
299
 
@@ -278,6 +311,8 @@ module GraphQL
278
311
  end
279
312
  end
280
313
 
314
+ # @!scope class
315
+ # @!visibility private
281
316
  def build_interface_type(type_name, types_by_location)
282
317
  builder = self
283
318
 
@@ -296,6 +331,8 @@ module GraphQL
296
331
  end
297
332
  end
298
333
 
334
+ # @!scope class
335
+ # @!visibility private
299
336
  def build_union_type(type_name, types_by_location)
300
337
  builder = self
301
338
 
@@ -309,6 +346,8 @@ module GraphQL
309
346
  end
310
347
  end
311
348
 
349
+ # @!scope class
350
+ # @!visibility private
312
351
  def build_input_object_type(type_name, types_by_location)
313
352
  builder = self
314
353
 
@@ -320,10 +359,14 @@ module GraphQL
320
359
  end
321
360
  end
322
361
 
362
+ # @!scope class
363
+ # @!visibility private
323
364
  def build_type_binding(type_name)
324
365
  GraphQL::Schema::LateBoundType.new(@mapped_type_names.fetch(type_name, type_name))
325
366
  end
326
367
 
368
+ # @!scope class
369
+ # @!visibility private
327
370
  def build_merged_fields(type_name, types_by_location, owner)
328
371
  # "field_name" => "location" => field
329
372
  fields_by_name_location = types_by_location.each_with_object({}) do |(location, type_candidate), memo|
@@ -356,6 +399,8 @@ module GraphQL
356
399
  end
357
400
  end
358
401
 
402
+ # @!scope class
403
+ # @!visibility private
359
404
  def build_merged_arguments(type_name, members_by_location, owner, field_name: nil, directive_name: nil)
360
405
  # "argument_name" => "location" => argument
361
406
  args_by_name_location = members_by_location.each_with_object({}) do |(location, member_candidate), memo|
@@ -410,6 +455,8 @@ module GraphQL
410
455
  end
411
456
  end
412
457
 
458
+ # @!scope class
459
+ # @!visibility private
413
460
  def build_merged_directives(type_name, members_by_location, owner, field_name: nil, argument_name: nil, enum_value: nil)
414
461
  directives_by_name_location = members_by_location.each_with_object({}) do |(location, member_candidate), memo|
415
462
  member_candidate.directives.each do |directive|
@@ -451,6 +498,8 @@ module GraphQL
451
498
  end
452
499
  end
453
500
 
501
+ # @!scope class
502
+ # @!visibility private
454
503
  def merge_value_types(type_name, type_candidates, field_name: nil, argument_name: nil)
455
504
  path = [type_name, field_name, argument_name].tap(&:compact!).join(".")
456
505
  alt_structures = type_candidates.map { Util.flatten_type_structure(_1) }
@@ -482,6 +531,8 @@ module GraphQL
482
531
  type
483
532
  end
484
533
 
534
+ # @!scope class
535
+ # @!visibility private
485
536
  def merge_descriptions(type_name, members_by_location, field_name: nil, argument_name: nil, enum_value: nil)
486
537
  strings_by_location = members_by_location.each_with_object({}) { |(l, m), memo| memo[l] = m.description }
487
538
  @description_merger.call(strings_by_location, {
@@ -492,6 +543,8 @@ module GraphQL
492
543
  }.tap(&:compact!))
493
544
  end
494
545
 
546
+ # @!scope class
547
+ # @!visibility private
495
548
  def merge_deprecations(type_name, members_by_location, field_name: nil, argument_name: nil, enum_value: nil)
496
549
  strings_by_location = members_by_location.each_with_object({}) { |(l, m), memo| memo[l] = m.deprecation_reason }
497
550
  @deprecation_merger.call(strings_by_location, {
@@ -502,6 +555,8 @@ module GraphQL
502
555
  }.tap(&:compact!))
503
556
  end
504
557
 
558
+ # @!scope class
559
+ # @!visibility private
505
560
  def extract_boundaries(type_name, types_by_location)
506
561
  types_by_location.each do |location, type_candidate|
507
562
  type_candidate.fields.each do |field_name, field_candidate|
@@ -554,6 +609,8 @@ module GraphQL
554
609
  end
555
610
  end
556
611
 
612
+ # @!scope class
613
+ # @!visibility private
557
614
  def select_root_field_locations(schema)
558
615
  [schema.query, schema.mutation].tap(&:compact!).each do |root_type|
559
616
  root_type.fields.each do |root_field_name, root_field|
@@ -572,6 +629,8 @@ module GraphQL
572
629
  end
573
630
  end
574
631
 
632
+ # @!scope class
633
+ # @!visibility private
575
634
  def expand_abstract_boundaries(schema)
576
635
  @boundary_map.keys.each do |type_name|
577
636
  boundary_type = schema.types[type_name]
@@ -585,6 +644,8 @@ module GraphQL
585
644
  end
586
645
  end
587
646
 
647
+ # @!scope class
648
+ # @!visibility private
588
649
  def build_enum_usage_map(schemas)
589
650
  reads = []
590
651
  writes = []
@@ -636,7 +697,3 @@ module GraphQL
636
697
  end
637
698
  end
638
699
  end
639
-
640
- require_relative "./composer/base_validator"
641
- require_relative "./composer/validate_interfaces"
642
- require_relative "./composer/validate_boundaries"
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module GraphQL
4
- module Stitching
5
- class Executor::BoundarySource < GraphQL::Dataloader::Source
3
+ module GraphQL::Stitching
4
+ class Executor
5
+ class BoundarySource < GraphQL::Dataloader::Source
6
6
  def initialize(executor, location)
7
7
  @executor = executor
8
8
  @location = location
@@ -29,7 +29,7 @@ module GraphQL
29
29
  @executor.request.operation_directives,
30
30
  )
31
31
  variables = @executor.request.variables.slice(*variable_names)
32
- raw_result = @executor.supergraph.execute_at_location(@location, query_document, variables, @executor.request)
32
+ raw_result = @executor.request.supergraph.execute_at_location(@location, query_document, variables, @executor.request)
33
33
  @executor.query_count += 1
34
34
 
35
35
  merge_results!(origin_sets_by_operation, raw_result.dig("data"))
@@ -183,9 +183,7 @@ module GraphQL
183
183
  end
184
184
 
185
185
  elsif forward_path.any?
186
- current_path << index
187
186
  repath_errors!(pathed_errors_by_object_id, forward_path, current_path, scope)
188
- current_path.pop
189
187
 
190
188
  elsif scope.is_a?(Array)
191
189
  scope.each_with_index do |element, index|
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module GraphQL
4
- module Stitching
5
- class Executor::RootSource < GraphQL::Dataloader::Source
3
+ module GraphQL::Stitching
4
+ class Executor
5
+ class RootSource < GraphQL::Dataloader::Source
6
6
  def initialize(executor, location)
7
7
  @executor = executor
8
8
  @location = location
@@ -17,7 +17,7 @@ module GraphQL
17
17
  @executor.request.operation_directives,
18
18
  )
19
19
  query_variables = @executor.request.variables.slice(*op.variables.keys)
20
- result = @executor.supergraph.execute_at_location(op.location, query_document, query_variables, @executor.request)
20
+ result = @executor.request.supergraph.execute_at_location(op.location, query_document, query_variables, @executor.request)
21
21
  @executor.query_count += 1
22
22
 
23
23
  @executor.data.merge!(result["data"]) if result["data"]
@@ -1,17 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require_relative "./executor/boundary_source"
5
+ require_relative "./executor/root_source"
4
6
 
5
7
  module GraphQL
6
8
  module Stitching
7
9
  class Executor
8
- attr_reader :supergraph, :request, :plan, :data, :errors
10
+ # @return [Request] the stitching request to execute.
11
+ attr_reader :request
12
+
13
+ # @return [Hash] an aggregate data payload to return.
14
+ attr_reader :data
15
+
16
+ # @return [Array<Hash>] aggregate GraphQL errors to return.
17
+ attr_reader :errors
18
+
19
+ # @return [Integer] tally of queries performed while executing.
9
20
  attr_accessor :query_count
10
21
 
11
22
  def initialize(request, nonblocking: false)
12
23
  @request = request
13
- @supergraph = request.supergraph
14
- @plan = request.plan
15
24
  @data = {}
16
25
  @errors = []
17
26
  @query_count = 0
@@ -37,13 +46,13 @@ module GraphQL
37
46
  private
38
47
 
39
48
  def exec!(next_steps = [0])
40
- if @exec_cycles > @plan.ops.length
49
+ if @exec_cycles > @request.plan.ops.length
41
50
  # sanity check... if we've exceeded queue size, then something went wrong.
42
51
  raise StitchingError, "Too many execution requests attempted."
43
52
  end
44
53
 
45
54
  @dataloader.append_job do
46
- tasks = @plan
55
+ tasks = @request.plan
47
56
  .ops
48
57
  .select { next_steps.include?(_1.after) }
49
58
  .group_by { [_1.location, _1.boundary.nil?] }
@@ -66,6 +75,3 @@ module GraphQL
66
75
  end
67
76
  end
68
77
  end
69
-
70
- require_relative "./executor/boundary_source"
71
- require_relative "./executor/root_source"
@@ -7,27 +7,33 @@ require "json"
7
7
  module GraphQL
8
8
  module Stitching
9
9
  class HttpExecutable
10
- def initialize(url:, headers:{}, upload_types: nil)
10
+ # Builds a new executable for proxying subgraph requests via HTTP.
11
+ # @param url [String] the url of the remote location to proxy.
12
+ # @param headers [Hash] headers to include in upstream requests.
13
+ # @param upload_types [Array<String>, nil] a list of scalar names that represent file uploads. These types extract into multipart forms.
14
+ def initialize(url:, headers: {}, upload_types: nil)
11
15
  @url = url
12
16
  @headers = { "Content-Type" => "application/json" }.merge!(headers)
13
17
  @upload_types = upload_types
14
18
  end
15
19
 
16
20
  def call(request, document, variables)
17
- multipart_form = if request.variable_definitions.any? && variables&.any?
18
- extract_multipart_form(request, document, variables)
19
- end
21
+ form_data = extract_multipart_form(request, document, variables)
20
22
 
21
- response = if multipart_form
22
- post_multipart(multipart_form)
23
+ response = if form_data
24
+ send_multipart_form(request, form_data)
23
25
  else
24
- post(document, variables)
26
+ send(request, document, variables)
25
27
  end
26
28
 
27
29
  JSON.parse(response.body)
28
30
  end
29
31
 
30
- def post(document, variables)
32
+ # Sends a POST request to the remote location.
33
+ # @param request [Request] the original supergraph request.
34
+ # @param document [String] the location-specific subgraph document to send.
35
+ # @param variables [Hash] a hash of variables specific to the subgraph document.
36
+ def send(_request, document, variables)
31
37
  Net::HTTP.post(
32
38
  URI(@url),
33
39
  JSON.generate({ "query" => document, "variables" => variables }),
@@ -35,7 +41,10 @@ module GraphQL
35
41
  )
36
42
  end
37
43
 
38
- def post_multipart(form_data)
44
+ # Sends a POST request to the remote location with multipart form data.
45
+ # @param request [Request] the original supergraph request.
46
+ # @param form_data [Hash] a rendered multipart form with an "operations", "map", and file sections.
47
+ def send_multipart_form(_request, form_data)
39
48
  uri = URI(@url)
40
49
  req = Net::HTTP::Post.new(uri)
41
50
  @headers.each_pair do |key, value|
@@ -48,16 +57,18 @@ module GraphQL
48
57
  end
49
58
  end
50
59
 
51
- # extract multipart upload forms
52
- # spec: https://github.com/jaydenseric/graphql-multipart-request-spec
60
+ # Extracts multipart upload forms per the spec:
61
+ # https://github.com/jaydenseric/graphql-multipart-request-spec
62
+ # @param request [Request] the original supergraph request.
63
+ # @param document [String] the location-specific subgraph document to send.
64
+ # @param variables [Hash] a hash of variables specific to the subgraph document.
53
65
  def extract_multipart_form(request, document, variables)
54
- return unless @upload_types
66
+ return unless @upload_types && request.variable_definitions.any? && variables&.any?
55
67
 
56
- path = []
57
68
  files_by_path = {}
58
69
 
59
70
  # extract all upload scalar values mapped by their input path
60
- variables.each do |key, value|
71
+ variables.each_with_object([]) do |(key, value), path|
61
72
  ast_node = request.variable_definitions[key]
62
73
  path << key
63
74
  extract_ast_node(ast_node, value, files_by_path, path, request) if ast_node
@@ -70,14 +81,14 @@ module GraphQL
70
81
  files = files_by_path.values.tap(&:uniq!)
71
82
  variables_copy = variables.dup
72
83
 
73
- files_by_path.keys.each do |path|
84
+ files_by_path.each_key do |path|
74
85
  orig = variables
75
86
  copy = variables_copy
76
87
  path.each_with_index do |key, i|
77
88
  if i == path.length - 1
78
- map_key = files.index(copy[key]).to_s
79
- map[map_key] ||= []
80
- map[map_key] << "variables.#{path.join(".")}"
89
+ file_index = files.index(copy[key]).to_s
90
+ map[file_index] ||= []
91
+ map[file_index] << "variables.#{path.join(".")}"
81
92
  copy[key] = nil
82
93
  elsif orig[key].object_id == copy[key].object_id
83
94
  copy[key] = copy[key].dup
@@ -324,7 +324,7 @@ module GraphQL
324
324
  end
325
325
 
326
326
  if expanded_selections
327
- @supergraph.memoized_schema_possible_types(parent_type.graphql_name).each do |possible_type|
327
+ @request.warden.possible_types(parent_type).each do |possible_type|
328
328
  next unless @supergraph.locations_by_type[possible_type.graphql_name].include?(current_location)
329
329
 
330
330
  type_name = GraphQL::Language::Nodes::TypeName.new(name: possible_type.graphql_name)
@@ -6,8 +6,30 @@ module GraphQL
6
6
  SUPPORTED_OPERATIONS = ["query", "mutation"].freeze
7
7
  SKIP_INCLUDE_DIRECTIVE = /@(?:skip|include)/
8
8
 
9
- attr_reader :supergraph, :document, :variables, :operation_name, :context
9
+ # @return [Supergraph] supergraph instance that resolves the request.
10
+ attr_reader :supergraph
10
11
 
12
+ # @return [GraphQL::Language::Nodes::Document] the parsed GraphQL AST document.
13
+ attr_reader :document
14
+
15
+ # @return [Hash] input variables for the request.
16
+ attr_reader :variables
17
+
18
+ # @return [String] operation name selected for the request.
19
+ attr_reader :operation_name
20
+
21
+ # @return [Hash] contextual object passed through resolver flows.
22
+ attr_reader :context
23
+
24
+ # @return [GraphQL::Schema::Warden] a visibility warden for this request.
25
+ attr_reader :warden
26
+
27
+ # Creates a new supergraph request.
28
+ # @param supergraph [Supergraph] supergraph instance that resolves the request.
29
+ # @param document [String, GraphQL::Language::Nodes::Document] the request string or parsed AST.
30
+ # @param operation_name [String, nil] operation name selected for the request.
31
+ # @param variables [Hash, nil] input variables for the request.
32
+ # @param context [Hash, nil] a contextual object passed through resolver flows.
11
33
  def initialize(supergraph, document, operation_name: nil, variables: nil, context: nil)
12
34
  @supergraph = supergraph
13
35
  @string = nil
@@ -29,25 +51,34 @@ module GraphQL
29
51
 
30
52
  @operation_name = operation_name
31
53
  @variables = variables || {}
32
- @context = context || GraphQL::Stitching::EMPTY_OBJECT
54
+
55
+ @query = GraphQL::Query.new(@supergraph.schema, document: @document, context: context)
56
+ @warden = @query.warden
57
+ @context = @query.context
58
+ @context[:request] = self
33
59
  end
34
60
 
61
+ # @return [String] the original document string, or a print of the parsed AST document.
35
62
  def string
36
63
  @string || normalized_string
37
64
  end
38
65
 
66
+ # @return [String] a print of the parsed AST document with consistent whitespace.
39
67
  def normalized_string
40
68
  @normalized_string ||= @document.to_query_string
41
69
  end
42
70
 
71
+ # @return [String] a digest of the original document string. Generally faster but less consistent.
43
72
  def digest
44
73
  @digest ||= Digest::SHA2.hexdigest(string)
45
74
  end
46
75
 
76
+ # @return [String] a digest of the normalized document string. Slower but more consistent.
47
77
  def normalized_digest
48
78
  @normalized_digest ||= Digest::SHA2.hexdigest(normalized_string)
49
79
  end
50
80
 
81
+ # @return [GraphQL::Language::Nodes::OperationDefinition] The selected root operation for the request.
51
82
  def operation
52
83
  @operation ||= begin
53
84
  operation_defs = @document.definitions.select do |d|
@@ -66,6 +97,7 @@ module GraphQL
66
97
  end
67
98
  end
68
99
 
100
+ # @return [String] A string of directives applied to the root operation. These are passed through in all subgraph requests.
69
101
  def operation_directives
70
102
  @operation_directives ||= if operation.directives.any?
71
103
  printer = GraphQL::Language::Printer.new
@@ -73,22 +105,27 @@ module GraphQL
73
105
  end
74
106
  end
75
107
 
108
+ # @return [Hash<String, GraphQL::Language::Nodes::AbstractNode>] map of variable names to AST type definitions.
76
109
  def variable_definitions
77
110
  @variable_definitions ||= operation.variables.each_with_object({}) do |v, memo|
78
111
  memo[v.name] = v.type
79
112
  end
80
113
  end
81
114
 
115
+ # @return [Hash<String, GraphQL::Language::Nodes::FragmentDefinition>] map of fragment names to their AST definitions.
82
116
  def fragment_definitions
83
117
  @fragment_definitions ||= @document.definitions.each_with_object({}) do |d, memo|
84
118
  memo[d.name] = d if d.is_a?(GraphQL::Language::Nodes::FragmentDefinition)
85
119
  end
86
120
  end
87
121
 
122
+ # Validates the request using the combined supergraph schema.
88
123
  def validate
89
- @supergraph.schema.validate(@document, context: @context)
124
+ result = @supergraph.static_validator.validate(@query)
125
+ result[:errors]
90
126
  end
91
127
 
128
+ # Prepares the request for stitching by rendering variable defaults and applying @skip/@include conditionals.
92
129
  def prepare!
93
130
  operation.variables.each do |v|
94
131
  @variables[v.name] = v.default_value if @variables[v.name].nil? && !v.default_value.nil?
@@ -106,6 +143,9 @@ module GraphQL
106
143
  self
107
144
  end
108
145
 
146
+ # Gets and sets the query plan for the request. Assigned query plans may pull from cache.
147
+ # @param new_plan [Plan, nil] a cached query plan for the request.
148
+ # @return [Plan] query plan for the request.
109
149
  def plan(new_plan = nil)
110
150
  if new_plan
111
151
  raise StitchingError, "Plan must be a `GraphQL::Stitching::Plan`." unless new_plan.is_a?(Plan)
@@ -115,6 +155,9 @@ module GraphQL
115
155
  end
116
156
  end
117
157
 
158
+ # Executes the request and returns the rendered response.
159
+ # @param raw [Boolean] specifies the result should be unshaped without pruning or null bubbling. Useful for debugging.
160
+ # @return [Hash] the rendered GraphQL response with "data" and "errors" sections.
118
161
  def execute(raw: false)
119
162
  GraphQL::Stitching::Executor.new(self).perform(raw: raw)
120
163
  end
@@ -5,6 +5,7 @@ module GraphQL
5
5
  module Stitching
6
6
  # Shapes the final results payload to the request selection and schema definition.
7
7
  # This eliminates unrequested export selections and applies null bubbling.
8
+ # @api private
8
9
  class Shaper
9
10
  def initialize(request)
10
11
  @request = request
@@ -117,7 +118,7 @@ module GraphQL
117
118
  def typename_in_type?(typename, type)
118
119
  return true if type.graphql_name == typename
119
120
 
120
- type.kind.abstract? && @supergraph.memoized_schema_possible_types(type.graphql_name).any? do |t|
121
+ type.kind.abstract? && @request.warden.possible_types(type).any? do |t|
121
122
  t.graphql_name == typename
122
123
  end
123
124
  end
@@ -2,11 +2,12 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
+ # Faster implementation of an AST visitor for prerendering
6
+ # @skip and @include conditional directives into a document.
7
+ # This avoids unnecessary planning steps, and prepares result shaping.
8
+ # @api private
5
9
  class SkipInclude
6
10
  class << self
7
- # Faster implementation of an AST visitor for prerendering
8
- # @skip and @include conditional directives into a document.
9
- # This avoids unnecessary planning steps, and prepares result shaping.
10
11
  def render(document, variables)
11
12
  changed = false
12
13
  definitions = document.definitions.map do |original_definition|
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL::Stitching
4
+ class Supergraph
5
+ class ResolverDirective < GraphQL::Schema::Directive
6
+ graphql_name "resolver"
7
+ locations OBJECT, INTERFACE, UNION
8
+ argument :location, String, required: true
9
+ argument :key, String, required: true
10
+ argument :field, String, required: true
11
+ argument :arg, String, required: true
12
+ argument :list, Boolean, required: false
13
+ argument :federation, Boolean, required: false
14
+ repeatable true
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL::Stitching
4
+ class Supergraph
5
+ class SourceDirective < GraphQL::Schema::Directive
6
+ graphql_name "source"
7
+ locations FIELD_DEFINITION
8
+ argument :location, String, required: true
9
+ repeatable true
10
+ end
11
+ end
12
+ end
@@ -1,29 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "./supergraph/resolver_directive"
4
+ require_relative "./supergraph/source_directive"
5
+
3
6
  module GraphQL
4
7
  module Stitching
5
8
  class Supergraph
6
9
  SUPERGRAPH_LOCATION = "__super"
7
10
 
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
19
-
20
- class SourceDirective < GraphQL::Schema::Directive
21
- graphql_name "source"
22
- locations FIELD_DEFINITION
23
- argument :location, String, required: true
24
- repeatable true
25
- end
26
-
27
11
  class << self
28
12
  def validate_executable!(location, executable)
29
13
  return true if executable.is_a?(Class) && executable <= GraphQL::Schema
@@ -88,19 +72,27 @@ module GraphQL
88
72
  end
89
73
  end
90
74
 
91
- attr_reader :schema, :boundaries, :locations_by_type_and_field, :executables
75
+ # @return [GraphQL::Schema] the composed schema for the supergraph.
76
+ attr_reader :schema
77
+
78
+ # @return [Hash<String, Executable>] a map of executable resources by location.
79
+ attr_reader :executables
80
+
81
+ attr_reader :boundaries, :locations_by_type_and_field
92
82
 
93
83
  def initialize(schema:, fields: {}, boundaries: {}, executables: {})
94
84
  @schema = schema
85
+ @schema.use(GraphQL::Schema::AlwaysVisible)
86
+
95
87
  @boundaries = boundaries
96
- @possible_keys_by_type = {}
97
- @possible_keys_by_type_and_location = {}
98
- @memoized_schema_possible_types = {}
99
- @memoized_schema_fields = {}
100
- @memoized_introspection_types = nil
101
- @memoized_schema_types = nil
102
88
  @fields_by_type_and_location = nil
103
89
  @locations_by_type = nil
90
+ @memoized_introspection_types = nil
91
+ @memoized_schema_fields = {}
92
+ @memoized_schema_types = nil
93
+ @possible_keys_by_type = {}
94
+ @possible_keys_by_type_and_location = {}
95
+ @static_validator = nil
104
96
 
105
97
  # add introspection types into the fields mapping
106
98
  @locations_by_type_and_field = memoized_introspection_types.each_with_object(fields) do |(type_name, type), memo|
@@ -172,6 +164,11 @@ module GraphQL
172
164
  @schema.to_definition
173
165
  end
174
166
 
167
+ # @return [GraphQL::StaticValidation::Validator] static validator for the supergraph schema.
168
+ def static_validator
169
+ @static_validator ||= @schema.static_validator
170
+ end
171
+
175
172
  def fields
176
173
  @locations_by_type_and_field.reject { |k, _v| memoized_introspection_types[k] }
177
174
  end
@@ -188,10 +185,6 @@ module GraphQL
188
185
  @memoized_schema_types ||= @schema.types
189
186
  end
190
187
 
191
- def memoized_schema_possible_types(type_name)
192
- @memoized_schema_possible_types[type_name] ||= @schema.possible_types(memoized_schema_types[type_name])
193
- end
194
-
195
188
  def memoized_schema_fields(type_name)
196
189
  @memoized_schema_fields[type_name] ||= begin
197
190
  fields = memoized_schema_types[type_name].fields
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
- VERSION = "1.2.0"
5
+ VERSION = "1.2.1"
6
6
  end
7
7
  end
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.2.0
4
+ version: 1.2.1
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-12-29 00:00:00.000000000 Z
11
+ date: 2024-01-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -74,6 +74,7 @@ extra_rdoc_files: []
74
74
  files:
75
75
  - ".github/workflows/ci.yml"
76
76
  - ".gitignore"
77
+ - ".yardopts"
77
78
  - Gemfile
78
79
  - LICENSE
79
80
  - README.md
@@ -125,6 +126,8 @@ files:
125
126
  - lib/graphql/stitching/shaper.rb
126
127
  - lib/graphql/stitching/skip_include.rb
127
128
  - lib/graphql/stitching/supergraph.rb
129
+ - lib/graphql/stitching/supergraph/resolver_directive.rb
130
+ - lib/graphql/stitching/supergraph/source_directive.rb
128
131
  - lib/graphql/stitching/util.rb
129
132
  - lib/graphql/stitching/version.rb
130
133
  homepage: https://github.com/gmac/graphql-stitching-ruby