Watch the chapter video
This example demonstrates the use of stitching directives to configure type merging via subschema SDLs. Shifting this configuration out of the gateway makes subschemas autonomous, and allows them to push their own configuration up to the gateway—enabling more sophisticated schema releases.
The @graphql-tools/stitching-directives
package provides importable directives that can be used to annotate types and fields within subschemas, a validator to ensure the directives are used appropriately, and a configuration transformer that can be used on the gateway to convert the subschema directives into explicit configuration setting.
Note: the service setup in this example is based on the official demonstration repository for Apollo Federation.
This example demonstrates:
@key
directive for type-level selection sets.@merge
directive for type merging services.@computed
directive for computed fields.@canonical
directive for preferred element definitions.
cd stitching-directives-sdl
yarn install
yarn start
The following services are available for interactive queries:
- Stitched gateway: http://localhost:4000/graphql
- Accounts subservice: http://localhost:4001/graphql
- Inventory subservice: http://localhost:4002/graphql
- Products subservice: http://localhost:4003/graphql
- Reviews subservice: http://localhost:4004/graphql
First, try a query that includes data from all services:
query {
products(upcs: [1, 2]) {
name
price
weight
inStock
shippingEstimate
reviews {
id
body
author {
name
username
totalReviews
}
product {
name
price
}
}
}
}
Neat, it works! All those merges were configured through schema SDL annotations. Let's review the patterns used in this example and compare them to their static configuration counterparts. Keep in mind: SDL directives are always just annotations for the static configuration that's been discussed throughout previous chapters.
Open the Accounts schema and see the expression of a single-record merge query:
type User {
id: ID!
name: String!
username: String!
}
type Query {
user(id: ID!): User @merge(keyField: "id")
}
This translates into the following configuration:
merge: {
User: {
selectionSet: '{ id }'
fieldName: 'user',
args: (id) => ({ id }),
}
}
Here the @merge(keyField: "id")
directive marks a merger query, and specifies that the id
field acts as a selectionSet to be fetched with the original object and picked as the query argument.
Next, open the Products schema and see the expression of an array-batched merge query:
type Product {
upc: ID!
}
type Query {
products(upcs: [ID!]!, order: String): [Product]! @merge(
keyField: "upc"
keyArg: "upcs"
additionalArgs: """ order: "price" """
)
}
This translates into the following configuration:
merge: {
Product: {
selectionSet: '{ upc }'
fieldName: 'products',
key: ({ upc }) => upc,
argsFromKeys: (upcs) => ({ upcs, order: 'price' }),
}
}
Again, the @merge(keyField: "upc")
directive marks a merger array query, and specifies that the upc
field acts as a selectionSet to be fetched with each original object and picked into the query argument array. The keyArg
argument specifies which query argument receives the array of keys, and additionalArgs
specifies static values to provide for other arguments.
Now open the Inventory schema and see the expression of an object key, denoted by a generic scalar (here called _Key
). In the absence of keyField
, a generated key assumes the shape of a typed object like those sent to federation services:
type Product @key(selectionSet: "{ upc }") {
upc: ID!
shippingEstimate: Int @computed(selectionSet: "{ price weight }")
}
scalar _Key
type Query {
_products(keys: [_Key!]!): [Product]! @merge
}
This translates into the following configuration:
// assume "pick" works like the lodash method...
merge: {
Product: {
selectionSet: '{ upc }',
fields: {
shippingEstimate: { selectionSet: '{ price weight }', computed: true },
},
fieldName: '_products',
key: (obj) => ({ __typename: 'Product', ...pick(obj, ['upc', 'price', 'weight']) }),
argsFromKeys: (keys) => ({ keys }),
}
}
The _Key
scalar is an object with a __typename
and all utilized selectionSet fields on the type. For example, when the computed shippingEstimate
field is requested, the resulting object keys look like:
[
{ upc: '1', price: 899, weight: 100, __typename: 'Product' },
{ upc: '2', price: 1299, weight: 1000, __typename: 'Product' }
]
However, when shippingEstimate
is NOT requested, the generated object keys will only contain fields from the base selectionSet:
[
{ upc: '1', __typename: 'Product' },
{ upc: '2', __typename: 'Product' }
]
Similar to the object keys discussed above, an input object type may be used to constrain the specific fields included on a object key:
type User @key(selectionSet: "{ id }") {
id: ID!
}
input UserKey {
id: ID!
}
type Query {
_users(keys: [UserKey!]!): [User]! @merge
}
This translates into the following configuration:
// assume "pick" works like the lodash method...
merge: {
User: {
selectionSet: '{ id }',
fieldName: '_users',
key: (obj) => pick(obj, ['id']),
argsFromKeys: (keys) => ({ keys }),
}
}
In this case, the generated object keys will contain nothing but utilized selectionSet fields that are whitelisted by the input type:
[
{ id: '1' },
{ id: '2' }
]
A more advanced form of typed input keys, this pattern uses the keyArg
parameter to specify an input path at which to format the stitching query arguments:
type Product @key(selectionSet: "{ upc }") {
upc: ID!
}
input ProductKey {
upc: ID!
}
input ProductInput {
keys: [ProductKey!]!
}
type Query {
_products(input: ProductInput): [Product]! @merge(keyArg: "input.keys")
}
Open the documentation sidebar of GraphiQL for the gateway schema, and have a look at the Product
type definition. While this type is defined in three different ways by three different services, you'll see that element descriptions from the Products subschema are favored in the combined gateway schema because it is marked as @canonical
:
"Represents a Product available for resale."
type Product @canonical {
# ...
}
Types marked as canonical will provide a preferred definition of the type and its fields to the combined gateway schema. That means we can write clean descriptions for a type and its fields in one service, and expect those definitions to be used in the combined gateway schema. Specific fields may also be marked as canonical to override a canonical type definition. Fields that are unique to a given subservice have no competing definitions, and are therefore canonical by default.
This example uses several needlessly complex key patterns for the sake of demonstration. For simplicity, it's generally best to use the basic picked key patterns whenever possible. Sending object keys is generally only necessary when implementing computed fields or communicating with federation services.