graphql-stitching 0.0.1 → 0.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a228b0fe5779d625285182f885a7d65b5f66541f275a5313602e68a4c9eb209d
4
- data.tar.gz: f6efcaeafd7c417db07f007641f84c800c0ebb32c99ad19c5004aa2bb9ac8685
3
+ metadata.gz: 5090330cd8b46c9d7dbfc7bbc68f5840e97f8bcbc5ec710de219441237fb61e5
4
+ data.tar.gz: 846a15ccc2734024dec751d61d456aa05d4b7c396d6789d629da21e11c182100
5
5
  SHA512:
6
- metadata.gz: 1d484e39db0ac919e68477e8733fa6f0ca31ffde74b2bfebc4e13ba6b55d7973dc793c6df4251ae7b912c450bdc38ab4478e300c350d7be67555a635687b04a9
7
- data.tar.gz: 6354381e960a922453cd38ae5c513b60f2d2ae4585ec2a53a1ab7b405c2da6ca6d01bce9999794f9c8b6df7fe0d49176a4f944ab0efabfcb8ff252393a60f88e
6
+ metadata.gz: 1c0c16a9bb49b3fad30057958d79fe92737bb659bf7135dd03ef716597f013249c99d9372478e20619f54e1e77f68f3b7f746b92e9e220b80be9ecaf88b656de
7
+ data.tar.gz: f370e520552f3e8be141073dd7ca06f68a463824c946b9cd03357b33ad5c9fc928893e846314f08941e2590d24202081450677fbfca480474be9833d341209a1
@@ -11,14 +11,20 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
  strategy:
13
13
  matrix:
14
- ruby-version: ['3.1.1']
14
+ include:
15
+ - gemfile: Gemfile
16
+ ruby: 3.2
17
+ - gemfile: Gemfile
18
+ ruby: 3.1
19
+ - gemfile: Gemfile
20
+ ruby: 2.7
15
21
 
16
22
  steps:
17
23
  - uses: actions/checkout@v2
18
24
  - name: Setup Ruby
19
25
  uses: ruby/setup-ruby@v1
20
26
  with:
21
- ruby-version: ${{ matrix.ruby-version }}
27
+ ruby-version: ${{ matrix.ruby }}
22
28
  bundler-cache: true # runs 'bundle install' and caches installed gems automatically
23
29
  - name: Run tests
24
30
  run: |
data/.gitignore CHANGED
@@ -10,6 +10,7 @@
10
10
  /test/version_tmp/
11
11
  /tmp/
12
12
  .envrc
13
+ Gemfile.lock
13
14
 
14
15
  # Used by dotenv library to load environment variables.
15
16
  # .env
@@ -17,6 +18,7 @@
17
18
 
18
19
  # Ignore Byebug command history file.
19
20
  .byebug_history
21
+ .ruby-version
20
22
  .DS_Store
21
23
 
22
24
  ## Specific to RubyMotion:
data/README.md CHANGED
@@ -46,7 +46,7 @@ showtimes_schema = <<~GRAPHQL
46
46
  GRAPHQL
47
47
 
48
48
  gateway = GraphQL::Stitching::Gateway.new(locations: {
49
- products: {
49
+ movies: {
50
50
  schema: GraphQL::Schema.from_definition(movies_schema),
51
51
  executable: GraphQL::Stitching::RemoteClient.new(url: "http://localhost:3000"),
52
52
  },
@@ -79,7 +79,6 @@ While the [`Gateway`](./docs/gateway.md) constructor is an easy quick start, the
79
79
  - [Document](./docs/document.md) - manages a parsed GraphQL request document.
80
80
  - [Planner](./docs/planner.md) - builds a cacheable query plan for a request document.
81
81
  - [Executor](./docs/executor.md) - executes a query plan with given request variables.
82
- - [Shaper](./docs/shaper.md) - takes the raw output of the executor and prepares it for delivery.
83
82
 
84
83
  ## Merged types
85
84
 
data/docs/executor.md CHANGED
@@ -21,9 +21,19 @@ plan = GraphQL::Stitching::Planner.new(
21
21
  document: document,
22
22
  ).perform
23
23
 
24
+ # get the raw result without shaping
24
25
  raw_result = GraphQL::Stitching::Executor.new(
25
26
  supergraph: supergraph,
26
27
  plan: plan.to_h,
27
28
  variables: variables,
28
29
  ).perform
30
+
31
+ # get the final result with shaping
32
+ final_result = GraphQL::Stitching::Executor.new(
33
+ supergraph: supergraph,
34
+ plan: plan.to_h,
35
+ variables: variables,
36
+ ).perform(document)
29
37
  ```
38
+
39
+ Note that an executor's `perform` method accepts a document argument. When provided, the raw execution result will be shaped for delivery to match the document. Without a document, the raw result will be returned with stitching inclusions and no null bubbling applied.
@@ -8,11 +8,11 @@ Gem::Specification.new do |spec|
8
8
  spec.version = GraphQL::Stitching::VERSION
9
9
  spec.authors = ['Greg MacWilliam']
10
10
  spec.summary = 'GraphQL schema stitching for Ruby'
11
- spec.description = spec.summary
11
+ spec.description = 'Combine GraphQL services into one unified graph'
12
12
  spec.homepage = 'https://github.com/gmac/graphql-stitching-ruby'
13
13
  spec.license = 'MIT'
14
14
 
15
- spec.required_ruby_version = '>= 3.1.1'
15
+ spec.required_ruby_version = '>= 2.7.0'
16
16
 
17
17
  spec.metadata = {
18
18
  'homepage_uri' => 'https://github.com/gmac/graphql-stitching-ruby',
@@ -219,9 +219,14 @@ module GraphQL
219
219
  result["data"] = @data if @data && @data.length > 0
220
220
  result["errors"] = @errors if @errors.length > 0
221
221
 
222
- result if document.nil?
223
-
224
- GraphQL::Stitching::Shaper.new(supergraph: @supergraph, document: document, raw: result).perform!
222
+ if document && result["data"]
223
+ GraphQL::Stitching::Shaper.new(
224
+ schema: @supergraph.schema,
225
+ document: document,
226
+ ).perform!(result)
227
+ else
228
+ result
229
+ end
225
230
  end
226
231
 
227
232
  private
@@ -4,88 +4,92 @@
4
4
  module GraphQL
5
5
  module Stitching
6
6
  class Shaper
7
- def initialize(supergraph:, document:, raw:)
8
- @supergraph = supergraph
7
+ def initialize(schema:, document:)
8
+ @schema = schema
9
9
  @document = document
10
- @result = raw
11
- @errors = []
12
10
  end
13
11
 
14
- def perform!
15
- if @result.key?("data") && ! @result["data"].empty?
16
- munge_entry(@result["data"], @document.operation.selections, @supergraph.schema.query)
17
- # hate doing a second pass, but cannot remove _STITCH_ fields until the fragements are processed
18
- clean_entry(@result["data"])
19
- end
20
-
21
- if @errors.length > 0
22
- @result = [] unless @result.key?("errors")
23
- @result["errors"] += @errors
24
- end
25
-
26
- @result
12
+ def perform!(raw)
13
+ raw["data"] = resolve_object_scope(raw["data"], @schema.query, @document.operation.selections)
14
+ raw
27
15
  end
28
16
 
29
17
  private
30
18
 
31
- def munge_entry(entry, selections, parent_type)
19
+ def resolve_object_scope(raw_object, parent_type, selections, typename = nil)
20
+ return nil if raw_object.nil?
21
+
22
+ typename ||= raw_object["_STITCH_typename"]
23
+ raw_object.reject! { |k, _v| k.start_with?("_STITCH_") }
24
+
32
25
  selections.each do |node|
33
26
  case node
34
27
  when GraphQL::Language::Nodes::Field
35
- next if node.respond_to?(:name) && node&.name == "__typename"
28
+ next if node.name.start_with?("__")
36
29
 
37
- munge_field(entry, node, parent_type)
30
+ field_name = node.alias || node.name
31
+ node_type = parent_type.fields[node.name].type
32
+ named_type = Util.get_named_type_for_field_node(@schema, parent_type, node)
33
+
34
+ raw_object[field_name] = if node_type.list?
35
+ resolve_list_scope(raw_object[field_name], Util.unwrap_non_null(node_type), node.selections)
36
+ elsif Util.is_leaf_type?(named_type)
37
+ raw_object[field_name]
38
+ else
39
+ resolve_object_scope(raw_object[field_name], named_type, node.selections)
40
+ end
41
+
42
+ return nil if raw_object[field_name].nil? && node_type.non_null?
38
43
 
39
44
  when GraphQL::Language::Nodes::InlineFragment
40
- next unless entry["_STITCH_typename"] == node.type.name
41
- fragment_type = @supergraph.schema.types[node.type.name]
42
- munge_entry(entry, node.selections, fragment_type)
45
+ next unless typename == node.type.name
46
+ fragment_type = @schema.types[node.type.name]
47
+ result = resolve_object_scope(raw_object, fragment_type, node.selections, typename)
48
+ return nil if result.nil?
43
49
 
44
50
  when GraphQL::Language::Nodes::FragmentSpread
45
- next unless entry["_STITCH_typename"] == node.name
46
51
  fragment = @document.fragment_definitions[node.name]
47
- fragment_type = @supergraph.schema.types[node.name]
48
- munge_entry(entry, fragment.selections, fragment_type)
52
+ fragment_type = @schema.types[fragment.type.name]
53
+ next unless typename == fragment_type.graphql_name
54
+
55
+ result = resolve_object_scope(raw_object, fragment_type, fragment.selections, typename)
56
+ return nil if result.nil?
57
+
49
58
  else
50
59
  raise "Unexpected node of type #{node.class.name} in selection set."
51
60
  end
52
61
  end
62
+
63
+ raw_object
53
64
  end
54
65
 
55
- def munge_field(entry, node, parent_type)
56
- field_identifier = (node.alias || node.name)
57
- node_type = Util.get_named_type_for_field_node(@supergraph.schema, parent_type, node)
66
+ def resolve_list_scope(raw_list, current_node_type, selections)
67
+ return nil if raw_list.nil?
58
68
 
59
- if entry.nil?
60
- return # TODO bubble up error if not nullable
61
- end
69
+ next_node_type = Util.unwrap_non_null(current_node_type).of_type
70
+ named_type = Util.get_named_type(next_node_type)
71
+ contains_null = false
62
72
 
63
- child_entry = entry[field_identifier]
64
- if child_entry.nil?
65
- entry[field_identifier] = nil
66
- elsif child_entry.is_a? Array
67
- child_entry.each do |raw_item|
68
- munge_entry(raw_item, node.selections, node_type)
73
+ resolved_list = raw_list.map! do |raw_list_element|
74
+ result = if next_node_type.list?
75
+ resolve_list_scope(raw_list_element, next_node_type, selections)
76
+ elsif Util.is_leaf_type?(named_type)
77
+ raw_list_element
78
+ else
79
+ resolve_object_scope(raw_list_element, named_type, selections)
69
80
  end
70
- elsif ! Util.is_leaf_type?(node_type)
71
- munge_entry(child_entry, node.selections, node_type)
72
- end
73
- end
74
81
 
75
- def clean_entry(entry)
76
- return if entry.nil?
77
-
78
- entry.each do |key, value|
79
- if key.start_with? "_STITCH_"
80
- entry.delete(key)
81
- elsif value.is_a?(Array)
82
- value.each do |item|
83
- clean_entry(item)
84
- end
85
- elsif value.is_a?(Hash)
86
- clean_entry(value)
82
+ if result.nil?
83
+ contains_null = true
84
+ return nil if current_node_type.non_null?
87
85
  end
86
+
87
+ result
88
88
  end
89
+
90
+ return nil if contains_null && next_node_type.non_null?
91
+
92
+ resolved_list
89
93
  end
90
94
  end
91
95
  end
@@ -4,7 +4,7 @@ module GraphQL
4
4
  module Stitching
5
5
  class Util
6
6
 
7
- # gets the named type at the bottom of a non-null/list wrapper chain
7
+ # gets the named type at the bottom of a non-null/list wrapper tree
8
8
  def self.get_named_type(type)
9
9
  while type.respond_to?(:of_type)
10
10
  type = type.of_type
@@ -12,6 +12,14 @@ module GraphQL
12
12
  type
13
13
  end
14
14
 
15
+ # strips non-null wrappers from a type
16
+ def self.unwrap_non_null(type)
17
+ while type.is_a?(GraphQL::Schema::NonNull)
18
+ type = type.of_type
19
+ end
20
+ type
21
+ end
22
+
15
23
  # gets a deep structural description of a list value type
16
24
  def self.get_list_structure(type)
17
25
  structure = []
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
- VERSION = "0.0.1"
5
+ VERSION = "0.1.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: 0.0.1
4
+ version: 0.1.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: 2023-02-09 00:00:00.000000000 Z
11
+ date: 2023-02-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -66,7 +66,7 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '5.12'
69
- description: GraphQL schema stitching for Ruby
69
+ description: Combine GraphQL services into one unified graph
70
70
  email:
71
71
  executables: []
72
72
  extensions: []
@@ -74,9 +74,7 @@ extra_rdoc_files: []
74
74
  files:
75
75
  - ".github/workflows/ci.yml"
76
76
  - ".gitignore"
77
- - ".ruby-version"
78
77
  - Gemfile
79
- - Gemfile.lock
80
78
  - LICENSE
81
79
  - Procfile
82
80
  - README.md
@@ -90,7 +88,6 @@ files:
90
88
  - docs/images/merging.png
91
89
  - docs/images/stitching.png
92
90
  - docs/planner.md
93
- - docs/shaper.md
94
91
  - docs/supergraph.md
95
92
  - example/gateway.rb
96
93
  - example/graphiql.html
@@ -128,7 +125,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
128
125
  requirements:
129
126
  - - ">="
130
127
  - !ruby/object:Gem::Version
131
- version: 3.1.1
128
+ version: 2.7.0
132
129
  required_rubygems_version: !ruby/object:Gem::Requirement
133
130
  requirements:
134
131
  - - ">="
data/.ruby-version DELETED
@@ -1 +0,0 @@
1
- 3.1.1
data/Gemfile.lock DELETED
@@ -1,49 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- graphql-stitching (0.0.1)
5
- graphql (~> 2.0.16)
6
-
7
- GEM
8
- remote: https://rubygems.org/
9
- specs:
10
- byebug (11.1.3)
11
- coderay (1.1.3)
12
- foreman (0.87.2)
13
- graphql (2.0.16)
14
- method_source (1.0.0)
15
- minitest (5.17.0)
16
- pry (0.14.2)
17
- coderay (~> 1.1)
18
- method_source (~> 1.0)
19
- pry-byebug (3.10.1)
20
- byebug (~> 11.0)
21
- pry (>= 0.13, < 0.15)
22
- rack (3.0.4.1)
23
- rackup (2.1.0)
24
- rack (>= 3)
25
- webrick (~> 1.8)
26
- rake (12.3.3)
27
- warning (1.3.0)
28
- webrick (1.8.1)
29
-
30
- PLATFORMS
31
- arm64-darwin-21
32
- ruby
33
- x86_64-darwin-21
34
- x86_64-linux
35
-
36
- DEPENDENCIES
37
- bundler (~> 2.0)
38
- foreman
39
- graphql-stitching!
40
- minitest (~> 5.12)
41
- pry
42
- pry-byebug
43
- rack
44
- rackup
45
- rake (~> 12.0)
46
- warning
47
-
48
- BUNDLED WITH
49
- 2.4.1
data/docs/shaper.md DELETED
@@ -1,20 +0,0 @@
1
- ## GraphQL::Stitching::Shaper
2
-
3
- The `Shaper` takes the raw output generated by the `GraphQL::Stitching::Executor` and does the final shaping and
4
- cleaning. It removes data that was added while building the result, it also handles cleaning up violations that can
5
- only occur at the end, such as bubbling up null violoations.
6
-
7
- See the [Executor](./docs/executor.md)
8
-
9
- ```ruby
10
- raw_result = GraphQL::Stitching::Executor.new(
11
- supergraph: supergraph,
12
- plan: plan.to_h,
13
- variables: variables,
14
- ).perform(document)
15
-
16
- final_result = GraphQL::Stitching::Shaper.new(
17
- supergraph: supergraph,
18
- document: document,
19
- raw: raw_result).perform!
20
- ```