Plugins

Directives plugin

Attach schema directives to Pothos types and fields for downstream tools like graphql-tools to consume.

Schema directives are annotations on your types and fields (@rateLimit, @auth, @deprecated) that downstream tooling reads and acts on. Pothos is code-first, so it never executes a directive itself; the directives plugin records what you declare into each type's extensions, where a schema transformer like graphql-tools can pick it up. You declare the directives you use on the Directives generic, then attach them with a directives option anywhere the schema allows.

Directives predate code-first schemas and are really an SDL-first idea. Use this plugin to keep a directive-based tool (rate limiting, auth, caching) working against a Pothos schema.

npm install --save @pothos/plugin-directives
import SchemaBuilder from '@pothos/core';
import DirectivePlugin from '@pothos/plugin-directives';

interface ITeam {
  id: number;
  name: string;
}

const Teams = new Map<number, ITeam>([
  [1, { id: 1, name: 'Comet' }],
  [2, { id: 2, name: 'Nova' }],
]);

const builder = new SchemaBuilder<{
  Directives: {
    rateLimit: {
      locations: 'OBJECT' | 'FIELD_DEFINITION';
      args: { limit: number; duration: number };
    };
    auth: {
      locations: 'FIELD_DEFINITION';
      args: { role: string };
    };
  };
}>({
  plugins: [DirectivePlugin],
  directives: {
    useGraphQLToolsUnorderedDirectives: true,
  },
});

const Team = builder.objectRef<ITeam>('Team');

Team.implement({
  // Object form: a map of directive name to its args.
  directives: {
    rateLimit: { limit: 60, duration: 60 },
  },
  fields: (t) => ({
    id: t.exposeID('id'),
    name: t.exposeString('name'),
  }),
});

Declaring directives

Each entry in the Directives generic names a directive and describes two things: the locations where it may appear, and the args it accepts. Pothos uses this to type-check every place you apply the directive: attach rateLimit somewhere its locations don't allow, or omit an arg, and you get a compile error.

The valid locations are the standard GraphQL directive locations:

  • ARGUMENT_DEFINITION
  • ENUM_VALUE
  • ENUM
  • FIELD_DEFINITION
  • INPUT_FIELD_DEFINITION
  • INPUT_OBJECT
  • INTERFACE
  • OBJECT
  • SCALAR
  • SCHEMA
  • UNION

Every builder method that defines one of those locations gains a directives option: object types, interfaces, unions, enums and their values, input objects and their fields, scalars, fields, and args. SCHEMA is the one exception; it lives on toSchema (see Schema directives below).

Applying directives

For fields and types, pass directives in the same options object you already use. Both formats below apply the same directives:

// Object form — a map of directive name to its args.
directives: {
  rateLimit: { limit: 5, duration: 60 },
}

// Array form — a list of { name, args } entries.
directives: [{ name: 'rateLimit', args: { limit: 5, duration: 60 } }]

Prefer the array form when order matters or a directive repeats on the same location, since it preserves both. The object form is more concise and reads well when each directive appears once. The playground schema mixes them: object form on the Team type and its teams field, array form for the auth directive on renameTeam.

builder.queryType({
  fields: (t) => ({
    teams: t.field({
      type: [Team],
      directives: {
        rateLimit: { limit: 5, duration: 60 },
      },
      resolve: () => [...Teams.values()],
    }),
  }),
});

builder.mutationType({
  fields: (t) => ({
    renameTeam: t.field({
      type: Team,
      // Array form: preserves order and allows repeats.
      directives: [{ name: 'auth', args: { role: 'admin' } }],
      args: {
        id: t.arg.id({ required: true }),
        name: t.arg.string({ required: true }),
      },
      resolve: (_root, { id, name }) => {
        const team = Teams.get(Number(id));
        if (!team) {
          throw new Error(`No team with id ${id}`);
        }
        team.name = name;
        return team;
      },
    }),
  }),
});

Schema directives

SCHEMA-location directives attach to the schema itself, so they go on toSchema under schemaDirectives, not directives, which is reserved for the plugin's own options:

export const schema = builder.toSchema({
  schemaDirectives: {
    rateLimit: { limit: 1000, duration: 60 },
  },
});

Output format

The plugin writes the directive data onto the extensions of the underlying GraphQL type; it doesn't enforce the directive itself. A downstream transformer reads those extensions and implements the directive's behavior.

By default the plugin uses the extensions format Gatsby uses (described here), which older versions of graphql-tools did not support. If your directive ships a schema visitor built against an older graphql-tools (the rate-limit directive below is one), set useGraphQLToolsUnorderedDirectives on the builder to emit the format those visitors expect.

useGraphQLToolsUnorderedDirectives does not preserve the order directives were declared in. That's fine for most schemas, but if a directive's behavior depends on ordering, leave it off and keep the default format.

Wiring a real directive

Applying a directive is a two-step handoff: declare and attach it with this plugin, then run the tool's schema transformer over the built schema. graphql-rate-limit-directive follows that pattern:

import { rateLimitDirective } from 'graphql-rate-limit-directive';

const { rateLimitDirectiveTransformer } = rateLimitDirective();

// builder.toSchema() carries the directive data in its extensions;
// the transformer reads it and installs the rate-limiting resolvers.
export const schema = rateLimitDirectiveTransformer(builder.toSchema());

The transformer and directive package live outside Pothos, so this step can't run in the playground sandbox, though the schema half above does. Any graphql-tools-based directive plugs in the same way.