graphql-stitching 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
- ```