Skip to content

Commit

Permalink
v1.4.0 (#141)
Browse files Browse the repository at this point in the history
  • Loading branch information
gmac committed Jul 2, 2024
1 parent 08094c1 commit 2d785ec
Show file tree
Hide file tree
Showing 45 changed files with 2,665 additions and 611 deletions.
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

0 comments on commit 2d785ec

Please sign in to comment.