• Docs
  • Schema Delegation

Schema Delegation

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 can execute the query. Delegation is useful when the parent schema shares a significant part of its data model with the subschema. For example:

  • A GraphQL gateway that connects multiple existing endpoints, each with its schema, could be implemented as a parent schema that delegates portions of queries to the relevant subschemas.
  • Any local schema can directly wrap remote schemas and optionally extend them with additional fields. As long as schema delegation is unidirectional, no gateway is necessary. Simple examples are schemas that wrap other autogenerated schemas (e.g. Postgraphile, Hasura, Prisma) to add custom functionality.

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 the subschema that knows how to execute it. Fields for the merged types use the defaultMergedResolver resolver to extract the correct data from the query response.

The graphql-tools package provides several related tools for managing schema delegation:

  • Remote schemas - turning a remote GraphQL endpoint into a local schema
  • Schema wrapping - modifying existing schemas – usually remote, but possibly local – when wrapping them to make delegation easier
  • Schema stitching - merging multiple schemas into one

Motivational Example

Let's consider two schemas, a subschema and a parent schema that reuses parts of a subschema. While the parent schema reuses the definitions of the subschema, we want to keep the implementations separate, so that the subschema can be tested independently, or even used as a remote service.

# Subschema
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
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:

query {
  userById(id: "1") {
    id
    username
    repositories {
      id
      url
      user {
        username
        id
      }
      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:

# To the subschema
query ($id: ID!) {
  repositoriesByUserId(id: $id) {
    id
    url
    issues {
      text
    }
  }
}

The 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.

Each field on the Repository and Issue types should use the defaultMergedResolver to properly extract data from the delegated response. Although in the simplest case, the default resolver can be used for the merged types, defaultMergedResolver resolves aliases, converts custom scalars and enums to their internal representations, and maps errors.

API

delegateToSchema

The delegateToSchema method should be called with the following named options:

delegateToSchema(options: {
  schema: GraphQLSchema
  operation: 'query' | 'mutation' | 'subscription'
  fieldName: string
  args?: Record<string, any>
  context: Record<string, any>
  info: GraphQLResolveInfo
  transforms?: Array<Transform>
}): Promise<any>

schema: GraphQLSchema

A subschema to delegate to.

operation: 'query' | 'mutation' | 'subscription'

The operation type to use during the delegation.

fieldName: string

A root field in a subschema from which the query should start.

args: Record<string, any>

Additional arguments to be passed to the field. Arguments passed to the field that is being resolved will be preserved if the subschema expects them, so you don't have to pass existing arguments explicitly, though you could use the additional arguments to override the existing ones. For example:

# 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 User.bookings to Query.bookingsByUser, we want to preserve the limit argument and add a userId argument by using the User.id. So the resolver would look like the following:

import { delegateToSchema } from '@graphql-tools/delegate'
 
const resolvers = {
  User: {
    bookings(parent, args, context, info) {
      return delegateToSchema({
        schema: subschema,
        operation: 'query',
        fieldName: 'bookingsByUser',
        args: {
          userId: parent.id
        },
        context,
        info
      })
    }
    // ...
  }
  // ...
}

context: Record<string, any>

GraphQL's context that is going to be passed to the subschema execution or subscription call.

info: GraphQLResolveInfo

GraphQL resolves info of the current resolver. Provides access to the subquery that starts at the current resolver.

transforms: Transform[]

Any additional operation transforms to apply to the query and results. Transforms are specified similarly to the transforms used in conjunction with schema wrapping, but only the operational components of transforms will be used by delegateToSchema, i.e. any specified transformRequest and transformResult functions. The following transforms are automatically applied during schema delegation to translate between source and target types and fields:

  • ExpandAbstractTypes: If an abstract type within a document does not exist within the target schema, expand the type to each and any of its implementations that do exist.
  • FilterToSchema: Remove all fields, variables and fragments for types that don't exist within the target schema.
  • AddTypenameToAbstract: Add __typename to all abstract types in the document, necessary for type resolution of interfaces within the source schema to work.
  • 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.
  • AddSelectionSets: activated by schema stitching to add selection sets into outgoing requests from the gateway schema. These selections collect key fields used to perform queries for related records from other subservices.
Last updated on October 4, 2022