mirror of
https://github.com/vale981/apollo-server
synced 2025-03-05 09:41:40 -05:00
Forklift Schema Stitching Docs from graphql-tools (#1008)
* docs: copy initial stitching sections from graphql-tools * docs: convert schema stitching introduction to use apollo-server * docs: remote-schema shortened to contain links only * docs: schema delegation shortened to remove api reference * docs: rename schema-stitching to features
This commit is contained in:
parent
011f0063aa
commit
de7f20990b
5 changed files with 864 additions and 0 deletions
|
@ -21,6 +21,11 @@ sidebar_categories:
|
|||
- deployment/lambda
|
||||
Advanced:
|
||||
- advanced/scalars-enums
|
||||
Features:
|
||||
- features/introduction
|
||||
- features/remote-schemas
|
||||
- features/schema-delegation
|
||||
- features/schema-transforms
|
||||
API Reference:
|
||||
- api/apollo-server
|
||||
- api/graphql-subscriptions
|
||||
|
|
64
docs/source/features/remote-schemas.md
Normal file
64
docs/source/features/remote-schemas.md
Normal file
|
@ -0,0 +1,64 @@
|
|||
---
|
||||
title: Remote schemas
|
||||
description: Generate GraphQL schema objects that delegate to a remote server
|
||||
---
|
||||
|
||||
It can be valuable to be able to treat remote GraphQL endpoints as if they were local executable schemas. This is especially useful for [schema stitching](./schema-stitching.html), but there may be other use cases.
|
||||
|
||||
Generally, there are three steps to create a remote schema:
|
||||
|
||||
1. Create a [link](#link) that can retrieve results from that schema
|
||||
2. Use [`introspectSchema`](#introspectSchema) to get the schema of the remote server
|
||||
3. Use [`makeRemoteExecutableSchema`](#makeRemoteExecutableSchema) to create a schema that uses the link to delegate requests to the underlying service
|
||||
|
||||
<h2 id="link" title="Creating a link">
|
||||
Creating a Link
|
||||
</h2>
|
||||
|
||||
A link is a function capable of retrieving GraphQL results. It is the same way that Apollo Client handles fetching data and is used by several `graphql-tools` features to do introspection or fetch results during execution. Using an Apollo Link brings with it a large feature set for common use cases. For instance, adding error handling to your request is super easy using the `apollo-link-error` package. You can set headers, batch requests, and even configure your app to retry on failed attempts all by including new links into your request chain.
|
||||
|
||||
```js
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
const link = new HttpLink({ uri: 'http://api.githunt.com/graphql', fetch });
|
||||
```
|
||||
|
||||
To add authentication headers, modify the link to include an authentication header:
|
||||
|
||||
```js
|
||||
const { setContext } = require('apollo-link-context');
|
||||
const { HttpLink } = require('apollo-link-http');
|
||||
|
||||
const http = new HttpLink({ uri: 'http://api.githunt.com/graphql', fetch });
|
||||
|
||||
const link = setContext((request, previousContext) => ({
|
||||
headers: {
|
||||
'Authentication': `Bearer ${previousContext.graphqlContext.authKey}`,
|
||||
}
|
||||
})).concat(http);
|
||||
```
|
||||
|
||||
<h3 id="link-api" title="Link API">Introspect and Delegate Requests</h3>
|
||||
|
||||
Since apollo-server supports using a link for the network layer, the API is the same as the client. To learn more about how Apollo Link works, check out the [docs](https://www.apollographql.com/docs/link/); Both GraphQL and Apollo Links have slightly varying concepts of `context`. For ease of use, `makeRemoteExecutableSchema` attaches the GraphQL context used in resolvers onto the link context under `graphqlContext`. The following example combined with the previous link construction shows basic usage:
|
||||
|
||||
```js
|
||||
const { introspectSchema, makeRemoteExecutableSchema, ApolloServer } = require('apollo-server');
|
||||
|
||||
const schema = await introspectSchema(link);
|
||||
|
||||
const executableSchema = makeRemoteExecutableSchema({
|
||||
schema,
|
||||
link,
|
||||
});
|
||||
|
||||
const server = new ApolloServer({ schema: executableSchema });
|
||||
|
||||
server.listen().then(({ url }) => {
|
||||
console.log(`🚀 Server ready at ${url}`)
|
||||
});
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
Point at `makeRemoteExecutableSchema(options)` and `introspectSchema(fetcher, [context])`
|
161
docs/source/features/schema-delegation.md
Normal file
161
docs/source/features/schema-delegation.md
Normal file
|
@ -0,0 +1,161 @@
|
|||
---
|
||||
title: Schema delegation
|
||||
description: Forward queries to other schemas automatically
|
||||
---
|
||||
|
||||
Schema delegation is a way to automatically forward a query (or a part of a query) from a parent schema to another schema (called a _subschema_) that is able to execute the query. Delegation is useful when the parent schema shares a significant part of its data model with the subschema. For example, the parent schema might be powering a GraphQL gateway that connects multiple existing endpoints together, each with its own schema. This kind of architecture could be implemented using schema delegation.
|
||||
|
||||
The `graphql-tools` package provides several related tools for managing schema delegation:
|
||||
|
||||
* [Remote schemas](./remote-schemas.html) - turning a remote GraphQL endpoint into a local schema
|
||||
* [Schema transforms](./schema-transforms.html) - modifying existing schemas to make delegation easier
|
||||
* [Schema stitching](./schema-stitching) - merging multiple schemas into one
|
||||
|
||||
Delegation is performed by one function, `delegateToSchema`, called from within a resolver function of the parent schema. The `delegateToSchema` function sends the query subtree received by the parent resolver to a subschema that knows how to execute it, then returns the result as if the parent resolver had executed the query.
|
||||
|
||||
<h2 id="example">Motivational example</h2>
|
||||
|
||||
Let's consider two schemas, a subschema and a parent schema that reuses parts of a subschema. In this example the parent schema reuses the *definitions* of the subschema. However the implementations separate should be kept separate, so that the subschema can be tested independently or retrieved from a remote service. The subschema:
|
||||
|
||||
```graphql
|
||||
type Repository {
|
||||
id: ID!
|
||||
url: String
|
||||
issues: [Issue]
|
||||
userId: ID!
|
||||
}
|
||||
|
||||
type Issue {
|
||||
id: ID!
|
||||
text: String!
|
||||
repository: Repository!
|
||||
}
|
||||
|
||||
type Query {
|
||||
repositoryById(id: ID!): Repository
|
||||
repositoriesByUserId(id: ID!): [Repository]
|
||||
}
|
||||
```
|
||||
|
||||
Parent Schema:
|
||||
|
||||
```graphql
|
||||
type Repository {
|
||||
id: ID!
|
||||
url: String
|
||||
issues: [Issue]
|
||||
userId: ID!
|
||||
user: User
|
||||
}
|
||||
|
||||
type Issue {
|
||||
id: ID!
|
||||
text: String!
|
||||
repository: Repository!
|
||||
}
|
||||
|
||||
type User {
|
||||
id: ID!
|
||||
username: String
|
||||
repositories: [Repository]
|
||||
}
|
||||
|
||||
type Query {
|
||||
userById(id: ID!): User
|
||||
}
|
||||
```
|
||||
|
||||
Suppose we want the parent schema to delegate retrieval of repositories to the subschema, in order to execute queries such as this one:
|
||||
|
||||
```graphql
|
||||
query {
|
||||
userById(id: "1") {
|
||||
id
|
||||
username
|
||||
repositories {
|
||||
id
|
||||
url
|
||||
user
|
||||
issues {
|
||||
text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The resolver function for the `repositories` field of the `User` type would be responsible for the delegation, in this case. While it's possible to call a remote GraphQL endpoint or resolve the data manually, this would require us to transform the query manually, or always fetch all possible fields, which could lead to overfetching. Delegation automatically extracts the appropriate query to send to the subschema:
|
||||
|
||||
```graphql
|
||||
# To the subschema
|
||||
query($id: ID!) {
|
||||
repositoriesByUserId(id: $id) {
|
||||
id
|
||||
url
|
||||
issues {
|
||||
text
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Delegation also removes the fields that don't exist on the subschema, such as `user`. This field would be retrieved from the parent schema using normal GraphQL resolvers.
|
||||
|
||||
<h2 id="delegateToSchema">Example</h2>
|
||||
|
||||
The `delegateToSchema` method can be found on the `info.mergeInfo` object within any resolver function, and should be called with the following named options:
|
||||
|
||||
|
||||
```graphql
|
||||
# Subschema
|
||||
|
||||
type Booking {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
type Query {
|
||||
bookingsByUser(userId: ID!, limit: Int): [Booking]
|
||||
}
|
||||
|
||||
# Schema
|
||||
|
||||
type User {
|
||||
id: ID!
|
||||
bookings(limit: Int): [Booking]
|
||||
}
|
||||
|
||||
type Booking {
|
||||
id: ID!
|
||||
}
|
||||
```
|
||||
|
||||
If we delegate at `User.bookings` to `Query.bookingsByUser`, we want to preserve the `limit` argument and add an `userId` argument by using the `User.id`. So the resolver would look like the following:
|
||||
|
||||
```js
|
||||
const resolvers = {
|
||||
User: {
|
||||
bookings: (parent, args, context, info) => {
|
||||
return info.mergeInfo.delegateToSchema({
|
||||
schema: subschema,
|
||||
operation: 'query',
|
||||
fieldName: 'bookingsByUser',
|
||||
args: {
|
||||
userId: parent.id,
|
||||
},
|
||||
context,
|
||||
info,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
<h2 id="considerations">Additional considerations</h2>
|
||||
|
||||
### Aliases
|
||||
|
||||
Delegation preserves aliases that are passed from the parent query. However that presents problems, because default GraphQL resolvers retrieve field from parent based on their name, not aliases. This way results with aliases will be missing from the delegated result. `mergeSchemas` and `transformSchemas` go around that by using `src/stitching/defaultMergedResolver` for all fields without explicit resolver. When building new libraries around delegation, one should consider how the aliases will be handled.
|
||||
|
||||
## API
|
||||
|
||||
TODO point to the `delegateToSchema` api reference
|
411
docs/source/features/schema-stitching.md
Normal file
411
docs/source/features/schema-stitching.md
Normal file
|
@ -0,0 +1,411 @@
|
|||
---
|
||||
title: Introduction
|
||||
description: Combining multiple GraphQL APIs into one
|
||||
---
|
||||
|
||||
Schema stitching is the process of creating a single GraphQL schema from multiple underlying GraphQL APIs.
|
||||
|
||||
One of the main benefits of GraphQL is that we can query all of our data as part of one schema, and get everything we need in one request. But as the schema grows, it might become cumbersome to manage it all as one codebase, and it starts to make sense to split it into different modules. We may also want to decompose your schema into separate microservices, which can be developed and deployed independently.
|
||||
|
||||
In both cases, we use `mergeSchemas` to combine multiple GraphQL schemas together and produce a merged schema that knows how to delegate parts of the query to the relevant subschemas. These subschemas can be either local to the server, or running on a remote server. They can even be services offered by 3rd parties, allowing us to connect to external data and create mashups.
|
||||
|
||||
<h2 id="remote-schemas" title="Remote schemas">Working with remote schemas</h2>
|
||||
|
||||
In order to merge with a remote schema, we first call [makeRemoteExecutableSchema](./remote-schemas.html) to create a local proxy for the schema that knows how to call the remote endpoint. We then merge that local proxy schema the same way we would merge any other locally implemented schema.
|
||||
|
||||
<h2 id="basic-example">Basic example</h2>
|
||||
|
||||
In this example we'll stitch together two very simple schemas. It doesn't matter whether these are local or proxies created with `makeRemoteExecutableSchema`, because the merging itself would be the same.
|
||||
|
||||
In this case, we're dealing with two schemas that implement a system with users and "chirps"—small snippets of text that users can post.
|
||||
|
||||
```js
|
||||
const {
|
||||
makeExecutableSchema,
|
||||
addMockFunctionsToSchema,
|
||||
mergeSchemas,
|
||||
ApolloServer,
|
||||
gql,
|
||||
} = require('apollo-server');
|
||||
|
||||
// Mocked chirp schema
|
||||
// We don't worry about the schema implementation right now since we're just
|
||||
// demonstrating schema stitching.
|
||||
const chirpSchema = makeExecutableSchema({
|
||||
typeDefs: gql`
|
||||
type Chirp {
|
||||
id: ID!
|
||||
text: String
|
||||
authorId: ID!
|
||||
}
|
||||
|
||||
type Query {
|
||||
chirpById(id: ID!): Chirp
|
||||
chirpsByAuthorId(authorId: ID!): [Chirp]
|
||||
}
|
||||
`
|
||||
});
|
||||
|
||||
addMockFunctionsToSchema({ schema: chirpSchema });
|
||||
|
||||
// Mocked author schema
|
||||
const authorSchema = makeExecutableSchema({
|
||||
typeDefs: gql`
|
||||
type User {
|
||||
id: ID!
|
||||
email: String
|
||||
}
|
||||
|
||||
type Query {
|
||||
userById(id: ID!): User
|
||||
}
|
||||
`
|
||||
});
|
||||
|
||||
addMockFunctionsToSchema({ schema: authorSchema });
|
||||
|
||||
const server = new ApolloServer({ schema });
|
||||
|
||||
server.listen().then(({ url }) => {
|
||||
console.log(`🚀 Server ready at ${url}`)
|
||||
});
|
||||
```
|
||||
|
||||
This gives us a new schema with the root fields on `Query` from both schemas (along with the `User` and `Chirp` types):
|
||||
|
||||
```graphql
|
||||
type Query {
|
||||
chirpById(id: ID!): Chirp
|
||||
chirpsByAuthorId(authorId: ID!): [Chirp]
|
||||
userById(id: ID!): User
|
||||
}
|
||||
```
|
||||
|
||||
We now have a single schema that supports asking for `userById` and `chirpsByAuthorId` in the same query!
|
||||
|
||||
<h3 id="adding-resolvers">Adding resolvers between schemas</h3>
|
||||
|
||||
Combining existing root fields is a great start, but in practice we will often want to introduce additional fields for working with the relationships between types that came from different subschemas. For example, we might want to go from a particular user to their chirps, or from a chirp to its author. Or we might want to query a `latestChirps` field and then get the author of each of those chirps. If the only way to obtain a chirp's author is to call the `userById(id)` root query field with the `authorId` of a given chirp, and we don't know the chirp's `authorId` until we receive the GraphQL response, then we won't be able to obtain the authors as part of the same query.
|
||||
|
||||
To add this ability to navigate between types, we need to _extend_ existing types with new fields that translate between the types:
|
||||
|
||||
```js
|
||||
const linkTypeDefs = gql`
|
||||
extend type User {
|
||||
chirps: [Chirp]
|
||||
}
|
||||
|
||||
extend type Chirp {
|
||||
author: User
|
||||
}
|
||||
`;
|
||||
```
|
||||
|
||||
We can now merge these three schemas together:
|
||||
|
||||
```js
|
||||
mergeSchemas({
|
||||
schemas: [
|
||||
chirpSchema,
|
||||
authorSchema,
|
||||
linkTypeDefs,
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
We won't be able to query `User.chirps` or `Chirp.author` yet, however, because we still need to define resolvers for these new fields.
|
||||
|
||||
How should these resolvers be implemented? When we resolve `User.chirps` or `Chirp.author`, we want to _delegate_ to the relevant root fields. To get from a user to the user's chirps, for example, we'll want to use the `id` of the user to call `Query.chirpsByAuthorId`. And to get from a chirp to its author, we can use the chirp's `authorId` field to call the existing `Query.userById` field.
|
||||
|
||||
Resolvers for fields in schemas created by `mergeSchema` have access to a handy `delegateToSchema` function (exposed via `info.mergeInfo.delegateToSchema`) that allows forwarding parts of queries (or even whole new queries) to one of the subschemas that was passed to `mergeSchemas`.
|
||||
|
||||
In order to delegate to these root fields, we'll need to make sure we've actually requested the `id` of the user or the `authorId` of the chirp. To avoid forcing users to add these fields to their queries manually, resolvers on a merged schema can define a `fragment` property that specifies the required fields, and they will be added to the query automatically.
|
||||
|
||||
A complete implementation of schema stitching for these schemas might look like this:
|
||||
|
||||
```js
|
||||
const mergedSchema = mergeSchemas({
|
||||
schemas: [
|
||||
chirpSchema,
|
||||
authorSchema,
|
||||
linkTypeDefs,
|
||||
],
|
||||
resolvers: {
|
||||
User: {
|
||||
chirps: {
|
||||
fragment: `fragment UserFragment on User { id }`,
|
||||
resolve(user, args, context, info) {
|
||||
return info.mergeInfo.delegateToSchema({
|
||||
schema: chirpSchema,
|
||||
operation: 'query',
|
||||
fieldName: 'chirpsByAuthorId',
|
||||
args: {
|
||||
authorId: user.id,
|
||||
},
|
||||
context,
|
||||
info,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
Chirp: {
|
||||
author: {
|
||||
fragment: `fragment ChirpFragment on Chirp { authorId }`,
|
||||
resolve(chirp, args, context, info) {
|
||||
return info.mergeInfo.delegateToSchema({
|
||||
schema: authorSchema,
|
||||
operation: 'query',
|
||||
fieldName: 'userById',
|
||||
args: {
|
||||
id: chirp.authorId,
|
||||
},
|
||||
context,
|
||||
info,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<h2 id="using-with-transforms">Using with Transforms</h2>
|
||||
|
||||
Often, when creating a GraphQL gateway that combines multiple existing schemas, we might want to modify one of the schemas. The most common tasks include renaming some of the types, and filtering the root fields. By using [transforms](./schema-transforms) with schema stitching, we can easily tweak the subschemas before merging them together.
|
||||
|
||||
Before, when we were simply merging schemas without first transforming them, we would typically delegate directly to one of the merged schemas. Once we add transforms to the mix, there are times when we want to delegate to fields of the new, transformed schemas, and other times when we want to delegate to the original, untransformed schemas.
|
||||
|
||||
For example, suppose we transform the `chirpSchema` by removing the `chirpsByAuthorId` field and add a `Chirp_` prefix to all types and field names, in order to make it very clear which types and fields came from `chirpSchema`:
|
||||
|
||||
```js
|
||||
const {
|
||||
makeExecutableSchema,
|
||||
addMockFunctionsToSchema,
|
||||
mergeSchemas,
|
||||
transformSchema,
|
||||
FilterRootFields,
|
||||
RenameTypes,
|
||||
RenameRootFields,
|
||||
} = require('apollo-server');
|
||||
|
||||
// Mocked chirp schema; we don't want to worry about the schema
|
||||
// implementation right now since we're just demonstrating
|
||||
// schema stitching
|
||||
const chirpSchema = makeExecutableSchema({
|
||||
typeDefs: gql`
|
||||
type Chirp {
|
||||
id: ID!
|
||||
text: String
|
||||
authorId: ID!
|
||||
}
|
||||
|
||||
type Query {
|
||||
chirpById(id: ID!): Chirp
|
||||
chirpsByAuthorId(authorId: ID!): [Chirp]
|
||||
}
|
||||
`
|
||||
});
|
||||
|
||||
addMockFunctionsToSchema({ schema: chirpSchema });
|
||||
|
||||
// create transform schema
|
||||
|
||||
const transformedChirpSchema = transformSchema(chirpSchema, [
|
||||
new FilterRootFields(
|
||||
(operation: string, rootField: string) => rootField !== 'chirpsByAuthorId'
|
||||
),
|
||||
new RenameTypes((name: string) => `Chirp_${name}`),
|
||||
new RenameRootFields((name: string) => `Chirp_${name}`),
|
||||
]);
|
||||
```
|
||||
|
||||
Now we have a schema that has all fields and types prefixed with `Chirp_` and has only the `chirpById` root field. Note that the original schema has not been modified, and remains fully functional. We've simply created a new, slightly different schema, which hopefully will be more convenient for merging with our other subschemas.
|
||||
|
||||
Now let's implement the resolvers:
|
||||
|
||||
```js
|
||||
const mergedSchema = mergeSchemas({
|
||||
schemas: [
|
||||
transformedChirpSchema,
|
||||
authorSchema,
|
||||
linkTypeDefs,
|
||||
],
|
||||
resolvers: {
|
||||
User: {
|
||||
chirps: {
|
||||
fragment: `fragment UserFragment on User { id }`,
|
||||
resolve(user, args, context, info) {
|
||||
return info.mergeInfo.delegateToSchema({
|
||||
schema: chirpSchema,
|
||||
operation: 'query',
|
||||
fieldName: 'chirpsByAuthorId',
|
||||
args: {
|
||||
authorId: user.id,
|
||||
},
|
||||
context,
|
||||
info,
|
||||
transforms: transformedChirpSchema.transforms,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
Chirp_Chirp: {
|
||||
author: {
|
||||
fragment: `fragment ChirpFragment on Chirp { authorId }`,
|
||||
resolve(chirp, args, context, info) {
|
||||
return info.mergeInfo.delegateToSchema({
|
||||
schema: authorSchema,
|
||||
operation: 'query',
|
||||
fieldName: 'userById',
|
||||
args: {
|
||||
id: chirp.authorId,
|
||||
},
|
||||
context,
|
||||
info,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const server = new ApolloServer({ schema: mergedSchema });
|
||||
|
||||
server.listen().then(({ url }) => {
|
||||
console.log(`🚀 Server ready at ${url}`)
|
||||
});
|
||||
```
|
||||
|
||||
Notice that `resolvers.Chirp_Chirp` has been renamed from just `Chirp`, but `resolvers.Chirp_Chirp.author.fragment` still refers to the original `Chirp` type and `authorId` field, rather than `Chirp_Chirp` and `Chirp_authorId`.
|
||||
|
||||
Also, when we call `info.mergeInfo.delegateToSchema` in the `User.chirps` resolvers, we can delegate to the original `chirpsByAuthorId` field, even though it has been filtered out of the final schema. That's because we're delegating to the original `chirpSchema`, which has not been modified by the transforms.
|
||||
|
||||
<h2 id="complex-example">Complex example</h2>
|
||||
|
||||
For a more complicated example involving properties and bookings, with implementations of all of the resolvers, check out the Launchpad links below:
|
||||
|
||||
* [Property schema](https://launchpad.graphql.com/v7l45qkw3)
|
||||
* [Booking schema](https://launchpad.graphql.com/41p4j4309)
|
||||
* [Merged schema](https://launchpad.graphql.com/q5kq9z15p)
|
||||
|
||||
<h2 id="api">API</h2>
|
||||
|
||||
<h3 id="mergeSchemas">mergeSchemas</h3>
|
||||
|
||||
```ts
|
||||
mergeSchemas({
|
||||
schemas: Array<string | GraphQLSchema | Array<GraphQLNamedType>>;
|
||||
resolvers?: Array<IResolvers> | IResolvers;
|
||||
onTypeConflict?: (
|
||||
left: GraphQLNamedType,
|
||||
right: GraphQLNamedType,
|
||||
info?: {
|
||||
left: {
|
||||
schema?: GraphQLSchema;
|
||||
};
|
||||
right: {
|
||||
schema?: GraphQLSchema;
|
||||
};
|
||||
},
|
||||
) => GraphQLNamedType;
|
||||
})
|
||||
```
|
||||
|
||||
This is the main function that implements schema stitching. Read below for a description of each option.
|
||||
|
||||
#### schemas
|
||||
|
||||
`schemas` is an array of `GraphQLSchema` objects, schema strings, or lists of `GraphQLNamedType`s. Strings can contain type extensions or GraphQL types, which will be added to resulting schema. Note that type extensions are always applied last, while types are defined in the order in which they are provided.
|
||||
|
||||
#### resolvers
|
||||
|
||||
`resolvers` accepts resolvers in same format as [makeExecutableSchema](./resolvers.html). It can also take an Array of resolvers. One addition to the resolver format is the possibility to specify a `fragment` for a resolver. The `fragment` must be a GraphQL fragment definition string, specifying which fields from the parent schema are required for the resolver to function properly.
|
||||
|
||||
```js
|
||||
resolvers: {
|
||||
Booking: {
|
||||
property: {
|
||||
fragment: 'fragment BookingFragment on Booking { propertyId }',
|
||||
resolve(parent, args, context, info) {
|
||||
return mergeInfo.delegateToSchema({
|
||||
schema: bookingSchema,
|
||||
operation: 'query',
|
||||
fieldName: 'propertyById',
|
||||
args: {
|
||||
id: parent.propertyId,
|
||||
},
|
||||
context,
|
||||
info,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
#### mergeInfo and delegateToSchema
|
||||
|
||||
The `info.mergeInfo` object provides the `delegateToSchema` method:
|
||||
|
||||
```js
|
||||
type MergeInfo = {
|
||||
delegateToSchema<TContext>(options: IDelegateToSchemaOptions<TContext>): any;
|
||||
}
|
||||
|
||||
interface IDelegateToSchemaOptions<TContext = {
|
||||
[key: string]: any;
|
||||
}> {
|
||||
schema: GraphQLSchema;
|
||||
operation: Operation;
|
||||
fieldName: string;
|
||||
args?: {
|
||||
[key: string]: any;
|
||||
};
|
||||
context: TContext;
|
||||
info: GraphQLResolveInfo;
|
||||
transforms?: Array<Transform>;
|
||||
}
|
||||
```
|
||||
|
||||
As described in the documentation above, `info.mergeInfo.delegateToSchema` allows delegating to any `GraphQLSchema` object, optionally applying transforms in the process. See [Schema Delegation](./schema-delegation.html) and the [*Using with transforms*](#using-with-transforms) section of this document.
|
||||
|
||||
#### onTypeConflict
|
||||
|
||||
```js
|
||||
type OnTypeConflict = (
|
||||
left: GraphQLNamedType,
|
||||
right: GraphQLNamedType,
|
||||
info?: {
|
||||
left: {
|
||||
schema?: GraphQLSchema;
|
||||
};
|
||||
right: {
|
||||
schema?: GraphQLSchema;
|
||||
};
|
||||
},
|
||||
) => GraphQLNamedType;
|
||||
```
|
||||
|
||||
The `onTypeConflict` option to `mergeSchemas` allows customization of type resolving logic.
|
||||
|
||||
The default behavior of `mergeSchemas` is to take the first encountered type of all the types with the same name. If there are conflicts, `onTypeConflict` enables explicit selection of the winning type.
|
||||
|
||||
For example, here's how we could select the last type among multiple types with the same name:
|
||||
|
||||
```js
|
||||
const onTypeConflict = (left, right) => right;
|
||||
```
|
||||
|
||||
And here's how we might select the type whose schema has the latest `version`:
|
||||
|
||||
```js
|
||||
const onTypeConflict = (left, right, info) => {
|
||||
if (info.left.schema.version >= info.right.schema.version) {
|
||||
return left;
|
||||
} else {
|
||||
return right;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When using schema transforms, `onTypeConflict` is often unnecessary, since transforms can be used to prevent conflicts before merging schemas. However, if you're not using schema transforms, `onTypeConflict` can be a quick way to make `mergeSchemas` produce more desirable results.
|
223
docs/source/features/schema-transforms.md
Normal file
223
docs/source/features/schema-transforms.md
Normal file
|
@ -0,0 +1,223 @@
|
|||
---
|
||||
title: Schema transforms
|
||||
description: Automatically transforming schemas
|
||||
---
|
||||
|
||||
Schema transforms are a tool for making modified copies of `GraphQLSchema` objects, while preserving the possibility of delegating back to original schema.
|
||||
|
||||
Transforms are useful when working with [remote schemas](./remote-schemas.html), building GraphQL gateways that combine multiple schemas, and/or using [schema stitching](./schema-stitching.html) to combine schemas together without conflicts between types or fields.
|
||||
|
||||
While it's possible to modify a schema by hand, the manual approach requires a deep understanding of all the relationships between `GraphQLSchema` properties, which makes it error-prone and labor-intensive. Transforms provide a generic abstraction over all those details, which improves code quality and saves time, not only now but also in the future, because transforms are designed to be reused again and again.
|
||||
|
||||
Each `Transform` may define three different kinds of transform functions:
|
||||
|
||||
```ts
|
||||
interface Transform = {
|
||||
transformSchema?: (schema: GraphQLSchema) => GraphQLSchema;
|
||||
transformRequest?: (request: Request) => Request;
|
||||
transformResult?: (result: Result) => Result;
|
||||
};
|
||||
```
|
||||
|
||||
The most commonly used transform function is `transformSchema`. However, some transforms require modifying incoming requests and/or outgoing results as well, especially if `transformSchema` adds or removes types or fields, since such changes require mapping new types/fields to the original types/fields at runtime.
|
||||
|
||||
For example, let's consider changing the name of the type in a simple schema. Imagine we've written a function that takes a `GraphQLSchema` and replaces all instances of type `Test` with `NewTest`.
|
||||
|
||||
```graphql
|
||||
# old schema
|
||||
type Test {
|
||||
id: ID!
|
||||
name: String
|
||||
}
|
||||
|
||||
type Query {
|
||||
returnTest: Test
|
||||
}
|
||||
|
||||
# new schema
|
||||
|
||||
type NewTest {
|
||||
id: ID!
|
||||
name: String
|
||||
}
|
||||
|
||||
type Query {
|
||||
returnTest: NewTest
|
||||
}
|
||||
```
|
||||
|
||||
At runtime, we want the `NewTest` type to be automatically mapped to the old `Test` type.
|
||||
|
||||
At first glance, it might seem as though most queries work the same way as before:
|
||||
|
||||
```graphql
|
||||
query {
|
||||
returnTest {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Since the fields of the type have not changed, delegating to the old schema is relatively easy here.
|
||||
|
||||
However, the new name begins to matter more when fragments and variables are used:
|
||||
|
||||
```graphql
|
||||
query {
|
||||
returnTest {
|
||||
id
|
||||
... on NewTest {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Since the `NewTest` type did not exist on old schema, this fragment will not match anything in the old schema, so it will be filtered out during delegation.
|
||||
|
||||
What we need is a `transformRequest` function that knows how to rename any occurrences of `NewTest` to `Test` before delegating to the old schema.
|
||||
|
||||
By the same reasoning, we also need a `transformResult` function, because any results contain a `__typename` field whose value is `Test`, that name needs to be updated to `NewTest` in the final result.
|
||||
|
||||
<h2 id="api">API</h2>
|
||||
|
||||
<h3 id="Transform">Transform</h3>
|
||||
|
||||
```ts
|
||||
interface Transform = {
|
||||
transformSchema?: (schema: GraphQLSchema) => GraphQLSchema;
|
||||
transformRequest?: (request: Request) => Request;
|
||||
transformResult?: (result: Result) => Result;
|
||||
};
|
||||
|
||||
type Request = {
|
||||
document: DocumentNode;
|
||||
variables: Record<string, any>;
|
||||
extensions?: Record<string, any>;
|
||||
};
|
||||
|
||||
type Result = ExecutionResult & {
|
||||
extensions?: Record<string, any>;
|
||||
};
|
||||
```
|
||||
|
||||
<h3 id="transformSchema">transformSchema</h3>
|
||||
|
||||
Given a `GraphQLSchema` and an array of `Transform` objects, produce a new schema with those transforms applied.
|
||||
|
||||
Delegating resolvers will also be generated to map from new schema root fields to old schema root fields. Often these automatic resolvers are sufficient, so you don't have to implement your own.
|
||||
|
||||
<h2 id="built-in">Built-in transforms</h2>
|
||||
|
||||
Built-in transforms are ready-made classes implementing the `Transform` interface. They are intended to cover many of the most common schema transformation use cases, but they also serve as examples of how to implement transforms for your own needs.
|
||||
|
||||
### Modifying types
|
||||
|
||||
* `FilterTypes(filter: (type: GraphQLNamedType) => boolean)`: Remove all types for which the `filter` function returns `false`.
|
||||
|
||||
* `RenameTypes(renamer, options?)`: Rename types by applying `renamer` to each type name. If `renamer` returns `undefined`, the name will be left unchanged. Options controls whether built-in types and scalars are renamed. Root objects are never renamed by this transform.
|
||||
|
||||
```ts
|
||||
RenameTypes(
|
||||
(name: string) => string | void,
|
||||
options?: {
|
||||
renameBuiltins: Boolean;
|
||||
renameScalars: Boolean;
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
### Modifying root fields
|
||||
|
||||
* `TransformRootFields(transformer: RootTransformer)`: Given a transformer, abritrarily transform root fields. The `transformer` can return a `GraphQLFieldConfig` definition, a object with new `name` and a `field`, `null` to remove the field, or `undefined` to leave the field unchanged.
|
||||
|
||||
```ts
|
||||
TransformRootFields(transformer: RootTransformer)
|
||||
|
||||
type RootTransformer = (
|
||||
operation: 'Query' | 'Mutation' | 'Subscription',
|
||||
fieldName: string,
|
||||
field: GraphQLField<any, any>,
|
||||
) =>
|
||||
| GraphQLFieldConfig<any, any>
|
||||
| { name: string; field: GraphQLFieldConfig<any, any> }
|
||||
| null
|
||||
| void;
|
||||
```
|
||||
|
||||
* `FilterRootFields(filter: RootFilter)`: Like `FilterTypes`, removes root fields for which the `filter` function returns `false`.
|
||||
|
||||
```ts
|
||||
FilterRootFields(filter: RootFilter)
|
||||
|
||||
type RootFilter = (
|
||||
operation: 'Query' | 'Mutation' | 'Subscription',
|
||||
fieldName: string,
|
||||
field: GraphQLField<any, any>,
|
||||
) => boolean;
|
||||
```
|
||||
|
||||
* `RenameRootFields(renamer)`: Rename root fields, by applying the `renamer` function to their names.
|
||||
|
||||
```ts
|
||||
RenameRootFields(
|
||||
renamer: (
|
||||
operation: 'Query' | 'Mutation' | 'Subscription',
|
||||
name: string,
|
||||
field: GraphQLField<any, any>,
|
||||
) => string,
|
||||
)
|
||||
```
|
||||
|
||||
### Other
|
||||
|
||||
* `ExractField({ from: Array<string>, to: Array<string> })` - move selection at `from` path to `to` path.
|
||||
|
||||
* `WrapQuery(
|
||||
path: Array<string>,
|
||||
wrapper: QueryWrapper,
|
||||
extractor: (result: any) => any,
|
||||
)` - wrap a selection at `path` using function `wrapper`. Apply `extractor` at the same path to get the result. This is used to get a result nested inside other result
|
||||
|
||||
```js
|
||||
transforms: [
|
||||
// Wrap document takes a subtree as an AST node
|
||||
new WrapQuery(
|
||||
// path at which to apply wrapping and extracting
|
||||
['userById'],
|
||||
(subtree: SelectionSetNode) => ({
|
||||
// we create a wrapping AST Field
|
||||
kind: Kind.FIELD,
|
||||
name: {
|
||||
kind: Kind.NAME,
|
||||
// that field is `address`
|
||||
value: 'address',
|
||||
},
|
||||
// Inside the field selection
|
||||
selectionSet: subtree,
|
||||
}),
|
||||
// how to process the data result at path
|
||||
result => result && result.address,
|
||||
),
|
||||
],
|
||||
```
|
||||
|
||||
* `ReplaceFieldWithFragment(targetSchema: GraphQLSchema, mapping: FieldToFragmentMapping)`: Replace the given fields with an inline fragment. Used by `mergeSchemas` to handle the `fragment` option.
|
||||
|
||||
```ts
|
||||
type FieldToFragmentMapping = {
|
||||
[typeName: string]: { [fieldName: string]: InlineFragmentNode };
|
||||
};
|
||||
```
|
||||
|
||||
<h2 id="other-built-in">delegateToSchema transforms</h2>
|
||||
|
||||
The following transforms are automatically applied by `delegateToSchema` during schema delegation, to translate between new and old types and fields:
|
||||
|
||||
* `AddArgumentsAsVariables`: Given a schema and arguments passed to a root field, make those arguments document variables.
|
||||
* `FilterToSchema`: Given a schema and document, remove all fields, variables and fragments for types that don't exist in that schema.
|
||||
* `AddTypenameToAbstract`: Add `__typename` to all abstract types in the document.
|
||||
* `CheckResultAndHandleErrors`: Given a result from a subschema, propagate errors so that they match the correct subfield. Also provide the correct key if aliases are used.
|
||||
|
||||
By passing a custom `transforms` array to `delegateToSchema`, it's possible to run additional transforms before these default transforms, though it is currently not possible to disable the default transforms.
|
Loading…
Add table
Reference in a new issue