Skip to content

Commit

Permalink
Doc updates (#162)
Browse files Browse the repository at this point in the history
  • Loading branch information
gmac authored Oct 4, 2024
1 parent aa5152d commit e8f0aba
Show file tree
Hide file tree
Showing 9 changed files with 36 additions and 41 deletions.
11 changes: 5 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
## GraphQL Stitching for Ruby

GraphQL stitching composes a single schema from multiple underlying GraphQL resources, then smartly proxies portions of incoming requests to their respective locations in dependency order and returns the merged results. This allows an entire location graph to be queried through one combined GraphQL surface area.
GraphQL stitching composes a single schema from multiple underlying GraphQL resources, then smartly proxies portions of incoming requests to their respective locations in dependency order and returns the merged results. This allows an entire graph of locations to be queried through one combined GraphQL surface area.

![Stitched graph](./docs/images/stitching.png)

**Supports:**
- All operation types: query, mutation, and [subscription](./docs/subscriptions.md).
- Merged object and abstract types.
- Merged object and abstract types joining though multiple keys.
- Shared objects, fields, enums, and inputs across locations.
- Multiple and composite type keys.
- Combining local and remote schemas.
- [File uploads](./docs/http_executable.md) via multipart forms.
- Tested with all minor versions of `graphql-ruby`.
Expand All @@ -17,7 +16,7 @@ GraphQL stitching composes a single schema from multiple underlying GraphQL reso
- Computed fields (ie: federation-style `@requires`).
- Defer/stream.

This Ruby implementation is a sibling to [GraphQL Tools](https://the-guild.dev/graphql/stitching) (JS) and [Bramble](https://movio.github.io/bramble/) (Go), and its capabilities fall somewhere in between them. GraphQL stitching is similar in concept to [Apollo Federation](https://www.apollographql.com/docs/federation/), though more generic. The opportunity here is for a Ruby application to stitch its local schemas together or onto remote sources without requiring an additional proxy service running in another language. If your goal is to build a purely high-throughput federated reverse proxy, consider not using Ruby.
This Ruby implementation is designed as a generic library to join basic spec-compliant GraphQL schemas using their existing types and fields in a [DIY](https://dictionary.cambridge.org/us/dictionary/english/diy) capacity. The opportunity here is for a Ruby application to stitch its local schemas together or onto remote sources without requiring an additional proxy service running in another language. If your goal is a purely high-throughput federation gateway with managed schema deployments, consider more opinionated frameworks such as [Apollo Federation](https://www.apollographql.com/docs/federation/).

## Getting started

Expand Down Expand Up @@ -88,7 +87,7 @@ While `Client` is sufficient for most usecases, the library offers several discr

![Merging types](./docs/images/merging.png)

To facilitate this merging of types, stitching must know how to cross-reference and fetch each variant of a type from its source location using [type resolver queries](#merged-type-resolver-queries). For those in an Apollo ecosystem, there's also _limited_ support for merging types though a [federation `_entities` protocol](./docs/federation_entities.md).
To facilitate this, schemas should be designed around [merged type keys](./docs/mechanics.md#modeling-foreign-keys-for-stitching) that stitching can cross-reference and fetch across locations using [type resolver queries](#merged-type-resolver-queries). For those in an Apollo ecosystem, there's also _limited_ support for merging types though a [federation `_entities` protocol](./docs/federation_entities.md).

### Merged type resolver queries

Expand Down Expand Up @@ -154,7 +153,7 @@ type Query {
* 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.
* 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](#argument-shapes) later).

Each location that provides a unique variant of a type must provide at least one resolver query for the type. The exception to this requirement are [outbound-only types](./docs/mechanics.md#outbound-only-merged-types) and/or [foreign key types](./docs/mechanics.md##modeling-foreign-keys-for-stitching) that contain no exclusive data:
Each location that provides a unique variant of a type must provide at least one resolver query for the type. The exception to this requirement are [outbound-only types](./docs/mechanics.md#outbound-only-merged-types) that contain no exclusive data:

```graphql
type Product {
Expand Down
13 changes: 7 additions & 6 deletions docs/composer.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ A `Composer` may be constructed with optional settings that tune how it builds a
composer = GraphQL::Stitching::Composer.new(
query_name: "Query",
mutation_name: "Mutation",
subscription_name: "Subscription",
description_merger: ->(values_by_location, info) { values_by_location.values.join("\n") },
deprecation_merger: ->(values_by_location, info) { values_by_location.values.first },
default_value_merger: ->(values_by_location, info) { values_by_location.values.first },
Expand All @@ -24,6 +25,8 @@ Constructor arguments:

- **`mutation_name:`** _optional_, the name of the root mutation type in the composed schema; `Mutation` by default. The root mutation types from all location schemas will be merged into this type, regardless of their local names.

- **`subscription_name:`** _optional_, the name of the root subscription type in the composed schema; `Subscription` by default. The root subscription types from all location schemas will be merged into this type, regardless of their local names.

- **`description_merger:`** _optional_, a [value merger function](#value-merger-functions) for merging element description strings from across locations.

- **`deprecation_merger:`** _optional_, a [value merger function](#value-merger-functions) for merging element deprecation strings from across locations.
Expand Down Expand Up @@ -98,17 +101,16 @@ Location settings have top-level keys that specify arbitrary location names, eac

The strategy used to merge source schemas into the combined schema is based on each element type:

- Arguments of fields, directives, and `InputObject` types intersect for each parent element across locations (an element's arguments must appear in all locations):
- Arguments must share a value type, and the strictest nullability across locations is used.
- Composition fails if argument intersection would eliminate a non-null argument.

- `Object` and `Interface` types merge their fields and directives together:
- Common fields across locations must share a value type, and the weakest nullability is used.
- Field and directive arguments merge using the same rules as `InputObject`.
- Objects with unique fields across locations must implement [`@stitch` accessors](../README.md#merged-types).
- Shared object types without `@stitch` accessors must contain identical fields.
- Merged interfaces must remain compatible with all underlying implementations.

- `InputObject` types intersect arguments from across locations (arguments must appear in all locations):
- Arguments must share a value type, and the strictest nullability across locations is used.
- Composition fails if argument intersection would eliminate a non-null argument.

- `Enum` types merge their values based on how the enum is used:
- Enums used anywhere as an argument will intersect their values (common values across all locations).
- Enums used exclusively in read contexts will provide a union of values (all values across all locations).
Expand All @@ -118,7 +120,6 @@ The strategy used to merge source schemas into the combined schema is based on e
- `Scalar` types are added for all scalar names across all locations.

- `Directive` definitions are added for all distinct names across locations:
- Arguments merge using the same rules as `InputObject`.
- Stitching directives (both definitions and assignments) are omitted.

Note that the structure of a composed schema may change based on new schema additions and/or element usage (ie: changing input object arguments in one service may cause the intersection of arguments to change). Therefore, it's highly recommended that you use a [schema comparator](https://github.com/xuorig/graphql-schema_comparator) to flag regressions across composed schema versions.
2 changes: 1 addition & 1 deletion docs/federation_entities.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,6 @@ It's perfectly fine to mix and match schemas that implement an `_entities` query

### Federation features that will most definitly break

- `@external` fields will confuse the stitching query planner.
- `@external` fields will confuse the stitching query planner (as the fields aren't natively resolvable at the location).
- `@requires` fields will not be sent any dependencies.
- No support for Apollo composition directives.
20 changes: 10 additions & 10 deletions docs/request.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ request = GraphQL::Stitching::Request.new(

A `Request` provides the following information:

- `req.document`: parsed AST of the GraphQL source
- `req.variables`: a hash of user-submitted variables
- `req.string`: the original GraphQL source string, or printed document
- `req.digest`: a SHA2 of the request string
- `req.normalized_string`: printed document string with consistent whitespace
- `req.normalized_digest`: a SHA2 of the normalized string
- `req.operation`: the operation definition selected for the request
- `req.variable_definitions`: a mapping of variable names to their type definitions
- `req.fragment_definitions`: a mapping of fragment names to their fragment definitions
- `req.document`: parsed AST of the GraphQL source.
- `req.variables`: a hash of user-submitted variables.
- `req.string`: the original GraphQL source string, or printed document.
- `req.digest`: a digest of the request string, hashed by the `Stitching.digest` implementation.
- `req.normalized_string`: printed document string with consistent whitespace.
- `req.normalized_digest`: a digest of the normalized string, hashed by the `Stitching.digest` implementation.
- `req.operation`: the operation definition selected for the request.
- `req.variable_definitions`: a mapping of variable names to their type definitions.
- `req.fragment_definitions`: a mapping of fragment names to their fragment definitions.

### Request lifecycle

Expand All @@ -32,5 +32,5 @@ component, or you may invoke them manually:

1. `request.validate`: runs static validations on the request using the combined schema.
2. `request.prepare!`: inserts variable defaults and pre-renders skip/include conditional shaping.
3. `request.plan`: builds a plan for the request. May act as a setting for plans pulled from cache.
3. `request.plan`: builds a plan for the request. May act as a setter for plans pulled from cache.
4. `request.execute`: executes the request, and returns the resulting data.
12 changes: 6 additions & 6 deletions docs/subscriptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Stitching is an interesting prospect for subscriptions because socket-based inte

### Composing a subscriptions schema

For simplicity, subscription resolvers should exist together in a single schema (multiple schemas with subscriptions probably aren't worth the confusion). This subscriptions schema may provide basic entity types that will merge with other locations. For example, here's a bare-bones subscriptions schema:
For simplicity, subscription resolvers are best kept together in a single schema (multiple schemas with subscriptions probably aren't worth the confusion). This subscriptions schema may provide basic entity types that will merge with other locations. For example, here's a bare-bones subscriptions schema:

```ruby
class SubscriptionSchema < GraphQL::Schema
Expand Down Expand Up @@ -122,7 +122,7 @@ class GraphqlController < ApplicationController
def execute
result = StitchedSchema.execute(
params[:query],
context: {},
context: {},
variables: params[:variables],
operation_name: params[:operationName],
)
Expand Down Expand Up @@ -164,15 +164,15 @@ class GraphqlChannel < ApplicationCable::Channel

def unsubscribed
@subscription_ids.each { |sid|
# Go directly through the subscriptions subschema
# Go directly through the subscriptions subschema
# when managing/triggering subscriptions:
SubscriptionSchema.subscriptions.delete_subscription(sid)
}
end
end
```

What happens behind the scenes here is that stitching filters the `execute` request down to just subscription selections, and passes those through to the subscriptions subschema where they register an event binding. The subscriber response gets stitched while passing back out through the stitching client.
What happens behind the scenes here is that stitching filters the `execute` request down to just subscription selections, and passes those through to the subscriptions subschema where they register an event binding. The subscriber response gets stitched while passing back out through the stitching client. The `unsubscribed` method works directly with the subschema where subscriptions are managed.

#### Plugin

Expand All @@ -188,7 +188,7 @@ class StitchedActionCableSubscriptions < GraphQL::Subscriptions::ActionCableSubs
end

class SubscriptionSchema < GraphQL::Schema
# switch the plugin on the subscriptions schema to use the patched class...
# switch the plugin on the subscriptions schema to use the patched class...
use StitchedActionCableSubscriptions
end
```
Expand All @@ -200,7 +200,7 @@ Subscription update events are triggered as normal directly through the subscrip
```ruby
class Comment < ApplicationRecord
after_create :trigger_subscriptions

def trigger_subscriptions
SubscriptionsSchema.subscriptions.trigger(:comment_added_to_post, { post_id: post_id }, self)
end
Expand Down
10 changes: 5 additions & 5 deletions docs/supergraph.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@ A Supergraph is designed to be composed, cached, and restored. Calling `to_defin
```ruby
supergraph_sdl = supergraph.to_definition

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

# or, write the composed schema as a file into your repo...
# write the composed schema as a file into your repo...
File.write("supergraph/schema.graphql", supergraph_sdl)

# or, stash this composed schema in a cache...
$cache.set("cached_supergraph_sdl", 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 = $cache.get("cached_supergraph_sdl")
supergraph_sdl = File.read("supergraph/schema.graphql")

supergraph = GraphQL::Stitching::Supergraph.from_definition(
supergraph_sdl,
Expand Down
2 changes: 1 addition & 1 deletion lib/graphql/stitching/executor/type_resolver_source.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def initialize(executor, location)
end

def fetch(ops)
origin_sets_by_operation = ops.each_with_object({}) do |op, memo|
origin_sets_by_operation = ops.each_with_object({}.compare_by_identity) do |op, memo|
origin_set = op.path.reduce([@executor.data]) do |set, path_segment|
set.flat_map { |obj| obj && obj[path_segment] }.tap(&:compact!)
end
Expand Down
2 changes: 1 addition & 1 deletion lib/graphql/stitching/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ def fragment_definitions
# Validates the request using the combined supergraph schema.
# @return [Array<GraphQL::ExecutionError>] an array of static validation errors
def validate
result = @supergraph.static_validator.validate(@query)
result = @supergraph.schema.static_validator.validate(@query)
result[:errors]
end

Expand Down
5 changes: 0 additions & 5 deletions lib/graphql/stitching/supergraph.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,6 @@ def initialize(schema:, fields: {}, resolvers: {}, executables: {})
end.freeze
end

# @return [GraphQL::StaticValidation::Validator] static validator for the supergraph schema.
def static_validator
@static_validator ||= @schema.static_validator
end

def resolvers_by_version
@resolvers_by_version ||= resolvers.values.tap(&:flatten!).each_with_object({}) do |resolver, memo|
memo[resolver.version] = resolver
Expand Down

0 comments on commit e8f0aba

Please sign in to comment.