Skip to content

Commit

Permalink
v1.2.0 (#108)
Browse files Browse the repository at this point in the history
  • Loading branch information
gmac authored Dec 29, 2023
1 parent dc15230 commit 8427951
Show file tree
Hide file tree
Showing 52 changed files with 877 additions and 445 deletions.
3 changes: 0 additions & 3 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
source 'https://rubygems.org'
gemspec

gem 'rack'
gem 'rackup'
gem 'foreman'
gem 'pry'
gem 'pry-byebug'
gem 'warning'
3 changes: 0 additions & 3 deletions Procfile

This file was deleted.

42 changes: 10 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ GraphQL stitching composes a single schema from multiple underlying GraphQL reso
- Shared objects, fields, enums, and inputs across locations.
- Combining local and remote schemas.
- Type merging via arbitrary queries or federation `_entities` protocol.
- File uploads via [multipart form spec](https://github.com/jaydenseric/graphql-multipart-request-spec).

**NOT Supported:**
- Computed fields (ie: federation-style `@requires`).
Expand Down Expand Up @@ -80,6 +81,7 @@ While the `Client` constructor is an easy quick start, the library also has seve
- [Request](./docs/request.md) - prepares a requested GraphQL document and variables for stitching.
- [Planner](./docs/planner.md) - builds a cacheable query plan for a request document.
- [Executor](./docs/executor.md) - executes a query plan with given request variables.
- [HttpExecutable](./docs/http_executable.md) - proxies requests to remotes with multipart file upload support.

## Merged types

Expand Down Expand Up @@ -360,11 +362,11 @@ It's perfectly fine to mix and match schemas that implement an `_entities` query

## Executables

An executable resource performs location-specific GraphQL requests. Executables may be `GraphQL::Schema` classes, or any object that responds to `.call(location, source, variables, context)` and returns a raw GraphQL response:
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:

```ruby
class MyExecutable
def call(location, source, variables, context)
def call(request, source, variables)
# process a GraphQL request...
return {
"data" => { ... },
Expand Down Expand Up @@ -392,12 +394,12 @@ supergraph = GraphQL::Stitching::Composer.new.perform({
},
fourth: {
schema: FourthSchema,
executable: ->(loc, query, vars, ctx) { ... },
executable: ->(req, query, vars) { ... },
},
})
```

The `GraphQL::Stitching::HttpExecutable` class is provided as a simple executable wrapper around `Net::HTTP.post`. 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)).
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)).

## Batching

Expand Down Expand Up @@ -431,36 +433,12 @@ The [Executor](./docs/executor.md) component builds atop the Ruby fiber-based im
- [Stitched errors](./docs/mechanics.md#stitched-errors)
- [Null results](./docs/mechanics.md#null-results)

## Example
## Examples

This repo includes a working example of several stitched schemas running across small Rack servers. Try running it:
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.

```shell
bundle install
foreman start
```

Then visit the gateway service at `http://localhost:3000` and try this query:

```graphql
query {
storefront(id: "1") {
id
products {
upc
name
price
manufacturer {
name
address
products { upc name }
}
}
}
}
```

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.
- [Merged types](./examples/merged_types)
- [File uploads](./examples/file_uploads)

## Tests

Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Major components include:
- [Request](./request.md) - prepares a requested GraphQL document and variables for stitching.
- [Planner](./planner.md) - builds a cacheable query plan for a request document.
- [Executor](./executor.md) - executes a query plan with given request variables.
- [HttpExecutable](./http_executable.md) - proxies requests to remotes with multipart file upload support.

Additional topics:

Expand Down
4 changes: 2 additions & 2 deletions docs/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,11 @@ The client provides cache hooks to enable caching query plans across requests. W

```ruby
client.on_cache_read do |request|
$redis.get(request.digest) # << 3P code
$cache.get(request.digest) # << 3P code
end

client.on_cache_write do |request, payload|
$redis.set(request.digest, payload) # << 3P code
$cache.set(request.digest, payload) # << 3P code
end
```

Expand Down
2 changes: 1 addition & 1 deletion docs/composer.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Location settings have top-level keys that specify arbitrary location names, eac

- **`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).

- **`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(location, source, variables, context)` method that returns a GraphQL response. Omitting the executable option will use the location's provided `schema` as the executable resource.
- **`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.

- **`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.

Expand Down
23 changes: 8 additions & 15 deletions docs/executor.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,35 +13,28 @@ query = <<~GRAPHQL
GRAPHQL

request = GraphQL::Stitching::Request.new(
supergraph,
query,
variables: { "id" => "123" },
operation_name: "MyQuery",
context: { ... },
)

plan = GraphQL::Stitching::Planner.new(
supergraph: supergraph,
request: request,
).perform
# Via Request:
result = request.execute

result = GraphQL::Stitching::Executor.new(
supergraph: supergraph,
request: request,
plan: plan,
).perform
# Via Executor:
result = GraphQL::Stitching::Executor.new(request).perform
```

### Raw results

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:

```ruby
# get the raw result without shaping
raw_result = GraphQL::Stitching::Executor.new(
supergraph: supergraph,
request: request,
plan: plan,
).perform(raw: true)
# get the raw result without shaping using either form:
raw_result = request.execute(raw: true)
raw_result = GraphQL::Stitching::Executor.new(request).perform(raw: true)
```

The raw result will contain many irregularities from the stitching process, however may be insightful when debugging inconsistencies in results:
Expand Down
51 changes: 51 additions & 0 deletions docs/http_executable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
## GraphQL::Stitching::HttpExecutable

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).

```ruby
exe = GraphQL::Stitching::HttpExecutable.new(
url: "http://localhost:3001",
headers: {
"Authorization" => "..."
}
)
```

### GraphQL file uploads

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:

#### 1. Input file uploads as Tempfile variables

```ruby
client.execute(
"mutation($file: Upload) { upload(file: $file) }",
variables: { "file" => Tempfile.new(...) }
)
```

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.

#### 2. Enable `HttpExecutable.upload_types`

```ruby
client = GraphQL::Stitching::Client.new(locations: {
alpha: {
schema: GraphQL::Schema.from_definition(...),
executable: GraphQL::Stitching::HttpExecutable.new(
url: "http://localhost:3000",
upload_types: ["Upload"], # << extract `Upload` scalars into multipart forms
),
},
bravo: {
schema: GraphQL::Schema.from_definition(...),
executable: GraphQL::Stitching::HttpExecutable.new(
url: "http://localhost:3001"
),
},
})
```

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.

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.
26 changes: 12 additions & 14 deletions docs/planner.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,34 +13,32 @@ document = <<~GRAPHQL
GRAPHQL

request = GraphQL::Stitching::Request.new(
supergraph,
document,
variables: { "id" => "1" },
operation_name: "MyQuery",
).prepare!

plan = GraphQL::Stitching::Planner.new(
supergraph: supergraph,
request: request,
).perform
# Via Request:
plan = request.plan

# Via Planner:
plan = GraphQL::Stitching::Planner.new(request).perform
```

### Caching

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.

```ruby
cached_plan = $redis.get(request.digest)
cached_plan = $cache.get(request.digest)

plan = if cached_plan
GraphQL::Stitching::Plan.from_json(JSON.parse(cached_plan))
if cached_plan
plan = GraphQL::Stitching::Plan.from_json(JSON.parse(cached_plan))
request.plan(plan)
else
plan = GraphQL::Stitching::Planner.new(
supergraph: supergraph,
request: request,
).perform

$redis.set(request.digest, JSON.generate(plan.as_json))
plan
plan = request.plan
$cache.set(request.digest, JSON.generate(plan.as_json))
end

# execute the plan...
Expand Down
2 changes: 2 additions & 0 deletions docs/request.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ A `Request` contains a parsed GraphQL document and variables, and handles the lo
```ruby
source = "query FetchMovie($id: ID!) { movie(id:$id) { id genre } }"
request = GraphQL::Stitching::Request.new(
supergraph,
source,
variables: { "id" => "1" },
operation_name: "FetchMovie",
Expand Down Expand Up @@ -42,6 +43,7 @@ document = <<~GRAPHQL
GRAPHQL

request = GraphQL::Stitching::Request.new(
supergraph,
document,
variables: { "id" => "1" },
operation_name: "FetchMovie",
Expand Down
4 changes: 2 additions & 2 deletions docs/supergraph.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ A Supergraph is designed to be composed, cached, and restored. Calling `to_defin
supergraph_sdl = supergraph.to_definition

# stash this composed schema in a cache...
$redis.set("cached_supergraph_sdl", supergraph_sdl)
$cache.set("cached_supergraph_sdl", supergraph_sdl)

# or, write the composed schema as a file into your repo...
File.write("supergraph/schema.graphql", supergraph_sdl)
Expand All @@ -19,7 +19,7 @@ File.write("supergraph/schema.graphql", supergraph_sdl)
To restore a Supergraph, call `from_definition` providing the cached SDL string and a hash of executables keyed by their location names:

```ruby
supergraph_sdl = $redis.get("cached_supergraph_sdl")
supergraph_sdl = $cache.get("cached_supergraph_sdl")

supergraph = GraphQL::Stitching::Supergraph.from_definition(
supergraph_sdl,
Expand Down
26 changes: 0 additions & 26 deletions example/remote1.rb

This file was deleted.

26 changes: 0 additions & 26 deletions example/remote2.rb

This file was deleted.

9 changes: 9 additions & 0 deletions examples/file_uploads/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

source 'https://rubygems.org'

gem 'rack'
gem 'rackup'
gem 'foreman'
gem 'graphql'
gem 'apollo_upload_server', '2.1'
2 changes: 2 additions & 0 deletions examples/file_uploads/Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
gateway: bundle exec ruby gateway.rb
remote: bundle exec ruby remote.rb
Loading

0 comments on commit 8427951

Please sign in to comment.