Writing plugins
Guide for writing plugins for Pothos
Writing plugins for Pothos may seem a little intimidating at first, because the types used by Pothos are fairly complex. Fortunately, for many types of plugins, the process is actually pretty easy, once you understand the core concepts of how Pothos's type system works. Don't worry if the descriptions don't make complete sense at first. Going through the examples in this guide will hopefully make things seem a lot easier. This guide aims to cover a lot of the most common use cases for creating plugins, but does not contain full API documentation. Exploring the types or source code to see what all is available is highly encouraged, but should not be required for most use cases.
The type system
Pothos has 2 main pieces to its type system:
PothosSchemaTypes: A global namespace for shared typesSchemaTypes: A collection of types passed around through Generics specific to each instance ofSchemaBuilder
PothosSchemaTypes
The PothosSchemaTypes contains interfaces for all the various options objects used throughout the
API, along with some other types that plugins may want to extend. Each of the interfaces can be
extended by a plugin to add new options. Each interface takes a number of relevant generic
parameters that can be used to make the options more useful. For example, the interface for field
options will be passed the shape of the parent, the expected return type, and any arguments.
SchemaTypes
The SchemaTypes type is based on the Generic argument passed to the SchemaBuilder, and extended
with reasonable defaults. Almost every interface in the PothosSchemaTypes will have access to it
(look for Types extends SchemaTypes in the generics of almost any interface). This Type contains
the types for Scalars, backing models for some object and interface types, and many custom
properties from various plugins. If your plugin needs the user to provide some types that will be
shared across the whole schema, this is how you will be able to access them when adding fields to
the options objects defined in PothosSchemaTypes.
Getting Started
The best place to start is by looking through the example plugin.
The general structure of a plugin has 3 main parts:
index.tswhich contains a plugin's actual implementationglobal-types.tswhich contains any additions to Pothos's built in types.types.tswhich should contain any types that do NOT belong to the globalPothosSchemaTypesnamespace.
To get set up quickly, you can copy these files from the example plugin to suit your needs. The first few things to change are:
- The plugin name in
index.ts - The name of the Plugin class in
index.ts - The name key/name for the plugin in the
Pluginsinterface inglobal-types.ts
After setting up the basic layout of your plugin, I recommend starting by defining the types for
your plugin first (in global-types.ts) and setting up a test schema that uses your plugin. This
allows you to get the user facing API for your plugin working first, so you can see that any new
options you add to the API are working as expected, and that any type constraints are enforced
correctly. Once you are happy with your API, you can start building out the functionality in
index.ts. Building the types first also make the implementation easier because the properties you
will need to access in your extension may not exist on the config objects until you have defined
your types.
global-types.ts
global-types.ts must contain the following:
-
A declaration of the
PothosSchemaTypesnamespacedeclare global { export namespace PothosSchemaTypes {} } -
An addition to the
Pluginsinterface that maps the plugin name, to the plugin type (this needs to be inside thePothosSchemaTypesnamespace)export interface Plugins<Types extends SchemaTypes> { example: PothosExamplePlugin<Types>; }
global-types.ts should NOT include definitions that do not belong to the PothosSchemaTypes
namespace. Types for your plugin should be added to a separate types.ts file, and imported as
needed into global-types.ts.
To add properties to the various config objects used by the SchemaBuilder, you should start by
finding the interface that defines that config object in @pothos/core. Currently there are 4 main
files that define the types that make up PothosSchemaTypes namespace.
-
Contains the interfaces that define the options objects for the various types (Object, Interface, Enum, etc).
-
Contains the interfaces that define the options objects for creating fields
-
Contains the interfaces for SchemaBuilder options, SchemaTypes, options for
toSchema, and other utility interfaces that may be useful for plugins to extend that do not fall into one of the other categories. -
Contains interfaces that describe the classes used by Pothos, include
SchemaBuilderand the various field builder classes.
Once you have identified a type you wish to extend, copy it into the PothosSchemaTypes namespace
in your global-types.ts, but remove all the existing properties. You will need to keep all the
Generics used by the interface, and should import the types used in generics from @pothos/core.
You can now add any new properties to the interface that your plugin needs. Making new properties
optional (newProp?: TypeOfProp) is recommended for most use cases.
index.ts
index.ts must contain the following:
-
A bare import of the global types (
import './global-types';) -
The plugins name, which should be typed as a string literal rather than as a generic string:
const pluginName = 'example' -
A default export of the plugin name
export default pluginName -
A class that extends BasePlugin:
export class PothosExamplePlugin<Types extends SchemaTypes> extends BasePlugin<Types> {}BasePluginandSchemaTypescan both be imported from@pothos/core -
A call to register the plugin:
SchemaBuilder.registerPlugin(pluginName, PothosExamplePlugin);SchemaBuildercan also be imported from@pothos/core
Run a complete plugin
The example adds a logLabel builder option, registers a local plugin, and wraps each resolver. The query logs two field resolutions. The request counter starts at one for each execution because it receives a fresh context; logLabel labels the messages.
export class ObserverPlugin<Types extends SchemaTypes> extends BasePlugin<
Types,
{ resolveCount: number }
> {
createRequestData(_context: Types['Context']) {
return { resolveCount: 0 };
}
wrapResolve(
resolver: GraphQLFieldResolver<unknown, Types['Context'], object>,
_fieldConfig: PothosOutputFieldConfig<Types>,
): GraphQLFieldResolver<unknown, Types['Context'], object> {
return (parent, args, context, info) => {
const data = this.requestData(context);
data.resolveCount += 1;
console.log(
`${this.builder.options.logLabel}: ${info.parentType}.${info.fieldName} (${data.resolveCount})`,
);
return resolver(parent, args, context, info);
};
}
}The plugin is registered before it is passed to the builder:
export const pluginName = 'architectureObserver';
SchemaBuilder.registerPlugin(pluginName, ObserverPlugin);Open global-types.ts to see the plugin name and logLabel option added to PothosSchemaTypes. The query includes synchronous and asynchronous resolvers; the wrapper returns their values unchanged.
The sections below link runnable hooks to complete examples. Interface declarations describe the types to merge into PothosSchemaTypes; they are reference fragments rather than independent schemas. Subscription hooks need a runtime that executes subscriptions.
Life cycle hooks
The SchemaBuilder will instantiate plugins each time the toSchema method is called on the
builder. As the schema is built, it will invoke the various life cycle methods on each plugin if
they have been defined.
To hook into each lifecycle event, simply define the corresponding function in your plugin class.
For the exact function signature, see the index.ts of the example plugin.
-
onTypeConfig: Invoked for each type, with the config object that will be used to construct the underlying GraphQL type. -
onOutputFieldConfig: Invoked for each Object, or Interface field, with the config object describing the field. -
onInputFieldConfig: Invoked for each InputObject field, or field argument, with the config object describing the field. -
onEnumValueConfig: Invoked for each value in an enum -
beforeBuild: Invoked before building schemas, last chance to add new types or fields. -
afterBuild: Invoked with the fully built Schema. -
wrapResolve: Invoked when creating the resolver for each field -
wrapSubscribe: Invoked for each field in theSubscriptionsobject. -
wrapArgMappers: Wraps outside argument mapping for resolvers and subscription setup; see Argument mapping and errors. -
wrapResolveType: Invoked for each Union and Interface. -
wrapIsTypeOf: Invoked for each Object, including objects without anisTypeOffunction.
Each of the lifecycle methods above (except beforeBuild) expect a return value that matches
their first argument (either a config object, or the resolve/subscribe/resolveType function). If
your plugin does not need to modify these values, it can simply return the value that was passed in.
When your plugin does need to change one of the config values, you should return a copy of the
config object with your modifications, rather than modifying the config object that was passed in.
This can be done by either using Object.assign, or spreading the original config into a new object
{...originalConfig, newProp: newValue }.
Inherited interface fields have a separate output field config for each owning object or interface.
onOutputFieldConfig runs once for each declaration and owner pair in a schema build. On an inherited
field, parentType names the owner and declaringType names the interface that originally declared
the field. Arguments also have their own configs with the owner's parentType. kind and
graphqlKind still describe the field builder that declared the field, so plugin-specific field
options keep their original meaning. Resolver and subscriber wrappers receive the owner's config.
Plugins that generate shared output types for interface fields must use
config.declaringType ?? config.parentType for the generated type's identity. Generating a different
union for each owner would make the object field incompatible with the interface field. Plugins
that apply type-level behavior should use parentType to look up the owner.
Each config object will have the properties expected by the GraphQL for creating the types or fields
(although some properties like resolve will be added later), but will also include a number of
Pothos specific properties. These properties include graphqlKind to indicate what kind of GraphQL
type the config object is for, pothosOptions, which contains all the options passed in to the
schema builder when creating the type or field.
If your plugin needs to add additional types or fields to the schema it should do this in the
beforeBuild hook. Any types added to the schema after this, may not be included correctly. Plugins
should also account for the fact that a new instance of the plugin will be created each time the
schema is called, so any types or fields added to the schema should only be applied once (per
builder), even if multiple instances of the plugin are created. To help with this, there is a
runUnique helper on the base plugin class, which accepts a key, and a callback, and will only run
a callback once per builder for the given key, even across multiple toSchema calls.
The example calls toSchema() twice, but the preparation callback runs only once:
beforeBuild() {
this.runUnique('architecture-added-field', () => {
console.log('Preparing schema once per builder');
});
}Use cases
Below are a few of the most common use cases for how a plugin might extend the Pothos with very simplified examples. Most plugins will likely need a combination of these strategies, and some uses cases may not be well documented. If you are unsure about how to solve a specific problem, feel free to open a GitHub Issue for more help.
In the examples below, when "extending an interface", the interface should be added to the
PothosSchemaTypes namespace in global-types.ts.
Adding options to the SchemaBuilder constructor
You may have noticed that plugins are not instantiated by the user, and therefore users can't pass
options directly into your plugin when creating it. Instead, the recommended way to configure your
plugin is by contributing new properties to the options object passed to the SchemaBuilder
constructor. This can be done by extending the SchemaBuilderOptions interface.
export interface SchemaBuilderOptions<Types extends SchemaTypes> {
optionInRootOfConfig?: boolean;
nestedOptionsObject?: ExamplePluginOptions; // imported from types.ts
}Extending this interface will allow the user to pass in these new options when creating an instance
of SchemaBuilder.
You can then access the options through this.builder.options in your plugin, with everything
correctly typed. The example reflects nestedOptionsObject.exampleOption in the object description:
const label = this.builder.options.nestedOptionsObject?.exampleOption;
const rootOption = this.builder.options.optionInRootOfConfig;Adding options when building a schema (toSchema)
In some cases, your plugin may be designed for schemas that be built in different modes. For example
the mocks plugin allows the schema to be built repeatedly with different sets of mocks, or the
subGraph allows building a schema multiple times to generate separate subgraphs. For these cases,
you can extend the options passed to toSchema instead:
export interface BuildSchemaOptions<Types extends SchemaTypes> {
customBuildTimeOptions?: boolean;
}These options can be accessed through this.options in your plugin:
const buildOption = this.options.customBuildTimeOptions;Adding options to types
Each GraphQL type has its own options interface which can be extended. For example, to extend the options for creating an Object type:
export interface ObjectTypeOptions<Types extends SchemaTypes, Shape> {
optionOnObject?: boolean;
}These options can then be accessed in your plugin when you receive the config for the type:
if (typeConfig.kind === 'Object' && typeConfig.pothosOptions.optionOnObject) {
return {
...typeConfig,
description: `${label}; root=${rootOption}; build=${buildOption}`,
};
}
return typeConfig;In the example above, we need to check typeConfig.kind to ensure that the type config is for an
object. Without this check, typescript will not know that the config object is for an object, and
will not let us access the property. typeConfig.kind corresponds to how Pothos splits up Types for
its config objects, meaning that it has separate kinds for Query, Mutation, and Subscription
even though these are all Objects in GraphQL terminology. The typeConfig.graphqlKind can be used
to get the actual GraphQL type instead.
Adding options to fields
Similar to Types, fields also have a number of interfaces that can be extended to add options to various types of fields:
export interface MutationFieldOptions<
Types extends SchemaTypes,
Type extends TypeParam<Types>,
Nullable extends FieldNullability<Type>,
Args extends InputFieldMap,
ResolveReturnShape,
> {
customMutationFieldOption?: boolean;
}Field interfaces have a few more generics than other interfaces we have looked at. These generics
can be used to make the options you add more specific to the field currently being defined. It is
important to copy all the generics of the interfaces as they are defined in @pothos/core even if
you do not use the generics in your own properties. If the generics do not match, typescript won't
be able to merge the definitions. You do NOT need to include the extends clause of the interface,
if the interface extends another interface (like FieldOptions).
Similar to Type options, Field options will be available in the fieldConfigs in your plugin, once
you check that the fieldConfig is for the correct kind of field.
onOutputFieldConfig(fieldConfig: PothosOutputFieldConfig<Types>) {
if (fieldConfig.name === 'removeMe') {
return null;
}
if (fieldConfig.kind === 'Mutation' && fieldConfig.pothosOptions.customMutationFieldOption) {
return { ...fieldConfig, description: 'Configured mutation' };
}
return fieldConfig;
}Adding new methods on builder classes
Adding new methods to SchemaBuilder or one of the FieldBuilder classes is also done through
extending interfaces. Extending these interfaces is how typescript is able to know these methods
exist, even though they are not defined on the original classes.
export interface SchemaBuilder<Types extends SchemaTypes> {
buildCustomObject: () => ObjectRef<Types, { custom: 'shape' }>;
}The above is a simple example of defining a new buildCustomObject method that takes no arguments,
and returns a reference to a new custom object type. Defining this type will not work on its own,
and we still need to define the actual implementation of this method. This might look like:
const schemaBuilderProto = SchemaBuilder.prototype as PothosSchemaTypes.SchemaBuilder<SchemaTypes>;
schemaBuilderProto.buildCustomObject = function buildCustomObject() {
return this.objectRef<{ custom: 'shape' }>('CustomObject').implement({
fields: (t) => ({ custom: t.exposeString('custom') }),
});
};Note that the above function does NOT use an arrow function, so that the function can access this
as a reference to the SchemaBuilder instance.
Wrapping resolvers to add runtime functionality
Some plugins will need to add runtime behavior. There are a few lifecycle hooks for wrapping
resolve, subscribe, and resolveType. These hooks will receive the function they are wrapping,
along with a config object for the field or type they are associated with, and should return either
the original function, or a wrapper function with the same API.
It is important to remember that resolvers can resolve values in a number of ways (normal values,
promises, or even something as complicated Promise<(Promise<T> | T)[]>. So be careful when using a
wrapper that introspected the return value of a resolve function. Plugins should only wrap resolvers
when absolutely necessary.
export class ObserverPlugin<Types extends SchemaTypes> extends BasePlugin<
Types,
{ resolveCount: number }
> {
createRequestData(_context: Types['Context']) {
return { resolveCount: 0 };
}
wrapResolve(
resolver: GraphQLFieldResolver<unknown, Types['Context'], object>,
_fieldConfig: PothosOutputFieldConfig<Types>,
): GraphQLFieldResolver<unknown, Types['Context'], object> {
return (parent, args, context, info) => {
const data = this.requestData(context);
data.resolveCount += 1;
console.log(
`${this.builder.options.logLabel}: ${info.parentType}.${info.fieldName} (${data.resolveCount})`,
);
return resolver(parent, args, context, info);
};
}
}Transforming a schema
For some plugins the other provided lifecycle hooks may not be sufficiently powerful to modify the schema
in all the ways a plugin may want. For example removing types from the schema (eg. the SubGraph
plugin). In these cases, the afterBuild hook can be used. It receives the built schema, and is
expected to return either the schema it was passed, or a completely new schema. This allows plugins
to use 3rd party libraries like graphql-tools to arbitrarily transform schemas if desired.
This example returns a new schema with a changed description. The changed description is available through __schema.description:
afterBuild(schema: GraphQLSchema) {
return new GraphQLSchema({
...schema.toConfig(),
description: 'Schema transformed after build',
});
}Using SchemaTypes
You may have noticed that almost every interface and type in @pothos/core take a generic that
looks like: Types extends SchemaTypes. This type is what allows Pothos and its plugins to share
type information across the entire schema, and to incorporate user defined types into that system.
These SchemaTypes are a combination of default types merged with the Types provided in the Generic
parameter of the SchemaBuilder constructor, and includes a wide variety of useful types:
- Types for all the scalars
- Types for backing models used by objects and interfaces when referenced via strings
- The type used for the context and root objects
- Settings for default nullability of fields
- Any user defined types specific to plugins (more info below)
There are many ways these types can be used, but one of the most common is to access the type for the context object, so that you can correctly type a callback function for your plugin that accepts the context object.
export interface SchemaBuilderOptions<Types extends SchemaTypes> {
exampleSetupFn?: (context: Types['Context']) => ExamplePluginSetupConfig;
}Using user defined types
As mentioned above, your plugin can also contribute its own user definable types to the SchemaTypes
interface. You can see examples of this in the several of the plugins including the directives and
scope-auth plugins. Adding your own types to SchemaTypes requires extending 2 interfaces: The
UserSchemaTypes which describes the type the user will need to provide, and the
ExtendDefaultTypes interface, which is used to set default values if the User does not provide
their own types.
export interface UserSchemaTypes {
NewExampleTypes: Record<string, ExampleShape>;
}
export interface ExtendDefaultTypes<PartialTypes extends Partial<UserSchemaTypes>> {
NewExampleTypes: PartialTypes['NewExampleTypes'] & {};
}The User provided type can then be accessed using Types['NewExampleTypes'] in any interface or
type that receives SchemaTypes as a generic argument.
Request data
Plugins that wrap resolvers may need to store some data that is unique to the current request. In these
cases your plugin can define a createRequestData method, and use the requestData method to get
the data for the current request.
export class ObserverPlugin<Types extends SchemaTypes> extends BasePlugin<
Types,
{ resolveCount: number }
> {
createRequestData(_context: Types['Context']) {
return { resolveCount: 0 };
}
wrapResolve(
resolver: GraphQLFieldResolver<unknown, Types['Context'], object>,
_fieldConfig: PothosOutputFieldConfig<Types>,
): GraphQLFieldResolver<unknown, Types['Context'], object> {
return (parent, args, context, info) => {
const data = this.requestData(context);
data.resolveCount += 1;
console.log(
`${this.builder.options.logLabel}: ${info.parentType}.${info.fieldName} (${data.resolveCount})`,
);
return resolver(parent, args, context, info);
};
}
}The shape of requestData can be defined via the second generic parameter of the BasePlugin class.
The requestData method expects the context object as its only argument, which is used to uniquely
identify the current request.
Wrapping arguments and inputs
The plugin API does not directly wrap individual input fields. wrapResolve and wrapSubscribe
can transform the args object before passing it to the original function. These wrappers receive
arguments after any registered argument mappers have run; use fieldConfig.argMappers when the
transformation needs to happen before resolver or subscription wrappers.
Figuring out how to wrap inputs can be a little complex, especially when dealing with recursive inputs, and optimizing to wrap as little as possible. To help with this, Pothos has a couple of utility functions that can make this easier:
mapInputFields: Used to select affected input fields and extract some configurationcreateInputValueMapper: Creates a mapping function that uses the result ofmapInputFieldsto map inputs in an args object to new values.
The relay plugin uses these methods to decode globalID inputs. This example uses the same utilities to trim marked strings, including strings inside a nested input list. Fields without the trimInput extension retain their whitespace:
export class TrimPlugin<Types extends SchemaTypes> extends BasePlugin<Types> {
// Safe to share: selection depends only on input-field extensions, not its enclosing output field.
private mappingCache = new Map<string, InputTypeFieldsMapping<Types, boolean>>();
wrapResolve(
resolver: GraphQLFieldResolver<unknown, Types['Context'], object>,
fieldConfig: PothosOutputFieldConfig<Types>,
): GraphQLFieldResolver<unknown, Types['Context'], object> {
const argMappings = mapInputFields(
fieldConfig.args,
this.buildCache,
(inputField) => (inputField.extensions?.trimInput ? true : null),
this.mappingCache,
);
if (!argMappings) {
return resolver;
}
const argMapper = createInputValueMapper(argMappings, (value) =>
typeof value === 'string' ? value.trim() : value,
);
return (parent, args, context, info) => resolver(parent, argMapper(args), context, info);
}
}Using these utilities allows moving more logic to build time (figuring out which fields need mapping) so that the runtime overhead is as small as possible.
createInputValueMapper may be useful for some use cases, for some plugins it may be better to
create a custom mapping function, but still use the result of mapInputFields.
mapInputFields returns a map whose keys are field/argument names, and whose values are objects
with the following shape:
interface InputFieldMapping<Types extends SchemaTypes, T> {
kind: 'Enum' | 'Scalar' | 'InputObject';
isList: boolean;
listDepth: number;
config: PothosInputFieldConfig<Types>;
value: T | null; // null is possible for an InputObject with mapped descendants.
}if the kind is InputObject then the mapping object will also have a fields property with an
object of the following shape:
interface InputTypeFieldsMapping<Types extends SchemaTypes, T> {
configs: Record<string, PothosInputFieldConfig<Types>>;
map: Map<string, InputFieldMapping<Types, T>> | null;
}Both the root map and nested fields.map maps contain fields with non-null mappings and
input objects with mapped descendants. If the mapping function returned null for all fields, the
mapInputFields will return null instead of returning a map to indicate no wrapping should occur
Argument mapping and errors
Register an argument mapper in onOutputFieldConfig by returning a config with an updated
argMappers array. Each mapper receives (args, context, info) and returns the next argument object,
or a promise of it. args is a Record<string, unknown>; info is Pothos's PartialResolveInfo.
Preserve existing mappers when appending your own. Mappers run sequentially in array order, awaiting
each result before passing it to the next mapper.
For a resolver call, execution enters these layers in order:
wrapArgMapperswrappers, which see the arguments before mapping.- The field's
argMappers. wrapResolvewrappers, which see the mapped arguments.- The application's resolver.
Subscription setup follows the same order with wrapSubscribe and the application's subscribe
function in the last two positions. The subscription's event resolver has its own resolver path;
do not assume mapping runs only once per subscription. Within each wrapper hook, the first plugin
in the builder's plugins array is outermost. Configuration hooks run from the last plugin to the
first, so appending in onOutputFieldConfig determines the final mapper-array order at build time.
A try/catch inside wrapResolve cannot catch a mapper failure: mapping happens before that wrapper
is entered. Use wrapArgMappers for an outer error boundary. The example below appends a synchronous
normalizer and an asynchronous validator for fields marked with extensions: { normalizeName: true }.
The boundary converts their NameInputError failures to a GraphQL error with a BAD_USER_INPUT code.
import SchemaBuilder, {
BasePlugin,
type PothosOutputFieldConfig,
type SchemaTypes,
} from '@pothos/core';
import { GraphQLError, type GraphQLFieldResolver } from 'graphql';
declare global {
namespace PothosSchemaTypes {
interface Plugins<Types extends SchemaTypes> {
architectureArgMappers: NameInputPlugin<Types>;
}
}
}
class NameInputError extends Error {}
export class NameInputPlugin<Types extends SchemaTypes> extends BasePlugin<Types> {
override onOutputFieldConfig(fieldConfig: PothosOutputFieldConfig<Types>) {
if (!fieldConfig.extensions?.normalizeName) {
return fieldConfig;
}
return {
...fieldConfig,
argMappers: [
...fieldConfig.argMappers,
(args: Record<string, unknown>) => {
if (typeof args.name !== 'string' || !args.name.trim()) {
throw new NameInputError('Name must not be blank');
}
return { ...args, name: args.name.trim() };
},
async (args: Record<string, unknown>) => {
// A real plugin could await a lookup or asynchronous validator here.
if (args.name === 'reserved') {
throw new NameInputError('Name is reserved');
}
return args;
},
],
};
}
override wrapArgMappers(
resolver: GraphQLFieldResolver<unknown, Types['Context'], object> | undefined,
fieldConfig: PothosOutputFieldConfig<Types>,
): GraphQLFieldResolver<unknown, Types['Context'], object> | undefined {
if (!resolver || !fieldConfig.extensions?.normalizeName) {
return resolver;
}
return async (parent, args, context, info) => {
try {
return await resolver(parent, args, context, info);
} catch (error) {
if (error instanceof NameInputError) {
throw new GraphQLError(error.message, { extensions: { code: 'BAD_USER_INPUT' } });
}
throw error;
}
};
}
}
export const pluginName = 'architectureArgMappers';
SchemaBuilder.registerPlugin(pluginName, NameInputPlugin);
The await inside try catches both a synchronous throw and an asynchronous rejection. Returning
the promise without awaiting it would let its rejection escape that catch. Preserve an absent
function by returning undefined unchanged: this hook is also called when a field has no subscribe
function. It should not accidentally manufacture one.
The boundary also encloses resolver/subscriber wrappers and the application function, so it can
receive their errors too. Handle only errors belonging to the policy you intend to implement;
this example rethrows every error other than NameInputError. It does not catch GraphQL document
validation or variable-coercion errors, which occur before field execution, or errors thrown by a
subscription's async iterable while producing events.
Use the registered plugin and opt in on the field in a separate schema.ts:
import SchemaBuilder from '@pothos/core';
import { pluginName } from './plugin';
const builder = new SchemaBuilder({ plugins: [pluginName] });
builder.queryType({
fields: (t) => ({
greeting: t.string({
extensions: { normalizeName: true },
args: { name: t.arg.string({ required: true }) },
resolve: (_parent, { name }) => {
if (name === 'broken') {
throw new Error('Greeting unavailable');
}
return `Hello, ${name}!`;
},
}),
}),
});
export const schema = builder.toSchema();
greeting(name: " Gina ") returns "Hello, Gina!". A blank name throws synchronously; "reserved"
rejects from the asynchronous mapper, even when surrounded by whitespace. Both failures prevent the
resolver from running. "broken" reaches the resolver and retains the "Greeting unavailable" error
without a BAD_USER_INPUT code.
These mappers preserve the schema's string argument shape. Registering a mapper does not itself
change the TypeScript argument types inferred from field definitions. The earlier trim example
remains a valid wrapResolve transformation; moving its mapper into argMappers would make it run
before all resolver wrappers instead of inside that plugin's wrapper.
Removing fields and enum values
Plugins can remove fields from objects, interfaces, and input objects, and remove specific values from enums. To do this, simply return null from the corresponding on*Config plugin hook:
onOutputFieldConfig(fieldConfig: PothosOutputFieldConfig<Types>) {
if (fieldConfig.name === 'removeMe') {
return null;
}
if (fieldConfig.kind === 'Mutation' && fieldConfig.pothosOptions.customMutationFieldOption) {
return { ...fieldConfig, description: 'Configured mutation' };
}
return fieldConfig;
}onInputFieldConfig(fieldConfig: PothosInputFieldConfig<Types>) {
return fieldConfig.name === 'removeMe' ? null : fieldConfig;
}
onEnumValueConfig(valueConfig: PothosEnumValueConfig<Types>) {
return valueConfig.value === 'removeMe' ? null : valueConfig;
}Introspection omits the removeMe output and input fields and the enum value backed by removeMe. The other fields and enum values remain.
Removing whole types from the schema needs to be done by transforming the schema during the
afterBuild hook. See the sub-graph plugin for a more complete example of removing types.
Useful methods:
-
builder.configStore.onTypeConfig: Takes a type ref and a callback, and will invoke the callback with the config for the referenced type once available. -
fieldRef.onFirstUseTakes a callback to invoke once the config for the field is available. -
buildCache.getTypeConfigGets the config for a given type after it has been passed through any modifications applied by plugins.