graphql-stitching 1.1.1 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +0 -3
- data/README.md +10 -32
- data/docs/README.md +1 -0
- data/docs/client.md +2 -2
- data/docs/composer.md +1 -1
- data/docs/executor.md +8 -15
- data/docs/http_executable.md +51 -0
- data/docs/planner.md +12 -14
- data/docs/request.md +2 -0
- data/docs/supergraph.md +2 -2
- data/examples/file_uploads/Gemfile +9 -0
- data/examples/file_uploads/Procfile +2 -0
- data/examples/file_uploads/README.md +37 -0
- data/examples/file_uploads/file.txt +1 -0
- data/examples/file_uploads/gateway.rb +37 -0
- data/examples/file_uploads/helpers.rb +62 -0
- data/examples/file_uploads/remote.rb +21 -0
- data/examples/merged_types/Gemfile +8 -0
- data/examples/merged_types/Procfile +3 -0
- data/examples/merged_types/README.md +33 -0
- data/{example → examples/merged_types}/gateway.rb +4 -5
- data/examples/merged_types/remote1.rb +22 -0
- data/examples/merged_types/remote2.rb +22 -0
- data/lib/graphql/stitching/client.rb +9 -19
- data/lib/graphql/stitching/executor/boundary_source.rb +1 -1
- data/lib/graphql/stitching/executor/root_source.rb +1 -1
- data/lib/graphql/stitching/executor.rb +4 -7
- data/lib/graphql/stitching/export_selection.rb +6 -1
- data/lib/graphql/stitching/http_executable.rb +134 -4
- data/lib/graphql/stitching/planner.rb +2 -2
- data/lib/graphql/stitching/request.rb +22 -3
- data/lib/graphql/stitching/shaper.rb +2 -2
- data/lib/graphql/stitching/supergraph.rb +4 -4
- data/lib/graphql/stitching/util.rb +0 -9
- data/lib/graphql/stitching/version.rb +1 -1
- metadata +17 -7
- data/Procfile +0 -3
- data/example/remote1.rb +0 -26
- data/example/remote2.rb +0 -26
- /data/{example → examples/merged_types}/graphiql.html +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 32918bd6b17708d712d3ac945f6007b0405299379372aabda8b0c3a5f0c246da
|
4
|
+
data.tar.gz: 608562f6b35cfe912e2388c6cbfce8c774bedc6f6518d257eb0de8b45fdc8b52
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c0f40d79e3b06c9477f5967f12f6e33c0534b916c3454669d2802357137ff78d7bb9bb7d99d7c82d43ea47f58fac1c4d0e5aa38243d4cbfcdec6ba192ef56c0e
|
7
|
+
data.tar.gz: eed8c018a8efde4500bb0eb93de78803f555c77fad284718e945648160442eea249439d2a2cb5b4c0a5121fc148197f48b9ea0d50a21e7cfa291499fe4533912
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -10,6 +10,7 @@ GraphQL stitching composes a single schema from multiple underlying GraphQL reso
|
|
10
10
|
- Shared objects, fields, enums, and inputs across locations.
|
11
11
|
- Combining local and remote schemas.
|
12
12
|
- Type merging via arbitrary queries or federation `_entities` protocol.
|
13
|
+
- File uploads via [multipart form spec](https://github.com/jaydenseric/graphql-multipart-request-spec).
|
13
14
|
|
14
15
|
**NOT Supported:**
|
15
16
|
- Computed fields (ie: federation-style `@requires`).
|
@@ -80,6 +81,7 @@ While the `Client` constructor is an easy quick start, the library also has seve
|
|
80
81
|
- [Request](./docs/request.md) - prepares a requested GraphQL document and variables for stitching.
|
81
82
|
- [Planner](./docs/planner.md) - builds a cacheable query plan for a request document.
|
82
83
|
- [Executor](./docs/executor.md) - executes a query plan with given request variables.
|
84
|
+
- [HttpExecutable](./docs/http_executable.md) - proxies requests to remotes with multipart file upload support.
|
83
85
|
|
84
86
|
## Merged types
|
85
87
|
|
@@ -360,11 +362,11 @@ It's perfectly fine to mix and match schemas that implement an `_entities` query
|
|
360
362
|
|
361
363
|
## Executables
|
362
364
|
|
363
|
-
An executable resource performs location-specific GraphQL requests. Executables may be `GraphQL::Schema` classes, or any object that responds to `.call(
|
365
|
+
An executable resource performs location-specific GraphQL requests. Executables may be `GraphQL::Schema` classes, or any object that responds to `.call(request, source, variables)` and returns a raw GraphQL response:
|
364
366
|
|
365
367
|
```ruby
|
366
368
|
class MyExecutable
|
367
|
-
def call(
|
369
|
+
def call(request, source, variables)
|
368
370
|
# process a GraphQL request...
|
369
371
|
return {
|
370
372
|
"data" => { ... },
|
@@ -392,12 +394,12 @@ supergraph = GraphQL::Stitching::Composer.new.perform({
|
|
392
394
|
},
|
393
395
|
fourth: {
|
394
396
|
schema: FourthSchema,
|
395
|
-
executable: ->(
|
397
|
+
executable: ->(req, query, vars) { ... },
|
396
398
|
},
|
397
399
|
})
|
398
400
|
```
|
399
401
|
|
400
|
-
The `GraphQL::Stitching::HttpExecutable` class is provided as a simple executable wrapper around `Net::HTTP.post
|
402
|
+
The `GraphQL::Stitching::HttpExecutable` class is provided as a simple executable wrapper around `Net::HTTP.post` with [file upload](./docs/http_executable.md#graphql-file-uploads) support. You should build your own executables to leverage your existing libraries and to add instrumentation. Note that you must manually assign all executables to a `Supergraph` when rehydrating it from cache ([see docs](./docs/supergraph.md)).
|
401
403
|
|
402
404
|
## Batching
|
403
405
|
|
@@ -431,36 +433,12 @@ The [Executor](./docs/executor.md) component builds atop the Ruby fiber-based im
|
|
431
433
|
- [Stitched errors](./docs/mechanics.md#stitched-errors)
|
432
434
|
- [Null results](./docs/mechanics.md#null-results)
|
433
435
|
|
434
|
-
##
|
436
|
+
## Examples
|
435
437
|
|
436
|
-
This repo includes
|
438
|
+
This repo includes working examples of stitched schemas running across small Rack servers. Clone the repo, `cd` into each example and try running it following its README instructions.
|
437
439
|
|
438
|
-
|
439
|
-
|
440
|
-
foreman start
|
441
|
-
```
|
442
|
-
|
443
|
-
Then visit the gateway service at `http://localhost:3000` and try this query:
|
444
|
-
|
445
|
-
```graphql
|
446
|
-
query {
|
447
|
-
storefront(id: "1") {
|
448
|
-
id
|
449
|
-
products {
|
450
|
-
upc
|
451
|
-
name
|
452
|
-
price
|
453
|
-
manufacturer {
|
454
|
-
name
|
455
|
-
address
|
456
|
-
products { upc name }
|
457
|
-
}
|
458
|
-
}
|
459
|
-
}
|
460
|
-
}
|
461
|
-
```
|
462
|
-
|
463
|
-
The above query collects data from all locations, two of which are remote schemas and the third a local schema. The combined graph schema is also stitched in to provide introspection capabilities.
|
440
|
+
- [Merged types](./examples/merged_types)
|
441
|
+
- [File uploads](./examples/file_uploads)
|
464
442
|
|
465
443
|
## Tests
|
466
444
|
|
data/docs/README.md
CHANGED
@@ -12,6 +12,7 @@ Major components include:
|
|
12
12
|
- [Request](./request.md) - prepares a requested GraphQL document and variables for stitching.
|
13
13
|
- [Planner](./planner.md) - builds a cacheable query plan for a request document.
|
14
14
|
- [Executor](./executor.md) - executes a query plan with given request variables.
|
15
|
+
- [HttpExecutable](./http_executable.md) - proxies requests to remotes with multipart file upload support.
|
15
16
|
|
16
17
|
Additional topics:
|
17
18
|
|
data/docs/client.md
CHANGED
@@ -60,11 +60,11 @@ The client provides cache hooks to enable caching query plans across requests. W
|
|
60
60
|
|
61
61
|
```ruby
|
62
62
|
client.on_cache_read do |request|
|
63
|
-
$
|
63
|
+
$cache.get(request.digest) # << 3P code
|
64
64
|
end
|
65
65
|
|
66
66
|
client.on_cache_write do |request, payload|
|
67
|
-
$
|
67
|
+
$cache.set(request.digest, payload) # << 3P code
|
68
68
|
end
|
69
69
|
```
|
70
70
|
|
data/docs/composer.md
CHANGED
@@ -90,7 +90,7 @@ Location settings have top-level keys that specify arbitrary location names, eac
|
|
90
90
|
|
91
91
|
- **`schema:`** _required_, provides a `GraphQL::Schema` class for the location. This may be a class-based schema that inherits from `GraphQL::Schema`, or built from SDL (Schema Definition Language) string using `GraphQL::Schema.from_definition` and mapped to a remote location. The provided schema is only used for type reference and does not require any real data resolvers (unless it is also used as the location's executable, see below).
|
92
92
|
|
93
|
-
- **`executable:`** _optional_, provides an executable resource to be called when delegating a request to this location. Executables are `GraphQL::Schema` classes or any object with a `.call(
|
93
|
+
- **`executable:`** _optional_, provides an executable resource to be called when delegating a request to this location. Executables are `GraphQL::Schema` classes or any object with a `.call(request, source, variables)` method that returns a GraphQL response. Omitting the executable option will use the location's provided `schema` as the executable resource.
|
94
94
|
|
95
95
|
- **`stitch:`** _optional_, an array of configs used to dynamically apply `@stitch` directives to select root fields prior to composing. This is useful when you can't easily render stitching directives into a location's source schema.
|
96
96
|
|
data/docs/executor.md
CHANGED
@@ -13,22 +13,18 @@ query = <<~GRAPHQL
|
|
13
13
|
GRAPHQL
|
14
14
|
|
15
15
|
request = GraphQL::Stitching::Request.new(
|
16
|
+
supergraph,
|
16
17
|
query,
|
17
18
|
variables: { "id" => "123" },
|
18
19
|
operation_name: "MyQuery",
|
19
20
|
context: { ... },
|
20
21
|
)
|
21
22
|
|
22
|
-
|
23
|
-
|
24
|
-
request: request,
|
25
|
-
).perform
|
23
|
+
# Via Request:
|
24
|
+
result = request.execute
|
26
25
|
|
27
|
-
|
28
|
-
|
29
|
-
request: request,
|
30
|
-
plan: plan,
|
31
|
-
).perform
|
26
|
+
# Via Executor:
|
27
|
+
result = GraphQL::Stitching::Executor.new(request).perform
|
32
28
|
```
|
33
29
|
|
34
30
|
### Raw results
|
@@ -36,12 +32,9 @@ result = GraphQL::Stitching::Executor.new(
|
|
36
32
|
By default, execution results are always returned with document shaping (stitching additions removed, missing fields added, null bubbling applied). You may access the raw execution result by calling the `perform` method with a `raw: true` argument:
|
37
33
|
|
38
34
|
```ruby
|
39
|
-
# get the raw result without shaping
|
40
|
-
raw_result =
|
41
|
-
|
42
|
-
request: request,
|
43
|
-
plan: plan,
|
44
|
-
).perform(raw: true)
|
35
|
+
# get the raw result without shaping using either form:
|
36
|
+
raw_result = request.execute(raw: true)
|
37
|
+
raw_result = GraphQL::Stitching::Executor.new(request).perform(raw: true)
|
45
38
|
```
|
46
39
|
|
47
40
|
The raw result will contain many irregularities from the stitching process, however may be insightful when debugging inconsistencies in results:
|
@@ -0,0 +1,51 @@
|
|
1
|
+
## GraphQL::Stitching::HttpExecutable
|
2
|
+
|
3
|
+
A `HttpExecutable` provides an out-of-the-box convenience for sending HTTP post requests to a remote location, or a base class for your own implementation with [GraphQL multipart uploads](https://github.com/jaydenseric/graphql-multipart-request-spec).
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
exe = GraphQL::Stitching::HttpExecutable.new(
|
7
|
+
url: "http://localhost:3001",
|
8
|
+
headers: {
|
9
|
+
"Authorization" => "..."
|
10
|
+
}
|
11
|
+
)
|
12
|
+
```
|
13
|
+
|
14
|
+
### GraphQL file uploads
|
15
|
+
|
16
|
+
The [GraphQL Upload Spec](https://github.com/jaydenseric/graphql-multipart-request-spec) defines a multipart form structure for submitting GraphQL requests with file upload attachments. It's possible to pass these requests through stitched schemas using the following:
|
17
|
+
|
18
|
+
#### 1. Input file uploads as Tempfile variables
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
client.execute(
|
22
|
+
"mutation($file: Upload) { upload(file: $file) }",
|
23
|
+
variables: { "file" => Tempfile.new(...) }
|
24
|
+
)
|
25
|
+
```
|
26
|
+
|
27
|
+
File uploads must enter the stitched schema as standard GraphQL variables with `Tempfile` values. The simplest way to recieve this input is to install [apollo_upload_server](https://github.com/jetruby/apollo_upload_server-ruby) into your stitching app's middleware so that multipart form submissions automatically unpack into standard variables.
|
28
|
+
|
29
|
+
#### 2. Enable `HttpExecutable.upload_types`
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
client = GraphQL::Stitching::Client.new(locations: {
|
33
|
+
alpha: {
|
34
|
+
schema: GraphQL::Schema.from_definition(...),
|
35
|
+
executable: GraphQL::Stitching::HttpExecutable.new(
|
36
|
+
url: "http://localhost:3000",
|
37
|
+
upload_types: ["Upload"], # << extract `Upload` scalars into multipart forms
|
38
|
+
),
|
39
|
+
},
|
40
|
+
bravo: {
|
41
|
+
schema: GraphQL::Schema.from_definition(...),
|
42
|
+
executable: GraphQL::Stitching::HttpExecutable.new(
|
43
|
+
url: "http://localhost:3001"
|
44
|
+
),
|
45
|
+
},
|
46
|
+
})
|
47
|
+
```
|
48
|
+
|
49
|
+
A location's `HttpExecutable` can then re-package `Tempfile` variables into multipart forms before sending them upstream. This is enabled with an `upload_types` parameter that specifies which scalar names require form extraction. Enabling `upload_types` does add some additional subgraph request processing, so it should only be enabled for locations that will actually recieve file uploads.
|
50
|
+
|
51
|
+
The upstream location will recieve a multipart form submission from stitching that can again be unpacked using [apollo_upload_server](https://github.com/jetruby/apollo_upload_server-ruby) or similar.
|
data/docs/planner.md
CHANGED
@@ -13,15 +13,17 @@ document = <<~GRAPHQL
|
|
13
13
|
GRAPHQL
|
14
14
|
|
15
15
|
request = GraphQL::Stitching::Request.new(
|
16
|
+
supergraph,
|
16
17
|
document,
|
17
18
|
variables: { "id" => "1" },
|
18
19
|
operation_name: "MyQuery",
|
19
20
|
).prepare!
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
# Via Request:
|
23
|
+
plan = request.plan
|
24
|
+
|
25
|
+
# Via Planner:
|
26
|
+
plan = GraphQL::Stitching::Planner.new(request).perform
|
25
27
|
```
|
26
28
|
|
27
29
|
### Caching
|
@@ -29,18 +31,14 @@ plan = GraphQL::Stitching::Planner.new(
|
|
29
31
|
Plans are designed to be cacheable. This is very useful for redundant GraphQL documents (commonly sent by frontend clients) where there's no sense in planning every request individually. It's far more efficient to generate a plan once and cache it, then simply retreive the plan and execute it for future requests.
|
30
32
|
|
31
33
|
```ruby
|
32
|
-
cached_plan = $
|
34
|
+
cached_plan = $cache.get(request.digest)
|
33
35
|
|
34
|
-
|
35
|
-
GraphQL::Stitching::Plan.from_json(JSON.parse(cached_plan))
|
36
|
+
if cached_plan
|
37
|
+
plan = GraphQL::Stitching::Plan.from_json(JSON.parse(cached_plan))
|
38
|
+
request.plan(plan)
|
36
39
|
else
|
37
|
-
plan =
|
38
|
-
|
39
|
-
request: request,
|
40
|
-
).perform
|
41
|
-
|
42
|
-
$redis.set(request.digest, JSON.generate(plan.as_json))
|
43
|
-
plan
|
40
|
+
plan = request.plan
|
41
|
+
$cache.set(request.digest, JSON.generate(plan.as_json))
|
44
42
|
end
|
45
43
|
|
46
44
|
# execute the plan...
|
data/docs/request.md
CHANGED
@@ -5,6 +5,7 @@ A `Request` contains a parsed GraphQL document and variables, and handles the lo
|
|
5
5
|
```ruby
|
6
6
|
source = "query FetchMovie($id: ID!) { movie(id:$id) { id genre } }"
|
7
7
|
request = GraphQL::Stitching::Request.new(
|
8
|
+
supergraph,
|
8
9
|
source,
|
9
10
|
variables: { "id" => "1" },
|
10
11
|
operation_name: "FetchMovie",
|
@@ -42,6 +43,7 @@ document = <<~GRAPHQL
|
|
42
43
|
GRAPHQL
|
43
44
|
|
44
45
|
request = GraphQL::Stitching::Request.new(
|
46
|
+
supergraph,
|
45
47
|
document,
|
46
48
|
variables: { "id" => "1" },
|
47
49
|
operation_name: "FetchMovie",
|
data/docs/supergraph.md
CHANGED
@@ -10,7 +10,7 @@ A Supergraph is designed to be composed, cached, and restored. Calling `to_defin
|
|
10
10
|
supergraph_sdl = supergraph.to_definition
|
11
11
|
|
12
12
|
# stash this composed schema in a cache...
|
13
|
-
$
|
13
|
+
$cache.set("cached_supergraph_sdl", supergraph_sdl)
|
14
14
|
|
15
15
|
# or, write the composed schema as a file into your repo...
|
16
16
|
File.write("supergraph/schema.graphql", supergraph_sdl)
|
@@ -19,7 +19,7 @@ File.write("supergraph/schema.graphql", supergraph_sdl)
|
|
19
19
|
To restore a Supergraph, call `from_definition` providing the cached SDL string and a hash of executables keyed by their location names:
|
20
20
|
|
21
21
|
```ruby
|
22
|
-
supergraph_sdl = $
|
22
|
+
supergraph_sdl = $cache.get("cached_supergraph_sdl")
|
23
23
|
|
24
24
|
supergraph = GraphQL::Stitching::Supergraph.from_definition(
|
25
25
|
supergraph_sdl,
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# File uploads example
|
2
|
+
|
3
|
+
This example demonstrates uploading files via the [GraphQL Upload spec](https://github.com/jaydenseric/graphql-multipart-request-spec).
|
4
|
+
|
5
|
+
Try running it:
|
6
|
+
|
7
|
+
```shell
|
8
|
+
cd examples/file_uploads
|
9
|
+
bundle install
|
10
|
+
foreman start
|
11
|
+
```
|
12
|
+
|
13
|
+
This example is headless, but you can verify the stitched schema is running by querying a field from each graph location:
|
14
|
+
|
15
|
+
```shell
|
16
|
+
curl -X POST http://localhost:3000 \
|
17
|
+
-H 'Content-Type: application/json' \
|
18
|
+
-d '{"query":"{ gateway remote }"}'
|
19
|
+
```
|
20
|
+
|
21
|
+
Now try submitting a multipart form upload with a file attachment, per the [spec](https://github.com/jaydenseric/graphql-multipart-request-spec?tab=readme-ov-file#curl-request). The response will echo the uploaded file contents:
|
22
|
+
|
23
|
+
```shell
|
24
|
+
curl http://localhost:3000 \
|
25
|
+
-H 'Content-Type: multipart/form-data' \
|
26
|
+
-F operations='{ "query": "mutation ($file: Upload!) { gateway upload(file: $file) }", "variables": { "file": null } }' \
|
27
|
+
-F map='{ "0": ["variables.file"] }' \
|
28
|
+
-F 0=@file.txt
|
29
|
+
```
|
30
|
+
|
31
|
+
This workflow has:
|
32
|
+
|
33
|
+
1. Submitted a multipart form to the stitched gateway.
|
34
|
+
2. The gateway server unpacked the request using [apollo_upload_server](https://github.com/jetruby/apollo_upload_server-ruby).
|
35
|
+
3. Stitching delegated the `upload` field to its appropraite subgraph location.
|
36
|
+
4. `HttpExecutable` has re-encoded the subgraph request into a multipart form.
|
37
|
+
5. The subgraph location has recieved, unpacked, and resolved the uploaded file.
|
@@ -0,0 +1 @@
|
|
1
|
+
Hello World!
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rackup'
|
4
|
+
require 'json'
|
5
|
+
require 'graphql'
|
6
|
+
require_relative '../../lib/graphql/stitching'
|
7
|
+
require_relative './helpers'
|
8
|
+
|
9
|
+
class StitchedApp
|
10
|
+
def initialize
|
11
|
+
@client = GraphQL::Stitching::Client.new(locations: {
|
12
|
+
gateway: {
|
13
|
+
schema: GatewaySchema,
|
14
|
+
},
|
15
|
+
remote: {
|
16
|
+
schema: RemoteSchema,
|
17
|
+
executable: GraphQL::Stitching::HttpExecutable.new(
|
18
|
+
url: "http://localhost:3001",
|
19
|
+
upload_types: ["Upload"]
|
20
|
+
),
|
21
|
+
},
|
22
|
+
})
|
23
|
+
end
|
24
|
+
|
25
|
+
def call(env)
|
26
|
+
params = apollo_upload_server_middleware_params(env)
|
27
|
+
result = @client.execute(
|
28
|
+
query: params["query"],
|
29
|
+
variables: params["variables"],
|
30
|
+
operation_name: params["operationName"],
|
31
|
+
)
|
32
|
+
|
33
|
+
[200, {"content-type" => "application/json"}, [JSON.generate(result)]]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
Rackup::Handler.default.run(StitchedApp.new, :Port => 3000)
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'action_dispatch'
|
4
|
+
require 'apollo_upload_server/graphql_data_builder'
|
5
|
+
require 'apollo_upload_server/upload'
|
6
|
+
|
7
|
+
# ApolloUploadServer middleware only modifies Rails request params;
|
8
|
+
# for simple Rack apps we need to extract the behavior.
|
9
|
+
def apollo_upload_server_middleware_params(env)
|
10
|
+
req = ActionDispatch::Request.new(env)
|
11
|
+
if env['CONTENT_TYPE'].to_s.include?('multipart/form-data')
|
12
|
+
ApolloUploadServer::GraphQLDataBuilder.new(strict_mode: true).call(req.params)
|
13
|
+
else
|
14
|
+
req.params
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Gateway local schema
|
19
|
+
class GatewaySchema < GraphQL::Schema
|
20
|
+
class Query < GraphQL::Schema::Object
|
21
|
+
field :gateway, Boolean, null: false
|
22
|
+
|
23
|
+
def gateway
|
24
|
+
true
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Mutation < GraphQL::Schema::Object
|
29
|
+
field :gateway, Boolean, null: false
|
30
|
+
|
31
|
+
def gateway
|
32
|
+
true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
query Query
|
37
|
+
mutation Mutation
|
38
|
+
end
|
39
|
+
|
40
|
+
# Remote local schema, with file upload
|
41
|
+
class RemoteSchema < GraphQL::Schema
|
42
|
+
class Query < GraphQL::Schema::Object
|
43
|
+
field :remote, Boolean, null: false
|
44
|
+
|
45
|
+
def remote
|
46
|
+
true
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class Mutation < GraphQL::Schema::Object
|
51
|
+
field :upload, String, null: true do
|
52
|
+
argument :file, ApolloUploadServer::Upload, required: true
|
53
|
+
end
|
54
|
+
|
55
|
+
def upload(file:)
|
56
|
+
file.read
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
query Query
|
61
|
+
mutation Mutation
|
62
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rackup'
|
4
|
+
require 'json'
|
5
|
+
require 'graphql'
|
6
|
+
require_relative './helpers'
|
7
|
+
|
8
|
+
class RemoteApp
|
9
|
+
def call(env)
|
10
|
+
params = apollo_upload_server_middleware_params(env)
|
11
|
+
result = RemoteSchema.execute(
|
12
|
+
query: params["query"],
|
13
|
+
variables: params["variables"],
|
14
|
+
operation_name: params["operationName"],
|
15
|
+
)
|
16
|
+
|
17
|
+
[200, {"content-type" => "application/json"}, [JSON.generate(result)]]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
Rackup::Handler.default.run(RemoteApp.new, :Port => 3001)
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# Merged types example
|
2
|
+
|
3
|
+
This example demonstrates several stitched schemas running across small Rack servers with types merged across locations. The main "gateway" location stitches its local schema onto two remote endpoints.
|
4
|
+
|
5
|
+
Try running it:
|
6
|
+
|
7
|
+
```shell
|
8
|
+
cd examples/merged_types
|
9
|
+
bundle install
|
10
|
+
foreman start
|
11
|
+
```
|
12
|
+
|
13
|
+
Then visit the gateway service at [`http://localhost:3000`](http://localhost:3000) and try this query:
|
14
|
+
|
15
|
+
```graphql
|
16
|
+
query {
|
17
|
+
storefront(id: "1") {
|
18
|
+
id
|
19
|
+
products {
|
20
|
+
upc
|
21
|
+
name
|
22
|
+
price
|
23
|
+
manufacturer {
|
24
|
+
name
|
25
|
+
address
|
26
|
+
products { upc name }
|
27
|
+
}
|
28
|
+
}
|
29
|
+
}
|
30
|
+
}
|
31
|
+
```
|
32
|
+
|
33
|
+
The above query collects data from all locations. You can also request introspections that resolve using the combined supergraph schema.
|
@@ -2,10 +2,9 @@
|
|
2
2
|
|
3
3
|
require 'rackup'
|
4
4
|
require 'json'
|
5
|
-
require 'byebug'
|
6
5
|
require 'graphql'
|
7
|
-
|
8
|
-
require_relative '
|
6
|
+
require_relative '../../lib/graphql/stitching'
|
7
|
+
require_relative '../../test/schemas/example'
|
9
8
|
|
10
9
|
class StitchedApp
|
11
10
|
def initialize
|
@@ -19,11 +18,11 @@ class StitchedApp
|
|
19
18
|
},
|
20
19
|
storefronts: {
|
21
20
|
schema: Schemas::Example::Storefronts,
|
22
|
-
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3001
|
21
|
+
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3001"),
|
23
22
|
},
|
24
23
|
manufacturers: {
|
25
24
|
schema: Schemas::Example::Manufacturers,
|
26
|
-
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3002
|
25
|
+
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3002"),
|
27
26
|
}
|
28
27
|
})
|
29
28
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rackup'
|
4
|
+
require 'json'
|
5
|
+
require 'graphql'
|
6
|
+
require_relative '../../test/schemas/example'
|
7
|
+
|
8
|
+
class FirstRemoteApp
|
9
|
+
def call(env)
|
10
|
+
req = Rack::Request.new(env)
|
11
|
+
params = JSON.parse(req.body.read)
|
12
|
+
result = Schemas::Example::Storefronts.execute(
|
13
|
+
query: params["query"],
|
14
|
+
variables: params["variables"],
|
15
|
+
operation_name: params["operationName"],
|
16
|
+
)
|
17
|
+
|
18
|
+
[200, {"content-type" => "application/json"}, [JSON.generate(result)]]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
Rackup::Handler.default.run(FirstRemoteApp.new, :Port => 3001)
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rackup'
|
4
|
+
require 'json'
|
5
|
+
require 'graphql'
|
6
|
+
require_relative '../../test/schemas/example'
|
7
|
+
|
8
|
+
class SecondRemoteApp
|
9
|
+
def call(env)
|
10
|
+
req = Rack::Request.new(env)
|
11
|
+
params = JSON.parse(req.body.read)
|
12
|
+
result = Schemas::Example::Manufacturers.execute(
|
13
|
+
query: params["query"],
|
14
|
+
variables: params["variables"],
|
15
|
+
operation_name: params["operationName"],
|
16
|
+
)
|
17
|
+
|
18
|
+
[200, {"content-type" => "application/json"}, [JSON.generate(result)]]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
Rackup::Handler.default.run(SecondRemoteApp.new, :Port => 3002)
|
@@ -28,6 +28,7 @@ module GraphQL
|
|
28
28
|
|
29
29
|
def execute(query:, variables: nil, operation_name: nil, context: nil, validate: true)
|
30
30
|
request = GraphQL::Stitching::Request.new(
|
31
|
+
@supergraph,
|
31
32
|
query,
|
32
33
|
operation_name: operation_name,
|
33
34
|
variables: variables,
|
@@ -35,24 +36,13 @@ module GraphQL
|
|
35
36
|
)
|
36
37
|
|
37
38
|
if validate
|
38
|
-
validation_errors =
|
39
|
+
validation_errors = request.validate
|
39
40
|
return error_result(validation_errors) if validation_errors.any?
|
40
41
|
end
|
41
42
|
|
42
43
|
request.prepare!
|
43
|
-
|
44
|
-
|
45
|
-
GraphQL::Stitching::Planner.new(
|
46
|
-
supergraph: @supergraph,
|
47
|
-
request: request,
|
48
|
-
).perform
|
49
|
-
end
|
50
|
-
|
51
|
-
GraphQL::Stitching::Executor.new(
|
52
|
-
supergraph: @supergraph,
|
53
|
-
request: request,
|
54
|
-
plan: plan,
|
55
|
-
).perform
|
44
|
+
load_plan(request)
|
45
|
+
request.execute
|
56
46
|
rescue GraphQL::ParseError, GraphQL::ExecutionError => e
|
57
47
|
error_result([e])
|
58
48
|
rescue StandardError => e
|
@@ -77,13 +67,13 @@ module GraphQL
|
|
77
67
|
|
78
68
|
private
|
79
69
|
|
80
|
-
def
|
81
|
-
if @on_cache_read
|
82
|
-
|
83
|
-
return
|
70
|
+
def load_plan(request)
|
71
|
+
if @on_cache_read && plan_json = @on_cache_read.call(request)
|
72
|
+
plan = GraphQL::Stitching::Plan.from_json(JSON.parse(plan_json))
|
73
|
+
return request.plan(plan)
|
84
74
|
end
|
85
75
|
|
86
|
-
plan =
|
76
|
+
plan = request.plan
|
87
77
|
|
88
78
|
if @on_cache_write
|
89
79
|
@on_cache_write.call(request, JSON.generate(plan.as_json))
|
@@ -29,7 +29,7 @@ module GraphQL
|
|
29
29
|
@executor.request.operation_directives,
|
30
30
|
)
|
31
31
|
variables = @executor.request.variables.slice(*variable_names)
|
32
|
-
raw_result = @executor.supergraph.execute_at_location(@location, query_document, variables, @executor.request
|
32
|
+
raw_result = @executor.supergraph.execute_at_location(@location, query_document, variables, @executor.request)
|
33
33
|
@executor.query_count += 1
|
34
34
|
|
35
35
|
merge_results!(origin_sets_by_operation, raw_result.dig("data"))
|
@@ -17,7 +17,7 @@ module GraphQL
|
|
17
17
|
@executor.request.operation_directives,
|
18
18
|
)
|
19
19
|
query_variables = @executor.request.variables.slice(*op.variables.keys)
|
20
|
-
result = @executor.supergraph.execute_at_location(op.location, query_document, query_variables, @executor.request
|
20
|
+
result = @executor.supergraph.execute_at_location(op.location, query_document, query_variables, @executor.request)
|
21
21
|
@executor.query_count += 1
|
22
22
|
|
23
23
|
@executor.data.merge!(result["data"]) if result["data"]
|
@@ -8,10 +8,10 @@ module GraphQL
|
|
8
8
|
attr_reader :supergraph, :request, :plan, :data, :errors
|
9
9
|
attr_accessor :query_count
|
10
10
|
|
11
|
-
def initialize(
|
12
|
-
@supergraph = supergraph
|
11
|
+
def initialize(request, nonblocking: false)
|
13
12
|
@request = request
|
14
|
-
@
|
13
|
+
@supergraph = request.supergraph
|
14
|
+
@plan = request.plan
|
15
15
|
@data = {}
|
16
16
|
@errors = []
|
17
17
|
@query_count = 0
|
@@ -24,10 +24,7 @@ module GraphQL
|
|
24
24
|
result = {}
|
25
25
|
|
26
26
|
if @data && @data.length > 0
|
27
|
-
result["data"] = raw ? @data : GraphQL::Stitching::Shaper.new(
|
28
|
-
supergraph: @supergraph,
|
29
|
-
request: @request,
|
30
|
-
).perform!(@data)
|
27
|
+
result["data"] = raw ? @data : GraphQL::Stitching::Shaper.new(@request).perform!(@data)
|
31
28
|
end
|
32
29
|
|
33
30
|
if @errors.length > 0
|
@@ -20,8 +20,13 @@ module GraphQL
|
|
20
20
|
"#{EXPORT_PREFIX}#{name}"
|
21
21
|
end
|
22
22
|
|
23
|
+
# The argument assigning Field.alias changed from
|
24
|
+
# a generic `alias` hash key to a structured `field_alias` kwarg.
|
25
|
+
# See https://github.com/rmosolgo/graphql-ruby/pull/4718
|
26
|
+
FIELD_ALIAS_KWARG = !GraphQL::Language::Nodes::Field.new(field_alias: "a").alias.nil?
|
27
|
+
|
23
28
|
def key_node(field_name)
|
24
|
-
if
|
29
|
+
if FIELD_ALIAS_KWARG
|
25
30
|
GraphQL::Language::Nodes::Field.new(field_alias: key(field_name), name: field_name)
|
26
31
|
else
|
27
32
|
GraphQL::Language::Nodes::Field.new(alias: key(field_name), name: field_name)
|
@@ -7,18 +7,148 @@ require "json"
|
|
7
7
|
module GraphQL
|
8
8
|
module Stitching
|
9
9
|
class HttpExecutable
|
10
|
-
def initialize(url:, headers:{})
|
10
|
+
def initialize(url:, headers:{}, upload_types: nil)
|
11
11
|
@url = url
|
12
12
|
@headers = { "Content-Type" => "application/json" }.merge!(headers)
|
13
|
+
@upload_types = upload_types
|
13
14
|
end
|
14
15
|
|
15
|
-
def call(
|
16
|
-
|
16
|
+
def call(request, document, variables)
|
17
|
+
multipart_form = if request.variable_definitions.any? && variables&.any?
|
18
|
+
extract_multipart_form(request, document, variables)
|
19
|
+
end
|
20
|
+
|
21
|
+
response = if multipart_form
|
22
|
+
post_multipart(multipart_form)
|
23
|
+
else
|
24
|
+
post(document, variables)
|
25
|
+
end
|
26
|
+
|
27
|
+
JSON.parse(response.body)
|
28
|
+
end
|
29
|
+
|
30
|
+
def post(document, variables)
|
31
|
+
Net::HTTP.post(
|
17
32
|
URI(@url),
|
18
33
|
JSON.generate({ "query" => document, "variables" => variables }),
|
19
34
|
@headers,
|
20
35
|
)
|
21
|
-
|
36
|
+
end
|
37
|
+
|
38
|
+
def post_multipart(form_data)
|
39
|
+
uri = URI(@url)
|
40
|
+
req = Net::HTTP::Post.new(uri)
|
41
|
+
@headers.each_pair do |key, value|
|
42
|
+
req[key] = value
|
43
|
+
end
|
44
|
+
|
45
|
+
req.set_form(form_data.to_a, "multipart/form-data")
|
46
|
+
Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
|
47
|
+
http.request(req)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# extract multipart upload forms
|
52
|
+
# spec: https://github.com/jaydenseric/graphql-multipart-request-spec
|
53
|
+
def extract_multipart_form(request, document, variables)
|
54
|
+
return unless @upload_types
|
55
|
+
|
56
|
+
path = []
|
57
|
+
files_by_path = {}
|
58
|
+
|
59
|
+
# extract all upload scalar values mapped by their input path
|
60
|
+
variables.each do |key, value|
|
61
|
+
ast_node = request.variable_definitions[key]
|
62
|
+
path << key
|
63
|
+
extract_ast_node(ast_node, value, files_by_path, path, request) if ast_node
|
64
|
+
path.pop
|
65
|
+
end
|
66
|
+
|
67
|
+
return if files_by_path.none?
|
68
|
+
|
69
|
+
map = {}
|
70
|
+
files = files_by_path.values.tap(&:uniq!)
|
71
|
+
variables_copy = variables.dup
|
72
|
+
|
73
|
+
files_by_path.keys.each do |path|
|
74
|
+
orig = variables
|
75
|
+
copy = variables_copy
|
76
|
+
path.each_with_index do |key, i|
|
77
|
+
if i == path.length - 1
|
78
|
+
map_key = files.index(copy[key]).to_s
|
79
|
+
map[map_key] ||= []
|
80
|
+
map[map_key] << "variables.#{path.join(".")}"
|
81
|
+
copy[key] = nil
|
82
|
+
elsif orig[key].object_id == copy[key].object_id
|
83
|
+
copy[key] = copy[key].dup
|
84
|
+
end
|
85
|
+
orig = orig[key]
|
86
|
+
copy = copy[key]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
form = {
|
91
|
+
"operations" => JSON.generate({
|
92
|
+
"query" => document,
|
93
|
+
"variables" => variables_copy,
|
94
|
+
}),
|
95
|
+
"map" => JSON.generate(map),
|
96
|
+
}
|
97
|
+
|
98
|
+
files.each_with_object(form).with_index do |(file, memo), index|
|
99
|
+
memo[index.to_s] = file.respond_to?(:tempfile) ? file.tempfile : file
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def extract_ast_node(ast_node, value, files_by_path, path, request)
|
106
|
+
return unless value
|
107
|
+
|
108
|
+
ast_node = ast_node.of_type while ast_node.is_a?(GraphQL::Language::Nodes::NonNullType)
|
109
|
+
|
110
|
+
if ast_node.is_a?(GraphQL::Language::Nodes::ListType)
|
111
|
+
if value.is_a?(Array)
|
112
|
+
value.each_with_index do |val, index|
|
113
|
+
path << index
|
114
|
+
extract_ast_node(ast_node.of_type, val, files_by_path, path, request)
|
115
|
+
path.pop
|
116
|
+
end
|
117
|
+
end
|
118
|
+
elsif @upload_types.include?(ast_node.name)
|
119
|
+
files_by_path[path.dup] = value
|
120
|
+
else
|
121
|
+
type_def = request.supergraph.schema.get_type(ast_node.name)
|
122
|
+
extract_type_node(type_def, value, files_by_path, path) if type_def&.kind&.input_object?
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def extract_type_node(parent_type, value, files_by_path, path)
|
127
|
+
return unless value
|
128
|
+
|
129
|
+
parent_type = Util.unwrap_non_null(parent_type)
|
130
|
+
|
131
|
+
if parent_type.list?
|
132
|
+
if value.is_a?(Array)
|
133
|
+
value.each_with_index do |val, index|
|
134
|
+
path << index
|
135
|
+
extract_type_node(parent_type.of_type, val, files_by_path, path)
|
136
|
+
path.pop
|
137
|
+
end
|
138
|
+
end
|
139
|
+
elsif parent_type.kind.input_object?
|
140
|
+
if value.is_a?(Enumerable)
|
141
|
+
arguments = parent_type.arguments
|
142
|
+
value.each do |key, val|
|
143
|
+
arg_type = arguments[key]&.type
|
144
|
+
path << key
|
145
|
+
extract_type_node(arg_type, val, files_by_path, path) if arg_type
|
146
|
+
path.pop
|
147
|
+
end
|
148
|
+
end
|
149
|
+
elsif @upload_types.include?(parent_type.graphql_name)
|
150
|
+
files_by_path[path.dup] = value
|
151
|
+
end
|
22
152
|
end
|
23
153
|
end
|
24
154
|
end
|
@@ -9,9 +9,9 @@ module GraphQL
|
|
9
9
|
MUTATION_OP = "mutation"
|
10
10
|
ROOT_INDEX = 0
|
11
11
|
|
12
|
-
def initialize(
|
13
|
-
@supergraph = supergraph
|
12
|
+
def initialize(request)
|
14
13
|
@request = request
|
14
|
+
@supergraph = request.supergraph
|
15
15
|
@planning_index = ROOT_INDEX
|
16
16
|
@steps_by_entrypoint = {}
|
17
17
|
end
|
@@ -6,9 +6,10 @@ module GraphQL
|
|
6
6
|
SUPPORTED_OPERATIONS = ["query", "mutation"].freeze
|
7
7
|
SKIP_INCLUDE_DIRECTIVE = /@(?:skip|include)/
|
8
8
|
|
9
|
-
attr_reader :document, :variables, :operation_name, :context
|
9
|
+
attr_reader :supergraph, :document, :variables, :operation_name, :context
|
10
10
|
|
11
|
-
def initialize(document, operation_name: nil, variables: nil, context: nil)
|
11
|
+
def initialize(supergraph, document, operation_name: nil, variables: nil, context: nil)
|
12
|
+
@supergraph = supergraph
|
12
13
|
@string = nil
|
13
14
|
@digest = nil
|
14
15
|
@normalized_string = nil
|
@@ -17,6 +18,7 @@ module GraphQL
|
|
17
18
|
@operation_directives = nil
|
18
19
|
@variable_definitions = nil
|
19
20
|
@fragment_definitions = nil
|
21
|
+
@plan = nil
|
20
22
|
|
21
23
|
@document = if document.is_a?(String)
|
22
24
|
@string = document
|
@@ -83,6 +85,10 @@ module GraphQL
|
|
83
85
|
end
|
84
86
|
end
|
85
87
|
|
88
|
+
def validate
|
89
|
+
@supergraph.schema.validate(@document, context: @context)
|
90
|
+
end
|
91
|
+
|
86
92
|
def prepare!
|
87
93
|
operation.variables.each do |v|
|
88
94
|
@variables[v.name] = v.default_value if @variables[v.name].nil? && !v.default_value.nil?
|
@@ -93,12 +99,25 @@ module GraphQL
|
|
93
99
|
@document = modified_ast
|
94
100
|
@string = @normalized_string = nil
|
95
101
|
@digest = @normalized_digest = nil
|
96
|
-
@operation = @operation_directives = @variable_definitions = nil
|
102
|
+
@operation = @operation_directives = @variable_definitions = @plan = nil
|
97
103
|
end
|
98
104
|
end
|
99
105
|
|
100
106
|
self
|
101
107
|
end
|
108
|
+
|
109
|
+
def plan(new_plan = nil)
|
110
|
+
if new_plan
|
111
|
+
raise StitchingError, "Plan must be a `GraphQL::Stitching::Plan`." unless new_plan.is_a?(Plan)
|
112
|
+
@plan = new_plan
|
113
|
+
else
|
114
|
+
@plan ||= GraphQL::Stitching::Planner.new(self).perform
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def execute(raw: false)
|
119
|
+
GraphQL::Stitching::Executor.new(self).perform(raw: raw)
|
120
|
+
end
|
102
121
|
end
|
103
122
|
end
|
104
123
|
end
|
@@ -6,9 +6,9 @@ module GraphQL
|
|
6
6
|
# Shapes the final results payload to the request selection and schema definition.
|
7
7
|
# This eliminates unrequested export selections and applies null bubbling.
|
8
8
|
class Shaper
|
9
|
-
def initialize(
|
10
|
-
@supergraph = supergraph
|
9
|
+
def initialize(request)
|
11
10
|
@request = request
|
11
|
+
@supergraph = request.supergraph
|
12
12
|
@root_type = nil
|
13
13
|
end
|
14
14
|
|
@@ -90,7 +90,7 @@ module GraphQL
|
|
90
90
|
|
91
91
|
attr_reader :schema, :boundaries, :locations_by_type_and_field, :executables
|
92
92
|
|
93
|
-
def initialize(schema:, fields
|
93
|
+
def initialize(schema:, fields: {}, boundaries: {}, executables: {})
|
94
94
|
@schema = schema
|
95
95
|
@boundaries = boundaries
|
96
96
|
@possible_keys_by_type = {}
|
@@ -209,7 +209,7 @@ module GraphQL
|
|
209
209
|
end
|
210
210
|
end
|
211
211
|
|
212
|
-
def execute_at_location(location, source, variables,
|
212
|
+
def execute_at_location(location, source, variables, request)
|
213
213
|
executable = executables[location]
|
214
214
|
|
215
215
|
if executable.nil?
|
@@ -218,11 +218,11 @@ module GraphQL
|
|
218
218
|
executable.execute(
|
219
219
|
query: source,
|
220
220
|
variables: variables,
|
221
|
-
context: context.frozen? ? context.dup : context,
|
221
|
+
context: request.context.frozen? ? request.context.dup : request.context,
|
222
222
|
validate: false,
|
223
223
|
)
|
224
224
|
elsif executable.respond_to?(:call)
|
225
|
-
executable.call(
|
225
|
+
executable.call(request, source, variables)
|
226
226
|
else
|
227
227
|
raise StitchingError, "Missing valid executable for #{location} location."
|
228
228
|
end
|
@@ -12,16 +12,7 @@ module GraphQL
|
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
|
-
GRAPHQL_VERSION = GraphQL::VERSION.split(".").map(&:to_i).freeze
|
16
|
-
|
17
15
|
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
|
-
|
25
16
|
# specifies if a type is a primitive leaf value
|
26
17
|
def is_leaf_type?(type)
|
27
18
|
type.kind.scalar? || type.kind.enum?
|
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.
|
4
|
+
version: 1.2.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-12-
|
11
|
+
date: 2023-12-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|
@@ -76,13 +76,13 @@ files:
|
|
76
76
|
- ".gitignore"
|
77
77
|
- Gemfile
|
78
78
|
- LICENSE
|
79
|
-
- Procfile
|
80
79
|
- README.md
|
81
80
|
- Rakefile
|
82
81
|
- docs/README.md
|
83
82
|
- docs/client.md
|
84
83
|
- docs/composer.md
|
85
84
|
- docs/executor.md
|
85
|
+
- docs/http_executable.md
|
86
86
|
- docs/images/library.png
|
87
87
|
- docs/images/merging.png
|
88
88
|
- docs/images/stitching.png
|
@@ -90,10 +90,20 @@ files:
|
|
90
90
|
- docs/planner.md
|
91
91
|
- docs/request.md
|
92
92
|
- docs/supergraph.md
|
93
|
-
-
|
94
|
-
-
|
95
|
-
-
|
96
|
-
-
|
93
|
+
- examples/file_uploads/Gemfile
|
94
|
+
- examples/file_uploads/Procfile
|
95
|
+
- examples/file_uploads/README.md
|
96
|
+
- examples/file_uploads/file.txt
|
97
|
+
- examples/file_uploads/gateway.rb
|
98
|
+
- examples/file_uploads/helpers.rb
|
99
|
+
- examples/file_uploads/remote.rb
|
100
|
+
- examples/merged_types/Gemfile
|
101
|
+
- examples/merged_types/Procfile
|
102
|
+
- examples/merged_types/README.md
|
103
|
+
- examples/merged_types/gateway.rb
|
104
|
+
- examples/merged_types/graphiql.html
|
105
|
+
- examples/merged_types/remote1.rb
|
106
|
+
- examples/merged_types/remote2.rb
|
97
107
|
- gemfiles/graphql_1.13.9.gemfile
|
98
108
|
- graphql-stitching.gemspec
|
99
109
|
- lib/graphql/stitching.rb
|
data/Procfile
DELETED
data/example/remote1.rb
DELETED
@@ -1,26 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'rackup'
|
4
|
-
require 'json'
|
5
|
-
require 'graphql'
|
6
|
-
require_relative '../test/schemas/example'
|
7
|
-
|
8
|
-
class FirstRemoteApp
|
9
|
-
def call(env)
|
10
|
-
req = Rack::Request.new(env)
|
11
|
-
case req.path_info
|
12
|
-
when /graphql/
|
13
|
-
params = JSON.parse(req.body.read)
|
14
|
-
result = Schemas::Example::Storefronts.execute(
|
15
|
-
query: params["query"],
|
16
|
-
variables: params["variables"],
|
17
|
-
operation_name: params["operationName"],
|
18
|
-
)
|
19
|
-
[200, {"content-type" => "application/json"}, [JSON.generate(result)]]
|
20
|
-
else
|
21
|
-
[404, {"content-type" => "text/html"}, ["not found"]]
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
Rackup::Handler.default.run(FirstRemoteApp.new, :Port => 3001)
|
data/example/remote2.rb
DELETED
@@ -1,26 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'rackup'
|
4
|
-
require 'json'
|
5
|
-
require 'graphql'
|
6
|
-
require_relative '../test/schemas/example'
|
7
|
-
|
8
|
-
class SecondRemoteApp
|
9
|
-
def call(env)
|
10
|
-
req = Rack::Request.new(env)
|
11
|
-
case req.path_info
|
12
|
-
when /graphql/
|
13
|
-
params = JSON.parse(req.body.read)
|
14
|
-
result = Schemas::Example::Manufacturers.execute(
|
15
|
-
query: params["query"],
|
16
|
-
variables: params["variables"],
|
17
|
-
operation_name: params["operationName"],
|
18
|
-
)
|
19
|
-
[200, {"content-type" => "application/json"}, [JSON.generate(result)]]
|
20
|
-
else
|
21
|
-
[404, {"content-type" => "text/html"}, ["not found"]]
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
Rackup::Handler.default.run(SecondRemoteApp.new, :Port => 3002)
|
File without changes
|