graphql-stitching 1.0.1 → 1.0.3

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: 4ce8bb1075d536f01aa63487df3e1686bc0c2899b58ca14e95383caef38812e2
4
- data.tar.gz: ae39e685bdb31cbf3962216cfce263b2970aa2aeac6be1901a7397cee6eb1656
3
+ metadata.gz: 665232d15bf3a10517a645afee2c74823221dd848d188681a86a482975031189
4
+ data.tar.gz: 746ee25d066ff080f7b63320aa2c0d5de4162c1a2119120db811f4f6acfc20dc
5
5
  SHA512:
6
- metadata.gz: e718527102969773accf28b2323cbde1d2c467339c44bb876144ae5387a7aaec64143b35b6dfff21401fb087c64ed8742f79cda78e743518ba045871c320f51f
7
- data.tar.gz: 74bc97f475b61ea51dd8ce9e42a1ff2783d0c1717a38ff812e256962d64b97ef0ccc092de11e529a22e274d8ad5cf77af9fd7ed2fd680e20e0c058534942a5d6
6
+ metadata.gz: 704cbc2753e452d2aa6fa5ec9bd55231754ee0c21cbd581b01d91d2af7f68c192a570922676e07143202b71564db674f6d5b8078ba276340c4863621547c7372
7
+ data.tar.gz: 255a20c78357854973e5d5818e4bbbcf8eb6f89bf01bc6ad9185306948a6e643dafe713e0943811d4e5841f3289b5fe7c9033683dd0ab97db282721e9d892109
data/README.md CHANGED
@@ -400,6 +400,26 @@ supergraph = GraphQL::Stitching::Composer.new.perform({
400
400
 
401
401
  The `GraphQL::Stitching::HttpExecutable` class is provided as a simple executable wrapper around `Net::HTTP.post`. You should build your own executables to leverage your existing libraries and to add instrumentation. Note that you must manually assign all executables to a `Supergraph` when rehydrating it from cache ([see docs](./docs/supergraph.md)).
402
402
 
403
+ ## Batching
404
+
405
+ The stitching executor automatically batches subgraph requests so that only one request is made per location per generation of data. This is done using batched queries that combine all data access for a given a location. For example:
406
+
407
+ ```graphql
408
+ query MyOperation_2 {
409
+ _0_result: widgets(ids:["a","b","c"]) { ... } # << 3 Widget
410
+ _1_0_result: sprocket(id:"x") { ... } # << 1 Sprocket
411
+ _1_1_result: sprocket(id:"y") { ... } # << 1 Sprocket
412
+ _1_2_result: sprocket(id:"z") { ... } # << 1 Sprocket
413
+ }
414
+ ```
415
+
416
+ Tips:
417
+
418
+ * List queries (like the `widgets` selection above) are more compact for accessing multiple records, and are therefore preferable as stitching accessors.
419
+ * Assure that root field resolvers across your subgraph implement batching to anticipate cases like the three `sprocket` selections above.
420
+
421
+ Otherwise, there's no developer intervention necessary (or generally possible) to improve upon data access. Note that multiple generations of data may still force the executor to return to a previous location for more data.
422
+
403
423
  ## Concurrency
404
424
 
405
425
  The [Executor](./docs/executor.md) component builds atop the Ruby fiber-based implementation of `GraphQL::Dataloader`. Non-blocking concurrency requires setting a fiber scheduler via `Fiber.set_scheduler`, see [graphql-ruby docs](https://graphql-ruby.org/dataloader/nonblocking.html). You may also need to build your own remote clients using corresponding HTTP libraries.
@@ -2,6 +2,7 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
+ # Defines a boundary query that provides direct access to an entity type.
5
6
  Boundary = Struct.new(
6
7
  :location,
7
8
  :type_name,
@@ -8,8 +8,8 @@ module GraphQL
8
8
 
9
9
  attr_reader :query_name, :mutation_name, :candidate_types_by_name_and_location, :schema_directives
10
10
 
11
- DEFAULT_VALUE_MERGER = ->(values_by_location, _info) { values_by_location.values.find { !_1.nil? } }
12
- DEFAULT_ROOT_FIELD_LOCATION_SELECTOR = ->(locations, _info) { locations.last }
11
+ BASIC_VALUE_MERGER = ->(values_by_location, _info) { values_by_location.values.find { !_1.nil? } }
12
+ BASIC_ROOT_FIELD_LOCATION_SELECTOR = ->(locations, _info) { locations.last }
13
13
 
14
14
  VALIDATORS = [
15
15
  "ValidateInterfaces",
@@ -21,15 +21,17 @@ module GraphQL
21
21
  mutation_name: "Mutation",
22
22
  description_merger: nil,
23
23
  deprecation_merger: nil,
24
+ default_value_merger: nil,
24
25
  directive_kwarg_merger: nil,
25
26
  root_field_location_selector: nil
26
27
  )
27
28
  @query_name = query_name
28
29
  @mutation_name = mutation_name
29
- @description_merger = description_merger || DEFAULT_VALUE_MERGER
30
- @deprecation_merger = deprecation_merger || DEFAULT_VALUE_MERGER
31
- @directive_kwarg_merger = directive_kwarg_merger || DEFAULT_VALUE_MERGER
32
- @root_field_location_selector = root_field_location_selector || DEFAULT_ROOT_FIELD_LOCATION_SELECTOR
30
+ @description_merger = description_merger || BASIC_VALUE_MERGER
31
+ @deprecation_merger = deprecation_merger || BASIC_VALUE_MERGER
32
+ @default_value_merger = default_value_merger || BASIC_VALUE_MERGER
33
+ @directive_kwarg_merger = directive_kwarg_merger || BASIC_VALUE_MERGER
34
+ @root_field_location_selector = root_field_location_selector || BASIC_ROOT_FIELD_LOCATION_SELECTOR
33
35
  @stitch_directives = {}
34
36
  end
35
37
 
@@ -362,8 +364,22 @@ module GraphQL
362
364
  next
363
365
  end
364
366
 
365
- # Getting double args sometimes... why?
366
- return if owner.arguments.any? { _1.first == argument_name }
367
+ # Getting double args sometimes on auto-generated connection types... why?
368
+ next if owner.arguments.any? { _1.first == argument_name }
369
+
370
+ kwargs = {}
371
+ default_values_by_location = arguments_by_location.each_with_object({}) do |(location, argument), memo|
372
+ next if argument.default_value.class == Object # << pass on NOT_CONFIGURED (todo: improve this check)
373
+ memo[location] = argument.default_value
374
+ end
375
+
376
+ if default_values_by_location.any?
377
+ kwargs[:default_value] = @default_value_merger.call(default_values_by_location, {
378
+ type_name: type_name,
379
+ field_name: field_name,
380
+ argument_name: argument_name,
381
+ })
382
+ end
367
383
 
368
384
  type = merge_value_types(type_name, value_types, argument_name: argument_name, field_name: field_name)
369
385
  schema_argument = owner.argument(
@@ -373,6 +389,7 @@ module GraphQL
373
389
  type: Util.unwrap_non_null(type),
374
390
  required: type.non_null?,
375
391
  camelize: false,
392
+ **kwargs,
376
393
  )
377
394
 
378
395
  build_merged_directives(type_name, arguments_by_location, schema_argument, field_name: field_name, argument_name: argument_name)
@@ -23,7 +23,11 @@ module GraphQL
23
23
  end
24
24
 
25
25
  if origin_sets_by_operation.any?
26
- query_document, variable_names = build_document(origin_sets_by_operation, @executor.request.operation_name)
26
+ query_document, variable_names = build_document(
27
+ origin_sets_by_operation,
28
+ @executor.request.operation_name,
29
+ @executor.request.operation_directives,
30
+ )
27
31
  variables = @executor.request.variables.slice(*variable_names)
28
32
  raw_result = @executor.supergraph.execute_at_location(@location, query_document, variables, @executor.request.context)
29
33
  @executor.query_count += 1
@@ -44,7 +48,7 @@ module GraphQL
44
48
  # _1_1_result: item(key:"y") { boundarySelections... }
45
49
  # _1_2_result: item(key:"z") { boundarySelections... }
46
50
  # }"
47
- def build_document(origin_sets_by_operation, operation_name = nil)
51
+ def build_document(origin_sets_by_operation, operation_name = nil, operation_directives = nil)
48
52
  variable_defs = {}
49
53
  query_fields = origin_sets_by_operation.map.with_index do |(op, origin_set), batch_index|
50
54
  variable_defs.merge!(op.variables)
@@ -80,6 +84,10 @@ module GraphQL
80
84
  doc << "(#{variable_str})"
81
85
  end
82
86
 
87
+ if operation_directives
88
+ doc << " #{operation_directives} "
89
+ end
90
+
83
91
  doc << "{ #{query_fields.join(" ")} }"
84
92
 
85
93
  return doc, variable_defs.keys
@@ -11,7 +11,11 @@ module GraphQL
11
11
  def fetch(ops)
12
12
  op = ops.first # There should only ever be one per location at a time
13
13
 
14
- query_document = build_document(op, @executor.request.operation_name)
14
+ query_document = build_document(
15
+ op,
16
+ @executor.request.operation_name,
17
+ @executor.request.operation_directives,
18
+ )
15
19
  query_variables = @executor.request.variables.slice(*op.variables.keys)
16
20
  result = @executor.supergraph.execute_at_location(op.location, query_document, query_variables, @executor.request.context)
17
21
  @executor.query_count += 1
@@ -27,7 +31,7 @@ module GraphQL
27
31
 
28
32
  # Builds root source documents
29
33
  # "query MyOperation_1($var:VarType) { rootSelections ... }"
30
- def build_document(op, operation_name = nil)
34
+ def build_document(op, operation_name = nil, operation_directives = nil)
31
35
  doc = String.new
32
36
  doc << op.operation_type
33
37
 
@@ -40,6 +44,10 @@ module GraphQL
40
44
  doc << "(#{variable_defs})"
41
45
  end
42
46
 
47
+ if operation_directives
48
+ doc << " #{operation_directives} "
49
+ end
50
+
43
51
  doc << op.selections
44
52
  doc
45
53
  end
@@ -5,13 +5,13 @@ require "json"
5
5
  module GraphQL
6
6
  module Stitching
7
7
  class Executor
8
- attr_reader :supergraph, :request, :data, :errors
8
+ attr_reader :supergraph, :request, :plan, :data, :errors
9
9
  attr_accessor :query_count
10
10
 
11
11
  def initialize(supergraph:, request:, plan:, nonblocking: false)
12
12
  @supergraph = supergraph
13
13
  @request = request
14
- @queue = plan.ops
14
+ @plan = plan
15
15
  @data = {}
16
16
  @errors = []
17
17
  @query_count = 0
@@ -39,22 +39,20 @@ module GraphQL
39
39
 
40
40
  private
41
41
 
42
- def exec!(next_ordinals = [0])
43
- if @exec_cycles > @queue.length
42
+ def exec!(next_steps = [0])
43
+ if @exec_cycles > @plan.ops.length
44
44
  # sanity check... if we've exceeded queue size, then something went wrong.
45
45
  raise StitchingError, "Too many execution requests attempted."
46
46
  end
47
47
 
48
48
  @dataloader.append_job do
49
- tasks = @queue
50
- .select { next_ordinals.include?(_1.after) }
49
+ tasks = @plan
50
+ .ops
51
+ .select { next_steps.include?(_1.after) }
51
52
  .group_by { [_1.location, _1.boundary.nil?] }
52
53
  .map do |(location, root_source), ops|
53
- if root_source
54
- @dataloader.with(RootSource, self, location).request_all(ops)
55
- else
56
- @dataloader.with(BoundarySource, self, location).request_all(ops)
57
- end
54
+ source_type = root_source ? RootSource : BoundarySource
55
+ @dataloader.with(source_type, self, location).request_all(ops)
58
56
  end
59
57
 
60
58
  tasks.each(&method(:exec_task))
@@ -65,8 +63,8 @@ module GraphQL
65
63
  end
66
64
 
67
65
  def exec_task(task)
68
- next_ordinals = task.load.tap(&:compact!)
69
- exec!(next_ordinals) if next_ordinals.any?
66
+ next_steps = task.load.tap(&:compact!)
67
+ exec!(next_steps) if next_steps.any?
70
68
  end
71
69
  end
72
70
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
+ # Immutable structures representing a query plan.
6
+ # May serialize to/from JSON.
5
7
  class Plan
6
8
  Op = Struct.new(
7
9
  :step,
@@ -4,6 +4,7 @@ module GraphQL
4
4
  module Stitching
5
5
  class Planner
6
6
  SUPERGRAPH_LOCATIONS = [Supergraph::LOCATION].freeze
7
+ TYPENAME = "__typename"
7
8
  QUERY_OP = "query"
8
9
  MUTATION_OP = "mutation"
9
10
  ROOT_INDEX = 0
@@ -35,18 +36,14 @@ module GraphQL
35
36
  # A.2) Partition mutation fields by consecutive location for serial execution.
36
37
  #
37
38
  # B) Extract contiguous selections for each entrypoint location.
38
- #
39
39
  # B.1) Selections on interface types that do not belong to the interface at the
40
- # entrypoint location are expanded into concrete type fragments prior to extraction.
41
- #
40
+ # entrypoint location are expanded into concrete type fragments prior to extraction.
42
41
  # B.2) Filter the selection tree down to just fields of the entrypoint location.
43
- # Adjoining selections not available here get split off into new entrypoints (C).
44
- #
42
+ # Adjoining selections not available here get split off into new entrypoints (C).
45
43
  # B.3) Collect all variable definitions used within the filtered selection.
46
- # These specify which request variables to pass along with the selection.
47
- #
48
- # B.4) Add a `__typename` selection to abstracts and concrete types that implement
49
- # fragments. This provides resolved type information used during execution.
44
+ # These specify which request variables to pass along with each step.
45
+ # B.4) Add a `__typename` hint to abstracts and types that implement fragments.
46
+ # This provides resolved type information used during execution.
50
47
  #
51
48
  # C) Delegate adjoining selections to new entrypoint locations.
52
49
  # C.1) Distribute unique fields among their required locations.
@@ -60,7 +57,7 @@ module GraphQL
60
57
  #
61
58
  # E) Translate boundary pathways into new entrypoints.
62
59
  # E.1) Add the key of each boundary query into the prior location's selection set.
63
- # E.2) Add a planner operation for each new entrypoint location, then extract it (B).
60
+ # E.2) Add a planner step for each new entrypoint location, then extract it (B).
64
61
  #
65
62
  # F) Wrap concrete selections targeting abstract boundaries in typed fragments.
66
63
  # **
@@ -77,8 +74,7 @@ module GraphQL
77
74
  boundary: nil
78
75
  )
79
76
  # coalesce repeat parameters into a single entrypoint
80
- boundary_key = boundary ? boundary.key : "_"
81
- entrypoint = String.new("#{parent_index}/#{location}/#{parent_type.graphql_name}/#{boundary_key}")
77
+ entrypoint = String.new("#{parent_index}/#{location}/#{parent_type.graphql_name}/#{boundary&.key}")
82
78
  path.each { entrypoint << "/#{_1}" }
83
79
 
84
80
  step = @steps_by_entrypoint[entrypoint]
@@ -89,11 +85,6 @@ module GraphQL
89
85
  end
90
86
 
91
87
  if step.nil?
92
- # concrete types that are not root Query/Mutation report themselves as a type condition
93
- # executor must check the __typename of loaded objects to see if they match subsequent operations
94
- # this prevents the executor from taking action on unused fragment selections
95
- conditional = !parent_type.kind.abstract? && parent_type != @supergraph.schema.root_type_for_operation(operation_type)
96
-
97
88
  @steps_by_entrypoint[entrypoint] = PlannerStep.new(
98
89
  index: next_index,
99
90
  after: parent_index,
@@ -103,7 +94,6 @@ module GraphQL
103
94
  selections: selections,
104
95
  variables: variables,
105
96
  path: path,
106
- if_type: conditional ? parent_type.graphql_name : nil,
107
97
  boundary: boundary,
108
98
  )
109
99
  else
@@ -209,7 +199,7 @@ module GraphQL
209
199
  input_selections.each do |node|
210
200
  case node
211
201
  when GraphQL::Language::Nodes::Field
212
- if node.name == "__typename"
202
+ if node.name == TYPENAME
213
203
  locale_selections << node
214
204
  next
215
205
  end
@@ -267,7 +257,7 @@ module GraphQL
267
257
  end
268
258
  end
269
259
 
270
- # B.4) Add a `__typename` selection to abstracts and concrete types that implement
260
+ # B.4) Add a `__typename` hint to abstracts and types that implement
271
261
  # fragments so that resolved type information is available during execution.
272
262
  if requires_typename
273
263
  locale_selections << SelectionHint.typename_node
@@ -297,13 +287,12 @@ module GraphQL
297
287
  parent_selections << SelectionHint.key_node(boundary.key) unless has_key
298
288
  parent_selections << SelectionHint.typename_node unless has_typename
299
289
 
300
- # E.2) Add a planner operation for each new entrypoint location.
301
- location = boundary.location
290
+ # E.2) Add a planner step for each new entrypoint location.
302
291
  add_step(
303
- location: location,
292
+ location: boundary.location,
304
293
  parent_index: parent_index,
305
294
  parent_type: parent_type,
306
- selections: remote_selections_by_location[location] || [],
295
+ selections: remote_selections_by_location[boundary.location] || [],
307
296
  path: path.dup,
308
297
  boundary: boundary,
309
298
  ).selections
@@ -323,7 +312,7 @@ module GraphQL
323
312
 
324
313
  expanded_selections = nil
325
314
  input_selections = input_selections.filter_map do |node|
326
- if node.is_a?(GraphQL::Language::Nodes::Field) && node.name != "__typename" && !local_interface_fields.include?(node.name)
315
+ if node.is_a?(GraphQL::Language::Nodes::Field) && node.name != TYPENAME && !local_interface_fields.include?(node.name)
327
316
  expanded_selections ||= []
328
317
  expanded_selections << node
329
318
  nil
@@ -345,7 +334,7 @@ module GraphQL
345
334
  end
346
335
 
347
336
  # B.3) Collect all variable definitions used within the filtered selection.
348
- # These specify which request variables to pass along with the selection.
337
+ # These specify which request variables to pass along with each step.
349
338
  def extract_node_variables(node_with_args, variable_definitions)
350
339
  node_with_args.arguments.each do |argument|
351
340
  case argument.value
@@ -391,15 +380,11 @@ module GraphQL
391
380
 
392
381
  # C.3) Distribute remaining fields among locations weighted by greatest availability.
393
382
  if remote_selections.any?
394
- field_count_by_location = if remote_selections.length > 1
395
- remote_selections.each_with_object({}) do |node, memo|
396
- possible_locations_by_field[node.name].each do |location|
397
- memo[location] ||= 0
398
- memo[location] += 1
399
- end
383
+ field_count_by_location = remote_selections.each_with_object({}) do |node, memo|
384
+ possible_locations_by_field[node.name].each do |location|
385
+ memo[location] ||= 0
386
+ memo[location] += 1
400
387
  end
401
- else
402
- GraphQL::Stitching::EMPTY_OBJECT
403
388
  end
404
389
 
405
390
  remote_selections.each do |node|
@@ -407,11 +392,11 @@ module GraphQL
407
392
  preferred_location = possible_locations.first
408
393
 
409
394
  possible_locations.reduce(0) do |max_availability, possible_location|
410
- available_fields = field_count_by_location.fetch(possible_location, 0)
395
+ availability = field_count_by_location.fetch(possible_location, 0)
411
396
 
412
- if available_fields > max_availability
397
+ if availability > max_availability
413
398
  preferred_location = possible_location
414
- available_fields
399
+ availability
415
400
  else
416
401
  max_availability
417
402
  end
@@ -427,15 +412,15 @@ module GraphQL
427
412
 
428
413
  # F) Wrap concrete selections targeting abstract boundaries in typed fragments.
429
414
  def expand_abstract_boundaries
430
- @steps_by_entrypoint.each_value do |op|
431
- next unless op.boundary
415
+ @steps_by_entrypoint.each_value do |step|
416
+ next unless step.boundary
432
417
 
433
- boundary_type = @supergraph.memoized_schema_types[op.boundary.type_name]
418
+ boundary_type = @supergraph.memoized_schema_types[step.boundary.type_name]
434
419
  next unless boundary_type.kind.abstract?
435
- next if boundary_type == op.parent_type
420
+ next if boundary_type == step.parent_type
436
421
 
437
422
  expanded_selections = nil
438
- op.selections.reject! do |node|
423
+ step.selections.reject! do |node|
439
424
  if node.is_a?(GraphQL::Language::Nodes::Field)
440
425
  expanded_selections ||= []
441
426
  expanded_selections << node
@@ -444,8 +429,8 @@ module GraphQL
444
429
  end
445
430
 
446
431
  if expanded_selections
447
- type_name = GraphQL::Language::Nodes::TypeName.new(name: op.parent_type.graphql_name)
448
- op.selections << GraphQL::Language::Nodes::InlineFragment.new(type: type_name, selections: expanded_selections)
432
+ type_name = GraphQL::Language::Nodes::TypeName.new(name: step.parent_type.graphql_name)
433
+ step.selections << GraphQL::Language::Nodes::InlineFragment.new(type: type_name, selections: expanded_selections)
449
434
  end
450
435
  end
451
436
  end
@@ -2,10 +2,13 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
+ # A planned step in the sequence of stitching entrypoints together.
6
+ # This is a mutable object that may change throughout the planning process.
7
+ # It ultimately builds an immutable Plan::Op at the end of planning.
5
8
  class PlannerStep
6
9
  GRAPHQL_PRINTER = GraphQL::Language::Printer.new
7
10
 
8
- attr_reader :index, :location, :parent_type, :if_type, :operation_type, :path
11
+ attr_reader :index, :location, :parent_type, :operation_type, :path
9
12
  attr_accessor :after, :selections, :variables, :boundary
10
13
 
11
14
  def initialize(
@@ -17,7 +20,6 @@ module GraphQL
17
20
  selections: [],
18
21
  variables: {},
19
22
  path: [],
20
- if_type: nil,
21
23
  boundary: nil
22
24
  )
23
25
  @location = location
@@ -28,7 +30,6 @@ module GraphQL
28
30
  @selections = selections
29
31
  @variables = variables
30
32
  @path = path
31
- @if_type = if_type
32
33
  @boundary = boundary
33
34
  end
34
35
 
@@ -41,13 +42,20 @@ module GraphQL
41
42
  selections: rendered_selections,
42
43
  variables: rendered_variables,
43
44
  path: @path,
44
- if_type: @if_type,
45
+ if_type: type_condition,
45
46
  boundary: @boundary,
46
47
  )
47
48
  end
48
49
 
49
50
  private
50
51
 
52
+ # Concrete types going to a boundary report themselves as a type condition.
53
+ # This is used by the executor to evalute which planned fragment selections
54
+ # actually apply to the resolved object types.
55
+ def type_condition
56
+ @parent_type.graphql_name if @boundary && !parent_type.kind.abstract?
57
+ end
58
+
51
59
  def rendered_selections
52
60
  op = GraphQL::Language::Nodes::OperationDefinition.new(operation_type: "", selections: @selections)
53
61
  GRAPHQL_PRINTER.print(op).gsub!(/\s+/, " ").strip!
@@ -48,6 +48,13 @@ module GraphQL
48
48
  end
49
49
  end
50
50
 
51
+ def operation_directives
52
+ @operation_directives ||= if operation.directives.any?
53
+ printer = GraphQL::Language::Printer.new
54
+ operation.directives.map { printer.print(_1) }.join(" ")
55
+ end
56
+ end
57
+
51
58
  def variable_definitions
52
59
  @variable_definitions ||= operation.variables.each_with_object({}) do |v, memo|
53
60
  memo[v.name] = v.type
@@ -2,6 +2,8 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
+ # Builds hidden selection fields added by stitiching code,
6
+ # used to request operational data about resolved objects.
5
7
  class SelectionHint
6
8
  HINT_PREFIX = "_STITCH_"
7
9
 
@@ -3,6 +3,8 @@
3
3
 
4
4
  module GraphQL
5
5
  module Stitching
6
+ # Shapes the final results payload to the request selection and schema definition.
7
+ # This eliminates unrequested selection hints and applies null bubbling.
6
8
  class Shaper
7
9
  def initialize(supergraph:, request:)
8
10
  @supergraph = supergraph
@@ -60,7 +60,7 @@ module GraphQL
60
60
  result << type
61
61
  result.push(*expand_abstract_type(schema, type)) if type.kind.interface?
62
62
  end
63
- result.uniq
63
+ result.tap(&:uniq!)
64
64
  end
65
65
  end
66
66
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
- VERSION = "1.0.1"
5
+ VERSION = "1.0.3"
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.0.1
4
+ version: 1.0.3
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-10-04 00:00:00.000000000 Z
11
+ date: 2023-11-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql