graphql-stitching 1.0.0 → 1.0.1

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.
@@ -21,10 +21,14 @@ module GraphQL
21
21
  end
22
22
  end
23
23
 
24
+ boundaries = delegation_map["boundaries"].map do |k, b|
25
+ [k, b.map { Boundary.new(**_1) }]
26
+ end
27
+
24
28
  new(
25
29
  schema: schema,
26
30
  fields: delegation_map["fields"],
27
- boundaries: delegation_map["boundaries"],
31
+ boundaries: boundaries.to_h,
28
32
  executables: executables,
29
33
  )
30
34
  end
@@ -68,7 +72,7 @@ module GraphQL
68
72
  return GraphQL::Schema::Printer.print_schema(@schema), {
69
73
  "locations" => locations,
70
74
  "fields" => fields,
71
- "boundaries" => @boundaries,
75
+ "boundaries" => @boundaries.map { |k, b| [k, b.map(&:as_json)] }.to_h,
72
76
  }
73
77
  end
74
78
 
@@ -144,7 +148,7 @@ module GraphQL
144
148
  # ("Type") => ["id", ...]
145
149
  def possible_keys_for_type(type_name)
146
150
  @possible_keys_by_type[type_name] ||= begin
147
- @boundaries[type_name].map { _1["key"] }.tap(&:uniq!)
151
+ @boundaries[type_name].map(&:key).tap(&:uniq!)
148
152
  end
149
153
  end
150
154
 
@@ -162,74 +166,76 @@ module GraphQL
162
166
  # used to connect a partial type across locations via boundary queries
163
167
  def route_type_to_locations(type_name, start_location, goal_locations)
164
168
  if possible_keys_for_type(type_name).length > 1
165
- # multiple keys use an a-star search to traverse intermediary locations
169
+ # multiple keys use an A* search to traverse intermediary locations
166
170
  return route_type_to_locations_via_search(type_name, start_location, goal_locations)
167
171
  end
168
172
 
169
173
  # types with a single key attribute must all be within a single hop of each other,
170
174
  # so can use a simple match to collect boundaries for the goal locations.
171
175
  @boundaries[type_name].each_with_object({}) do |boundary, memo|
172
- if goal_locations.include?(boundary["location"])
173
- memo[boundary["location"]] = [boundary]
176
+ if goal_locations.include?(boundary.location)
177
+ memo[boundary.location] = [boundary]
174
178
  end
175
179
  end
176
180
  end
177
181
 
178
182
  private
179
183
 
180
- # tunes a-star search to favor paths with fewest joining locations, ie:
184
+ PathNode = Struct.new(:location, :key, :cost, :boundary, keyword_init: true)
185
+
186
+ # tunes A* search to favor paths with fewest joining locations, ie:
181
187
  # favor longer paths through target locations over shorter paths with additional locations.
182
188
  def route_type_to_locations_via_search(type_name, start_location, goal_locations)
183
189
  results = {}
184
190
  costs = {}
185
191
 
186
192
  paths = possible_keys_for_type_and_location(type_name, start_location).map do |possible_key|
187
- [{ location: start_location, key: possible_key, cost: 0 }]
193
+ [PathNode.new(location: start_location, key: possible_key, cost: 0)]
188
194
  end
189
195
 
190
196
  while paths.any?
191
197
  path = paths.pop
192
- current_location = path.last[:location]
193
- current_key = path.last[:key]
194
- current_cost = path.last[:cost]
198
+ current_location = path.last.location
199
+ current_key = path.last.key
200
+ current_cost = path.last.cost
195
201
 
196
202
  @boundaries[type_name].each do |boundary|
197
- forward_location = boundary["location"]
198
- next if current_key != boundary["key"]
199
- next if path.any? { _1[:location] == forward_location }
203
+ forward_location = boundary.location
204
+ next if current_key != boundary.key
205
+ next if path.any? { _1.location == forward_location }
200
206
 
201
207
  best_cost = costs[forward_location] || Float::INFINITY
202
208
  next if best_cost < current_cost
203
209
 
204
210
  path.pop
205
- path << {
211
+ path << PathNode.new(
206
212
  location: current_location,
207
213
  key: current_key,
208
214
  cost: current_cost,
209
215
  boundary: boundary,
210
- }
216
+ )
211
217
 
212
218
  if goal_locations.include?(forward_location)
213
219
  current_result = results[forward_location]
214
220
  if current_result.nil? || current_cost < best_cost || (current_cost == best_cost && path.length < current_result.length)
215
- results[forward_location] = path.map { _1[:boundary] }
221
+ results[forward_location] = path.map(&:boundary)
216
222
  end
217
223
  else
218
- path.last[:cost] += 1
224
+ path.last.cost += 1
219
225
  end
220
226
 
221
- forward_cost = path.last[:cost]
227
+ forward_cost = path.last.cost
222
228
  costs[forward_location] = forward_cost if forward_cost < best_cost
223
229
 
224
230
  possible_keys_for_type_and_location(type_name, forward_location).each do |possible_key|
225
- paths << [*path, { location: forward_location, key: possible_key, cost: forward_cost }]
231
+ paths << [*path, PathNode.new(location: forward_location, key: possible_key, cost: forward_cost)]
226
232
  end
227
233
  end
228
234
 
229
235
  paths.sort! do |a, b|
230
- cost_diff = a.last[:cost] - b.last[:cost]
231
- cost_diff.zero? ? a.length - b.length : cost_diff
232
- end.reverse!
236
+ cost_diff = b.last.cost - a.last.cost
237
+ cost_diff.zero? ? b.length - a.length : cost_diff
238
+ end
233
239
  end
234
240
 
235
241
  results
@@ -3,54 +3,65 @@
3
3
  module GraphQL
4
4
  module Stitching
5
5
  class Util
6
- # specifies if a type is a primitive leaf value
7
- def self.is_leaf_type?(type)
8
- type.kind.scalar? || type.kind.enum?
9
- end
6
+ TypeStructure = Struct.new(:list, :null, :name, keyword_init: true) do
7
+ alias_method :list?, :list
8
+ alias_method :null?, :null
10
9
 
11
- # strips non-null wrappers from a type
12
- def self.unwrap_non_null(type)
13
- type = type.of_type while type.non_null?
14
- type
10
+ def non_null?
11
+ !null
12
+ end
15
13
  end
16
14
 
17
- # builds a single-dimensional representation of a wrapped type structure
18
- def self.flatten_type_structure(type)
19
- structure = []
15
+ class << self
16
+ # specifies if a type is a primitive leaf value
17
+ def is_leaf_type?(type)
18
+ type.kind.scalar? || type.kind.enum?
19
+ end
20
+
21
+ # strips non-null wrappers from a type
22
+ def unwrap_non_null(type)
23
+ type = type.of_type while type.non_null?
24
+ type
25
+ end
26
+
27
+ # builds a single-dimensional representation of a wrapped type structure
28
+ def flatten_type_structure(type)
29
+ structure = []
30
+
31
+ while type.list?
32
+ structure << TypeStructure.new(
33
+ list: true,
34
+ null: !type.non_null?,
35
+ name: nil,
36
+ )
37
+
38
+ type = unwrap_non_null(type).of_type
39
+ end
20
40
 
21
- while type.list?
22
- structure << {
23
- list: true,
41
+ structure << TypeStructure.new(
42
+ list: false,
24
43
  null: !type.non_null?,
25
- name: nil,
26
- }
44
+ name: type.unwrap.graphql_name,
45
+ )
27
46
 
28
- type = unwrap_non_null(type).of_type
47
+ structure
29
48
  end
30
49
 
31
- structure << {
32
- list: false,
33
- null: !type.non_null?,
34
- name: type.unwrap.graphql_name,
35
- }
36
-
37
- structure
38
- end
50
+ # expands interfaces and unions to an array of their memberships
51
+ # like `schema.possible_types`, but includes child interfaces
52
+ def expand_abstract_type(schema, parent_type)
53
+ return [] unless parent_type.kind.abstract?
54
+ return parent_type.possible_types if parent_type.kind.union?
39
55
 
40
- # expands interfaces and unions to an array of their memberships
41
- # like `schema.possible_types`, but includes child interfaces
42
- def self.expand_abstract_type(schema, parent_type)
43
- return [] unless parent_type.kind.abstract?
44
- return parent_type.possible_types if parent_type.kind.union?
45
-
46
- result = []
47
- schema.types.values.each do |type|
48
- next unless type <= GraphQL::Schema::Interface && type != parent_type
49
- next unless type.interfaces.include?(parent_type)
50
- result << type
51
- result.push(*expand_abstract_type(schema, type)) if type.kind.interface?
56
+ result = []
57
+ schema.types.values.each do |type|
58
+ next unless type <= GraphQL::Schema::Interface && type != parent_type
59
+ next unless type.interfaces.include?(parent_type)
60
+ result << type
61
+ result.push(*expand_abstract_type(schema, type)) if type.kind.interface?
62
+ end
63
+ result.uniq
52
64
  end
53
- result.uniq
54
65
  end
55
66
  end
56
67
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
- VERSION = "1.0.0"
5
+ VERSION = "1.0.1"
6
6
  end
7
7
  end
@@ -10,7 +10,6 @@ module GraphQL
10
10
  class StitchingError < StandardError; end
11
11
 
12
12
  class << self
13
-
14
13
  def stitch_directive
15
14
  @stitch_directive ||= "stitch"
16
15
  end
@@ -25,13 +24,17 @@ module GraphQL
25
24
  end
26
25
 
27
26
  require_relative "stitching/supergraph"
27
+ require_relative "stitching/boundary"
28
28
  require_relative "stitching/client"
29
29
  require_relative "stitching/composer"
30
30
  require_relative "stitching/executor"
31
31
  require_relative "stitching/http_executable"
32
- require_relative "stitching/planner_operation"
32
+ require_relative "stitching/plan"
33
+ require_relative "stitching/planner_step"
33
34
  require_relative "stitching/planner"
34
35
  require_relative "stitching/request"
36
+ require_relative "stitching/selection_hint"
35
37
  require_relative "stitching/shaper"
38
+ require_relative "stitching/skip_include"
36
39
  require_relative "stitching/util"
37
40
  require_relative "stitching/version"
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.0
4
+ version: 1.0.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-08-01 00:00:00.000000000 Z
11
+ date: 2023-10-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -97,6 +97,7 @@ files:
97
97
  - gemfiles/graphql_1.13.9.gemfile
98
98
  - graphql-stitching.gemspec
99
99
  - lib/graphql/stitching.rb
100
+ - lib/graphql/stitching/boundary.rb
100
101
  - lib/graphql/stitching/client.rb
101
102
  - lib/graphql/stitching/composer.rb
102
103
  - lib/graphql/stitching/composer/base_validator.rb
@@ -106,10 +107,13 @@ files:
106
107
  - lib/graphql/stitching/executor/boundary_source.rb
107
108
  - lib/graphql/stitching/executor/root_source.rb
108
109
  - lib/graphql/stitching/http_executable.rb
110
+ - lib/graphql/stitching/plan.rb
109
111
  - lib/graphql/stitching/planner.rb
110
- - lib/graphql/stitching/planner_operation.rb
112
+ - lib/graphql/stitching/planner_step.rb
111
113
  - lib/graphql/stitching/request.rb
114
+ - lib/graphql/stitching/selection_hint.rb
112
115
  - lib/graphql/stitching/shaper.rb
116
+ - lib/graphql/stitching/skip_include.rb
113
117
  - lib/graphql/stitching/supergraph.rb
114
118
  - lib/graphql/stitching/util.rb
115
119
  - lib/graphql/stitching/version.rb
@@ -1,63 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module GraphQL
4
- module Stitching
5
- class PlannerOperation
6
- LANGUAGE_PRINTER = GraphQL::Language::Printer.new
7
-
8
- attr_reader :order, :location, :parent_type, :if_type, :operation_type, :path
9
- attr_accessor :after, :selections, :variables, :boundary
10
-
11
- def initialize(
12
- location:,
13
- parent_type:,
14
- order:,
15
- after: nil,
16
- operation_type: "query",
17
- selections: [],
18
- variables: [],
19
- path: [],
20
- if_type: nil,
21
- boundary: nil
22
- )
23
- @location = location
24
- @parent_type = parent_type
25
- @order = order
26
- @after = after
27
- @operation_type = operation_type
28
- @selections = selections
29
- @variables = variables
30
- @path = path
31
- @if_type = if_type
32
- @boundary = boundary
33
- end
34
-
35
- def selection_set
36
- op = GraphQL::Language::Nodes::OperationDefinition.new(selections: @selections)
37
- LANGUAGE_PRINTER.print(op).gsub!(/\s+/, " ").strip!
38
- end
39
-
40
- def variable_set
41
- @variables.each_with_object({}) do |(variable_name, value_type), memo|
42
- memo[variable_name] = LANGUAGE_PRINTER.print(value_type)
43
- end
44
- end
45
-
46
- def to_h
47
- data = {
48
- "order" => @order,
49
- "after" => @after,
50
- "location" => @location,
51
- "operation_type" => @operation_type,
52
- "selections" => selection_set,
53
- "variables" => variable_set,
54
- "path" => @path,
55
- }
56
-
57
- data["if_type"] = @if_type if @if_type
58
- data["boundary"] = @boundary if @boundary
59
- data
60
- end
61
- end
62
- end
63
- end