graphql-stitching 1.2.0 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
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