graphql-stitching 0.1.0 → 0.2.2

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.
@@ -4,14 +4,14 @@
4
4
  module GraphQL
5
5
  module Stitching
6
6
  class Shaper
7
- def initialize(schema:, document:)
7
+ def initialize(schema:, request:)
8
8
  @schema = schema
9
- @document = document
9
+ @request = request
10
10
  end
11
11
 
12
12
  def perform!(raw)
13
- raw["data"] = resolve_object_scope(raw["data"], @schema.query, @document.operation.selections)
14
- raw
13
+ root_type = @schema.public_send(@request.operation.operation_type)
14
+ resolve_object_scope(raw, root_type, @request.operation.selections)
15
15
  end
16
16
 
17
17
  private
@@ -29,7 +29,7 @@ module GraphQL
29
29
 
30
30
  field_name = node.alias || node.name
31
31
  node_type = parent_type.fields[node.name].type
32
- named_type = Util.get_named_type_for_field_node(@schema, parent_type, node)
32
+ named_type = Util.named_type_for_field_node(@schema, parent_type, node)
33
33
 
34
34
  raw_object[field_name] = if node_type.list?
35
35
  resolve_list_scope(raw_object[field_name], Util.unwrap_non_null(node_type), node.selections)
@@ -48,7 +48,7 @@ module GraphQL
48
48
  return nil if result.nil?
49
49
 
50
50
  when GraphQL::Language::Nodes::FragmentSpread
51
- fragment = @document.fragment_definitions[node.name]
51
+ fragment = @request.fragment_definitions[node.name]
52
52
  fragment_type = @schema.types[fragment.type.name]
53
53
  next unless typename == fragment_type.graphql_name
54
54
 
@@ -67,7 +67,7 @@ module GraphQL
67
67
  return nil if raw_list.nil?
68
68
 
69
69
  next_node_type = Util.unwrap_non_null(current_node_type).of_type
70
- named_type = Util.get_named_type(next_node_type)
70
+ named_type = next_node_type.unwrap
71
71
  contains_null = false
72
72
 
73
73
  resolved_list = raw_list.map! do |raw_list_element|
@@ -29,6 +29,7 @@ module GraphQL
29
29
  end
30
30
  end
31
31
 
32
+ @possible_keys_by_type = {}
32
33
  @possible_keys_by_type_and_location = {}
33
34
  @executables = { LOCATION => @schema }.merge!(executables)
34
35
  end
@@ -57,31 +58,33 @@ module GraphQL
57
58
  def assign_executable(location, executable = nil, &block)
58
59
  executable ||= block
59
60
  unless executable.is_a?(Class) && executable <= GraphQL::Schema
60
- raise "A client or block handler must be provided." unless executable
61
- raise "A client must be callable" unless executable.respond_to?(:call)
61
+ raise StitchingError, "A client or block handler must be provided." unless executable
62
+ raise StitchingError, "A client must be callable" unless executable.respond_to?(:call)
62
63
  end
63
64
  @executables[location] = executable
64
65
  end
65
66
 
66
- def execute_at_location(location, query, variables)
67
+ def execute_at_location(location, query, variables, context)
67
68
  executable = executables[location]
68
69
 
69
70
  if executable.nil?
70
- raise "No executable assigned for #{location} location."
71
+ raise StitchingError, "No executable assigned for #{location} location."
71
72
  elsif executable.is_a?(Class) && executable <= GraphQL::Schema
72
73
  executable.execute(
73
74
  query: query,
74
75
  variables: variables,
76
+ context: context.frozen? ? context.dup : context,
75
77
  validate: false,
76
78
  )
77
79
  elsif executable.respond_to?(:call)
78
- executable.call(location, query, variables)
80
+ executable.call(location, query, variables, context)
79
81
  else
80
- raise "Missing valid executable for #{location} location."
82
+ raise StitchingError, "Missing valid executable for #{location} location."
81
83
  end
82
84
  end
83
85
 
84
86
  # inverts fields map to provide fields for a type/location
87
+ # "Type" => "location" => ["field1", "field2", ...]
85
88
  def fields_by_type_and_location
86
89
  @fields_by_type_and_location ||= @locations_by_type_and_field.each_with_object({}) do |(type_name, fields), memo|
87
90
  memo[type_name] = fields.each_with_object({}) do |(field_name, locations), memo|
@@ -93,24 +96,55 @@ module GraphQL
93
96
  end
94
97
  end
95
98
 
99
+ # { "Type" => ["location1", "location2", ...] }
96
100
  def locations_by_type
97
101
  @locations_by_type ||= @locations_by_type_and_field.each_with_object({}) do |(type_name, fields), memo|
98
102
  memo[type_name] = fields.values.flatten.uniq
99
103
  end
100
104
  end
101
105
 
106
+ # collects all possible boundary keys for a given type
107
+ # { "Type" => ["id", ...] }
108
+ def possible_keys_for_type(type_name)
109
+ @possible_keys_by_type[type_name] ||= begin
110
+ keys = @boundaries[type_name].map { _1["selection"] }
111
+ keys.uniq!
112
+ keys
113
+ end
114
+ end
115
+
116
+ # collects possible boundary keys for a given type and location
117
+ # ("Type", "location") => ["id", ...]
102
118
  def possible_keys_for_type_and_location(type_name, location)
103
119
  possible_keys_by_type = @possible_keys_by_type_and_location[type_name] ||= {}
104
120
  possible_keys_by_type[location] ||= begin
105
121
  location_fields = fields_by_type_and_location[type_name][location] || []
106
- location_fields & @boundaries[type_name].map { _1["selection"] }
122
+ location_fields & possible_keys_for_type(type_name)
107
123
  end
108
124
  end
109
125
 
110
- # For a given type, route from one origin service to one or more remote locations.
111
- # Tunes a-star search to favor paths with fewest joining locations, ie:
112
- # favor longer paths through target locations over shorter paths with additional locations.
126
+ # For a given type, route from one origin location to one or more remote locations
127
+ # used to connect a partial type across locations via boundary queries
113
128
  def route_type_to_locations(type_name, start_location, goal_locations)
129
+ if possible_keys_for_type(type_name).length > 1
130
+ # multiple keys use an a-star search to traverse intermediary locations
131
+ return route_type_to_locations_via_search(type_name, start_location, goal_locations)
132
+ end
133
+
134
+ # types with a single key attribute must all be within a single hop of each other,
135
+ # so can use a simple match to collect boundaries for the goal locations.
136
+ @boundaries[type_name].each_with_object({}) do |boundary, memo|
137
+ if goal_locations.include?(boundary["location"])
138
+ memo[boundary["location"]] = [boundary]
139
+ end
140
+ end
141
+ end
142
+
143
+ private
144
+
145
+ # tunes a-star search to favor paths with fewest joining locations, ie:
146
+ # favor longer paths through target locations over shorter paths with additional locations.
147
+ def route_type_to_locations_via_search(type_name, start_location, goal_locations)
114
148
  results = {}
115
149
  costs = {}
116
150
 
@@ -3,13 +3,9 @@
3
3
  module GraphQL
4
4
  module Stitching
5
5
  class Util
6
-
7
- # gets the named type at the bottom of a non-null/list wrapper tree
8
- def self.get_named_type(type)
9
- while type.respond_to?(:of_type)
10
- type = type.of_type
11
- end
12
- type
6
+ # specifies if a type is a primitive leaf value
7
+ def self.is_leaf_type?(type)
8
+ type.kind.scalar? || type.kind.enum?
13
9
  end
14
10
 
15
11
  # strips non-null wrappers from a type
@@ -20,6 +16,31 @@ module GraphQL
20
16
  type
21
17
  end
22
18
 
19
+ # gets a named type for a field node, including hidden root introspections
20
+ def self.named_type_for_field_node(schema, parent_type, node)
21
+ if node.name == "__schema" && parent_type == schema.query
22
+ schema.types["__Schema"]
23
+ else
24
+ parent_type.fields[node.name].type.unwrap
25
+ end
26
+ end
27
+
28
+ # expands interfaces and unions to an array of their memberships
29
+ # like `schema.possible_types`, but includes child interfaces
30
+ def self.expand_abstract_type(schema, parent_type)
31
+ return [] unless parent_type.kind.abstract?
32
+ return parent_type.possible_types if parent_type.kind.union?
33
+
34
+ result = []
35
+ schema.types.values.each do |type|
36
+ next unless type <= GraphQL::Schema::Interface && type != parent_type
37
+ next unless type.interfaces.include?(parent_type)
38
+ result << type
39
+ result.push(*expand_abstract_type(schema, type)) if type.kind.interface?
40
+ end
41
+ result.uniq
42
+ end
43
+
23
44
  # gets a deep structural description of a list value type
24
45
  def self.get_list_structure(type)
25
46
  structure = []
@@ -38,34 +59,6 @@ module GraphQL
38
59
  end
39
60
  structure
40
61
  end
41
-
42
- # Gets all objects and interfaces that implement a given interface
43
- def self.get_possible_types(schema, parent_type)
44
- return [parent_type] unless parent_type.kind.abstract?
45
- return parent_type.possible_types if parent_type.kind.union?
46
-
47
- result = []
48
- schema.types.values.each do |type|
49
- next unless type <= GraphQL::Schema::Interface && type != parent_type
50
- next unless type.interfaces.include?(parent_type)
51
- result << type
52
- result.push(*get_possible_types(schema, type)) if type.kind.interface?
53
- end
54
- result.uniq
55
- end
56
-
57
- # Specifies if a type is a leaf node (no children)
58
- def self.is_leaf_type?(type)
59
- type.kind.scalar? || type.kind.enum?
60
- end
61
-
62
- def self.get_named_type_for_field_node(schema, parent_type, node)
63
- if node.name == "__schema" && parent_type == schema.query
64
- schema.types["__Schema"] # type mapped to phantom introspection field
65
- else
66
- Util.get_named_type(parent_type.fields[node.name].type)
67
- end
68
- end
69
62
  end
70
63
  end
71
64
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.2"
6
6
  end
7
7
  end
@@ -4,6 +4,8 @@ require "graphql"
4
4
 
5
5
  module GraphQL
6
6
  module Stitching
7
+ EMPTY_OBJECT = {}.freeze
8
+
7
9
  class StitchingError < StandardError; end
8
10
 
9
11
  class << self
@@ -13,6 +15,10 @@ module GraphQL
13
15
  end
14
16
 
15
17
  attr_writer :stitch_directive
18
+
19
+ def stitching_directive_names
20
+ [stitch_directive]
21
+ end
16
22
  end
17
23
  end
18
24
  end
@@ -20,11 +26,11 @@ end
20
26
  require_relative "stitching/gateway"
21
27
  require_relative "stitching/supergraph"
22
28
  require_relative "stitching/composer"
23
- require_relative "stitching/document"
24
29
  require_relative "stitching/executor"
25
30
  require_relative "stitching/planner_operation"
26
31
  require_relative "stitching/planner"
27
32
  require_relative "stitching/remote_client"
33
+ require_relative "stitching/request"
28
34
  require_relative "stitching/shaper"
29
35
  require_relative "stitching/util"
30
36
  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: 0.1.0
4
+ version: 0.2.2
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-02-11 00:00:00.000000000 Z
11
+ date: 2023-02-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 2.0.16
19
+ version: 2.0.3
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 2.0.16
26
+ version: 2.0.3
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -81,13 +81,13 @@ files:
81
81
  - Rakefile
82
82
  - docs/README.md
83
83
  - docs/composer.md
84
- - docs/document.md
85
84
  - docs/executor.md
86
85
  - docs/gateway.md
87
86
  - docs/images/library.png
88
87
  - docs/images/merging.png
89
88
  - docs/images/stitching.png
90
89
  - docs/planner.md
90
+ - docs/request.md
91
91
  - docs/supergraph.md
92
92
  - example/gateway.rb
93
93
  - example/graphiql.html
@@ -99,12 +99,12 @@ files:
99
99
  - lib/graphql/stitching/composer/base_validator.rb
100
100
  - lib/graphql/stitching/composer/validate_boundaries.rb
101
101
  - lib/graphql/stitching/composer/validate_interfaces.rb
102
- - lib/graphql/stitching/document.rb
103
102
  - lib/graphql/stitching/executor.rb
104
103
  - lib/graphql/stitching/gateway.rb
105
104
  - lib/graphql/stitching/planner.rb
106
105
  - lib/graphql/stitching/planner_operation.rb
107
106
  - lib/graphql/stitching/remote_client.rb
107
+ - lib/graphql/stitching/request.rb
108
108
  - lib/graphql/stitching/shaper.rb
109
109
  - lib/graphql/stitching/supergraph.rb
110
110
  - lib/graphql/stitching/util.rb
data/docs/document.md DELETED
@@ -1,15 +0,0 @@
1
- ## GraphQL::Stitching::Document
2
-
3
- A `Document` wraps a parsed GraphQL request, and handles the logistics of extracting its appropriate operation, variable definitions, and fragments. A `Document` should be built once for a request and passed through to other stitching components that utilize document information.
4
-
5
- ```ruby
6
- query = "query FetchMovie($id: ID!) { movie(id:$id) { id genre } }"
7
- document = GraphQL::Stitching::Document.new(query, operation_name: "FetchMovie")
8
-
9
- document.ast # parsed AST via GraphQL.parse
10
- document.string # normalized printed string
11
- document.digest # SHA digest of the normalized string
12
-
13
- document.variables # mapping of variable names to type definitions
14
- document.fragments # mapping of fragment names to fragment definitions
15
- ```
@@ -1,59 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module GraphQL
4
- module Stitching
5
- class Document
6
- SUPPORTED_OPERATIONS = ["query", "mutation"].freeze
7
-
8
- attr_reader :ast, :operation_name
9
-
10
- def initialize(string_or_ast, operation_name: nil)
11
- @ast = if string_or_ast.is_a?(String)
12
- GraphQL.parse(string_or_ast)
13
- else
14
- string_or_ast
15
- end
16
-
17
- @operation_name = operation_name
18
- end
19
-
20
- def string
21
- @string ||= GraphQL::Language::Printer.new.print(@ast)
22
- end
23
-
24
- def digest
25
- @digest ||= Digest::SHA2.hexdigest(string)
26
- end
27
-
28
- def operation
29
- @operation ||= begin
30
- operation_defs = @ast.definitions.select do |d|
31
- next unless d.is_a?(GraphQL::Language::Nodes::OperationDefinition)
32
- next unless SUPPORTED_OPERATIONS.include?(d.operation_type)
33
- @operation_name ? d.name == @operation_name : true
34
- end
35
-
36
- if operation_defs.length < 1
37
- raise GraphQL::ExecutionError, "Invalid root operation."
38
- elsif operation_defs.length > 1
39
- raise GraphQL::ExecutionError, "An operation name is required when sending multiple operations."
40
- end
41
-
42
- operation_defs.first
43
- end
44
- end
45
-
46
- def variable_definitions
47
- @variable_definitions ||= operation.variables.each_with_object({}) do |v, memo|
48
- memo[v.name] = v.type
49
- end
50
- end
51
-
52
- def fragment_definitions
53
- @fragment_definitions ||= @ast.definitions.each_with_object({}) do |d, memo|
54
- memo[d.name] = d if d.is_a?(GraphQL::Language::Nodes::FragmentDefinition)
55
- end
56
- end
57
- end
58
- end
59
- end