Add GraphQL plugin

Import existing GraphQL types and executable schemas into Pothos.

Import types from an existing executable GraphQL schema while migrating fields to Pothos. Use the builder's add option to import a schema or a group of types. Use addGraphQLObject and the other builder methods to obtain refs for individual types and customize their fields.

Install

npm install --save @pothos/plugin-add-graphql

Import a schema

This example starts with a small executable schema defined with GraphQL.js. In an application, existingSchema can instead be an imported schema from another schema-building library.

import SchemaBuilder from '@pothos/core';
import AddGraphQLPlugin from '@pothos/plugin-add-graphql';
import { GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql';

type User = { name: string };

const ExistingUser = new GraphQLObjectType<User>({
  name: 'User',
  fields: { name: { type: GraphQLString } },
});

const existingSchema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Query',
    fields: {
      user: {
        type: ExistingUser,
        resolve: () => ({ name: 'Leia' }),
      },
    },
  }),
});

const builder = new SchemaBuilder<{ Objects: { User: User } }>({
  plugins: [AddGraphQLPlugin],
  add: { schema: existingSchema },
});

builder.queryFields((t) => ({
  otherUser: t.field({
    type: 'User',
    resolve: () => ({ name: 'Luke' }),
  }),
}));

export const schema = builder.toSchema();

Both { user { name } } and { otherUser { name } } work on the resulting schema. The imported user field keeps its resolver, and the new field returns the same backing shape.

Non-root types are imported only if Pothos has no registered type with that name. For roots of add.schema, a configured type with the same name keeps its fields and type metadata, while non-overlapping imported fields are added. For example, a field in queryType({ fields }) overrides a same-named imported query field without discarding the imported root's other fields. Mutation and subscription roots follow the same rule. Inherited interface fields also take precedence, and local overrides can be added before or after building the schema.

Use queryFields to add fields without configuring a replacement root. Those additions must have unique names; a collision with an imported field remains an error. To customize an individual import explicitly, use addGraphQLObject with a fields callback as shown below. Duplicate local field declarations remain errors.

Import selected types

Use add.types instead of add.schema to import individual named types:

const builder = new SchemaBuilder<{ Objects: { User: User } }>({
  plugins: [AddGraphQLPlugin],
  add: { types: [ExistingUser] },
});

builder.queryType({
  fields: (t) => ({
    user: t.field({ type: 'User', resolve: () => ({ name: 'Leia' }) }),
  }),
});

This is an alternative to the schema-import builder above. Dependencies reached through fields, arguments, interfaces, and union members are imported recursively.

Declare imported object, interface, and scalar backing shapes in the builder's Objects, Interfaces, or Scalars schema types to reference them by name. For other kinds, use a ref from one of the methods below.

Import a type as a ref

The individual methods return refs that work in ordinary Pothos field definitions. This alternative builder imports ExistingUser, renames it, and replaces its name field with displayName:

const builder = new SchemaBuilder({ plugins: [AddGraphQLPlugin] });

const ImportedUser = builder.addGraphQLObject<User>(ExistingUser, {
  name: 'ImportedUser',
  fields: (t) => ({
    name: null,
    displayName: t.exposeString('name'),
  }),
});

builder.queryType({
  fields: (t) => ({
    user: t.field({ type: ImportedUser, resolve: () => ({ name: 'Leia' }) }),
  }),
});

The resulting field accepts { user { displayName } }. A null entry removes an imported field; a field ref adds or replaces one. Fields not mentioned in the callback retain their imported definitions.

MethodSource typeReturned ref
addGraphQLObject<Shape>GraphQLObjectTypeObject
addGraphQLInterface<Shape>GraphQLInterfaceTypeInterface
addGraphQLUnion<Shape>GraphQLUnionTypeUnion
addGraphQLEnum<Shape>GraphQLEnumTypeEnum
addGraphQLInput<Shape>GraphQLInputObjectTypeInput object

Pass the backing shape as the generic parameter. Keep the source type in a typed variable, as ExistingUser is above; when looking up a type by name with schema.getType, check that it exists and is the expected GraphQL kind before passing it to a method.

Object, interface, and input methods support field overrides. Unions accept a types override, and enums accept values. Object and interface descriptions and type-resolution functions are copied from the source, so configure those on the source type. The name option renames the import, and extensions are merged with the imported extensions.

Interface, union, enum, and input refs

This separate example imports an interface and union backed by member records, an enum whose values are strings, and an input with an optional name. The refs can be used in output fields and arguments like types defined directly with Pothos:

import SchemaBuilder from '@pothos/core';
import AddGraphQLPlugin from '@pothos/plugin-add-graphql';
import {
  GraphQLEnumType,
  GraphQLInputObjectType,
  GraphQLInterfaceType,
  GraphQLObjectType,
  GraphQLString,
  GraphQLUnionType,
} from 'graphql';

type MemberShape = { name: string };
type FilterShape = { name?: string | null };

const ExistingNamed = new GraphQLInterfaceType({
  name: 'Named',
  fields: { name: { type: GraphQLString } },
  resolveType: () => 'Member',
});
const ExistingMember = new GraphQLObjectType<MemberShape>({
  name: 'Member',
  interfaces: [ExistingNamed],
  fields: { name: { type: GraphQLString } },
});
const ExistingSearchResult = new GraphQLUnionType({
  name: 'SearchResult',
  types: [ExistingMember],
  resolveType: () => 'Member',
});
const ExistingOrder = new GraphQLEnumType({
  name: 'Order',
  values: { ASC: { value: 'asc' }, DESC: { value: 'desc' } },
});
const ExistingFilter = new GraphQLInputObjectType({
  name: 'MemberFilter',
  fields: { name: { type: GraphQLString } },
});

const builder = new SchemaBuilder({ plugins: [AddGraphQLPlugin] });
const Named = builder.addGraphQLInterface<MemberShape>(ExistingNamed);
const SearchResult = builder.addGraphQLUnion<MemberShape>(ExistingSearchResult);
const Order = builder.addGraphQLEnum<'asc' | 'desc'>(ExistingOrder);
const MemberFilter = builder.addGraphQLInput<FilterShape>(ExistingFilter);

const members: MemberShape[] = [{ name: 'Leia' }, { name: 'Luke' }];
builder.queryType({
  fields: (t) => ({
    member: t.field({ type: Named, resolve: () => members[0] }),
    search: t.field({
      type: [SearchResult],
      args: {
        filter: t.arg({ type: MemberFilter }),
        order: t.arg({ type: Order }),
      },
      resolve: (_parent, { filter, order }) => {
        const matches = members.filter((member) => !filter?.name || member.name === filter.name);
        return order === 'desc' ? matches.reverse() : matches;
      },
    }),
  }),
});

The source interface and union keep their resolveType functions. The enum exposes ASC and DESC to GraphQL clients while resolvers receive 'asc' and 'desc':

query {
  member {
    name
  }
  search(filter: { name: "Leia" }, order: ASC) {
    ... on Member {
      name
    }
  }
}

Referencing imported interfaces and scalars by name

To use names instead of refs, declare the backing shapes in Interfaces and Scalars. This alternative builder uses ExistingNamed above and a DateTime GraphQL scalar imported from your application. Its parser must produce JavaScript Date values, and its serializer must accept them:

import { DateTime } from './scalars';

const builder = new SchemaBuilder<{
  Interfaces: { Named: MemberShape };
  Scalars: { DateTime: { Input: Date; Output: Date } };
}>({
  plugins: [AddGraphQLPlugin],
  add: { types: [ExistingNamed, ExistingMember, DateTime] },
});

builder.queryType({
  fields: (t) => ({
    member: t.field({ type: 'Named', resolve: () => ({ name: 'Leia' }) }),
    now: t.field({ type: 'DateTime', resolve: () => new Date() }),
  }),
});

Include concrete implementations such as ExistingMember when importing an interface alone; its field definitions do not identify all the types that implement it.

Scalars

Use core Pothos's builder.addScalarType for an existing GraphQLScalarType; this plugin does not add a scalar-specific method. Declare that scalar's input and output shapes in the builder's Scalars schema type, as described in Scalars.