graphql-stitching 1.7.2 → 1.8.0

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.
@@ -10,6 +10,15 @@ module GraphQL
10
10
  SUPERGRAPH_LOCATIONS = [Supergraph::SUPERGRAPH_LOCATION].freeze
11
11
  ROOT_INDEX = 0
12
12
 
13
+ class ScopePartition
14
+ attr_reader :location, :selections
15
+
16
+ def initialize(location:, selections:)
17
+ @location = location
18
+ @selections = selections
19
+ end
20
+ end
21
+
13
22
  def initialize(request)
14
23
  @request = request
15
24
  @supergraph = request.supergraph
@@ -76,11 +85,15 @@ module GraphQL
76
85
  resolver: nil
77
86
  )
78
87
  # coalesce repeat parameters into a single entrypoint
79
- entrypoint = [parent_index, location, parent_type.graphql_name, resolver&.key&.to_definition, "#", *path].join("/")
88
+ entrypoint = String.new
89
+ entrypoint << parent_index.to_s << "/" << location << "/" << parent_type.graphql_name
90
+ entrypoint << "/" << (resolver&.key&.to_s || "") << "/#"
91
+ path.each { entrypoint << "/" << _1 }
92
+
80
93
  step = @steps_by_entrypoint[entrypoint]
81
- next_index = step ? parent_index : @planning_index += 1
94
+ next_index = step ? step.index : @planning_index += 1
82
95
 
83
- if selections.any?
96
+ unless selections.empty?
84
97
  selections = extract_locale_selections(location, parent_type, next_index, selections, path, variables)
85
98
  end
86
99
 
@@ -97,13 +110,12 @@ module GraphQL
97
110
  resolver: resolver,
98
111
  )
99
112
  else
113
+ step.variables.merge!(variables)
100
114
  step.selections.concat(selections)
101
115
  step
102
116
  end
103
117
  end
104
118
 
105
- ScopePartition = Struct.new(:location, :selections, keyword_init: true)
106
-
107
119
  # A) Group all root selections by their preferred entrypoint locations.
108
120
  def build_root_entrypoints
109
121
  parent_type = @request.query.root_type_for_operation(@request.operation.operation_type)
@@ -111,10 +123,9 @@ module GraphQL
111
123
  case @request.operation.operation_type
112
124
  when QUERY_OP
113
125
  # A.1) Group query fields by location for parallel execution.
114
- selections_by_location = {}
126
+ selections_by_location = Hash.new { |h, k| h[k] = [] }
115
127
  each_field_in_scope(parent_type, @request.operation.selections) do |node|
116
128
  locations = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name] || SUPERGRAPH_LOCATIONS
117
- selections_by_location[locations.first] ||= []
118
129
  selections_by_location[locations.first] << node
119
130
  end
120
131
 
@@ -132,7 +143,8 @@ module GraphQL
132
143
  # A.2) Partition mutation fields by consecutive location for serial execution.
133
144
  partitions = []
134
145
  each_field_in_scope(parent_type, @request.operation.selections) do |node|
135
- next_location = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name].first
146
+ locations = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name] || SUPERGRAPH_LOCATIONS
147
+ next_location = locations.first
136
148
 
137
149
  if partitions.none? || partitions.last.location != next_location
138
150
  partitions << ScopePartition.new(location: next_location, selections: [])
@@ -213,7 +225,7 @@ module GraphQL
213
225
  input_selections.each do |node|
214
226
  case node
215
227
  when GraphQL::Language::Nodes::Field
216
- if node.alias&.start_with?(TypeResolver::EXPORT_PREFIX) && node != TypeResolver::TYPENAME_EXPORT_NODE
228
+ if node.alias&.start_with?(TypeResolver::EXPORT_PREFIX) && node.object_id != TypeResolver::TYPENAME_EXPORT_NODE.object_id
217
229
  raise StitchingError, %(Alias "#{node.alias}" is not allowed because "#{TypeResolver::EXPORT_PREFIX}" is a reserved prefix.)
218
230
  elsif node.name == TYPENAME
219
231
  locale_selections << node
@@ -228,8 +240,10 @@ module GraphQL
228
240
  end
229
241
 
230
242
  # B.3) Collect all variable definitions used within the filtered selection.
231
- extract_node_variables(node, locale_variables)
232
- field_type = @supergraph.memoized_schema_fields(parent_type.graphql_name)[node.name].type.unwrap
243
+ extract_node_argument_variables(node, locale_variables)
244
+ extract_node_directive_variables(node, locale_variables)
245
+ schema_fields = @supergraph.memoized_schema_fields(parent_type.graphql_name)
246
+ field_type = schema_fields[node.name].type.unwrap
233
247
 
234
248
  if Util.is_leaf_type?(field_type)
235
249
  locale_selections << node
@@ -245,7 +259,8 @@ module GraphQL
245
259
  fragment_type = node.type ? @supergraph.memoized_schema_types[node.type.name] : parent_type
246
260
  next unless @supergraph.locations_by_type[fragment_type.graphql_name].include?(current_location)
247
261
 
248
- is_same_scope = fragment_type == parent_type
262
+ extract_node_directive_variables(node, locale_variables)
263
+ is_same_scope = fragment_type == parent_type && node.directives.empty?
249
264
  selection_set = is_same_scope ? locale_selections : []
250
265
  extract_locale_selections(current_location, fragment_type, parent_index, node.selections, path, locale_variables, selection_set)
251
266
 
@@ -258,14 +273,17 @@ module GraphQL
258
273
  fragment = @request.fragment_definitions[node.name]
259
274
  next unless @supergraph.locations_by_type[fragment.type.name].include?(current_location)
260
275
 
276
+ extract_node_directive_variables(node, locale_variables)
277
+ extract_node_directive_variables(fragment, locale_variables)
261
278
  requires_typename = true
262
279
  fragment_type = @supergraph.memoized_schema_types[fragment.type.name]
263
- is_same_scope = fragment_type == parent_type
280
+ directives = fragment.directives.empty? && node.directives.empty? ? EMPTY_ARRAY : fragment.directives + node.directives
281
+ is_same_scope = fragment_type == parent_type && directives.empty?
264
282
  selection_set = is_same_scope ? locale_selections : []
265
283
  extract_locale_selections(current_location, fragment_type, parent_index, fragment.selections, path, locale_variables, selection_set)
266
284
 
267
285
  unless is_same_scope
268
- locale_selections << GraphQL::Language::Nodes::InlineFragment.new(type: fragment.type, selections: selection_set)
286
+ locale_selections << GraphQL::Language::Nodes::InlineFragment.new(type: fragment.type, directives: directives, selections: selection_set)
269
287
  end
270
288
 
271
289
  else
@@ -344,20 +362,28 @@ module GraphQL
344
362
 
345
363
  # B.3) Collect all variable definitions used within the filtered selection.
346
364
  # These specify which request variables to pass along with each step.
347
- def extract_node_variables(node_with_args, variable_definitions)
348
- node_with_args.arguments.each do |argument|
349
- case argument.value
350
- when GraphQL::Language::Nodes::InputObject
351
- extract_node_variables(argument.value, variable_definitions)
352
- when GraphQL::Language::Nodes::VariableIdentifier
353
- variable_definitions[argument.value.name] ||= @request.variable_definitions[argument.value.name]
354
- end
355
- end
365
+ def extract_node_argument_variables(node, variable_definitions)
366
+ arguments = node.arguments
367
+ return if arguments.empty?
356
368
 
357
- if node_with_args.respond_to?(:directives)
358
- node_with_args.directives.each do |directive|
359
- extract_node_variables(directive, variable_definitions)
360
- end
369
+ arguments.each { |argument| extract_value_variables(argument.value, variable_definitions) }
370
+ end
371
+
372
+ def extract_node_directive_variables(node, variable_definitions)
373
+ directives = node.directives
374
+ return if directives.empty?
375
+
376
+ directives.each { |directive| extract_node_argument_variables(directive, variable_definitions) }
377
+ end
378
+
379
+ def extract_value_variables(value, variable_definitions)
380
+ case value
381
+ when GraphQL::Language::Nodes::InputObject
382
+ extract_node_argument_variables(value, variable_definitions)
383
+ when GraphQL::Language::Nodes::VariableIdentifier
384
+ variable_definitions[value.name] ||= @request.variable_definitions[value.name]
385
+ when Array
386
+ value.each { extract_value_variables(_1, variable_definitions) }
361
387
  end
362
388
  end
363
389
 
@@ -377,9 +403,11 @@ module GraphQL
377
403
  end
378
404
 
379
405
  # C.2) Distribute non-unique fields among locations that were added during C.1.
380
- if selections_by_location.any? && remote_selections.any?
406
+ if !selections_by_location.empty? && !remote_selections.empty?
407
+ available_locations = Set.new(selections_by_location.each_key)
408
+
381
409
  remote_selections.reject! do |node|
382
- used_location = possible_locations_by_field[node.name].find { selections_by_location[_1] }
410
+ used_location = possible_locations_by_field[node.name].find { available_locations.include?(_1) }
383
411
  if used_location
384
412
  selections_by_location[used_location] << node
385
413
  true
@@ -388,29 +416,17 @@ module GraphQL
388
416
  end
389
417
 
390
418
  # C.3) Distribute remaining fields among locations weighted by greatest availability.
391
- if remote_selections.any?
392
- field_count_by_location = remote_selections.each_with_object({}) do |node, memo|
419
+ if !remote_selections.empty?
420
+ field_count_by_location = Hash.new(0)
421
+ remote_selections.each do |node|
393
422
  possible_locations_by_field[node.name].each do |location|
394
- memo[location] ||= 0
395
- memo[location] += 1
423
+ field_count_by_location[location] += 1
396
424
  end
397
425
  end
398
426
 
399
427
  remote_selections.each do |node|
400
428
  possible_locations = possible_locations_by_field[node.name]
401
- preferred_location = possible_locations.first
402
-
403
- possible_locations.reduce(0) do |max_availability, possible_location|
404
- availability = field_count_by_location.fetch(possible_location, 0)
405
-
406
- if availability > max_availability
407
- preferred_location = possible_location
408
- availability
409
- else
410
- max_availability
411
- end
412
- end
413
-
429
+ preferred_location = possible_locations.max_by { field_count_by_location[_1] } || possible_locations.first
414
430
  selections_by_location[preferred_location] ||= []
415
431
  selections_by_location[preferred_location] << node
416
432
  end
@@ -34,7 +34,7 @@ module GraphQL::Stitching
34
34
  next nil
35
35
  end
36
36
 
37
- node = render_node(node, variables) if node.selections.any?
37
+ node = render_node(node, variables) unless node.selections.empty?
38
38
  changed ||= node.object_id != original_node.object_id
39
39
  node
40
40
  end
@@ -51,7 +51,7 @@ module GraphQL::Stitching
51
51
  end
52
52
 
53
53
  def prune_node(node, variables)
54
- return node unless node.directives.any?
54
+ return node if node.directives.empty?
55
55
 
56
56
  delete_node = false
57
57
  filtered_directives = node.directives.reject do |directive|
@@ -101,7 +101,7 @@ module GraphQL
101
101
 
102
102
  # @return [String] A string of directives applied to the root operation. These are passed through in all subgraph requests.
103
103
  def operation_directives
104
- @operation_directives ||= if operation.directives.any?
104
+ @operation_directives ||= unless operation.directives.empty?
105
105
  printer = GraphQL::Language::Printer.new
106
106
  operation.directives.map { printer.print(_1) }.join(" ")
107
107
  end
@@ -50,11 +50,11 @@ module GraphQL
50
50
  end
51
51
  end.freeze
52
52
 
53
- if visibility_profiles.any?
53
+ if visibility_profiles.empty?
54
+ @schema.use(GraphQL::Schema::AlwaysVisible)
55
+ else
54
56
  profiles = visibility_profiles.each_with_object({ nil => {} }) { |p, m| m[p.to_s] = {} }
55
57
  @schema.use(GraphQL::Schema::Visibility, profiles: profiles)
56
- else
57
- @schema.use(GraphQL::Schema::AlwaysVisible)
58
58
  end
59
59
  end
60
60
 
@@ -187,7 +187,17 @@ module GraphQL
187
187
 
188
188
  private
189
189
 
190
- PathNode = Struct.new(:location, :key, :cost, :resolver, keyword_init: true)
190
+ class PathNode
191
+ attr_reader :location, :key, :resolver
192
+ attr_accessor :cost
193
+
194
+ def initialize(location:, key:, resolver: nil, cost: 0)
195
+ @location = location
196
+ @key = key
197
+ @resolver = resolver
198
+ @cost = cost
199
+ end
200
+ end
191
201
 
192
202
  # tunes A* search to favor paths with fewest joining locations, ie:
193
203
  # favor longer paths through target locations over shorter paths with additional locations.
@@ -196,10 +206,10 @@ module GraphQL
196
206
  costs = {}
197
207
 
198
208
  paths = possible_keys_for_type_and_location(type_name, start_location).map do |possible_key|
199
- [PathNode.new(location: start_location, key: possible_key, cost: 0)]
209
+ [PathNode.new(location: start_location, key: possible_key)]
200
210
  end
201
211
 
202
- while paths.any?
212
+ while !paths.empty?
203
213
  path = paths.pop
204
214
  current_location = path.last.location
205
215
  current_key = path.last.key
@@ -10,6 +10,13 @@ module GraphQL
10
10
  extend ArgumentsParser
11
11
  extend KeysParser
12
12
 
13
+ class << self
14
+ # only intended for testing...
15
+ def use_static_version?
16
+ @use_static_version ||= false
17
+ end
18
+ end
19
+
13
20
  # location name providing the resolver query.
14
21
  attr_reader :location
15
22
 
@@ -47,7 +54,11 @@ module GraphQL
47
54
  end
48
55
 
49
56
  def version
50
- @version ||= Stitching.digest.call("#{Stitching::VERSION}/#{as_json.to_json}")
57
+ @version ||= if self.class.use_static_version?
58
+ [location, field, key.to_definition, type_name].join(".")
59
+ else
60
+ Stitching.digest.call("#{Stitching::VERSION}/#{as_json.to_json}")
61
+ end
51
62
  end
52
63
 
53
64
  def ==(other)
@@ -4,12 +4,29 @@ module GraphQL
4
4
  module Stitching
5
5
  # General utilities to aid with stitching.
6
6
  class Util
7
- TypeStructure = Struct.new(:list, :null, :name, keyword_init: true) do
8
- alias_method :list?, :list
9
- alias_method :null?, :null
7
+ class TypeStructure
8
+ attr_reader :name
9
+
10
+ def initialize(list:, null:, name:)
11
+ @list = list
12
+ @null = null
13
+ @name = name
14
+ end
15
+
16
+ def list?
17
+ @list
18
+ end
19
+
20
+ def null?
21
+ @null
22
+ end
10
23
 
11
24
  def non_null?
12
- !null
25
+ !@null
26
+ end
27
+
28
+ def ==(other)
29
+ @list == other.list? && @null == other.null? && @name == other.name
13
30
  end
14
31
  end
15
32
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
- VERSION = "1.7.2"
5
+ VERSION = "1.8.0"
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.7.2
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Greg MacWilliam
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-05-19 00:00:00.000000000 Z
11
+ date: 2026-06-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -28,42 +28,42 @@ dependencies:
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
33
  version: '2.0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '2.0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rake
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "~>"
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
47
  version: '12.0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - "~>"
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '12.0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: minitest
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - "~>"
59
+ - - ">="
60
60
  - !ruby/object:Gem::Version
61
61
  version: '5.12'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - "~>"
66
+ - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '5.12'
69
69
  description: Combine GraphQL services into one unified graph
@@ -79,13 +79,14 @@ files:
79
79
  - LICENSE
80
80
  - README.md
81
81
  - Rakefile
82
+ - benchmark/run.rb
83
+ - docs/README.md
82
84
  - docs/composing_a_supergraph.md
83
85
  - docs/error_handling.md
84
86
  - docs/executables.md
85
87
  - docs/images/library.png
86
88
  - docs/images/merging.png
87
89
  - docs/images/stitching.png
88
- - docs/introduction.md
89
90
  - docs/merged_types.md
90
91
  - docs/merged_types_apollo.md
91
92
  - docs/performance.md
@@ -167,6 +168,7 @@ files:
167
168
  - lib/graphql/stitching/composer/validate_type_resolvers.rb
168
169
  - lib/graphql/stitching/directives.rb
169
170
  - lib/graphql/stitching/executor.rb
171
+ - lib/graphql/stitching/executor/path_access.rb
170
172
  - lib/graphql/stitching/executor/root_source.rb
171
173
  - lib/graphql/stitching/executor/shaper.rb
172
174
  - lib/graphql/stitching/executor/type_resolver_source.rb
File without changes