graphql-stitching 1.1.0 → 1.1.1

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: 66c85693da3a07eb7b3ab81fe76b69ce095aaffa8b9cab68a1f34b86e20963ba
4
- data.tar.gz: f1e8bbbe3ffdaf23189f219cad6fe604203527e12911a0609fb1711375effe58
3
+ metadata.gz: 9cdcb3361e391ba9b6dd78aa38c308c08ddb6fde2720d15208e8dfb96c3006ea
4
+ data.tar.gz: 9f64b024bda3987d1c523cf52b49f0ca2dfd688ac4c89bbf18de30c88c098156
5
5
  SHA512:
6
- metadata.gz: a86ac6f20dda8dc5ae68d548d85d9c4482856c56e86a07cee9ff5689ba95cf73f19dadd80d6f711a55a0b9e25796522b1b1feb4c7d70f7fa499a0f38beb0d24a
7
- data.tar.gz: 61b2d6f21dc8001fab5e6d8c27d3cc04582fc506daa17f45354475cb9ef6812a355ec5819257d9d582d7f4a312b85b10b539322e3fa14b73a255376a75687658
6
+ metadata.gz: b51bcfeaa20e9cdea6c2d4b4e472e8c23e13ec8af36e1b23852603d0329045af064e3ae738467a0823be0c9f545105acba001b708c207eb7f08d51ebf0791e1f
7
+ data.tar.gz: c529e74c88fc7193d7823d8c92a488b3e459b5ba9f88e8d62355ae2bd5e9eb0ed2b0d6f7fd14b6b8ed13ab1bb53a93bc5da49dcecd5deb92ee180fa70604c80b
@@ -16,7 +16,7 @@ jobs:
16
16
  ruby: 3.2
17
17
  - gemfile: Gemfile
18
18
  ruby: 3.1
19
- - gemfile: gemfiles/graphql_1.13.gemfile
19
+ - gemfile: gemfiles/graphql_1.13.9.gemfile
20
20
  ruby: 3.1
21
21
  - gemfile: Gemfile
22
22
  ruby: 2.7
@@ -30,6 +30,6 @@ jobs:
30
30
  bundler-cache: true # runs 'bundle install' and caches installed gems automatically
31
31
  - name: Run tests
32
32
  run: |
33
- gem install bundler
33
+ gem install bundler -v 2.4.22
34
34
  bundle install --jobs 4 --retry 3
35
35
  bundle exec rake test
data/README.md CHANGED
@@ -113,12 +113,12 @@ products_schema = <<~GRAPHQL
113
113
  }
114
114
  GRAPHQL
115
115
 
116
- shipping_schema = <<~GRAPHQL
116
+ catalog_schema = <<~GRAPHQL
117
117
  directive @stitch(key: String!) repeatable on FIELD_DEFINITION
118
118
 
119
119
  type Product {
120
120
  id: ID!
121
- weight: Float!
121
+ price: Float!
122
122
  }
123
123
 
124
124
  type Query {
@@ -131,7 +131,7 @@ client = GraphQL::Stitching::Client.new(locations: {
131
131
  schema: GraphQL::Schema.from_definition(products_schema),
132
132
  executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3001"),
133
133
  },
134
- shipping: {
134
+ catalog: {
135
135
  schema: GraphQL::Schema.from_definition(shipping_schema),
136
136
  executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3002"),
137
137
  },
@@ -151,9 +151,9 @@ type Query {
151
151
  ```
152
152
 
153
153
  * The `@stitch` directive is applied to a root query where the merged type may be accessed. The merged type identity is inferred from the field return.
154
- * The `key: "id"` parameter indicates that an `{ id }` must be selected from prior locations so it may be submitted as an argument to this query. The query argument used to send the key is inferred when possible (more on arguments later).
154
+ * The `key: "id"` parameter indicates that an `{ id }` must be selected from prior locations so it may be submitted as an argument to this query. The query argument used to send the key is inferred when possible ([more on arguments](#multiple-query-arguments) later).
155
155
 
156
- Each location that provides a unique variant of a type must provide one stitching query per key. The exception to this requirement are types that contain only a single key field:
156
+ Each location that provides a unique variant of a type must provide at least one stitching query. The exception to this requirement are types that contain only a single key field:
157
157
 
158
158
  ```graphql
159
159
  type Product {
@@ -165,7 +165,7 @@ The above representation of a `Product` type provides no unique data beyond a ke
165
165
 
166
166
  #### List queries
167
167
 
168
- It's okay ([even preferable](https://www.youtube.com/watch?v=VmK0KBHTcWs) in many circumstances) to provide a list accessor as a stitching query. The only requirement is that both the field argument and return type must be lists, and the query results are expected to be a mapped set with `null` holding the position of missing results.
168
+ It's okay ([even preferable](#batching) in most circumstances) to provide a list accessor as a stitching query. The only requirement is that both the field argument and return type must be lists, and the query results are expected to be a mapped set with `null` holding the position of missing results.
169
169
 
170
170
  ```graphql
171
171
  type Query {
@@ -228,7 +228,7 @@ type Query {
228
228
  }
229
229
  ```
230
230
 
231
- The `@stitch` directive is also repeatable (_requires graphql-ruby >= v2.0.15_), allowing a single query to associate with multiple keys:
231
+ The `@stitch` directive is also repeatable, allowing a single query to associate with multiple keys:
232
232
 
233
233
  ```graphql
234
234
  type Product {
@@ -311,16 +311,15 @@ The [Apollo Federation specification](https://www.apollographql.com/docs/federat
311
311
  The composer will automatcially detect and stitch schemas with an `_entities` query, for example:
312
312
 
313
313
  ```ruby
314
- accounts_schema = <<~GRAPHQL
314
+ products_schema = <<~GRAPHQL
315
315
  directive @key(fields: String!) repeatable on OBJECT
316
316
 
317
- type User @key(fields: "id") {
317
+ type Product @key(fields: "id") {
318
318
  id: ID!
319
319
  name: String!
320
- address: String!
321
320
  }
322
321
 
323
- union _Entity = User
322
+ union _Entity = Product
324
323
  scalar _Any
325
324
 
326
325
  type Query {
@@ -329,15 +328,15 @@ accounts_schema = <<~GRAPHQL
329
328
  }
330
329
  GRAPHQL
331
330
 
332
- comments_schema = <<~GRAPHQL
331
+ catalog_schema = <<~GRAPHQL
333
332
  directive @key(fields: String!) repeatable on OBJECT
334
333
 
335
- type User @key(fields: "id") {
334
+ type Product @key(fields: "id") {
336
335
  id: ID!
337
- comments: [String!]!
336
+ price: Float!
338
337
  }
339
338
 
340
- union _Entity = User
339
+ union _Entity = Product
341
340
  scalar _Any
342
341
 
343
342
  type Query {
@@ -346,12 +345,12 @@ comments_schema = <<~GRAPHQL
346
345
  GRAPHQL
347
346
 
348
347
  client = GraphQL::Stitching::Client.new(locations: {
349
- accounts: {
350
- schema: GraphQL::Schema.from_definition(accounts_schema),
348
+ products: {
349
+ schema: GraphQL::Schema.from_definition(products_schema),
351
350
  executable: ...,
352
351
  },
353
- comments: {
354
- schema: GraphQL::Schema.from_definition(comments_schema),
352
+ catalog: {
353
+ schema: GraphQL::Schema.from_definition(catalog_schema),
355
354
  executable: ...,
356
355
  },
357
356
  })
@@ -426,6 +425,7 @@ The [Executor](./docs/executor.md) component builds atop the Ruby fiber-based im
426
425
 
427
426
  ## Additional topics
428
427
 
428
+ - [Deploying a stitched schema](./docs/mechanics.md#deploying-a-stitched-schema)
429
429
  - [Field selection routing](./docs/mechanics.md#field-selection-routing)
430
430
  - [Root selection routing](./docs/mechanics.md#root-selection-routing)
431
431
  - [Stitched errors](./docs/mechanics.md#stitched-errors)
data/docs/executor.md CHANGED
@@ -16,6 +16,7 @@ request = GraphQL::Stitching::Request.new(
16
16
  query,
17
17
  variables: { "id" => "123" },
18
18
  operation_name: "MyQuery",
19
+ context: { ... },
19
20
  )
20
21
 
21
22
  plan = GraphQL::Stitching::Planner.new(
@@ -26,7 +27,7 @@ plan = GraphQL::Stitching::Planner.new(
26
27
  result = GraphQL::Stitching::Executor.new(
27
28
  supergraph: supergraph,
28
29
  request: request,
29
- plan: plan.to_h,
30
+ plan: plan,
30
31
  ).perform
31
32
  ```
32
33
 
@@ -39,7 +40,7 @@ By default, execution results are always returned with document shaping (stitchi
39
40
  raw_result = GraphQL::Stitching::Executor.new(
40
41
  supergraph: supergraph,
41
42
  request: request,
42
- plan: plan.to_h,
43
+ plan: plan,
43
44
  ).perform(raw: true)
44
45
  ```
45
46
 
data/docs/mechanics.md CHANGED
@@ -1,5 +1,59 @@
1
1
  ## Schema Stitching, mechanics
2
2
 
3
+ ### Deploying a stitched schema
4
+
5
+ Among the simplest and most effective ways to manage a stitched schema is to compose it locally, write the composed SDL as a `.graphql` file in your repo, and then load the composed schema into a stitching client at runtime. For example, setup a `rake` task that loads/fetches subgraph schemas, composes them, and then writes the composed schema definition as a file committed to the repo:
6
+
7
+ ```ruby
8
+ task :compose_graphql do
9
+ schema1_sdl = ... # load schema 1
10
+ schema2_sdl = ... # load schema 2
11
+
12
+ supergraph = GraphQL::Stitching::Composer.new.perform({
13
+ schema1: {
14
+ schema: GraphQL::Schema.from_definition(schema1_sdl)
15
+ },
16
+ schema2: {
17
+ schema: GraphQL::Schema.from_definition(schema2_sdl)
18
+ }
19
+ })
20
+
21
+ File.write("schema/supergraph.graphql", supergraph.to_definition)
22
+ puts "Schema composition was successful."
23
+ end
24
+
25
+ # bundle exec rake compose-graphql
26
+ ```
27
+
28
+ Then at runtime, load the composed schema into a stitching client:
29
+
30
+ ```ruby
31
+ class GraphQlController
32
+ class < self
33
+ def client
34
+ @client ||= begin
35
+ supergraph_sdl = File.read("schema/supergraph.graphql")
36
+ supergraph = GraphQL::Stitching::Supergraph.from_definition(supergraph_sdl, executables: {
37
+ schema1: GraphQL::Stitching::HttpExecutable.new("http://localhost:3001/graphql"),
38
+ schema2: GraphQL::Stitching::HttpExecutable.new("http://localhost:3002/graphql"),
39
+ })
40
+ GraphQL::Stitching::Client.new(supergraph: supergraph)
41
+ end
42
+ end
43
+ end
44
+
45
+ def exec
46
+ self.class.client.execute(
47
+ params[:query],
48
+ variables: params[:variables],
49
+ operation_name: params[:operation_name]
50
+ )
51
+ end
52
+ end
53
+ ```
54
+
55
+ This process assures that composition always happens before deployment where failures can be detected. Hot reloading of the supergraph can also be accommodated by uploading the composed schema to a sync location (cloud storage, etc) that is polled by the application runtime. When the schema changes, load it into a new stitching client and swap that into the application.
56
+
3
57
  ### Field selection routing
4
58
 
5
59
  Fields of a merged type may exist in multiple locations. For example, the `title` field below is provided by both locations:
data/docs/planner.md CHANGED
@@ -32,15 +32,15 @@ Plans are designed to be cacheable. This is very useful for redundant GraphQL do
32
32
  cached_plan = $redis.get(request.digest)
33
33
 
34
34
  plan = if cached_plan
35
- JSON.parse(cached_plan)
35
+ GraphQL::Stitching::Plan.from_json(JSON.parse(cached_plan))
36
36
  else
37
- plan_hash = GraphQL::Stitching::Planner.new(
37
+ plan = GraphQL::Stitching::Planner.new(
38
38
  supergraph: supergraph,
39
39
  request: request,
40
- ).perform.to_h
40
+ ).perform
41
41
 
42
- $redis.set(request.digest, JSON.generate(plan_hash))
43
- plan_hash
42
+ $redis.set(request.digest, JSON.generate(plan.as_json))
43
+ plan
44
44
  end
45
45
 
46
46
  # execute the plan...
data/docs/request.md CHANGED
@@ -4,7 +4,12 @@ A `Request` contains a parsed GraphQL document and variables, and handles the lo
4
4
 
5
5
  ```ruby
6
6
  source = "query FetchMovie($id: ID!) { movie(id:$id) { id genre } }"
7
- request = GraphQL::Stitching::Request.new(source, variables: { "id" => "1" }, operation_name: "FetchMovie")
7
+ request = GraphQL::Stitching::Request.new(
8
+ source,
9
+ variables: { "id" => "1" },
10
+ operation_name: "FetchMovie",
11
+ context: { ... },
12
+ )
8
13
  ```
9
14
 
10
15
  A `Request` provides the following information:
@@ -20,6 +20,10 @@ module GraphQL
20
20
  composer ||= GraphQL::Stitching::Composer.new
21
21
  composer.perform(locations)
22
22
  end
23
+
24
+ @on_cache_read = nil
25
+ @on_cache_write = nil
26
+ @on_error = nil
23
27
  end
24
28
 
25
29
  def execute(query:, variables: nil, operation_name: nil, context: nil, validate: true)
@@ -39,6 +39,12 @@ module GraphQL
39
39
  @directive_kwarg_merger = directive_kwarg_merger || BASIC_VALUE_MERGER
40
40
  @root_field_location_selector = root_field_location_selector || BASIC_ROOT_FIELD_LOCATION_SELECTOR
41
41
  @stitch_directives = {}
42
+
43
+ @field_map = nil
44
+ @boundary_map = nil
45
+ @mapped_type_names = nil
46
+ @candidate_directives_by_name_and_location = nil
47
+ @schema_directives = nil
42
48
  end
43
49
 
44
50
  def perform(locations_input)
@@ -8,6 +8,8 @@ module GraphQL
8
8
  EXPORT_PREFIX = "_export_"
9
9
 
10
10
  class << self
11
+ @typename_node = nil
12
+
11
13
  def key?(name)
12
14
  return false unless name
13
15
 
@@ -19,7 +21,11 @@ module GraphQL
19
21
  end
20
22
 
21
23
  def key_node(field_name)
22
- GraphQL::Language::Nodes::Field.new(alias: key(field_name), name: field_name)
24
+ if Util.graphql_version?(2, 2)
25
+ GraphQL::Language::Nodes::Field.new(field_alias: key(field_name), name: field_name)
26
+ else
27
+ GraphQL::Language::Nodes::Field.new(alias: key(field_name), name: field_name)
28
+ end
23
29
  end
24
30
 
25
31
  def typename_node
@@ -9,6 +9,15 @@ module GraphQL
9
9
  attr_reader :document, :variables, :operation_name, :context
10
10
 
11
11
  def initialize(document, operation_name: nil, variables: nil, context: nil)
12
+ @string = nil
13
+ @digest = nil
14
+ @normalized_string = nil
15
+ @normalized_digest = nil
16
+ @operation = nil
17
+ @operation_directives = nil
18
+ @variable_definitions = nil
19
+ @fragment_definitions = nil
20
+
12
21
  @document = if document.is_a?(String)
13
22
  @string = document
14
23
  GraphQL.parse(document)
@@ -9,6 +9,7 @@ module GraphQL
9
9
  def initialize(supergraph:, request:)
10
10
  @supergraph = supergraph
11
11
  @request = request
12
+ @root_type = nil
12
13
  end
13
14
 
14
15
  def perform!(raw)
@@ -97,6 +97,10 @@ module GraphQL
97
97
  @possible_keys_by_type_and_location = {}
98
98
  @memoized_schema_possible_types = {}
99
99
  @memoized_schema_fields = {}
100
+ @memoized_introspection_types = nil
101
+ @memoized_schema_types = nil
102
+ @fields_by_type_and_location = nil
103
+ @locations_by_type = nil
100
104
 
101
105
  # add introspection types into the fields mapping
102
106
  @locations_by_type_and_field = memoized_introspection_types.each_with_object(fields) do |(type_name, type), memo|
@@ -12,7 +12,16 @@ module GraphQL
12
12
  end
13
13
  end
14
14
 
15
+ GRAPHQL_VERSION = GraphQL::VERSION.split(".").map(&:to_i).freeze
16
+
15
17
  class << self
18
+ def graphql_version?(major, minor = nil, patch = nil)
19
+ result = GRAPHQL_VERSION[0] >= major
20
+ result &&= GRAPHQL_VERSION[1] >= minor if minor
21
+ result &&= GRAPHQL_VERSION[2] >= patch if patch
22
+ result
23
+ end
24
+
16
25
  # specifies if a type is a primitive leaf value
17
26
  def is_leaf_type?(type)
18
27
  type.kind.scalar? || type.kind.enum?
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
- VERSION = "1.1.0"
5
+ VERSION = "1.1.1"
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: 1.1.0
4
+ version: 1.1.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-12-02 00:00:00.000000000 Z
11
+ date: 2023-12-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql