graphql-stitching 1.7.1 → 1.7.3

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: 43ca2665bc1d0e0a87eead760ee4f7b1caddf324861b66414bc011958901eeff
4
- data.tar.gz: 18a14928e703744d29b57665c51ecdb6a17b1d9d2b7bc58fe8727a37b55fbcd5
3
+ metadata.gz: 65951348a6c34feff379cdbfc7483380dce295dbd86cc24870eee264de079a46
4
+ data.tar.gz: c16673030f3be9728c986b8e4fe5e8084ca7a4e0240ac7e495da26261f1f0a3a
5
5
  SHA512:
6
- metadata.gz: b5ee7817b699447b35c937eb3df872433f8c318310debab6fea1577513171260a39e900c64cf37b4bfcb6f4fde4b509b5216ddf95879dd53a9878a3924016b5d
7
- data.tar.gz: 0ddda7a4df33d5b6946a3a6283a4388d507c18d7ff758db1b2f09179546d6be87b6e78390fa6f623442e0f6e79a91aac69b0922542c4134673175377c47cfc0f
6
+ metadata.gz: 255495cde4ac822edc3b93ac67af6eb3b49ab657adedebd2870058468d6b49d241fca5e01c494fd3f345e134b21755e1a3f3ce2d2d43bad6e078b1833f47768c
7
+ data.tar.gz: ac375cfe4ac73d9b1ce732180efada3d56aab8fa6e5cca75b40e0a6a8d133a45e67637090b9e49a76f9b2b20a81de9014bde03f88f03cdf277f85b5aa435504a
data/README.md CHANGED
@@ -21,7 +21,7 @@ This Ruby implementation is designed as a generic library to join basic spec-com
21
21
 
22
22
  ## Documentation
23
23
 
24
- 1. [Introduction](./docs/introduction.md)
24
+ 1. [Introduction](./docs/README.md)
25
25
  1. [Composing a supergraph](./docs/composing_a_supergraph.md)
26
26
  1. [Merged types](./docs/merged_types.md)
27
27
  1. [Executables & file uploads](./docs/executables.md)
data/docs/performance.md CHANGED
@@ -57,6 +57,34 @@ query($id: ID!) {
57
57
  # variables: { "id" => "1" }
58
58
  ```
59
59
 
60
+ ### Subgraph validations
61
+
62
+ Requests are validated by the supergraph, and should always divide into valid subgraph documents. Therefore, you can skip redundant subgraph validations for requests sent by the supergraph, ex:
63
+
64
+ ```ruby
65
+ exe = GraphQL::Stitching::HttpExecutable.new(
66
+ url: "http://localhost:3001",
67
+ headers: {
68
+ "Authorization" => "...",
69
+ "X-Supergraph-Secret" => "<shared-secret>",
70
+ },
71
+ )
72
+ ```
73
+
74
+ A shared secret allows a subgraph location to trust the supergraph origin, at which time it can disable validations:
75
+
76
+ ```ruby
77
+ def query
78
+ sg_header = request.headers["X-Supergraph-Secret"]
79
+ MySchema.execute(
80
+ query: params[:query],
81
+ variables: params[:variables],
82
+ operation_name: params[:operationName],
83
+ validate: sg_header.nil? || sg_header != Rails.env.credentials.supergraph,
84
+ )
85
+ end
86
+ ```
87
+
60
88
  ### Digests
61
89
 
62
90
  All computed digests use SHA2 hashing by default. You can swap in [a faster algorithm](https://github.com/Shopify/blake3-rb) and/or add base state by reconfiguring `Stitching.digest`:
@@ -25,7 +25,7 @@ module GraphQL
25
25
  raise ArgumentError, "Cannot provide both locations and a supergraph."
26
26
  elsif supergraph && !supergraph.is_a?(Supergraph)
27
27
  raise ArgumentError, "Provided supergraph must be a GraphQL::Stitching::Supergraph instance."
28
- elsif supergraph && composer_options.any?
28
+ elsif supergraph && !composer_options.empty?
29
29
  raise ArgumentError, "Cannot provide composer options with a pre-built supergraph."
30
30
  elsif supergraph
31
31
  supergraph
@@ -50,7 +50,7 @@ module GraphQL
50
50
 
51
51
  if validate
52
52
  validation_errors = request.validate
53
- return error_result(request, validation_errors) if validation_errors.any?
53
+ return error_result(request, validation_errors) unless validation_errors.empty?
54
54
  end
55
55
 
56
56
  load_plan(request)
@@ -8,7 +8,7 @@ module GraphQL::Stitching
8
8
 
9
9
  class << self
10
10
  def extract_directive_assignments(schema, location, assignments)
11
- return EMPTY_OBJECT unless assignments && assignments.any?
11
+ return EMPTY_OBJECT unless assignments && !assignments.empty?
12
12
 
13
13
  assignments.each_with_object({}) do |kwargs, memo|
14
14
  type = kwargs[:parent_type_name] ? schema.get_type(kwargs[:parent_type_name]) : schema.query
@@ -423,7 +423,7 @@ module GraphQL
423
423
  memo[location] = argument.default_value
424
424
  end
425
425
 
426
- if default_values_by_location.any?
426
+ unless default_values_by_location.empty?
427
427
  kwargs[:default_value] = @default_value_merger.call(default_values_by_location, {
428
428
  type_name: type_name,
429
429
  field_name: field_name,
@@ -468,8 +468,6 @@ module GraphQL
468
468
  kwarg_values_by_name_location = directives_by_location.each_with_object({}) do |(location, directive), memo|
469
469
  directive.arguments.keyword_arguments.each do |key, value|
470
470
  key = key.to_s
471
- next unless directive_class.arguments[key]
472
-
473
471
  memo[key] ||= {}
474
472
  memo[key][location] = value
475
473
  end
@@ -16,12 +16,12 @@ module GraphQL::Stitching
16
16
  @executor.request.operation_name,
17
17
  @executor.request.operation_directives,
18
18
  )
19
- query_variables = @executor.request.variables.slice(*op.variables.keys)
19
+ query_variables = @executor.request.variables.slice(*op.variables.each_key)
20
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
  if result["data"]
24
- if op.path.any?
24
+ unless op.path.empty?
25
25
  # Nested root scopes must expand their pathed origin set
26
26
  origin_set = op.path.reduce([@executor.data]) do |set, ns|
27
27
  set.flat_map { |obj| obj && obj[ns] }.tap(&:compact!)
@@ -44,24 +44,28 @@ module GraphQL::Stitching
44
44
  # Builds root source documents
45
45
  # "query MyOperation_1($var:VarType) { rootSelections ... }"
46
46
  def build_document(op, operation_name = nil, operation_directives = nil)
47
- doc = String.new
48
- doc << op.operation_type
47
+ doc_buffer = String.new
48
+ doc_buffer << op.operation_type
49
49
 
50
50
  if operation_name
51
- doc << " #{operation_name}_#{op.step}"
51
+ doc_buffer << " " << operation_name << "_" << op.step.to_s
52
52
  end
53
53
 
54
- if op.variables.any?
55
- variable_defs = op.variables.map { |k, v| "$#{k}:#{v}" }.join(",")
56
- doc << "(#{variable_defs})"
54
+ unless op.variables.empty?
55
+ doc_buffer << "("
56
+ op.variables.each_with_index do |(k, v), i|
57
+ doc_buffer << "," unless i.zero?
58
+ doc_buffer << "$" << k << ":" << v
59
+ end
60
+ doc_buffer << ")"
57
61
  end
58
62
 
59
63
  if operation_directives
60
- doc << " #{operation_directives} "
64
+ doc_buffer << " " << operation_directives << " "
61
65
  end
62
66
 
63
- doc << op.selections
64
- doc
67
+ doc_buffer << op.selections
68
+ doc_buffer
65
69
  end
66
70
 
67
71
  # Format response errors without a document location (because it won't match the request doc),
@@ -69,7 +73,7 @@ module GraphQL::Stitching
69
73
  def format_errors!(errors, path)
70
74
  errors.each do |err|
71
75
  err.delete("locations")
72
- err["path"].unshift(*path) if err["path"] && path.any?
76
+ err["path"].unshift(*path) if err["path"] && !path.empty?
73
77
  end
74
78
  errors
75
79
  end
@@ -30,10 +30,15 @@ module GraphQL::Stitching
30
30
  case node
31
31
  when GraphQL::Language::Nodes::Field
32
32
  field_name = node.alias || node.name
33
+ raw_value = raw_object.delete(field_name)
33
34
 
34
35
  if @request.query.get_field(parent_type, node.name).introspection?
35
- if node.name == TYPENAME && parent_type == @root_type
36
- raw_object[field_name] = @root_type.graphql_name
36
+ next if TypeResolver.export_key?(field_name)
37
+
38
+ raw_object[field_name] = if node.name == TYPENAME && parent_type == @root_type
39
+ @root_type.graphql_name
40
+ else
41
+ raw_value
37
42
  end
38
43
  next
39
44
  end
@@ -42,14 +47,14 @@ module GraphQL::Stitching
42
47
  named_type = node_type.unwrap
43
48
 
44
49
  raw_object[field_name] = if node_type.list?
45
- resolve_list_scope(raw_object[field_name], Util.unwrap_non_null(node_type), node.selections)
50
+ resolve_list_scope(raw_value, Util.unwrap_non_null(node_type), node.selections)
46
51
  elsif Util.is_leaf_type?(named_type)
47
- raw_object[field_name]
52
+ raw_value
48
53
  else
49
- resolve_object_scope(raw_object[field_name], named_type, node.selections)
54
+ resolve_object_scope(raw_value, named_type, node.selections)
50
55
  end
51
56
 
52
- return nil if raw_object[field_name].nil? && node_type.non_null?
57
+ return nil if node_type.non_null? && raw_object[field_name].nil?
53
58
 
54
59
  when GraphQL::Language::Nodes::InlineFragment
55
60
  fragment_type = node.type ? @supergraph.memoized_schema_types[node.type.name] : parent_type
@@ -20,10 +20,10 @@ module GraphQL::Stitching
20
20
  origin_set.select! { _1[TypeResolver::TYPENAME_EXPORT_NODE.alias] == op.if_type }
21
21
  end
22
22
 
23
- memo[op] = origin_set if origin_set.any?
23
+ memo[op] = origin_set unless origin_set.empty?
24
24
  end
25
25
 
26
- if origin_sets_by_operation.any?
26
+ unless origin_sets_by_operation.empty?
27
27
  query_document, variable_names = build_document(
28
28
  origin_sets_by_operation,
29
29
  @executor.request.operation_name,
@@ -51,61 +51,76 @@ module GraphQL::Stitching
51
51
  # }"
52
52
  def build_document(origin_sets_by_operation, operation_name = nil, operation_directives = nil)
53
53
  variable_defs = {}
54
- query_fields = origin_sets_by_operation.map.with_index do |(op, origin_set), batch_index|
54
+ fields_buffer = String.new
55
+
56
+ origin_sets_by_operation.each_with_index do |(op, origin_set), batch_index|
55
57
  variable_defs.merge!(op.variables)
56
58
  resolver = @executor.request.supergraph.resolvers_by_version[op.resolver]
59
+ fields_buffer << " " unless batch_index.zero?
57
60
 
58
61
  if resolver.list?
59
- arguments = resolver.arguments.map.with_index do |arg, i|
62
+ fields_buffer << "_" << batch_index.to_s << "_result: " << resolver.field << "("
63
+
64
+ resolver.arguments.each_with_index do |arg, i|
65
+ fields_buffer << "," unless i.zero?
60
66
  if arg.key?
61
67
  variable_name = "_#{batch_index}_key_#{i}".freeze
62
68
  @variables[variable_name] = origin_set.map { arg.build(_1) }
63
69
  variable_defs[variable_name] = arg.to_type_signature
64
- "#{arg.name}:$#{variable_name}"
70
+ fields_buffer << arg.name << ":$" << variable_name
65
71
  else
66
- "#{arg.name}:#{arg.value.print}"
72
+ fields_buffer << arg.name << ":" << arg.value.print
67
73
  end
68
74
  end
69
75
 
70
- "_#{batch_index}_result: #{resolver.field}(#{arguments.join(",")}) #{op.selections}"
76
+ fields_buffer << ") " << op.selections
71
77
  else
72
- origin_set.map.with_index do |origin_obj, index|
73
- arguments = resolver.arguments.map.with_index do |arg, i|
78
+ origin_set.each_with_index do |origin_obj, index|
79
+ fields_buffer << " " unless index.zero?
80
+ fields_buffer << "_" << batch_index.to_s << "_" << index.to_s << "_result: " << resolver.field << "("
81
+
82
+ resolver.arguments.each_with_index do |arg, i|
83
+ fields_buffer << "," unless i.zero?
74
84
  if arg.key?
75
85
  variable_name = "_#{batch_index}_#{index}_key_#{i}".freeze
76
86
  @variables[variable_name] = arg.build(origin_obj)
77
87
  variable_defs[variable_name] = arg.to_type_signature
78
- "#{arg.name}:$#{variable_name}"
88
+ fields_buffer << arg.name << ":$" << variable_name
79
89
  else
80
- "#{arg.name}:#{arg.value.print}"
90
+ fields_buffer << arg.name << ":" << arg.value.print
81
91
  end
82
92
  end
83
93
 
84
- "_#{batch_index}_#{index}_result: #{resolver.field}(#{arguments.join(",")}) #{op.selections}"
94
+ fields_buffer << ") " << op.selections
85
95
  end
86
96
  end
87
97
  end
88
98
 
89
- doc = String.new(QUERY_OP) # << resolver fulfillment always uses query
99
+ doc_buffer = String.new(QUERY_OP) # << resolver fulfillment always uses query
90
100
 
91
101
  if operation_name
92
- doc << " #{operation_name}"
102
+ doc_buffer << " " << operation_name
93
103
  origin_sets_by_operation.each_key do |op|
94
- doc << "_#{op.step}"
104
+ doc_buffer << "_" << op.step.to_s
95
105
  end
96
106
  end
97
107
 
98
- if variable_defs.any?
99
- doc << "(#{variable_defs.map { |k, v| "$#{k}:#{v}" }.join(",")})"
108
+ unless variable_defs.empty?
109
+ doc_buffer << "("
110
+ variable_defs.each_with_index do |(k, v), i|
111
+ doc_buffer << "," unless i.zero?
112
+ doc_buffer << "$" << k << ":" << v
113
+ end
114
+ doc_buffer << ")"
100
115
  end
101
116
 
102
117
  if operation_directives
103
- doc << " #{operation_directives} "
118
+ doc_buffer << " " << operation_directives << " "
104
119
  end
105
120
 
106
- doc << "{ #{query_fields.join(" ")} }"
121
+ doc_buffer << "{ " << fields_buffer << " }"
107
122
 
108
- return doc, variable_defs.keys.tap do |names|
123
+ return doc_buffer, variable_defs.keys.tap do |names|
109
124
  names.reject! { @variables.key?(_1) }
110
125
  end
111
126
  end
@@ -120,10 +135,11 @@ module GraphQL::Stitching
120
135
  origin_set.map.with_index { |_, index| raw_result["_#{batch_index}_#{index}_result"] }
121
136
  end
122
137
 
123
- next unless results&.any?
138
+ next if results.nil? || results.empty?
124
139
 
125
140
  origin_set.each_with_index do |origin_obj, index|
126
- origin_obj.merge!(results[index]) if results[index]
141
+ result = results[index]
142
+ origin_obj.merge!(result) if result
127
143
  end
128
144
  end
129
145
  end
@@ -132,7 +148,7 @@ module GraphQL::Stitching
132
148
  def extract_errors!(origin_sets_by_operation, errors)
133
149
  ops = origin_sets_by_operation.keys
134
150
  origin_sets = origin_sets_by_operation.values
135
- pathed_errors_by_op_index_and_object_id = {}
151
+ pathed_errors_by_op_index_and_object_id = Hash.new { |h, k| h[k] = {} }
136
152
 
137
153
  errors_result = errors.each_with_object([]) do |err, memo|
138
154
  err.delete("locations")
@@ -151,8 +167,8 @@ module GraphQL::Stitching
151
167
  end
152
168
 
153
169
  if origin_obj
154
- by_op_index = pathed_errors_by_op_index_and_object_id[result_alias[1].to_i] ||= {}
155
- by_object_id = by_op_index[origin_obj.object_id] ||= []
170
+ pathed_errors_by_op_index = pathed_errors_by_op_index_and_object_id[result_alias[1].to_i]
171
+ by_object_id = pathed_errors_by_op_index[origin_obj.object_id] ||= []
156
172
  by_object_id << err
157
173
  next
158
174
  end
@@ -162,10 +178,10 @@ module GraphQL::Stitching
162
178
  memo << err
163
179
  end
164
180
 
165
- if pathed_errors_by_op_index_and_object_id.any?
181
+ unless pathed_errors_by_op_index_and_object_id.empty?
166
182
  pathed_errors_by_op_index_and_object_id.each do |op_index, pathed_errors_by_object_id|
167
- repath_errors!(pathed_errors_by_object_id, ops.dig(op_index, "path"))
168
- errors_result.concat(pathed_errors_by_object_id.values)
183
+ repath_errors!(pathed_errors_by_object_id, ops[op_index].path)
184
+ errors_result.push(*pathed_errors_by_object_id.each_value)
169
185
  end
170
186
  end
171
187
 
@@ -180,7 +196,7 @@ module GraphQL::Stitching
180
196
  current_path.push(forward_path.shift)
181
197
  scope = root[current_path.last]
182
198
 
183
- if forward_path.any? && scope.is_a?(Array)
199
+ if !forward_path.empty? && scope.is_a?(Array)
184
200
  scope.each_with_index do |element, index|
185
201
  inner_elements = element.is_a?(Array) ? element.flatten : [element]
186
202
  inner_elements.each do |inner_element|
@@ -190,7 +206,7 @@ module GraphQL::Stitching
190
206
  end
191
207
  end
192
208
 
193
- elsif forward_path.any?
209
+ elsif !forward_path.empty?
194
210
  repath_errors!(pathed_errors_by_object_id, forward_path, current_path, scope)
195
211
 
196
212
  elsif scope.is_a?(Array)
@@ -79,7 +79,7 @@ module GraphQL
79
79
 
80
80
  def exec_task(task)
81
81
  next_steps = task.load.tap(&:compact!)
82
- exec!(next_steps) if next_steps.any?
82
+ exec!(next_steps) unless next_steps.empty?
83
83
  end
84
84
  end
85
85
  end
@@ -80,7 +80,7 @@ module GraphQL
80
80
 
81
81
  return if files_by_path.none?
82
82
 
83
- map = {}
83
+ map = Hash.new { |h, k| h[k] = [] }
84
84
  files = files_by_path.values.tap(&:uniq!)
85
85
  variables_copy = variables.dup
86
86
 
@@ -90,7 +90,6 @@ module GraphQL
90
90
  path.each_with_index do |key, i|
91
91
  if i == path.length - 1
92
92
  file_index = files.index(copy[key]).to_s
93
- map[file_index] ||= []
94
93
  map[file_index] << "variables.#{path.join(".")}"
95
94
  copy[key] = nil
96
95
  elsif orig[key].object_id == copy[key].object_id
@@ -2,21 +2,42 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
- # Immutable-ish structures representing a query plan.
5
+ # Immutable structures representing a query plan.
6
6
  # May serialize to/from JSON.
7
7
  class Plan
8
- Op = Struct.new(
9
- :step,
10
- :after,
11
- :location,
12
- :operation_type,
13
- :selections,
14
- :variables,
15
- :path,
16
- :if_type,
17
- :resolver,
18
- keyword_init: true
19
- ) do
8
+ class Op
9
+ attr_reader :step
10
+ attr_reader :after
11
+ attr_reader :location
12
+ attr_reader :operation_type
13
+ attr_reader :selections
14
+ attr_reader :variables
15
+ attr_reader :path
16
+ attr_reader :if_type
17
+ attr_reader :resolver
18
+
19
+ def initialize(
20
+ step:,
21
+ after:,
22
+ location:,
23
+ operation_type:,
24
+ selections:,
25
+ variables: nil,
26
+ path: nil,
27
+ if_type: nil,
28
+ resolver: nil
29
+ )
30
+ @step = step
31
+ @after = after
32
+ @location = location
33
+ @operation_type = operation_type
34
+ @selections = selections
35
+ @variables = variables
36
+ @path = path
37
+ @if_type = if_type
38
+ @resolver = resolver
39
+ end
40
+
20
41
  def as_json
21
42
  {
22
43
  step: step,
@@ -30,6 +51,18 @@ module GraphQL
30
51
  resolver: resolver
31
52
  }.tap(&:compact!)
32
53
  end
54
+
55
+ def ==(other)
56
+ step == other.step &&
57
+ after == other.after &&
58
+ location == other.location &&
59
+ operation_type == other.operation_type &&
60
+ selections == other.selections &&
61
+ variables == other.variables &&
62
+ path == other.path &&
63
+ if_type == other.if_type &&
64
+ resolver == other.resolver
65
+ end
33
66
  end
34
67
 
35
68
  class << self
@@ -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
94
  next_index = step ? parent_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
 
@@ -102,8 +115,6 @@ module GraphQL
102
115
  end
103
116
  end
104
117
 
105
- ScopePartition = Struct.new(:location, :selections, keyword_init: true)
106
-
107
118
  # A) Group all root selections by their preferred entrypoint locations.
108
119
  def build_root_entrypoints
109
120
  parent_type = @request.query.root_type_for_operation(@request.operation.operation_type)
@@ -111,10 +122,9 @@ module GraphQL
111
122
  case @request.operation.operation_type
112
123
  when QUERY_OP
113
124
  # A.1) Group query fields by location for parallel execution.
114
- selections_by_location = {}
125
+ selections_by_location = Hash.new { |h, k| h[k] = [] }
115
126
  each_field_in_scope(parent_type, @request.operation.selections) do |node|
116
127
  locations = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name] || SUPERGRAPH_LOCATIONS
117
- selections_by_location[locations.first] ||= []
118
128
  selections_by_location[locations.first] << node
119
129
  end
120
130
 
@@ -213,7 +223,7 @@ module GraphQL
213
223
  input_selections.each do |node|
214
224
  case node
215
225
  when GraphQL::Language::Nodes::Field
216
- if node.alias&.start_with?(TypeResolver::EXPORT_PREFIX)
226
+ if node.alias&.start_with?(TypeResolver::EXPORT_PREFIX) && node.object_id != TypeResolver::TYPENAME_EXPORT_NODE.object_id
217
227
  raise StitchingError, %(Alias "#{node.alias}" is not allowed because "#{TypeResolver::EXPORT_PREFIX}" is a reserved prefix.)
218
228
  elsif node.name == TYPENAME
219
229
  locale_selections << node
@@ -229,7 +239,8 @@ module GraphQL
229
239
 
230
240
  # B.3) Collect all variable definitions used within the filtered selection.
231
241
  extract_node_variables(node, locale_variables)
232
- field_type = @supergraph.memoized_schema_fields(parent_type.graphql_name)[node.name].type.unwrap
242
+ schema_fields = @supergraph.memoized_schema_fields(parent_type.graphql_name)
243
+ field_type = schema_fields[node.name].type.unwrap
233
244
 
234
245
  if Util.is_leaf_type?(field_type)
235
246
  locale_selections << node
@@ -284,7 +295,7 @@ module GraphQL
284
295
  remote_selections_by_location = delegate_remote_selections(parent_type, remote_selections)
285
296
 
286
297
  # D) Create paths routing to new entrypoint locations via resolver queries.
287
- routes = @supergraph.route_type_to_locations(parent_type.graphql_name, current_location, remote_selections_by_location.keys)
298
+ routes = @supergraph.route_type_to_locations(parent_type.graphql_name, current_location, remote_selections_by_location.each_key)
288
299
 
289
300
  # E) Translate resolver pathways into new entrypoints.
290
301
  routes.each_value do |route|
@@ -377,9 +388,11 @@ module GraphQL
377
388
  end
378
389
 
379
390
  # C.2) Distribute non-unique fields among locations that were added during C.1.
380
- if selections_by_location.any? && remote_selections.any?
391
+ if !selections_by_location.empty? && !remote_selections.empty?
392
+ available_locations = Set.new(selections_by_location.each_key)
393
+
381
394
  remote_selections.reject! do |node|
382
- used_location = possible_locations_by_field[node.name].find { selections_by_location[_1] }
395
+ used_location = possible_locations_by_field[node.name].find { available_locations.include?(_1) }
383
396
  if used_location
384
397
  selections_by_location[used_location] << node
385
398
  true
@@ -388,29 +401,17 @@ module GraphQL
388
401
  end
389
402
 
390
403
  # 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|
404
+ if !remote_selections.empty?
405
+ field_count_by_location = Hash.new(0)
406
+ remote_selections.each do |node|
393
407
  possible_locations_by_field[node.name].each do |location|
394
- memo[location] ||= 0
395
- memo[location] += 1
408
+ field_count_by_location[location] += 1
396
409
  end
397
410
  end
398
411
 
399
412
  remote_selections.each do |node|
400
413
  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
-
414
+ preferred_location = possible_locations.max_by { field_count_by_location[_1] } || possible_locations.first
414
415
  selections_by_location[preferred_location] ||= []
415
416
  selections_by_location[preferred_location] << node
416
417
  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
@@ -147,6 +147,11 @@ module GraphQL
147
147
  @query.static_errors
148
148
  end
149
149
 
150
+ # @return [Boolean] is the request valid?
151
+ def valid?
152
+ validate.empty?
153
+ end
154
+
150
155
  # Gets and sets the query plan for the request. Assigned query plans may pull from a cache,
151
156
  # which is useful for redundant GraphQL documents (commonly sent by frontend clients).
152
157
  # ```ruby
@@ -21,11 +21,8 @@ module GraphQL::Stitching
21
21
  field_map = {}
22
22
  resolver_map = {}
23
23
  possible_locations = {}
24
- visibility_profiles = if (visibility_def = schema.directives[GraphQL::Stitching.visibility_directive])
25
- visibility_def.get_argument("profiles").default_value
26
- else
27
- []
28
- end
24
+ visibility_definition = schema.directives[GraphQL::Stitching.visibility_directive]
25
+ visibility_profiles = visibility_definition&.get_argument("profiles")&.default_value || EMPTY_ARRAY
29
26
 
30
27
  schema.types.each do |type_name, type|
31
28
  next if type.introspection?
@@ -75,7 +72,7 @@ module GraphQL::Stitching
75
72
  end
76
73
  end
77
74
 
78
- executables = possible_locations.keys.each_with_object({}) do |location, memo|
75
+ executables = possible_locations.each_key.each_with_object({}) do |location, memo|
79
76
  executable = executables[location] || executables[location.to_sym]
80
77
  if validate_executable!(location, executable)
81
78
  memo[location] = executable
@@ -38,7 +38,7 @@ module GraphQL
38
38
  @locations_by_type_and_field = @memoized_introspection_types.each_with_object(fields) do |(type_name, type), memo|
39
39
  next unless type.kind.fields?
40
40
 
41
- memo[type_name] = type.fields.keys.each_with_object({}) do |field_name, m|
41
+ memo[type_name] = type.fields.each_key.each_with_object({}) do |field_name, m|
42
42
  m[field_name] = [SUPERGRAPH_LOCATION]
43
43
  end
44
44
  end.freeze
@@ -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
 
@@ -75,7 +75,7 @@ module GraphQL
75
75
  end
76
76
 
77
77
  def locations
78
- @executables.keys.reject { _1 == SUPERGRAPH_LOCATION }
78
+ @executables.each_key.reject { _1 == SUPERGRAPH_LOCATION }
79
79
  end
80
80
 
81
81
  def memoized_schema_fields(type_name)
@@ -130,7 +130,7 @@ module GraphQL
130
130
  # "Type" => ["location1", "location2", ...]
131
131
  def locations_by_type
132
132
  @locations_by_type ||= @locations_by_type_and_field.each_with_object({}) do |(type_name, fields), memo|
133
- memo[type_name] = fields.values.flatten.uniq
133
+ memo[type_name] = fields.values.tap(&:flatten!).tap(&:uniq!)
134
134
  end
135
135
  end
136
136
 
@@ -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.1"
5
+ VERSION = "1.7.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.7.1
4
+ version: 1.7.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: 2025-05-10 00:00:00.000000000 Z
11
+ date: 2025-06-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -79,13 +79,13 @@ files:
79
79
  - LICENSE
80
80
  - README.md
81
81
  - Rakefile
82
+ - docs/README.md
82
83
  - docs/composing_a_supergraph.md
83
84
  - docs/error_handling.md
84
85
  - docs/executables.md
85
86
  - docs/images/library.png
86
87
  - docs/images/merging.png
87
88
  - docs/images/stitching.png
88
- - docs/introduction.md
89
89
  - docs/merged_types.md
90
90
  - docs/merged_types_apollo.md
91
91
  - docs/performance.md
File without changes