Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v1.4.0 #141

Merged
merged 12 commits into from
Jul 2, 2024
64 changes: 57 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ GraphQL stitching composes a single schema from multiple underlying GraphQL reso

**Supports:**
- Merged object and abstract types.
- Multiple keys per merged type.
- Multiple and composite keys per merged type.
- Shared objects, fields, enums, and inputs across locations.
- Combining local and remote schemas.
- File uploads via [multipart form spec](https://github.com/jaydenseric/graphql-multipart-request-spec).
Expand Down Expand Up @@ -94,7 +94,7 @@ To facilitate this merging of types, stitching must know how to cross-reference
Types merge through resolver queries identified by a `@stitch` directive:

```graphql
directive @stitch(key: String!) repeatable on FIELD_DEFINITION
directive @stitch(key: String!, arguments: String) repeatable on FIELD_DEFINITION
```

This directive (or [static configuration](#sdl-based-schemas)) is applied to root queries where a merged type may be accessed in each location, and a `key` argument specifies a field needed from other locations to be used as a query argument.
Expand Down Expand Up @@ -151,7 +151,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](#multiple-query-arguments) later).
* 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:

Expand Down Expand Up @@ -198,7 +198,7 @@ type Query {
To customize which types an abstract query provides and their respective keys, you may extend the `@stitch` directive with a `typeName` constraint. This can be repeated to select multiple types.

```graphql
directive @stitch(key: String!, typeName: String) repeatable on FIELD_DEFINITION
directive @stitch(key: String!, arguments: String, typeName: String) repeatable on FIELD_DEFINITION

type Product { sku: ID! }
type Order { id: ID! }
Expand All @@ -212,19 +212,69 @@ type Query {
}
```

#### Multiple query arguments
#### Argument shapes

Stitching infers which argument to use for queries with a single argument, or when the key name matches its intended argument. For queries that accept multiple arguments with unmatched names, the key should provide an argument alias specified as `"<arg>:<key>"`.
Stitching infers which argument to use for queries with a single argument, or when the key name matches its intended argument. For custom mappings, the `arguments` option may specify a template of GraphQL arguments that insert key selections:

```graphql
type Product {
id: ID!
}
type Query {
product(byId: ID, bySku: ID): Product @stitch(key: "byId:id")
product(byId: ID, bySku: ID): Product
@stitch(key: "id", arguments: "byId: $.id")
}
```

Key insertions are prefixed by `$` and specify a dot-notation path to any selections made by the resolver `key`, or `__typename`. This syntax allows sending multiple arguments that intermix stitching keys with complex input shapes and other static values:

```graphql
type Product {
id: ID!
}
union Entity = Product
input EntityKey {
id: ID!
type: String!
}

type Query {
entities(keys: [EntityKey!]!, source: String="database"): [Entity]!
@stitch(key: "id", arguments: "keys: { id: $.id, type: $.__typename }, source: 'cache'")
}
```

See [resolver arguments](./docs/resolver.md#arguments) for full documentation on shaping input.

#### Composite type keys

Resolver keys may make composite selections for multiple key fields and/or nested scopes, for example:

```graphql
interface FieldOwner {
id: ID!
type: String!
}
type CustomField {
owner: FieldOwner!
key: String!
value: String
}
input CustomFieldLookup {
ownerId: ID!
ownerType: String!
key: String!
}
type Query {
customFields(lookups: [CustomFieldLookup!]!): [CustomField]! @stitch(
key: "owner { id type } key",
arguments: "lookups: { ownerId: $.owner.id, ownerType: $.owner.type, key: $.key }"
)
}
```

Note that composite key selections may _not_ be distributed across locations. The complete selection criteria must be available in each location that provides the key.

#### Multiple type keys

A type may exist in multiple locations across the graph using different keys, for example:
Expand Down
101 changes: 101 additions & 0 deletions docs/resolver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
## GraphQL::Stitching::Resolver

A `Resolver` contains all information about a root query used by stitching to fetch location-specific variants of a merged type. Specifically, resolvers manage parsed keys and argument structures.

### Arguments

Resolvers configure arguments through a template string of [GraphQL argument literal syntax](https://spec.graphql.org/October2021/#sec-Language.Arguments). This allows sending multiple arguments that intermix stitching keys with complex object shapes and other static values.

#### Key insertions

Key values fetched from previous locations may be inserted into arguments. Key insertions are prefixed by `$` and specify a dot-notation path to any selections made by the resolver `key`, or `__typename`.

```graphql
type Query {
entity(id: ID!, type: String!): [Entity]!
@stitch(key: "owner { id }", arguments: "id: $.owner.id, type: $.__typename")
}
```

Key insertions are _not_ quoted to differentiate them from other literal values.

#### Lists

List arguments may specify input just like non-list arguments, and [GraphQL list input coercion](https://spec.graphql.org/October2021/#sec-List.Input-Coercion) will assume the shape represents a list item:

```graphql
type Query {
product(ids: [ID!]!, source: DataSource!): [Product]!
@stitch(key: "id", arguments: "ids: $.id, source: CACHE")
}
```

List resolvers (that return list types) may _only_ insert keys into repeatable list arguments, while non-list arguments may only contain static values. Nested list inputs are neither common nor practical, so are not supported.

#### Built-in scalars

Built-in scalars are written as normal literal values. For convenience, string literals may be enclosed in single quotes rather than escaped double-quotes:

```graphql
type Query {
product(id: ID!, source: String!): Product
@stitch(key: "id", arguments: "id: $.id, source: 'cache'")

variant(id: ID!, limit: Int!): Variant
@stitch(key: "id", arguments: "id: $.id, limit: 100")
}
```

All scalar usage must be legal to the resolver field's arguments schema.

#### Enums

Enum literals may be provided anywhere in the input structure. They are _not_ quoted:

```graphql
enum DataSource {
CACHE
}
type Query {
product(id: ID!, source: DataSource!): [Product]!
@stitch(key: "id", arguments: "id: $.id, source: CACHE")
}
```

All enum usage must be legal to the resolver field's arguments schema.

#### Input Objects

Input objects may be provided anywhere in the input, even as nested structures. The stitching resolver will build the specified object shape:

```graphql
input ComplexKey {
id: ID
nested: ComplexKey
}
type Query {
product(key: ComplexKey!): [Product]!
@stitch(key: "id", arguments: "key: { nested: { id: $.id } }")
}
```

Input object shapes must conform to their respective schema definitions based on their placement within resolver arguments.

#### Custom scalars

Custom scalar keys allow any input shape to be submitted, from primitive scalars to complex object structures. These values will be sent and recieved as untyped JSON input:

```graphql
type Product {
id: ID!
}
union Entity = Product
scalar Key

type Query {
entities(representations: [Key!]!): [Entity]!
@stitch(key: "id", arguments: "representations: { id: $.id, __typename: $.__typename }")
}
```

Custom scalar arguments have no structured schema definition to validate against. This makes them flexible but quite lax, for better or worse.
3 changes: 2 additions & 1 deletion lib/graphql/stitching.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ module Stitching
EMPTY_ARRAY = [].freeze

class StitchingError < StandardError; end
class CompositionError < StitchingError; end
class ValidationError < CompositionError; end

class << self
def stitch_directive
Expand All @@ -28,7 +30,6 @@ def stitching_directive_names
require_relative "stitching/client"
require_relative "stitching/composer"
require_relative "stitching/executor"
require_relative "stitching/export_selection"
require_relative "stitching/http_executable"
require_relative "stitching/plan"
require_relative "stitching/planner_step"
Expand Down
6 changes: 5 additions & 1 deletion lib/graphql/stitching/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@ def on_error(&block)
def load_plan(request)
if @on_cache_read && plan_json = @on_cache_read.call(request)
plan = GraphQL::Stitching::Plan.from_json(JSON.parse(plan_json))
return request.plan(plan)

# only use plans referencing current resolver versions
if plan.ops.all? { |op| !op.resolver || @supergraph.resolvers_by_version[op.resolver] }
return request.plan(plan)
end
end

plan = request.plan
Expand Down
Loading
Loading