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.
@@ -11,6 +11,7 @@ module GraphQL::Stitching
11
11
  @request = request
12
12
  @supergraph = request.supergraph
13
13
  @root_type = nil
14
+ @possible_type_names_by_type = nil
14
15
  end
15
16
 
16
17
  def perform!(raw)
@@ -24,16 +25,23 @@ module GraphQL::Stitching
24
25
  return nil if raw_object.nil?
25
26
 
26
27
  typename ||= raw_object[TypeResolver::TYPENAME_EXPORT_NODE.alias]
28
+ typename ||= parent_type.graphql_name unless parent_type.kind.abstract?
29
+
27
30
  raw_object.reject! { |key, _v| TypeResolver.export_key?(key) }
28
31
 
29
32
  selections.each do |node|
30
33
  case node
31
34
  when GraphQL::Language::Nodes::Field
32
35
  field_name = node.alias || node.name
36
+ raw_value = raw_object.delete(field_name)
33
37
 
34
38
  if @request.query.get_field(parent_type, node.name).introspection?
35
- if node.name == TYPENAME && parent_type == @root_type && node != TypeResolver::TYPENAME_EXPORT_NODE
36
- raw_object[field_name] = @root_type.graphql_name
39
+ next if TypeResolver.export_key?(field_name)
40
+
41
+ raw_object[field_name] = if node.name == TYPENAME && parent_type == @root_type
42
+ @root_type.graphql_name
43
+ else
44
+ raw_value
37
45
  end
38
46
  next
39
47
  end
@@ -42,14 +50,14 @@ module GraphQL::Stitching
42
50
  named_type = node_type.unwrap
43
51
 
44
52
  raw_object[field_name] = if node_type.list?
45
- resolve_list_scope(raw_object[field_name], Util.unwrap_non_null(node_type), node.selections)
53
+ resolve_list_scope(raw_value, Util.unwrap_non_null(node_type), node.selections)
46
54
  elsif Util.is_leaf_type?(named_type)
47
- raw_object[field_name]
55
+ raw_value
48
56
  else
49
- resolve_object_scope(raw_object[field_name], named_type, node.selections)
57
+ resolve_object_scope(raw_value, named_type, node.selections)
50
58
  end
51
59
 
52
- return nil if raw_object[field_name].nil? && node_type.non_null?
60
+ return nil if node_type.non_null? && raw_object[field_name].nil?
53
61
 
54
62
  when GraphQL::Language::Nodes::InlineFragment
55
63
  fragment_type = node.type ? @supergraph.memoized_schema_types[node.type.name] : parent_type
@@ -79,36 +87,37 @@ module GraphQL::Stitching
79
87
 
80
88
  next_node_type = Util.unwrap_non_null(current_node_type).of_type
81
89
  named_type = next_node_type.unwrap
82
- contains_null = false
90
+
91
+ if Util.is_leaf_type?(named_type)
92
+ return nil if next_node_type.non_null? && raw_list.include?(nil)
93
+
94
+ return raw_list
95
+ end
83
96
 
84
97
  resolved_list = raw_list.map! do |raw_list_element|
85
98
  result = if next_node_type.list?
86
99
  resolve_list_scope(raw_list_element, next_node_type, selections)
87
- elsif Util.is_leaf_type?(named_type)
88
- raw_list_element
89
100
  else
90
101
  resolve_object_scope(raw_list_element, named_type, selections)
91
102
  end
92
103
 
93
- if result.nil?
94
- contains_null = true
95
- return nil if current_node_type.non_null?
96
- end
104
+ return nil if result.nil? && next_node_type.non_null?
97
105
 
98
106
  result
99
107
  end
100
108
 
101
- return nil if contains_null && next_node_type.non_null?
102
-
103
109
  resolved_list
104
110
  end
105
111
 
106
112
  def typename_in_type?(typename, type)
107
113
  return true if type.graphql_name == typename
114
+ return false unless typename && type.kind.abstract?
108
115
 
109
- type.kind.abstract? && @request.query.possible_types(type).any? do |t|
110
- t.graphql_name == typename
111
- end
116
+ possible_type_names(type).include?(typename)
117
+ end
118
+
119
+ def possible_type_names(type)
120
+ (@possible_type_names_by_type ||= {})[type.graphql_name] ||= @request.query.possible_types(type).map(&:graphql_name)
112
121
  end
113
122
  end
114
123
  end
@@ -3,40 +3,39 @@
3
3
  module GraphQL::Stitching
4
4
  class Executor
5
5
  class TypeResolverSource < GraphQL::Dataloader::Source
6
+ include PathAccess
7
+
6
8
  def initialize(executor, location)
7
9
  @executor = executor
8
10
  @location = location
9
- @variables = {}
10
11
  end
11
12
 
12
13
  def fetch(ops)
13
14
  origin_sets_by_operation = ops.each_with_object({}.compare_by_identity) do |op, memo|
14
- origin_set = op.path.reduce([@executor.data]) do |set, path_segment|
15
- set.flat_map { |obj| obj && obj[path_segment] }.tap(&:compact!)
16
- end
15
+ origin_set = path_objects(@executor.data, op.path)
17
16
 
18
17
  if op.if_type
19
18
  # operations planned around unused fragment conditions should not trigger requests
20
- origin_set.select! { _1[TypeResolver::TYPENAME_EXPORT_NODE.alias] == op.if_type }
19
+ origin_set.select! { |origin_obj| origin_obj[TypeResolver::TYPENAME_EXPORT_NODE.alias] == op.if_type }
21
20
  end
22
21
 
23
- memo[op] = origin_set if origin_set.any?
22
+ memo[op] = origin_set unless origin_set.empty?
24
23
  end
25
24
 
26
- if origin_sets_by_operation.any?
27
- query_document, variable_names = build_document(
25
+ unless origin_sets_by_operation.empty?
26
+ query_document, variable_names, generated_variables = build_document(
28
27
  origin_sets_by_operation,
29
28
  @executor.request.operation_name,
30
29
  @executor.request.operation_directives,
31
30
  )
32
- variables = @variables.merge!(@executor.request.variables.slice(*variable_names))
31
+ variables = generated_variables.merge(@executor.request.variables.slice(*variable_names))
33
32
  raw_result = @executor.request.supergraph.execute_at_location(@location, query_document, variables, @executor.request)
34
33
  @executor.query_count += 1
35
34
 
36
35
  merge_results!(origin_sets_by_operation, raw_result.dig("data"))
37
36
 
38
37
  errors = raw_result.dig("errors")
39
- @executor.errors.concat(extract_errors!(origin_sets_by_operation, errors)) if errors&.any?
38
+ @executor.errors.concat(extract_errors!(origin_sets_by_operation, errors)) if errors && !errors.empty?
40
39
  end
41
40
 
42
41
  ops.map { origin_sets_by_operation[_1] ? _1.step : nil }
@@ -51,63 +50,81 @@ module GraphQL::Stitching
51
50
  # }"
52
51
  def build_document(origin_sets_by_operation, operation_name = nil, operation_directives = nil)
53
52
  variable_defs = {}
54
- query_fields = origin_sets_by_operation.map.with_index do |(op, origin_set), batch_index|
53
+ generated_variables = {}
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
- @variables[variable_name] = origin_set.map { arg.build(_1) }
68
+ generated_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
- @variables[variable_name] = arg.build(origin_obj)
86
+ generated_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|
109
- names.reject! { @variables.key?(_1) }
123
+ variable_names = variable_defs.keys.tap do |names|
124
+ names.reject! { generated_variables.key?(_1) }
110
125
  end
126
+
127
+ return doc_buffer, variable_names, generated_variables
111
128
  end
112
129
 
113
130
  def merge_results!(origin_sets_by_operation, raw_result)
@@ -120,94 +137,67 @@ module GraphQL::Stitching
120
137
  origin_set.map.with_index { |_, index| raw_result["_#{batch_index}_#{index}_result"] }
121
138
  end
122
139
 
123
- next unless results&.any?
140
+ next if results.nil? || results.empty?
124
141
 
125
142
  origin_set.each_with_index do |origin_obj, index|
126
- origin_obj.merge!(results[index]) if results[index]
143
+ result = results[index]
144
+ origin_obj.merge!(result) if result
127
145
  end
128
146
  end
129
147
  end
130
148
 
131
149
  # https://spec.graphql.org/June2018/#sec-Errors
132
- def extract_errors!(origin_sets_by_operation, errors)
150
+ def extract_errors!(origin_sets_by_operation, errors, origin_paths_by_operation = nil)
133
151
  ops = origin_sets_by_operation.keys
134
152
  origin_sets = origin_sets_by_operation.values
135
- pathed_errors_by_op_index_and_object_id = {}
153
+ origin_paths_by_operation ||= origin_sets_by_operation.each_with_object({}.compare_by_identity) do |(op, origin_set), memo|
154
+ memo[op] = paths_for_origin_set(op, origin_set)
155
+ end
136
156
 
137
- errors_result = errors.each_with_object([]) do |err, memo|
138
- err.delete("locations")
157
+ errors.each_with_object([]) do |err, memo|
139
158
  path = err["path"]
140
159
 
141
160
  if path && path.length > 0
142
161
  result_alias = /^_(\d+)(?:_(\d+))?_result$/.match(path.first.to_s)
143
162
 
144
163
  if result_alias
145
- path = err["path"] = path[1..-1]
164
+ path = path[1..-1]
165
+ batch_index = result_alias[1].to_i
146
166
 
147
- origin_obj = if result_alias[2]
148
- origin_sets.dig(result_alias[1].to_i, result_alias[2].to_i)
149
- elsif path[0].is_a?(Integer) || /\d+/.match?(path[0].to_s)
150
- origin_sets.dig(result_alias[1].to_i, path.shift.to_i)
167
+ origin_index = if result_alias[2]
168
+ result_alias[2].to_i
169
+ elsif path[0].is_a?(Integer) || /\A\d+\z/.match?(path[0].to_s)
170
+ path.shift.to_i
151
171
  end
172
+ origin_obj = origin_sets.dig(batch_index, origin_index) if origin_index
152
173
 
153
174
  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] ||= []
156
- by_object_id << err
157
- next
175
+ op = ops[batch_index]
176
+ object_path = origin_paths_by_operation.dig(op, origin_index)
177
+
178
+ if object_path
179
+ memo << sanitized_error(err, path: object_path + path)
180
+ next
181
+ end
158
182
  end
183
+
184
+ memo << sanitized_error(err, path: path)
185
+ next
159
186
  end
160
187
  end
161
188
 
162
- memo << err
189
+ memo << sanitized_error(err)
163
190
  end
164
-
165
- if pathed_errors_by_op_index_and_object_id.any?
166
- 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.push(*pathed_errors_by_object_id.each_value)
169
- end
170
- end
171
-
172
- errors_result.tap(&:flatten!)
173
191
  end
174
192
 
175
193
  private
176
194
 
177
- # traverse forward through origin data, expanding arrays to follow all paths
178
- # any errors found for an origin object_id have their path prefixed by the object path
179
- def repath_errors!(pathed_errors_by_object_id, forward_path, current_path=[], root=@executor.data)
180
- current_path.push(forward_path.shift)
181
- scope = root[current_path.last]
182
-
183
- if forward_path.any? && scope.is_a?(Array)
184
- scope.each_with_index do |element, index|
185
- inner_elements = element.is_a?(Array) ? element.flatten : [element]
186
- inner_elements.each do |inner_element|
187
- current_path << index
188
- repath_errors!(pathed_errors_by_object_id, forward_path, current_path, inner_element)
189
- current_path.pop
190
- end
191
- end
192
-
193
- elsif forward_path.any?
194
- repath_errors!(pathed_errors_by_object_id, forward_path, current_path, scope)
195
-
196
- elsif scope.is_a?(Array)
197
- scope.each_with_index do |element, index|
198
- inner_elements = element.is_a?(Array) ? element.flatten : [element]
199
- inner_elements.each do |inner_element|
200
- errors = pathed_errors_by_object_id[inner_element.object_id]
201
- errors.each { _1["path"] = [*current_path, index, *_1["path"]] } if errors
202
- end
203
- end
204
-
205
- else
206
- errors = pathed_errors_by_object_id[scope.object_id]
207
- errors.each { _1["path"] = [*current_path, *_1["path"]] } if errors
195
+ def paths_for_origin_set(op, origin_set)
196
+ paths_by_object_id = path_entries(@executor.data, op.path).each_with_object(Hash.new { |h, k| h[k] = [] }) do |(object, path), memo|
197
+ memo[object.object_id] << path
208
198
  end
209
199
 
210
- forward_path.unshift(current_path.pop)
200
+ origin_set.map { |origin_obj| paths_by_object_id[origin_obj.object_id].shift }
211
201
  end
212
202
  end
213
203
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require_relative "executor/path_access"
4
5
  require_relative "executor/root_source"
5
6
  require_relative "executor/type_resolver_source"
6
7
  require_relative "executor/shaper"
@@ -79,7 +80,7 @@ module GraphQL
79
80
 
80
81
  def exec_task(task)
81
82
  next_steps = task.load.tap(&:compact!)
82
- exec!(next_steps) if next_steps.any?
83
+ exec!(next_steps) unless next_steps.empty?
83
84
  end
84
85
  end
85
86
  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