# Design
URL: /docs/design
Design overview for Pothos
## Type System [#type-system]
The type system that powers most of the Pothos type checking has 2 components. The first is the
SchemaTypes type param passed into the SchemaBuilder. This allows a shared set of types to be reused
throughout the schema, and is responsible for providing type information for shared types like the
[Context](./guide/context) object, and any Object, Interface, or Scalar types that you want to
reference by name (as a string). Having all type information in a single object can be convenient
at times, but with large schemas, can become unwieldy.
To support a number of additional use cases, including Unions and Enums, large schemas, and plugins
that extract type information from other sources (eg the Prisma, or the simple-objects
plugin), Pothos has another way of passing around type information. This system uses `Ref`
objects whose TypeScript types carry the backing shapes they represent. Every builder method for creating a type or
a field returns a `Ref` object.
Using Ref objects allows us to separate the type information from the implementation, and allows for
a more modular design. For example, `builder.objectRef<{ id: string }>('User')` declares
the backing shape before fields are defined with `implement`. The ref carries that shape at compile
time and identifies the GraphQL type at runtime; it does not validate resolver values at runtime.
See [Objects](./guide/objects) and [Circular References](./guide/circular-references) for examples.
# Getting started
URL: /docs
Install Pothos, build a schema, and run your first GraphQL query.
Define your GraphQL types, fields, and resolvers in TypeScript. Pothos checks how they fit together
without generating resolver types or maintaining a separate schema definition.
This guide creates a small schema, serves it with
[GraphQL Yoga](https://the-guild.dev/graphql/yoga-server), and runs a query against it.
You can try the schema immediately with its Playground action below. Follow the setup steps to run
the same schema in your own server. [Using the playground](/docs/guide/playground) explains the editor controls.
## Installing [#installing]
Start in an empty directory with Node.js and npm installed:
```sh
npm init -y
npm pkg set type=module
```
Install Pothos, its `graphql` peer dependency, and Yoga:
npm
pnpm
yarn
bun
```bash
npm install @pothos/core graphql graphql-yoga
```
```bash
pnpm add @pothos/core graphql graphql-yoga
```
```bash
yarn add @pothos/core graphql graphql-yoga
```
```bash
bun add @pothos/core graphql graphql-yoga
```
Install TypeScript and `tsx` to check and run the example:
npm
pnpm
yarn
bun
```bash
npm install --save-dev typescript @types/node tsx
```
```bash
pnpm add --save-dev typescript @types/node tsx
```
```bash
yarn add --dev typescript @types/node tsx
```
```bash
bun add --dev typescript @types/node tsx
```
## Set up TypeScript [#set-up-typescript]
Create `tsconfig.json`:
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"types": ["node"],
"strict": true,
"noEmit": true,
"skipLibCheck": true
},
"include": ["*.ts"]
}
```
Pothos relies on strict null checking for its type inference. Keep `strict` enabled when adding
Pothos to an existing project too.
## Create a simple schema [#create-a-simple-schema]
Create `schema.ts`:
```typescript playground example="foundations-getting-started"
import SchemaBuilder from '@pothos/core';
const builder = new SchemaBuilder({});
builder.queryType({
fields: (t) => ({
hello: t.string({
args: {
name: t.arg.string(),
},
resolve: (_parent, { name }) => `hello, ${name ?? 'World'}`,
}),
}),
});
export const schema = builder.toSchema();
```
`queryType` defines the fields clients can query. Here, `hello` returns a string and accepts an
optional `name` argument. Pothos infers the resolver's argument types from that definition.
`toSchema()` builds a graphql-js `GraphQLSchema`. You can use it with Yoga or another server that
accepts a `GraphQLSchema`.
## Create a server [#create-a-server]
Create `server.ts` alongside `schema.ts`:
```typescript
import { createServer } from 'node:http';
import { createYoga } from 'graphql-yoga';
import { schema } from './schema.js';
const yoga = createYoga({ schema });
const server = createServer(yoga);
server.listen(4000, () => {
console.log('Visit http://localhost:4000/graphql');
});
```
Check the types, then start the server:
```sh
npx tsc
npx tsx server.ts
```
`tsx` runs TypeScript without checking types, so run `tsc` separately to catch type errors.
## Run a query [#run-a-query]
Open [http://localhost:4000/graphql](http://localhost:4000/graphql) to use Yoga's GraphiQL explorer.
Run this query:
```graphql playground example="foundations-getting-started"
query Hello {
hello(name: "Pothos")
}
```
The response is:
```json
{
"data": {
"hello": "hello, Pothos"
}
}
```
Omit the `name` argument to get `"hello, World"`. Stop the server with Ctrl+C when you're done.
## Continue building [#continue-building]
[Objects](/docs/guide/objects) shows how to expose your application's data. [Fields](/docs/guide/fields) covers
resolvers, lists, and nullability, and [Arguments](/docs/guide/args) explains how clients pass values to them.
When your schema needs request data, see [Context](/docs/guide/context). [App layout](/docs/guide/app-layout) shows how
to organize a larger schema across files.
# LLM Integration
URL: /docs/llms
Read the Pothos documentation as plain text for use with LLMs and other tools.
The documentation is also available as plain text. Use the index to find a topic, fetch an individual
page, or download the complete documentation.
## Documentation index [#documentation-index]
[`/llms.txt`](/llms.txt) lists each page's title, description, and link to its plain-text version.
## Full Documentation [#full-documentation]
[`/llms-full.txt`](/llms-full.txt) combines all documentation pages into one response. Each page
includes its title, URL, description, and processed content.
## Individual Pages [#individual-pages]
Append `.mdx` to a documentation URL to read that page as text. For example,
[`/docs/guide/objects.mdx`](/docs/guide/objects.mdx) contains the Objects guide.
Despite the `.mdx` URL suffix, these responses contain processed documentation text, rather than the
original source file with its component imports.
# Resources
URL: /docs/resources
External guides, tools, and libraries created by members of the Pothos community.
## Guides and Tutorials [#guides-and-tutorials]
* [End-To-End Type-Safety with GraphQL, Prisma & React: GraphQL API](https://www.prisma.io/blog/e2e-type-safety-graphql-react-3-fbV2ZVIGWg#start-up-a-graphql-server)
by [Sabin Adams](https://twitter.com/sabinthedev)
* [Code-first GraphQL with Pothos](https://graphql.wtf/episodes/60-code-first-graphql-with-pothos)
by [Jamie Barton](https://twitter.com/notrab)
* [Type-safe GraphQL Server with Pothos](https://omkarkulkarni.hashnode.dev/type-safe-graphql-server-with-pothos-formerly-giraphql)
by [Omkar Kulkarni](https://twitter.com/omkar_k45)
* [Build a GraphQL server running on Cloudflare Workers](https://the-guild.dev/blog/graphql-yoga-worker)
by [Rito Tamata](https://twitter.com/chimame_rt)
## Third-party tools and libraries [#third-party-tools-and-libraries]
* [Prisma Generator Pothos Codegen](https://github.com/Cauen/prisma-generator-pothos-codegen) by
[Emanuel](https://twitter.com/cauenor)
* [Nexus to Pothos codemod](https://github.com/villesau/nexus-to-pothos-codemod) by
[Ville Saukkonen](https://twitter.com/SaukkonenVille)
* [protoc-gen-pothos](https://github.com/proto-graphql/proto-graphql-js/tree/main/packages/protoc-gen-pothos)
by [Masayuki Izumi](https://twitter.com/izumin5210)
* [@smatch-corp/nestjs-pothos](https://github.com/smatch-corp/nestjs-pothos) by
[Chanhee Lee](https://github.com/iamchanii)
* [pothos-protoc-gen](https://iamchanii.github.io/pothos-protoc-gen/) by [Chanhee Lee](https://github.com/iamchanii)
* [rumble](https://github.com/m1212e/rumble) (GraphQL + Drizzle + Abilities) by [m1212e](https://github.com/m1212e) [(introduction)](https://github.com/hayes/pothos/discussions/1414)
## Templates and Examples [#templates-and-examples]
* [Server Template with Pothos](https://github.com/nkzw-tech/server-template) by [Nakazawa Tech](https://nakazawa.tech)
* [Pothos GraphQL Server](https://github.com/theogravity/graphql-pothos-server-example) by
[Theo Gravity](https://github.com/theogravity)
* [GraphQL countries server](https://github.com/gbicou/countries-server) by
[Benjamin VIELLARD](https://github.com/gbicou)
* [datalake-graphql-wrapper](https://github.com/dbsystel/datalake-graphql-wrapper) by
[noxify](https://github.com/noxify)
## Conference talks [#conference-talks]
* [Pothos + Prisma: delightful, type-safe and efficient GraphQL](https://www.youtube.com/watch?v=LqKPfMmxFxw)
by [Michael Hayes](https://twitter.com/yavascript)
* [Building Scalable Applications](https://www.youtube.com/watch?v=rxPTEko8J7c\&t=36s) by [Christoph Nakazawa](https://cpojer.net)
## Paid tools [#paid-tools]
* [Bedrock](https://bedrock.mxstbr.com/) by [Max Stoiber](https://twitter.com/mxstbr)
* [nytro](https://www.nytro.dev/) by Jordan Gensler
# Sponsors
URL: /docs/sponsors
The generous people supporting Pothos development
Pothos development is supported by [sponsorships](https://github.com/sponsors/hayes) from these
generous people and organizations:
*
*
*
*
* [@saevarb](https://github.com/saevarb)
* [@seanaye](https://github.com/seanaye)
* [@arimgibson](https://github.com/arimgibson)
* [@ccfiel](https://github.com/ccfiel)
* [@JoviDeCroock](https://github.com/JoviDeCroock)
* [@hellopivot](https://github.com/hellopivot)
* [@robmcguinness](https://github.com/robmcguinness)
* [@Gomah](https://github.com/Gomah)
*
* [@garth](https://github.com/garth)
* [@lifedup](https://github.com/lifedup)
* [@skworden](https://github.com/skworden)
* [@jacobgmathew](https://github.com/jacobgmathew)
* [@aniravi24](https://github.com/aniravi24)
* [@mizdra](https://github.com/mizdra)
* [@3nk1du](https://github.com/3nk1du)
* [@FarazPatankar](https://github.com/FarazPatankar)
* [@noxify](https://github.com/noxify)
* [@matthawk60](https://github.com/matthawk60)
* [@BitPhinix](https://github.com/BitPhinix)
* [@nathanchapman](https://github.com/nathanchapman)
* [@pradyuman](https://github.com/pradyuman)
* [@tmm](https://github.com/tmm)
# ArgBuilder
URL: /docs/api/arg-builder
API docs for Pothos ArgBuilder
This reference describes the options for an argument. For examples, see [Arguments](../guide/args).
The option types below are summaries; Pothos infers their concrete types from your schema.
## `arg(options)` [#argoptions]
* `options`: \[`FieldOptions`]
### FieldOptions [#fieldoptions]
```typescript
type FieldOptions = {
type: InputType;
required?: boolean | { list: boolean; items: boolean };
defaultValue?: DefaultValue;
description?: string;
deprecationReason?: string;
extensions?: Readonly>;
astNode?: InputValueDefinitionNode;
};
```
* `type`: [Type Parameter](./arg-builder#type-parameter)
* `required`: defaults to `false`, unless overwritten in SchemaBuilder see
[Changing Default Nullability](../guide/changing-default-nullability).
* `astNode`: an optional `InputValueDefinitionNode` from `graphql` attached to this schema element.
See [AST metadata](./schema-builder#ast-metadata).
* `description`: string
* `deprecationReason`: marks the input as deprecated.
* `extensions`: metadata for tools and plugins.
* `defaultValue`: default value for field, type based on `type` option.
### Type Parameter [#type-parameter]
A Type Parameter for a Field can be any `InputTypeRef` returned by one of the
[`SchemaBuilder`](./schema-builder) methods for defining an `InputObject`, `Enum`, or `Scalar`, a ts
enum used to define a graphql enum type, or a string that corresponds to one of the keys of the
`Scalars` object defined in `SchemaTypes`.
For lists, wrap the type in an array, for example `['ID']`. A boolean `required` controls
the list itself; items are required by default. Use `{ list: false, items: false }` to allow both
an omitted or null list and null items. Input object names declared in `SchemaTypes.Inputs`
can also be used as strings.
## helpers [#helpers]
A set of helpers for creating scalar fields. These work the same as ArgBuilder, but omit the `type`
field from options.
### Scalars [#scalars]
* `arg.string(options)`
* `arg.id(options)`
* `arg.boolean(options)`
* `arg.int(options)`
* `arg.float(options)`
* `arg.stringList(options)`
* `arg.idList(options)`
* `arg.booleanList(options)`
* `arg.intList(options)`
* `arg.floatList(options)`
### `arg.listRef(type, options?)` [#arglistreftype-options]
Creates a list type reference, including nested lists. `options.required` controls the items
and defaults to `true`; the field or argument options control the outer list. See
[Arguments](../guide/args) for a nested-list example.
# FieldBuilder
URL: /docs/api/field-builder
API docs for Pothos FieldBuilder
The field builder is the `t` passed to a `fields` callback. See [Fields](../guide/fields)
for examples. The option types below summarize an API whose concrete types are inferred from your schema.
## `field(options)` [#fieldoptions]
* `options`: `FieldOptions`
### FieldOptions [#fieldoptions-1]
```typescript
type FieldOptions = {
type: ReturnType;
args?: Args;
nullable?: boolean | { list: boolean; items: boolean };
description?: string;
deprecationReason?: string;
resolve: (parent, args, context, info) => ResolveValue;
extensions?: Readonly>;
astNode?: FieldDefinitionNode;
};
```
* `type`: [Type Parameter](./field-builder#type-parameter)
* `args`: a map of argument names to refs, created with [`t.arg`](./arg-builder) or `builder.args`.
* `nullable`: defaults to `true`, unless overwritten in SchemaBuilder see
[Changing Default Nullability](../guide/changing-default-nullability).
* `astNode`: an optional `FieldDefinitionNode` from `graphql` attached to this schema element.
See [AST metadata](./schema-builder#ast-metadata).
* `description`: string
* `deprecationReason`: string
* `resolve`: [Resolver](./field-builder#resolver)
### Type Parameter [#type-parameter]
A Type Parameter for a Field can be any `TypeRef` returned by one of the
[`SchemaBuilder`](./schema-builder) methods for defining a type, a class used to create an object or
interface type, a ts enum used to define a graphql enum type, or a string that corresponds to one of
the keys of the `Objects`, `Interfaces`, or `Scalars` objects defined in `SchemaTypes`.
For list fields, wrap the type in an array, for example `['User']`. A boolean `nullable`
controls the list itself; items are non-null by default. Use `{ list: true, items: true }` to allow
both a null list and null items.
### Resolver [#resolver]
A function to resolve the value of this field.
#### Return type [#return-type]
Field resolvers should return a value (or promise) that matches the expected type for this field.
For scalars, this is their output shape. For objects and interfaces, it is the backing shape
carried by a ref, class, or `SchemaTypes` entry. For Unions, the type may be any of the corresponding shapes of members of the union.
For Enums, the value is dependent on the implementation of the enum. See the [Enums](../guide/enums) guide for more
details.
#### Args [#args]
* `parent`: the backing value of the current type. Root fields receive the root value, typed by `SchemaTypes.Root`.
* `args`: an object matching the shape of the args option for the current field
* `context`: The `Context` type defined in `SchemaTypes`.
* `info`: a GraphQLResolveInfo object see
[https://graphql.org/graphql-js/type/#graphqlobjecttype](https://graphql.org/graphql-js/type/#graphqlobjecttype)
for more details.
## helpers [#helpers]
A set of helpers for creating scalar fields. These work the same as
[`field`](./field-builder#fieldoptions), but omit the `type` field from options.
### Scalars [#scalars]
* `string(options)`
* `id(options)`
* `boolean(options)`
* `int(options)`
* `float(options)`
* `stringList(options)`
* `idList(options)`
* `booleanList(options)`
* `intList(options)`
* `floatList(options)`
### `listRef(type, options?)` [#listreftype-options]
Creates a list type reference, including nested lists. `options.nullable` controls the items
and defaults to `false`; the field options control the outer list.
### expose [#expose]
A set of helpers to expose fields from the backing model. The `name` arg can be any field from the
backing model that matches the type being exposed. Options are the same as
[`field`](./field-builder#fieldoptions), but `type`, `resolve`, and `args` are omitted.
The general `expose(name, { type, ...options })` helper also supports object, enum, and custom scalar properties.
* `exposeString(name, options)`
* `exposeID(name, options)`
* `exposeBoolean(name, options)`
* `exposeInt(name, options)`
* `exposeFloat(name, options)`
* `exposeStringList(name, options)`
* `exposeIDList(name, options)`
* `exposeBooleanList(name, options)`
* `exposeIntList(name, options)`
* `exposeFloatList(name, options)`
# InputFieldBuilder
URL: /docs/api/input-field-builder
API docs for Pothos InputFieldBuilder
This reference describes the options for an input field. For examples, see [Input Objects](../guide/inputs).
The option types below are summaries; Pothos infers their concrete types from your schema.
## `field(options)` [#fieldoptions]
* `options`: \[`FieldOptions`]
### FieldOptions [#fieldoptions-1]
```typescript
type FieldOptions = {
type: InputType;
required?: boolean | { list: boolean; items: boolean };
defaultValue?: DefaultValue;
description?: string;
deprecationReason?: string;
extensions?: Readonly>;
astNode?: InputValueDefinitionNode;
};
```
* `type`: [Type Parameter](./input-field-builder#type-parameter)
* `required`: defaults to `false`, unless overwritten in SchemaBuilder. See
[Changing Default Nullability](../guide/changing-default-nullability).
* `astNode`: an optional `InputValueDefinitionNode` from `graphql` attached to this schema element.
See [AST metadata](./schema-builder#ast-metadata).
* `description`: string
* `deprecationReason`: marks the input as deprecated.
* `extensions`: metadata for tools and plugins.
* `defaultValue`: default value for field, type based on `type` option.
### Type Parameter [#type-parameter]
A Type Parameter for a Field can be any `InputTypeRef` returned by one of the
[`SchemaBuilder`](./schema-builder) methods for defining an `InputObject`, `Enum`, or `Scalar`, a ts
enum used to define a graphql enum type, or a string that corresponds to one of the keys of the
`Scalars` object defined in [`SchemaTypes`](./schema-builder#schematypes).
For lists, wrap the type in an array, for example `['ID']`. A boolean `required` controls
the list itself; items are required by default. Use `{ list: false, items: false }` to allow both
an omitted or null list and null items. Input object names declared in `SchemaTypes.Inputs`
can also be used as strings.
## helpers [#helpers]
A set of helpers for creating scalar fields. These work the same as `field`, but omit the `type`
field from options.
### Scalars [#scalars]
* `string(options)`
* `id(options)`
* `boolean(options)`
* `int(options)`
* `float(options)`
* `stringList(options)`
* `idList(options)`
* `booleanList(options)`
* `intList(options)`
* `floatList(options)`
### `listRef(type, options?)` [#listreftype-options]
Creates a list type reference, including nested lists. `options.required` controls the items
and defaults to `true`; the field or argument options control the outer list. See
[Arguments](../guide/args) for a nested-list example.
# SchemaBuilder
URL: /docs/api/schema-builder
API docs for Pothos SchemaBuilder
SchemaBuilder is the core class of Pothos. It can be used to build types, and merge them into a
graphql.js Schema. The signatures below summarize the API; placeholder types such as
`FieldsFunction` stand for types inferred from your schema. See the
[Schema Builder guide](../guide/schema-builder) for configuration examples.
The optional `astNode` properties attach [AST metadata](#ast-metadata) for schema tooling.
## `constructor(options)` [#constructorschematypesoptions]
* typeParam: `SchemaTypes`: A type that describes the backing models for your schema
* options: `SchemaBuilderOptions`
### `SchemaTypes` [#schematypes]
```typescript
type SchemaTypes = {
// Shape of the context arg in your resolvers
Context?: object;
// Shape of the root value passed to root field resolvers
Root?: object;
// Select v3 compatibility defaults when needed
Defaults?: 'v3' | 'v4';
// A map of Object type names to their backing models.
Objects?: object;
// A map of Input type names to their backing models.
Inputs?: object;
// A map of Interface type names to their backing models.
Interfaces?: object;
// Map of scalar names to Input and Output shapes. Can be used to overwrite default scalar types,
// or to add type information for custom scalars
Scalars?: {
[s: string]: {
Input: unknown;
Output: unknown;
};
};
// When set to false, fields will be NonNullable by default (requires corresponding change in builder options)
DefaultFieldNullability?: false;
// When provided, input fields and arguments will be required by default (requires corresponding change in builder options)
DefaultInputFieldRequiredness?: true;
}
```
### SchemaBuilderOptions [#schemabuilderoptions]
```typescript
type SchemaBuilderOptions = {
plugins?: PluginName[];
defaultFieldNullability?: boolean;
defaultInputFieldRequiredness?: boolean;
defaults?: 'v3' | 'v4';
};
```
Plugins may contribute additional options. When changing a default, set both the corresponding
`SchemaTypes` entry and runtime option. Fields are nullable and inputs optional by default in v4.
See [Changing Default Nullability](../guide/changing-default-nullability).
## `queryType(options, fields?)` [#querytypeoptions-fields]
creates the `Query` with a set of Query fields
* `options`: `QueryTypeOptions`
* `fields?`: a function that receives a [`FieldBuilder`](./field-builder), and returns an object of
field names to field refs. See [`FieldBuilder`](./field-builder) for more details.
### QueryTypeOptions [#querytypeoptions]
```typescript
type QueryTypeOptions = {
astNode?: ObjectTypeDefinitionNode;
description?: string;
fields?: FieldsFunction;
};
```
* `description`: A description of the current type
* `fields`: a function that receives a [`FieldBuilder`](./field-builder), and returns an object of
field names to field refs. See [`FieldBuilder`](./field-builder) for more details.
## `queryFields(fields)` [#queryfieldsfields]
add a set of fields to the `Query` type.
* `fields`: a function that receives a [`FieldBuilder`](./field-builder), and returns an object of
field names to field refs. See [`FieldBuilder`](./field-builder) for more details.
## `queryField(name, field)` [#queryfieldname-field]
add a single field to the `Query` type.
* `name`: the name of the field
* `field`: a function that receives a [`FieldBuilder`](./field-builder), and returns field ref. See
[`FieldBuilder`](./field-builder) for more details.
## `mutationType(options, fields?)` [#mutationtypeoptions-fields]
creates the `Mutation` with a set of Mutation fields
* `options`: `MutationTypeOptions`
* `fields?`: a function that receives a [`FieldBuilder`](./field-builder), and returns an object of
field names to field refs. See [`FieldBuilder`](./field-builder) for more details.
### MutationTypeOptions [#mutationtypeoptions]
```typescript
type MutationTypeOptions = {
astNode?: ObjectTypeDefinitionNode;
description?: string;
fields?: FieldsFunction;
};
```
* `description`: A description of the current type
* `fields`: a function that receives a [`FieldBuilder`](./field-builder), and returns an object of
field names to field refs. See [`FieldBuilder`](./field-builder) for more details.
## `mutationFields(fields)` [#mutationfieldsfields]
add a set of fields to the `Mutation` type.
* `fields`: a function that receives a [`FieldBuilder`](./field-builder), and returns an object of
field names to field refs. See [`FieldBuilder`](./field-builder) for more details.
## `mutationField(name, field)` [#mutationfieldname-field]
add a single field to the `Mutation` type.
* `name`: the name of the field
* `field`: a function that receives a [`FieldBuilder`](./field-builder), and returns field ref. See
[`FieldBuilder`](./field-builder) for more details.
## `subscriptionType(options, fields?)` [#subscriptiontypeoptions-fields]
creates the `Subscription` with a set of Subscription fields
* `options`: `SubscriptionTypeOptions`
* `fields?`: a function that receives a [`FieldBuilder`](./field-builder), and returns an object of
field names to field refs. See [`FieldBuilder`](./field-builder) for more details.
### SubscriptionTypeOptions [#subscriptiontypeoptions]
```typescript
type SubscriptionTypeOptions = {
astNode?: ObjectTypeDefinitionNode;
description?: string;
fields?: FieldsFunction;
};
```
* `description`: A description of the current type
* `fields`: a function that receives a [`FieldBuilder`](./field-builder), and returns an object of
field names to field refs. See [`FieldBuilder`](./field-builder) for more details.
## `subscriptionFields(fields)` [#subscriptionfieldsfields]
add a set of fields to the `Subscription` type.
* `fields`: a function that receives a [`FieldBuilder`](./field-builder), and returns an object of
field names to field refs. See [`FieldBuilder`](./field-builder) for more details.
## `subscriptionField(name, field)` [#subscriptionfieldname-field]
add a single field to the `Subscription` type.
* `name`: the name of the field
* `field`: a function that receives a [`FieldBuilder`](./field-builder), and returns field ref. See
[`FieldBuilder`](./field-builder) for more details.
## `objectType(param, options, fields?)` [#objecttypeparam-options-fields]
* `param`: A key of the `Objects` property in `SchemaTypes`, a class, or a TypeRef created by
`builder.objectRef`
* `options`: `ObjectTypeOptions`
* `fields`: a function that receives a [`FieldBuilder`](./field-builder), and returns an object of
field names to field refs. See [`FieldBuilder`](./field-builder) for more details.
### `ObjectTypeOptions` [#objecttypeoptions]
```typescript
type ObjectTypeOptions = {
astNode?: ObjectTypeDefinitionNode;
description?: string;
fields?: FieldsFunction;
interfaces?: Interfaces | (() => Interfaces);
isTypeOf?: (obj: unknown, context, info) => boolean | Promise;
name?: string;
};
```
* `description`: A description of the current type
* `fields`: a function that receives a [`FieldBuilder`](./field-builder), and returns an object of
field names to field refs. See [`FieldBuilder`](./field-builder) for more details.
* `isTypeOf`: Recommended when implementing interfaces. This is a method that will be used when
determining if a value of an implemented interface is of the current type.
* `interfaces`: an array of interfaces implemented by this type. Items in this array
should be an interface param. See `param` argument of `interfaceType`
* `name`: name of GraphQL type. Required when param is a class
## `objectFields(param, fields)` [#objectfieldsparam-fields]
add a set of fields to the object type.
* `param`: A key of the `Objects` property in `SchemaTypes`, a class, or a TypeRef created by
`builder.objectRef`
* `fields`: a function that receives a [`FieldBuilder`](./field-builder), and returns an object of
field names to field refs. See [`FieldBuilder`](./field-builder) for more details.
## `objectField(param, name, field)` [#objectfieldparam-name-field]
add a single field to the object type.
* `name`: the name of the field
* `param`: A key of the `Objects` property in `SchemaTypes`, a class, or a TypeRef created by
`builder.objectRef`
* `field`: a function that receives a [`FieldBuilder`](./field-builder), and returns field ref. See
[`FieldBuilder`](./field-builder) for more details.
## `objectRef(name)` [#objectreftname]
Creates a Ref object representing an object that has not been implemented. This can be useful for
building certain types of plugins, or when building a modular schema where you don't want to define
all types in SchemaTypes, or import the actual implementation of each object type you use.
* `name`: string, name of the type that this ref represents. Can be overwritten when implemented.
* `T`: a type param to define the backing shape for the type that this ref represents
## `interfaceType(param, options, fields?)` [#interfacetypeparam-options-fields]
* `param`: A key of the `Interfaces` property in `SchemaTypes`, a class, or a TypeRef created by
`builder.interfaceRef`
* `options`: `InterfaceTypeOptions`
* `fields`: a function that receives a [`FieldBuilder`](./field-builder), and returns an object of
field names to field refs. See [`FieldBuilder`](./field-builder) for more details.
### `InterfaceTypeOptions` [#interfacetypeoptions]
```typescript
type InterfaceTypeOptions = {
astNode?: InterfaceTypeDefinitionNode;
description?: string;
resolveType?: (parent: InterfaceShape, context, info, abstractType) =>
MaybePromise;
fields?: FieldsFunction;
interfaces?: Interfaces | (() => Interfaces);
name?: string;
};
```
* `description`: A description of the current type
* `fields`: a function that receives a [`FieldBuilder`](./field-builder), and returns an object of
field names to field refs. See [`FieldBuilder`](./field-builder) for more details.
* `interfaces`: an array of interfaces implemented by this type. Items in this array
should be an interface param. See `param` argument of `interfaceType`
* `name`: name of GraphQL type. Required when param is a class
`resolveType` returns an implementing object ref, registered class, or GraphQL type name.
If omitted, GraphQL uses `__typename` or object `isTypeOf` checks. See [Interfaces](../guide/interfaces).
## `interfaceFields(param, fields)` [#interfacefieldsparam-fields]
add a set of fields to the interface type.
* `param`: A key of the `Interfaces` property in `SchemaTypes`, a class, or a TypeRef created by
`builder.interfaceRef`
* `fields`: a function that receives a [`FieldBuilder`](./field-builder), and returns an object of
field names to field refs. See [`FieldBuilder`](./field-builder) for more details.
## `interfaceField(param, name, field)` [#interfacefieldparam-name-field]
add a single field to the interface type.
* `param`: A key of the `Interfaces` property in `SchemaTypes`, a class, or a TypeRef created by
`builder.interfaceRef`
* `name`: the name of the field
* `field`: a function that receives a [`FieldBuilder`](./field-builder), and returns field ref. See
[`FieldBuilder`](./field-builder) for more details.
## `interfaceRef(name)` [#interfacereftname]
Creates a Ref object representing an interface that has not been implemented. This can be useful for
building certain types of plugins, or when building a modular schema where you don't want to define
all types in SchemaTypes, or import the actual implementation of each interface type you use.
* `name`: string, name of the type that this ref represents. Can be overwritten when implemented.
* `T`: a type param to define the backing shape for the type that this ref represents
## `unionType(name, options)` [#uniontypename-options]
* `name`: A string
* `options`: `UnionTypeOptions`
### `UnionTypeOptions` [#uniontypeoptions]
```typescript
type UnionTypeOptions = {
astNode?: UnionTypeDefinitionNode;
description?: string;
types: Member[] | (() => Member[]);
resolveType?: (parent: UnionShape, context, info, abstractType) =>
MaybePromise;
};
```
* `description`: A description of the current type
* `types`: an array of object types included in the union type. Items in this array should be Object
params. See `param` argument in `builder.objectType`.
* `resolveType`: A function called when resolving the type of a union value. `parent` will be a
union of the backing models of the types provided in `types`. Return a member ref, its registered
class, or its GraphQL type name. If omitted, GraphQL uses
`__typename` or member `isTypeOf` checks. See [Unions](../guide/unions).
`types` can also be a record, or a function returning one. `resolveType` then returns one of its
keys:
```typescript
type UnionTypeRecordOptions = {
types: Record | (() => Record);
resolveType?: (parent: UnionShape, context, info, abstractType) =>
MaybePromise;
};
```
With `StrictResolveType: true` in `SchemaTypes`, use the record form, or an array of type names;
`resolveType` may only return members. See
[Strict resolveType](../guide/unions#strict-resolvetype).
## `enumType(param, options)` [#enumtypeparam-options]
* `param`: A string name of the enum or a typescript enum
* `options`: `EnumTypeOptions`
### `EnumTypeOptions` [#enumtypeoptions]
```typescript
type EnumTypeOptions = {
astNode?: EnumTypeDefinitionNode;
description?: string;
values?: Values;
name?: string;
};
```
* `description`: A description of the current type
* `values`: can be either an array of strings (you may need to use `as const` to get proper type
names) or a `GraphQLEnumValueConfigMap`. values is only required when param is not an enum
* `name`: required when param is an enum
When `values` is a map, each entry accepts these options:
```typescript
type EnumValueConfig = {
value?: string | number;
description?: string;
deprecationReason?: string;
extensions?: Readonly>;
astNode?: EnumValueDefinitionNode;
};
```
## `addScalarType(name, scalar, options?)` [#addscalartypename-scalar-options]
* `name`: A key of the `Scalars` property in `SchemaTypes`
* `scalar`: A `GraphQLScalarType`
* `options`: optional scalar options that override the supplied scalar configuration.
## `scalarType(name, options)` [#scalartypename-options]
* `name`: A key of the `Scalars` property in `SchemaTypes`
* `options`: `ScalarTypeOptions`
### ScalarTypeOptions [#scalartypeoptions]
```typescript
type ScalarTypeOptions = {
astNode?: ScalarTypeDefinitionNode;
description?: string;
// Serializes an internal value to include in a response.
serialize?: (value: OutputShape) => unknown;
// Parses an externally provided value to use as an input.
parseValue?: GraphQLScalarValueParser;
// Parses an externally provided literal value to use as an input.
parseLiteral?: GraphQLScalarLiteralParser;
// GraphQL.js 17+ coercion hooks
coerceOutputValue?: (value: OutputShape) => unknown;
coerceInputValue?: GraphQLScalarValueParser;
coerceInputLiteral?: (node: ConstValueNode) => InputShape | null | undefined;
valueToLiteral?: (value: unknown) => ConstValueNode | undefined;
extensions?: Readonly>;
};
```
For portable scalar definitions, provide `serialize`, `parseValue`, and `parseLiteral`.
GraphQL.js 17 also supports `coerceOutputValue`, `coerceInputValue`, `coerceInputLiteral`, and
`valueToLiteral`. These hooks have version-specific requirements; see [Scalars](../guide/scalars).
## `inputType(param, options)` [#inputtypeparam-options]
* `param`: a string or InputRef created by `builder.inputRef`
* `options`: `InputTypeOptions`
### `InputTypeOptions` [#inputtypeoptions]
```typescript
type InputTypeOptions = {
astNode?: InputObjectTypeDefinitionNode;
description?: string;
fields: InputFieldsFunction;
isOneOf?: boolean;
};
```
* `description`: A description of the current type
* `fields`: a function that receives an `InputFieldBuilder`, and returns an object of field names to
field definitions. See [`InputFieldBuilder`](./input-field-builder) for more details. If `param` is
a key of the `Inputs` property in `SchemaTypes`, shape will show type errors for any fields that do
not match the types provided in `SchemaTypes`.
* `isOneOf`: defines a OneOf input object on supported GraphQL.js versions. See
[Input Objects](../guide/inputs). A OneOf input accepts exactly one non-null field; its fields
must be optional and have no defaults. Use a GraphQL.js version with OneOf support.
## `inputRef(name)` [#inputreftname]
Creates a Ref object representing an input object that has not been implemented. This can be useful for
defining recursive input types, for building certain types of plugins, or when building a modular
schema where you don't want to define all types in SchemaTypes, or import the actual implementation
of each input type you use.
* `name`: string, name of the type that this ref represents. Can be overwritten when implemented.
* `T`: a type param to define the backing shape for the type that this ref represents
## `args(fields)` [#argsfields]
Creates an arguments object which can be used as the `args` option in a field definition.
* `fields`: a function that receives an [`ArgBuilder`](./arg-builder), and returns an object of
field names to field definitions. See [`ArgBuilder`](./arg-builder) for more details.
## AST metadata [#ast-metadata]
Type definitions, output fields, input fields, arguments, and enum values accept an optional
`astNode` from `graphql`. Use the matching definition-node type shown in the option summaries;
for example, an object type takes an `ObjectTypeDefinitionNode`, while an enum value takes an
`EnumValueDefinitionNode`. Pothos attaches the node to the corresponding GraphQL.js schema element
for tools that read AST metadata. Continue to define fields, values, and resolver behavior through
the Pothos options; attaching a node does not build those definitions from SDL.
For applied directive metadata, see [Directives](../plugins/directives). GraphQL.js `printSchema`
does not preserve applied custom directives; see [Printing Schemas](../guide/printing-schemas)
when choosing an output format for downstream tools.
## `toSchema(options?)` [#toschemaoptions]
Builds a `GraphQLSchema` from the types registered on the builder. The optional object accepts:
* `directives`: custom `GraphQLDirective` instances to include in the schema.
* `extensions`: schema metadata.
* `sortSchema`: defaults to `true`; set to `false` to skip lexicographic sorting.
* `astNode`: a `SchemaDefinitionNode` from `graphql`; see [AST metadata](#ast-metadata).
Plugins may contribute build-time options. Calling `toSchema` again creates a new schema and
new plugin instances.
## `SchemaBuilder.allowPluginReRegistration` [#schemabuilderallowpluginreregistration]
`SchemaBuilder.allowPluginReRegistration` is a static `boolean` on the SchemaBuilder class that can
be set to allow plugins to call registerPlugin multiple times. This is useful for hot-module
reloading, but is `false` by default to catch any issues with duplicate versions of a plugin.
# File layout
URL: /docs/guide/app-layout
Guide for Pothos app layouts
Pothos tries not to be opinionated about how you structure your code, and provides multiple ways of
doing many things. This short guide covers a few conventions I use, as a starting place for anyone
who is just looking for a decent setup that should just work. Everything suggested here is just a
recommendation and is completely optional.
## Files [#files]
Here are a few files I create in almost every Pothos schema I have built:
* `src/server.ts`: Setup and run your server (This might be graphql-yoga or @apollo/server)
* `src/builder.ts`: Setup for your schema builder. Does not contain any definitions for types in
your schema
* `src/schema.ts` or `src/schema/index.ts`: Imports the schema modules, initializes root types such
as `Query`, and exports `builder.toSchema()`
* `src/types.ts`: Define shared types used across your schema including a type for your context
object. This should be imported when creating your builder, and may be used by many other files.
* `src/schema/*.ts`: Actual definitions for your schema types.
## Imports [#imports]
Import types directly from the files that define them rather than importing from another file like
`index.ts` that re-exports them. `index.ts` files can still be useful for loading all files in a
directory, but they should generally NOT export any values.
## Plugins [#plugins]
Which plugins you use is completely up to you. For my own projects, I will use the `simple-objects`,
`scope-auth`, and `mocks` plugins in every project, and some of the other plugins as needed.
`mocks` and `scope-auth` are fairly self explanatory. The `simple-objects` plugin can make building
out a graph much quicker, because you don't have to have explicit types or models for every object
in your graph. I frequently find that I just want to add an object of a specific shape, and then let
the parent field figure out how to return an object of the right shape.
## Backing models [#backing-models]
You can use your application's TypeScript types as backing models with `objectRef`. For example,
keep the user data type in `src/types.ts` and define its GraphQL fields in `src/schema/user.ts`:
```typescript playground example="architecture-layout"
// src/types.ts
export interface User {
id: string;
name: string;
}
```
```typescript playground example="architecture-layout"
// src/builder.ts
import SchemaBuilder from '@pothos/core';
export const builder = new SchemaBuilder({});
```
```typescript playground example="architecture-layout"
// src/schema/user.ts
import { builder } from '../builder';
import type { User as UserModel } from '../types';
export const User = builder.objectRef('User').implement({
fields: (t) => ({
id: t.exposeID('id'),
name: t.exposeString('name'),
}),
});
builder.queryField('user', (t) =>
t.field({
type: User,
resolve: () => ({ id: '1', name: 'Alex' }),
}),
);
```
Create the query root and import the schema modules before building the schema:
```typescript playground example="architecture-layout"
// src/schema.ts
import { builder } from './builder';
import './schema/user';
builder.queryType({});
export const schema = builder.toSchema();
```
Other schema modules can import `User` directly from `schema/user.ts` when they need to reference
the GraphQL type. If your types reference each other, you may need to separate ref creation from
`implement`, as described in [Circular References](./circular-references).
Classes and types registered on `SchemaTypes` are also supported. See the
[Objects guide](./objects#different-ways-to-define-object-types) for examples of each style.
## Co-locating queries [#co-locating-queries]
The `user` query above lives alongside the `User` type. Other schema modules can add queries with
`builder.queryField` in the same way.
# Using args
URL: /docs/guide/args
Guide for defining field args in Pothos
Arguments let a client pass values to a field's resolver. They work on query, mutation, object,
and interface fields. Define them in a field's `args` option and read them from the resolver's
second parameter.
## Scalars [#scalars]
### Using the `t.arg` method [#using-the-targ-method]
Use `t.arg` with a scalar type name:
```typescript playground example="foundations-arguments"
import SchemaBuilder from '@pothos/core';
const builder = new SchemaBuilder({});
builder.queryType({
fields: (t) => ({
greeting: t.string({
args: {
name: t.arg({ type: 'String', required: true }),
},
resolve: (_parent, args) => `Hello, ${args.name}!`,
}),
}),
});
```
### Using convenience methods [#using-convenience-methods]
The equivalent shorthand for the argument above is `t.arg.string({ required: true })`.
Other scalar helpers include `t.arg.id`, `t.arg.int`, `t.arg.float`, and `t.arg.boolean`.
Each also has a list helper, such as `t.arg.stringList`.
## Required args [#required-args]
Arguments are optional by default. Set `required: true` to reject omitted or `null` values.
See [Changing Default Nullability](./changing-default-nullability) to change this default.
An optional argument can be omitted or explicitly set to `null`. A `defaultValue` applies when
an argument is omitted; it does not replace an explicit `null`. The resolver below handles both:
```typescript playground example="foundations-arguments"
builder.queryField('repeat', (t) =>
t.string({
args: {
text: t.arg.string({ required: true }),
times: t.arg.int({ defaultValue: 2 }),
},
resolve: (_parent, args) => args.text.repeat(Math.max(0, args.times ?? 1)),
}),
);
```
## Lists [#lists]
Wrap a type in an array to create a list argument, or use a list helper:
```typescript playground example="foundations-arguments"
builder.queryField('knownGiraffes', (t) =>
t.stringList({
args: {
names: t.arg.stringList({ required: true }),
moreNames: t.arg({ type: ['String'], required: true }),
},
resolve: (_parent, args) =>
[...args.names, ...args.moreNames].filter((name) => ['Gina', 'James'].includes(name)),
}),
);
```
List items are non-null by default, even when the list argument is optional. Use separate `list`
and `items` settings to allow null entries in a required list:
```typescript playground example="foundations-arguments"
builder.queryField('nonNullNames', (t) =>
t.stringList({
args: {
names: t.arg.stringList({
required: { list: true, items: false },
}),
},
resolve: (_parent, args) => args.names.filter((name) => name != null),
}),
);
```
## Other types [#other-types]
Arguments can also use [enums](./enums) and [input objects](./inputs). Output objects, interfaces,
and unions cannot be argument types.
This object field accepts an enum argument to choose the unit for its result:
```typescript playground example="foundations-arguments"
const LengthUnit = builder.enumType('LengthUnit', {
values: { Feet: {}, Meters: {} },
});
const GiraffeRef = builder.objectRef<{ heightInMeters: number }>('Giraffe').implement({
fields: (t) => ({
height: t.float({
args: {
unit: t.arg({ type: LengthUnit, defaultValue: 'Meters' }),
},
resolve: (giraffe, args) =>
args.unit === 'Feet' ? giraffe.heightInMeters * 3.281 : giraffe.heightInMeters,
}),
}),
});
builder.queryField('giraffe', (t) =>
t.field({
type: GiraffeRef,
resolve: () => ({ heightInMeters: 5 }),
}),
);
```
## Nested Lists [#nested-lists]
Use `t.arg.listRef` to nest lists. Its `required` option controls the items of that list;
`required` on `t.arg` controls whether the outermost list can be omitted or null.
```typescript playground example="foundations-arguments"
builder.queryField('countNames', (t) =>
t.int({
args: {
groups: t.arg({
type: t.arg.listRef(t.arg.listRef('String', { required: false })),
required: true,
}),
},
resolve: (_parent, args) =>
args.groups.reduce((count, group) => count + group.filter((name) => name != null).length, 0),
}),
);
export const schema = builder.toSchema();
```
Here, `groups` has GraphQL type `[[String]!]!`: the outer list and each inner list are required,
but strings inside the inner lists can be null. Without `{ required: false }`, those strings
would also be non-null.
## Querying with arguments [#querying-with-arguments]
The examples above form one schema. This query exercises required arguments, defaults, lists,
and the enum argument on an object field:
```graphql playground example="foundations-arguments"
query Arguments {
greeting(name: "Gina")
repeat(text: "ha")
once: repeat(text: "ha", times: null)
knownGiraffes(names: ["Gina", "Unknown"], moreNames: ["James"])
nonNullNames(names: ["Gina", null, "James"])
giraffe {
meters: height
feet: height(unit: Feet)
}
countNames(groups: [["Gina", null], ["James"]])
}
```
```json
{
"data": {
"greeting": "Hello, Gina!",
"repeat": "haha",
"once": "ha",
"knownGiraffes": ["Gina", "James"],
"nonNullNames": ["Gina", "James"],
"giraffe": { "meters": 5, "feet": 16.405 },
"countNames": 2
}
}
```
# Default nullability
URL: /docs/guide/changing-default-nullability
Configure schema-wide defaults and override them for individual fields.
By default, Pothos output fields are nullable, and arguments and input object fields are optional.
Use `nullable: false` to make an output field non-null, or `required: true` to require an input.
You can change these defaults for a builder. Set each default in both the `SchemaTypes` generic
for TypeScript checking and the constructor options for runtime schema generation:
```typescript playground example="types-nullability"
import SchemaBuilder from '@pothos/core';
const builder = new SchemaBuilder<{
DefaultFieldNullability: false;
DefaultInputFieldRequiredness: true;
}>({
defaultFieldNullability: false,
defaultInputFieldRequiredness: true,
});
const GreetingInput = builder.inputType('GreetingInput', {
fields: (t) => ({
name: t.string(),
title: t.string({ required: false }),
}),
});
builder.queryType({
fields: (t) => ({
greeting: t.string({
args: { input: t.arg({ type: GreetingInput }) },
resolve: (_parent, { input }) =>
`Hello, ${input.title ? `${input.title} ` : ''}${input.name}!`,
}),
nickname: t.string({ nullable: true, resolve: () => null }),
names: t.stringList({ resolve: () => ['Ada'] }),
}),
});
export const schema = builder.toSchema();
```
These settings are independent: omit the input setting from both places if you only want to change
output nullability, or omit the output setting to change only input requiredness.
The example produces these field types:
```graphql
input GreetingInput {
name: String!
title: String
}
type Query {
greeting(input: GreetingInput!): String!
names: [String!]!
nickname: String
}
```
`nullable: true` and `required: false` override the new defaults on individual fields. List items
remain non-null by default; changing the builder defaults controls the list itself. Use the object
forms of `nullable` or `required` to configure items separately, as shown in [Fields](./fields) and
[Arguments](./args).
```graphql playground example="types-nullability"
query Nullability {
greeting(input: { name: "Ada" })
names
nickname
}
```
```json
{
"data": {
"greeting": "Hello, Ada!",
"names": ["Ada"],
"nickname": null
}
}
```
Changing defaults changes the public schema for fields without explicit settings. Requiring an
existing optional argument or input field can break client requests. Non-null output fields also
change error propagation: if a resolver returns null or throws, GraphQL propagates the null to the
nearest nullable parent, potentially making the entire response's `data` null.
For example, if `nickname`'s resolver throws `new Error('Nickname unavailable')`,
`nullable: true` makes only `nickname` null. Without that override, the builder's non-null default
makes the whole response's `data` null.
# Circular References
URL: /docs/guide/circular-references
Managing circular imports and recursive types in Pothos
Two GraphQL types can refer to each other. The problems that sometimes accompany this come from
JavaScript module initialization or TypeScript inference. They need different fixes.
## Circular imports [#circular-imports]
If two modules import each other, reading an export before its initialization can produce an
undefined value or a `ReferenceError`, depending on the module system. Pothos defers field callbacks
until `builder.toSchema()`, so references inside those callbacks can work even when the modules
import each other.
Keep the builder in a module that does not import your schema definitions. Import all schema modules
before calling `toSchema()`, and do not import that schema entry point from a type definition. See
[App Layout](./app-layout) for a complete file layout.
In this example, `user.ts` and `post.ts` import each other, but neither reads the other module's ref
until its field callback runs. The backing types use type-only imports, which do not run JavaScript.
```typescript playground example="architecture-circular"
// builder.ts
import SchemaBuilder from '@pothos/core';
export const builder = new SchemaBuilder({});
```
```typescript playground example="architecture-circular"
// user.ts
import { builder } from './builder';
import { Post, type PostShape } from './post';
export interface UserShape {
name: string;
posts: PostShape[];
}
export const User = builder.objectRef('User');
User.implement({
fields: (t) => ({
name: t.exposeString('name'),
posts: t.expose('posts', { type: [Post] }),
}),
});
```
```typescript playground example="architecture-circular"
// post.ts
import { builder } from './builder';
import { User, type UserShape } from './user';
export interface PostShape {
title: string;
author: UserShape;
}
export const Post = builder.objectRef('Post');
Post.implement({
fields: (t) => ({
title: t.exposeString('title'),
author: t.expose('author', { type: User }),
}),
});
```
```typescript playground example="architecture-circular"
// schema.ts
import { builder } from './builder';
import { User, type UserShape } from './user';
import './post';
const user: UserShape = { name: 'Alex', posts: [] };
user.posts.push({ title: 'First post', author: user });
builder.queryType({
fields: (t) => ({
user: t.field({ type: User, resolve: () => user }),
}),
});
export const schema = builder.toSchema();
```
The schema supports queries that follow the relationship in both directions:
```graphql
{
user {
name
posts {
title
author {
name
}
}
}
}
```
Import values directly from the modules that define them. A barrel file that re-exports every type
can introduce extra cycles. An entry point can import type-definition modules for their side effects
without re-exporting their values.
Deferring fields does not defer every option. For example, an `interfaces: [SomeInterface]` array is
evaluated immediately. If a cycle reads an uninitialized ref there, change the imports or move the
refs into a separate module that does not import their implementations.
## TypeScript inference [#typescript-inference]
A chained declaration such as `const User = builder.objectRef('User').implement(...)`
can create an inference cycle when its fields refer back to `User`, directly or through another type.
Declare the ref first, then call `User.implement(...)` separately, as in the example above. TypeScript
can determine the ref's type without inspecting the fields. This breaks the inference dependency;
it does not remove the JavaScript import cycle.
You can also pass the declared ref to `builder.objectType`. As an alternative to the `Post.implement`
call above, define its fields in separate calls:
```typescript playground example="architecture-circular-variant-object-type"
builder.objectType(Post, {
fields: (t) => ({ title: t.exposeString('title') }),
});
builder.objectField(Post, 'author', (t) => t.expose('author', { type: User }));
```
Classes already used by your application and names registered in the builder's `Objects` or
`Interfaces` [SchemaTypes](./objects#using-schematypes) also supply backing types independently of
field inference. String names can avoid importing refs between type-definition modules, but all
implementations still need to be loaded before `toSchema()`.
## Recursive inputs [#recursive-inputs]
Input types infer their shape from their fields. For recursive inputs, declare the backing shape and
an input ref explicitly, as described in [Recursive Inputs](./inputs#recursive-inputs).
Plugins that infer backing types from their options may also need explicit types to break an
inference cycle; check the relevant plugin's documentation.
# Using Context
URL: /docs/guide/context
Guide for using context object in Pothos
The GraphQL context object gives resolvers access to shared state for the current request. For
example, you can use it to make the current user available throughout your schema.
First, let's define the user data and add a `Context` type to the builder:
```typescript playground example="foundations-context"
import SchemaBuilder from '@pothos/core';
interface User {
id: string;
firstName: string;
username: string;
}
const builder = new SchemaBuilder<{
Context: {
currentUser: User | null;
};
}>({});
```
Next, we can define a GraphQL object for the user and a query that reads it from context:
```typescript playground example="foundations-context"
const UserRef = builder.objectRef('User').implement({
fields: (t) => ({
id: t.exposeID('id'),
firstName: t.exposeString('firstName'),
username: t.exposeString('username'),
}),
});
builder.queryType({
fields: (t) => ({
currentUser: t.field({
type: UserRef,
nullable: true,
resolve: (_root, _args, context) => context.currentUser,
}),
}),
});
```
The resolver's `context` parameter has the type we provided to the builder. Clients can query the
current user with:
```graphql playground example="foundations-context"
query SignedIn {
currentUser {
id
username
}
}
```
The example context values simulate request data; the server setup below performs the actual
user lookup.
When there is no signed-in user, the response is `{ "data": { "currentUser": null } }`.
Your GraphQL server creates the context for each request. Here is an example using GraphQL Yoga,
where `getUserFromAuthHeader` is an application function that looks up a user and returns `User | null`:
```typescript
import { createServer } from 'node:http';
import { initContextCache } from '@pothos/core';
import { createYoga } from 'graphql-yoga';
import { getUserFromAuthHeader } from './auth';
const yoga = createYoga({
schema: builder.toSchema(),
context: async ({ request }) => ({
...initContextCache(),
currentUser: await getUserFromAuthHeader(request.headers.get('authorization')),
}),
});
const server = createServer(yoga);
server.listen(3000);
```
## Initialize context cache [#initialize-context-cache]
Create a new context object for each request. Plugins such as dataloader and scope-auth use the
context object's identity to cache data for that request's resolvers.
`initContextCache()` lets those plugins share their cache when a server copies or extends the
context. Call it inside the context factory so each request gets a separate cache key. If your server
passes the same context object to every resolver, plugins can cache by its identity without this
helper.
## Context when using multiple protocols [#context-when-using-multiple-protocols]
If your HTTP and WebSocket handlers provide different context properties, a
[discriminated union](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions)
lets resolvers check which properties are available:
```typescript playground example="foundations-context-transports"
type Context =
| {
transport: 'http';
request: Request;
}
| {
transport: 'websocket';
connectionParams: { clientName: string };
};
const builder = new SchemaBuilder<{
Context: Context;
}>({});
builder.queryType({
fields: (t) => ({
clientName: t.string({
nullable: true,
resolve: (_root, _args, context) => {
if (context.transport === 'http') {
return context.request.headers.get('x-client-name');
}
return context.connectionParams.clientName;
},
}),
}),
});
```
The example runs the WebSocket-shaped branch with a JSON context fixture. Its `Context` editor
can represent `connectionParams`, but an HTTP `Request` must come from your server; editing the
playground's **Headers** does not create one or start a WebSocket connection.
Checking `transport` narrows the context type, so the resolver can access the properties for that
transport. Set the discriminator and the corresponding properties when creating the context in each
handler.
# Enums
URL: /docs/guide/enums
Define enum values and use them in fields and arguments.
A GraphQL enum defines a fixed set of named values. `builder.enumType` returns a reference you can
use as a field or argument type.
## Defining an enum [#defining-an-enum]
An array of strings uses the same values in GraphQL and in your resolvers:
```typescript playground example="types-enums"
import SchemaBuilder from '@pothos/core';
const builder = new SchemaBuilder({});
const LengthUnit = builder.enumType('LengthUnit', {
values: ['Feet', 'Meters'],
});
builder.queryType({
fields: (t) => ({
height: t.float({
args: {
unit: t.arg({ type: LengthUnit, required: true, defaultValue: 'Meters' }),
},
resolve: (_parent, { unit }) => (unit === 'Meters' ? 5 : 5 * 3.281),
}),
unit: t.field({
type: LengthUnit,
args: { value: t.arg({ type: LengthUnit, required: true }) },
resolve: (_parent, { value }) => value,
}),
}),
});
export const schema = builder.toSchema();
```
Pothos infers the inline array as the literal values `'Feet' | 'Meters'`. If you define the array
in a separate variable, use `as const` to retain those literal types.
```graphql playground example="types-enums"
query Enums {
height
feet: height(unit: Feet)
unit(value: Meters)
}
```
```json
{
"data": {
"height": 5,
"feet": 16.405,
"unit": "Meters"
}
}
```
Enum literals in a query are unquoted names. When passing an enum in JSON variables, use a string
such as `"Meters"` or `"Feet"`.
## Descriptions and internal values [#descriptions-and-internal-values]
Use a values object to describe or deprecate individual values, or to map GraphQL names to different
values in your application:
```typescript playground example="types-enum-species"
const GiraffeSpecies = builder.enumType('GiraffeSpecies', {
values: {
Southern: {
description: 'Also known as two-horned giraffe',
value: 'giraffa',
},
Masai: { value: 'tippelskirchi' },
Reticulated: { value: 'reticulata' },
Northern: { value: 'camelopardalis' },
Unknown: { deprecationReason: 'Use a nullable species field instead.' },
},
});
```
The keys are GraphQL names. An argument containing `Northern` reaches the resolver as
`'camelopardalis'`; returning `'camelopardalis'` from a `GiraffeSpecies` field produces `"Northern"`
in the response. If `value` is omitted, the key is also the internal value. Deprecating a value
leaves it usable by clients.
## TypeScript enums [#typescript-enums]
If your application already uses a TypeScript enum, register it directly:
```typescript playground example="types-enum-diet"
enum Diet {
HERBIVOROUS = 0,
CARNIVOROUS = 1,
OMNIVOROUS = 2,
}
builder.enumType(Diet, { name: 'Diet' });
builder.queryField('diet', (t) =>
t.field({
type: Diet,
resolve: () => Diet.HERBIVOROUS,
}),
);
```
The enum's keys become GraphQL value names. Resolvers receive and return the TypeScript enum's
values, including numeric values in this example. You can use either the TypeScript enum itself
or the reference returned by `enumType` as the field or argument type.
## Objects with `as const` [#objects-with-as-const]
You can also derive enum values from an existing object:
Mapped values
Values as names
Keys as names
```typescript playground example="types-enum-objects"
const VehicleType = {
sedan: 'SEDAN',
suv: 'SUV',
truck: 'TRUCK',
motorcycle: 'MOTORCYCLE',
} as const;
const VehicleTypeEnum = builder.enumType('VehicleType', {
values: Object.fromEntries(Object.entries(VehicleType).map(([name, value]) => [name, { value }])),
});
```
```typescript playground example="types-enum-objects-variant-values"
const VehicleType = {
sedan: 'SEDAN',
suv: 'SUV',
truck: 'TRUCK',
motorcycle: 'MOTORCYCLE',
} as const;
const VehicleTypeEnum = builder.enumType('VehicleType', {
values: Object.values(VehicleType),
});
```
```typescript playground example="types-enum-objects-variant-keys"
const VehicleType = {
sedan: 'SEDAN',
suv: 'SUV',
truck: 'TRUCK',
motorcycle: 'MOTORCYCLE',
} as const;
const VehicleTypeEnum = builder.enumType('VehicleType', {
values: Object.keys(VehicleType) as (keyof typeof VehicleType)[],
});
```
In **Mapped values**, the GraphQL names are `sedan`, `suv`, `truck`, and `motorcycle`, while resolvers
use `SEDAN`, `SUV`, `TRUCK`, and `MOTORCYCLE`.
**Values as names** uses `Object.values(VehicleType)` for both names and internal values.
**Keys as names** uses `Object.keys(VehicleType)` for both. TypeScript types `Object.keys` as
`string[]`, so the assertion preserves the known keys of this local object.
In each variant, `vehicle` returns the GraphQL enum name, while `internalValue` exposes the value
the argument resolver receives.
# Fields
URL: /docs/guide/fields
Guide for defining fields in Pothos
Fields for [Object](./objects) and [Interface](./interfaces) types are defined using a shape
function. This is a function that takes a [FieldBuilder](../api/field-builder) as an argument, and
returns an object whose keys are field names, and whose values are fields created by the
[FieldBuilder](../api/field-builder). These examples will mostly add fields to the `Query` type, but
the topics covered in this guide should apply to any object or interface type.
## Scalars [#scalars]
Scalar fields can be defined a couple of different ways:
### Field method [#field-method]
```typescript playground example="foundations-field-scalars"
builder.queryType({
fields: (t) => ({
name: t.field({
description: 'Name field',
type: 'String',
resolve: () => 'Gina',
}),
}),
});
```
### Convenience methods [#convenience-methods]
Convenience methods are just wrappers around the `field` method that omit the `type` option.
```typescript playground example="foundations-field-scalars"
builder.queryFields((t) => ({
id: t.id({ resolve: () => '123' }),
int: t.int({ resolve: () => 123 }),
float: t.float({ resolve: () => 1.23 }),
boolean: t.boolean({ resolve: () => false }),
string: t.string({ resolve: () => 'abc' }),
idList: t.idList({ resolve: () => ['123'] }),
intList: t.intList({ resolve: () => [123] }),
floatList: t.floatList({ resolve: () => [1.23] }),
booleanList: t.booleanList({ resolve: () => [false] }),
stringList: t.stringList({ resolve: () => ['abc'] }),
}));
```
Each Playground action opens a complete schema. In the scalar example, `queryFields` adds
convenience fields to the `Query` type already defined with `queryType`.
## Other types [#other-types]
Fields for non-scalar fields can also be created with the `field` method.
Some types like [Objects](./objects) and [Interfaces](./interfaces) can be referenced by name if
they have a backing model defined in the schema builder.
```typescript playground example="foundations-field-types"
const builder = new SchemaBuilder<{
Objects: { Giraffe: { name: string } };
}>({});
builder.objectType('Giraffe', {
fields: (t) => ({
name: t.exposeString('name', {}),
}),
});
builder.queryType({
fields: (t) => ({
giraffe: t.field({
description: 'A giraffe',
type: 'Giraffe',
resolve: () => ({ name: 'Gina' }),
}),
}),
});
```
For types not described in the `SchemaTypes` type provided to the builder, including types that can
not be added there like [Unions](./unions) and [Enums](./enums), you can use a `Ref` returned by the
builder method that created them in the `type` parameter. For types created using a class
([Objects](./objects) or [Interfaces](./interfaces)) or [Enums](./enums) created using a typescript
enum, you can also use the `class` or `enum` that was used to define them.
Here we add an enum field to the `Giraffe` type defined above:
```typescript playground example="foundations-field-types"
const LengthUnit = builder.enumType('LengthUnit', {
values: { Feet: {}, Meters: {} },
});
builder.objectField('Giraffe', 'preferredNeckLengthUnit', (t) =>
t.field({
type: LengthUnit,
resolve: () => 'Feet' as const,
}),
);
```
## Lists [#lists]
To create a list field, you can wrap the type in an array
```typescript playground example="foundations-field-types"
builder.queryFields((t) => ({
giraffes: t.field({
description: 'multiple giraffes',
type: ['Giraffe'],
resolve: () => [{ name: 'Gina' }, { name: 'James' }],
}),
giraffeNames: t.field({
type: ['String'],
resolve: () => ['Gina', 'James'],
}),
}));
```
## NonNullable fields [#nonnullable-fields]
Fields in Pothos are nullable by default, but fields can be made NonNullable by setting the
`nullable` option to `false`. This default can also be changed in the SchemaBuilder constructor, see
[Changing Default Nullability](./changing-default-nullability) for more details.
```typescript playground example="foundations-field-nullability"
builder.queryType({
fields: (t) => ({
nonNullableField: t.field({
type: 'String',
nullable: false,
resolve: () => 'Gina',
}),
nonNullableString: t.string({
nullable: false,
resolve: () => 'Gina',
}),
nonNullableList: t.field({
type: ['String'],
nullable: false,
resolve: () => ['Gina', 'James'],
}),
sparseList: t.field({
type: ['String'],
nullable: {
list: false,
items: true,
},
resolve: () => [null],
}),
}),
});
```
Note that by default even if a list field is nullable, the items in that list are not. The last
example above shows how you can make list items nullable.
## Exposing fields from the underlying data [#exposing-fields-from-the-underlying-data]
Some GraphQL implementations have a concept of "default resolvers" that can automatically resolve
fields that have a property of the same name in the underlying data. In Pothos, these relationships
need to be explicitly defined, but there are helper methods that make exposing fields easier.
These helpers are not available for root types (Query, Mutation and Subscription), but will work
on any other object type or interface.
```typescript playground example="foundations-field-types"
const builder = new SchemaBuilder<{
Objects: { Giraffe: { name: string } };
}>({});
builder.objectType('Giraffe', {
fields: (t) => ({
name: t.exposeString('name', {}),
}),
});
```
The available expose helpers are:
* `exposeString`
* `exposeInt`
* `exposeFloat`
* `exposeBoolean`
* `exposeID`
* `exposeStringList`
* `exposeIntList`
* `exposeFloatList`
* `exposeBooleanList`
* `exposeIDList`
## Arguments [#arguments]
Arguments for a field can be defined in the options for a field:
```typescript playground example="foundations-field-types"
builder.queryFields((t) => ({
giraffeByName: t.field({
type: 'Giraffe',
args: {
name: t.arg.string({ required: true }),
},
resolve: (_root, args) => {
if (args.name !== 'Gina') {
throw new Error(`Unknown Giraffe ${args.name}`);
}
return { name: 'Gina' };
},
}),
}));
```
For more information see the [Arguments Guide](./args).
## Adding fields to existing type [#adding-fields-to-existing-type]
In addition to being able to define fields when defining types, you can also add additional fields
independently. This is useful for breaking up types with a lot of fields into multiple files, or
co-locating fields with their type (e.g., add all query/mutation fields for a type in the same file
where the type is defined).
```typescript playground example="foundations-field-types"
builder.queryFields((t) => ({
newestGiraffe: t.field({
type: 'Giraffe',
resolve: () => ({ name: 'James' }),
}),
}));
builder.objectField('Giraffe', 'nameLength', (t) =>
t.int({
resolve: (parent) => parent.name.length,
}),
);
```
To see all the methods available for defining fields see the [SchemaBuilder API](../api/schema-builder)
The example adds `newestGiraffe` separately from the original `giraffe` field. Query
`newestGiraffe { name nameLength }` to see both the added root field and the computed object field.
## Nested Lists [#nested-lists]
You can use `t.listRef` to create a list of lists
```typescript playground example="foundations-field-nested-lists"
builder.queryType({
fields: (t) => ({
example: t.field({
type: t.listRef(
t.listRef('String'),
// items are non-nullable by default, this can be overridden
// by passing `nullable: true`
{ nullable: true },
),
resolve: () => {
return [['a', 'b'], ['c', 'd'], null];
},
}),
}),
});
```
# Generating client types
URL: /docs/guide/generating-client-types
Guide for generating client types from a Pothos schema
Pothos does not have a built in mechanism for generating types to use with a client, but
[graphql-code-generator](https://www.graphql-code-generator.com/) can be configured to consume a
schema directly from your typescript files.
## Export your schema [#export-your-schema]
The first thing you will need is a file that exports your built schema. The schema should be
exported as `schema` or as the default export. This will be used to generate your client types, but
can also be the schema you use in your server.
```typescript playground example="architecture-client-types"
import SchemaBuilder from '@pothos/core';
const builder = new SchemaBuilder({});
const Post = builder.objectRef<{ id: string; title: string }>('Post');
Post.implement({ fields: (t) => ({ id: t.exposeID('id'), title: t.exposeString('title') }) });
const User = builder.objectRef<{ id: string; name: string }>('User');
User.implement({
fields: (t) => ({
id: t.exposeID('id'),
name: t.exposeString('name'),
posts: t.field({ type: [Post], resolve: () => [{ id: 'post-1', title: 'First post' }] }),
}),
});
builder.queryType({
fields: (t) => ({ user: t.field({ type: User, resolve: () => ({ id: '1', name: 'Alex' }) }) }),
});
export const schema = builder.toSchema();
```
Save the example as `src/schema.ts` and the operation as `src/queries.graphql` to use them with the local Codegen configuration below. Codegen runs in your project, not in the playground.
In a larger application, this entry point can import your builder and type-definition modules before exporting `builder.toSchema()`. Keep server startup separate so Codegen can import the schema without opening a listening port.
## Setting up graphql-code-generator [#setting-up-graphql-code-generator]
There are many different ways to set up graphql-code-generator, and the details depend a lot on your
needs.
See the
[graphql-code-generator documentation](https://www.graphql-code-generator.com/docs/getting-started/installation)
for more details.
### Install the codegen packages [#install-the-codegen-packages]
npm
pnpm
yarn
bun
```bash
npm install --save graphql
npm install --save -D typescript @graphql-codegen/cli @graphql-codegen/client-preset
```
```bash
pnpm add graphql
pnpm add -D typescript @graphql-codegen/cli @graphql-codegen/client-preset
```
```bash
yarn add graphql
yarn add --dev typescript @graphql-codegen/cli @graphql-codegen/client-preset
```
```bash
bun add graphql
bun add --dev typescript @graphql-codegen/cli @graphql-codegen/client-preset
```
### Configure the codegen to import your schema [#configure-the-codegen-to-import-your-schema]
Create a `codegen.ts` file in the root of your project:
```typescript
import type { CodegenConfig } from '@graphql-codegen/cli';
import { printSchema } from 'graphql';
import { schema } from './src/schema';
const config: CodegenConfig = {
schema: printSchema(schema),
documents: ['src/**/*.{graphql,ts,tsx}', '!src/gql/**'],
generates: {
'./src/gql/': {
preset: 'client',
plugins: [],
},
},
};
export default config;
```
You can customize this config as needed, but the relevant parts are:
* Importing your GraphQL schema, this should be the result of calling `builder.toSchema({})`
* using `printSchema` from `graphql` to convert the schema to a string
## Run generation [#run-generation]
The `documents` pattern must match your client queries, mutations, and fragments. Adjust it for your
project and exclude the generated directory. For example, a file named `src/queries.graphql` can
contain a named query that selects fields from your schema.
```sh
npx graphql-codegen --config codegen.ts
```
Run this again when your schema or operations change. The
[client preset](https://the-guild.dev/graphql/codegen/plugins/presets/preset-client) generates typed
operation documents and helpers in `src/gql/`; the result types reflect the fields each operation
selects. The CLI imports your schema module, so its environment variables and other runtime
dependencies must be available. Keep server startup outside that module.
## Generating a schema.graphql file with graphql-code-generator [#generating-a-schemagraphql-file-with-graphql-code-generator]
You can generate a schema.graphql file with graphql-code-generator by adding the `schema-ast`
plugin:
npm
pnpm
yarn
bun
```bash
npm install --save -D @graphql-codegen/schema-ast
```
```bash
pnpm add -D @graphql-codegen/schema-ast
```
```bash
yarn add --dev @graphql-codegen/schema-ast
```
```bash
bun add --dev @graphql-codegen/schema-ast
```
```typescript
// codegen.ts
import { printSchema } from 'graphql';
import type { CodegenConfig } from '@graphql-codegen/cli';
import { schema } from './src/schema';
const config: CodegenConfig = {
schema: printSchema(schema),
documents: ['src/**/*.{graphql,ts,tsx}', '!src/gql/**'],
generates: {
'./src/gql/': {
preset: 'client',
plugins: [],
},
'schema.graphql': {
plugins: ['schema-ast'],
},
},
};
export default config;
```
## Adding scalars [#adding-scalars]
Configure custom scalars to match their serialized values on the client. Add the following `config`
property to the configuration above:
```typescript
config: {
strictScalars: true,
scalars: {
UUID: 'string',
DateTime: 'string',
},
},
```
A DateTime scalar commonly serializes to an ISO date string. Mapping it to `Date` changes only the
TypeScript type; Codegen does not convert JSON strings into JavaScript `Date` instances. Use `Date`
only if your client actually performs that conversion. `strictScalars` makes generation fail when a
custom scalar has no mapping; see the [client preset configuration](https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#config-api).
## Alternatives [#alternatives]
In some cases you may want to use an alternative method for loading your schema.
### Printing the schema to a file [#printing-the-schema-to-a-file]
You can use the `printSchema` function from `graphql` to print your schema to a file, see
[Printing Schemas](/docs/guide/printing-schemas) for more details:
By writing the schema to a file, you will be able to load the schema from a file instead of importing
it each time you want to generate your schema.
Having your schema written to a file, and checked into source control has many benefits, like easier
code reviews, and better interoperability with other schema dependent graphql tools, so setting this
up is worthwhile even if you do not need it for generating client types:
```typescript
import type { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
schema: './path/to/schema.graphql',
documents: ['src/**/*.{graphql,ts,tsx}', '!src/gql/**'],
generates: {
'./src/gql/': {
preset: 'client',
plugins: [],
},
},
};
export default config;
```
### Using introspection from your dev (or production) server [#using-introspection-from-your-dev-or-production-server]
Rather than using a schema SDL file, graphql-code-generator can also use introspection to load
your schema. To do this, you will need to ensure that your server has introspection enabled and that Codegen can authenticate if the endpoint requires it.
Introspection defaults depend on the server and its configuration.
You can then configure graphql-code-generator to use introspection by passing the URL to your
graphql endpoint:
```typescript
import type { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
schema: 'http://localhost:3000/graphql',
documents: ['src/**/*.{graphql,ts,tsx}', '!src/gql/**'],
generates: {
'./src/gql/': {
preset: 'client',
plugins: [],
},
},
};
export default config;
```
# Inferring Types
URL: /docs/guide/inferring-types
Extract backing types and builder types for TypeScript helpers
In some cases you may want to use the types from your input or object refs to build helpers, or
provide accurate types for other functions.
To get types from any Pothos `ref` object, you can use the `$inferType` and `$inferInput` properties
on the corresponding output or input ref. Use these properties in TypeScript type expressions,
not to read runtime data. An object ref carries its backing model, not a GraphQL query result: clients
select fields, and resolvers can expose fields that are not properties of the backing model.
```typescript playground example="types-inference"
import SchemaBuilder from '@pothos/core';
const builder = new SchemaBuilder({});
const MyInput = builder.inputType('MyInput', {
fields: (t) => ({
id: t.id({ required: true }),
name: t.string({ required: true }),
}),
});
// { id: string; name: string; }
type MyInputShape = typeof MyInput.$inferInput;
const UserRef = builder.objectRef<{ id: string; name: string }>('User').implement({
fields: (t) => ({
id: t.exposeID('id'),
name: t.exposeString('name'),
}),
});
type UserType = typeof UserRef.$inferType;
```
When building helpers, most Pothos types have a generic called `Types` that extends `SchemaTypes`.
This combines all the defaults and settings passed in when creating the SchemaBuilder. To make your
own type helpers and utility functions, you often need access to the `Types` used by your builder.
This can be inferred from the builder using `typeof builder.$inferSchemaTypes`.
The following is a simple helper for creating objects that have an `id` field. The helper
itself isn't that useful, but shows how inferring SchemaTypes from a builder can work.
```typescript playground example="types-inference"
import type { FieldMap } from '@pothos/core';
type BuilderTypes = typeof builder.$inferSchemaTypes;
function createObjectWithId(
name: string,
fields: (t: PothosSchemaTypes.ObjectFieldBuilder) => FieldMap,
) {
const ref = builder.objectRef(name);
ref.implement({
fields: (t) => ({
...fields(t),
id: t.id({
resolve: (parent) => parent.id,
nullable: false,
}),
}),
});
return ref;
}
const UserWithId = createObjectWithId<{
id: string;
name: string;
}>('UserWithId', (t) => ({
name: t.exposeString('name'),
}));
```
Rather than explicitly using the inferred type, you can also infer SchemaTypes from the builder in
an argument. In the following example, we pass in the builder to the createPaginationArgs, and infer
the `Types` from the provided builder. This is useful when building helpers that might be used with
multiple builder instances.
```typescript playground example="types-inference"
import type { SchemaTypes } from '@pothos/core';
function createPaginationArgs(
builder: PothosSchemaTypes.SchemaBuilder,
) {
return builder.args((t) => ({
limit: t.int(),
offset: t.int(),
}));
}
const users: UserType[] = [
{ id: '1', name: 'Ada' },
{ id: '2', name: 'Grace' },
];
function toUser(input: MyInputShape): UserType {
return { id: input.id, name: input.name };
}
builder.queryType({
fields: (t) => ({
user: t.field({
type: UserRef,
args: { input: t.arg({ type: MyInput, required: true }) },
resolve: (_parent, { input }) => toUser(input),
}),
}),
});
builder.queryField('getUsers', (t) =>
t.field({
type: [UserWithId],
args: {
...createPaginationArgs(builder),
},
resolve: (_parent, { limit, offset }) =>
users.slice(offset ?? 0, (offset ?? 0) + (limit ?? users.length)),
}),
);
```
The example uses the inferred input shape in `toUser` and returns real users from `getUsers`.
A `limit` of `1` returns one user; `2` returns both. If `MyInput.name` is defined with
`t.int({ required: true })` instead of `t.string({ required: true })`, the inferred input has a
numeric name, so returning it from
`toUser` produces a TypeScript error: `UserType` still requires a string. The GraphQL input field
also changes from `String!` to `Int!`.
# Input Objects
URL: /docs/guide/inputs
Guide for defining Input Object types in Pothos
Input objects group related argument values. Define one with `builder.inputType`, then use the
returned ref as an argument type. Pothos infers the input shape from its fields.
## Creating input objects [#creating-input-objects]
The “Creating input objects” and “Recursive inputs” sections build on the same schema. Start with a backing model and
an object ref for the mutation result:
```typescript playground example="foundations-inputs"
import SchemaBuilder from '@pothos/core';
type Giraffe = {
name: string;
birthdate: string;
height: number;
};
const builder = new SchemaBuilder({});
const giraffes: Giraffe[] = [];
const GiraffeRef = builder.objectRef('Giraffe').implement({
fields: (t) => ({
name: t.exposeString('name'),
birthdate: t.exposeString('birthdate'),
height: t.exposeFloat('height'),
}),
});
builder.queryType({
fields: (t) => ({
giraffes: t.field({ type: [GiraffeRef], resolve: () => giraffes }),
}),
});
```
Input objects and output objects are separate GraphQL types, even when they have the same fields.
Here, `GiraffeInput` describes the argument and `Giraffe` describes the result. The array stores the
created giraffes in memory for this example.
```typescript playground example="foundations-inputs"
const GiraffeInput = builder.inputType('GiraffeInput', {
fields: (t) => ({
name: t.string({ required: true }),
birthdate: t.string({ required: true }),
height: t.float({ required: true }),
}),
});
builder.mutationType({
fields: (t) => ({
createGiraffe: t.field({
type: GiraffeRef,
args: {
input: t.arg({ type: GiraffeInput, required: true }),
},
resolve: (_root, { input }) => {
giraffes.push(input);
return input;
},
}),
}),
});
```
`required: true` on the argument requires an input object. Each input field has its own
requiredness; here all three fields must also be provided and cannot be `null`.
```graphql playground example="foundations-inputs"
mutation CreateGiraffe {
createGiraffe(input: { name: "Gina", birthdate: "2020-03-15", height: 4.8 }) {
name
height
}
}
```
```json
{ "data": { "createGiraffe": { "name": "Gina", "height": 4.8 } } }
```
The mutation appends each created giraffe to the in-memory array, which the `giraffes` query returns.
## OneOf inputs [#oneof-inputs]
A OneOf input accepts exactly one of its fields, with a non-null value. This is useful for a lookup
that accepts either an ID or a name. Use a GraphQL.js version with OneOf support, and define the
input with `isOneOf: true`:
```typescript playground example="inputs-oneof"
import SchemaBuilder from '@pothos/core';
const builder = new SchemaBuilder({});
export const GiraffeLookup = builder.inputType('GiraffeLookup', {
isOneOf: true,
fields: (t) => ({
id: t.id({ required: false }),
name: t.string({ required: false }),
}),
});
const giraffes = [{ id: '1', name: 'Gina' }];
builder.queryType({
fields: (t) => ({
giraffeName: t.string({
args: {
by: t.arg({ type: GiraffeLookup, required: true }),
},
resolve: (_root, { by }) => {
if (by.id !== undefined) {
return giraffes.find((giraffe) => giraffe.id === by.id)?.name;
}
return giraffes.find((giraffe) => giraffe.name === by.name)?.name;
},
}),
}),
});
export const schema = builder.toSchema();
```
Each member must be optional (`required: false`) and must not have a `defaultValue`. Explicitly
setting `required: false` also works when the builder uses `defaultInputFieldRequiredness: true`.
The selected member still cannot be `null`: OneOf coercion enforces that constraint.
The `by` argument above is required. This requires the input object itself; it does not make both
alternatives required. With the default scalar types, Pothos infers `by` as
`{ id: string; name?: never } | { name: string; id?: never }`, so checking `by.id !== undefined`
selects the ID branch without a type assertion.
Both `{ id: "1" }` and `{ name: "Gina" }` select Gina in this example. The following operation takes the lookup as a variable:
```graphql playground example="inputs-oneof"
query FindGiraffe($by: GiraffeLookup!) {
giraffeName(by: $by)
}
```
| Value of the `by` variable | Result |
| ----------------------------------- | --------------------------------------------- |
| `{ "id": "1" }` | `giraffeName` is `"Gina"` |
| `{ "name": "Gina" }` | `giraffeName` is `"Gina"` |
| `{}` | Rejected: exactly one field is required |
| `{ "id": "1", "name": "Gina" }` | Rejected: more than one field is provided |
| `{ "id": null }` | Rejected: the selected value must be non-null |
| `null`, or an omitted `by` variable | Rejected: the argument uses `GiraffeLookup!` |
Invalid inputs are rejected before the resolver runs. The same OneOf constraints apply to inline
input literals.
## Recursive inputs [#recursive-inputs]
Input objects can reference other input refs directly. For circular references, declare the input
shape explicitly with `builder.inputRef` so TypeScript does not have to infer it through the cycle.
Create the ref before implementing its fields.
Continue with the builder from [Creating input objects](#creating-input-objects), and add this input
and mutation field:
```typescript playground example="foundations-inputs"
interface RecursiveGiraffeInputShape {
name: string;
birthdate: string;
height: number;
friends?: RecursiveGiraffeInputShape[] | null;
}
const RecursiveGiraffeInput = builder.inputRef('RecursiveGiraffeInput');
RecursiveGiraffeInput.implement({
fields: (t) => ({
name: t.string({ required: true }),
birthdate: t.string({ required: true }),
height: t.float({ required: true }),
friends: t.field({
type: [RecursiveGiraffeInput],
required: { list: false, items: true },
}),
}),
});
function createGiraffes(input: RecursiveGiraffeInputShape): Giraffe[] {
const giraffe: Giraffe = {
name: input.name,
birthdate: input.birthdate,
height: input.height,
};
return [giraffe, ...(input.friends ?? []).flatMap(createGiraffes)];
}
builder.mutationField('createGiraffeWithFriends', (t) =>
t.field({
type: [GiraffeRef],
args: {
input: t.arg({ type: RecursiveGiraffeInput, required: true }),
},
resolve: (_root, { input }) => {
const created = createGiraffes(input);
giraffes.push(...created);
return created;
},
}),
);
export const schema = builder.toSchema();
```
The `friends` list can be omitted or set to `null`, but its items cannot be `null`. The TypeScript
shape includes both optionality and `null` to match those values. The resolver uses each friend's
own fields and follows nested `friends` lists, returning the parent before its descendants.
```graphql playground example="foundations-inputs"
mutation CreateFriends {
createGiraffeWithFriends(
input: {
name: "Gina"
birthdate: "2020-03-15"
height: 4.8
friends: [
{
name: "George"
birthdate: "2021-06-01"
height: 4.5
friends: [{ name: "Gemma", birthdate: "2022-08-10", height: 3.9 }]
}
]
}
) {
name
height
}
}
```
```json
{
"data": {
"createGiraffeWithFriends": [
{ "name": "Gina", "height": 4.8 },
{ "name": "George", "height": 4.5 },
{ "name": "Gemma", "height": 3.9 }
]
}
}
```
## Declaring inputs in SchemaTypes [#declaring-inputs-in-schematypes]
Alternatively, register input shapes in the builder's `Inputs` map to reference them by name.
To use this approach for `createGiraffe`, keep the import and `Giraffe` backing model from the first
example and replace its builder declaration with:
```typescript playground example="foundations-inputs-named"
const builder = new SchemaBuilder<{
Inputs: {
GiraffeInput: Giraffe;
};
}>({});
```
Keep the `giraffes` array, `GiraffeRef`, and query definition. Replace the `GiraffeInput` ref
declaration with this definition; registering a shape alone does not create the GraphQL input type.
```typescript playground example="foundations-inputs-named"
builder.inputType('GiraffeInput', {
fields: (t) => ({
name: t.string({ required: true }),
birthdate: t.string({ required: true }),
height: t.float({ required: true }),
}),
});
```
In the `createGiraffe` mutation, the `input` argument now references that registered name:
```typescript playground example="foundations-inputs-named"
input: t.arg({ type: 'GiraffeInput', required: true }),
```
Keep the rest of that mutation unchanged and call `builder.toSchema()` after defining it. The first
mutation and result on this page also work with this schema; the recursive example is independent
of this alternative.
# Interfaces
URL: /docs/guide/interfaces
Guide for defining Interface types in Pothos
## Defining Interface Types [#defining-interface-types]
An interface defines fields that several object types have in common. Like objects, interfaces have
[a backing model](./objects) that describes the data their field resolvers receive. You can
use `builder.interfaceRef` to associate a TypeScript type with a GraphQL interface.
In this example, giraffes and lions have a diet, but each has some other data of its own:
```typescript playground example="types-interfaces"
import SchemaBuilder from '@pothos/core';
type Giraffe = {
kind: 'giraffe';
diet: string;
heightInMeters: number;
};
type Lion = {
kind: 'lion';
diet: string;
hasMane: boolean;
};
type Animal = Giraffe | Lion;
const builder = new SchemaBuilder({});
const AnimalRef = builder.interfaceRef('Animal');
const GiraffeRef = builder.objectRef('Giraffe');
const LionRef = builder.objectRef('Lion');
AnimalRef.implement({
fields: (t) => ({
diet: t.exposeString('diet'),
}),
resolveType: (animal) => {
switch (animal.kind) {
case 'giraffe':
return 'Giraffe';
case 'lion':
return 'Lion';
}
},
});
```
The `resolveType` function returns the GraphQL object type name to use when a field returns an
`Animal`. Here, the `kind` property distinguishes the two shapes. It is part of our backing data,
but isn't exposed as a GraphQL field.
## implementing interfaces with object types [#implementing-interfaces-with-object-types]
Use the `interfaces` option to specify which interfaces an object implements:
```typescript playground example="types-interfaces"
GiraffeRef.implement({
interfaces: [AnimalRef],
fields: (t) => ({
height: t.exposeFloat('heightInMeters'),
}),
});
LionRef.implement({
interfaces: [AnimalRef],
fields: (t) => ({
hasMane: t.exposeBoolean('hasMane'),
}),
});
```
Pothos adds the interface's fields to each implementing object, so both types have a `diet` field
without defining it again. The object's backing model must be assignable to the interface's backing
model so that the inherited field resolvers can use it. In this example, both `Giraffe` and `Lion`
are members of the `Animal` TypeScript union.
## Using an Interface as a return type [#using-an-interface-as-a-return-type]
A field returning a list of animals can include both giraffes and lions. The return annotation
keeps each `kind` value typed as its specific string literal:
```typescript playground example="types-interfaces"
builder.queryType({
fields: (t) => ({
animals: t.field({
type: [AnimalRef],
resolve: (): Animal[] => [
{ kind: 'giraffe', diet: 'herbivore', heightInMeters: 5.2 },
{ kind: 'lion', diet: 'carnivore', hasMane: true },
],
}),
}),
});
export const schema = builder.toSchema();
```
## Querying interface fields [#querying-interface-fields]
We can query `diet` on every animal. Fields that belong to a particular object type go inside an
inline fragment. The `__typename` field tells us which object type each result has:
```graphql playground example="types-interfaces"
query Interfaces {
animals {
__typename
diet
... on Giraffe {
height
}
... on Lion {
hasMane
}
}
}
```
The result includes the shared field and the matching fragment's fields for each animal:
```json
{
"data": {
"animals": [
{ "__typename": "Giraffe", "diet": "herbivore", "height": 5.2 },
{ "__typename": "Lion", "diet": "carnivore", "hasMane": true }
]
}
}
```
## Other ways to resolve an interface [#other-ways-to-resolve-an-interface]
If your application already uses classes for its data, prefer those classes as backing models.
You can pass them to `builder.interfaceType` and `builder.objectType`, as described in the
[Objects guide](./objects#using-classes).
Classes make automatic interface resolution straightforward. For the `Giraffe` class in that
example, set `isTypeOf: (value) => value instanceof Giraffe` on its object type. GraphQL can then
select the matching type without a `resolveType` function on the interface. Return actual class
instances so the check succeeds.
Existing `Error` subclasses are another good use case: the [Errors plugin](../plugins/errors)
matches errors against the configured classes using `instanceof`.
```typescript playground example="types-interface-classes"
class Animal {
constructor(public diet: string) {}
}
class Giraffe extends Animal {
constructor(public heightInMeters: number) {
super('herbivore');
}
}
class Lion extends Animal {
constructor(public hasMane: boolean) {
super('carnivore');
}
}
builder.interfaceType(Animal, {
name: 'Animal',
fields: (t) => ({ diet: t.exposeString('diet') }),
});
builder.objectType(Giraffe, {
name: 'Giraffe',
interfaces: [Animal],
isTypeOf: (value) => value instanceof Giraffe,
fields: (t) => ({ height: t.exposeFloat('heightInMeters') }),
});
builder.objectType(Lion, {
name: 'Lion',
interfaces: [Animal],
isTypeOf: (value) => value instanceof Lion,
fields: (t) => ({ hasMane: t.exposeBoolean('hasMane') }),
});
```
```typescript playground example="types-interface-classes"
builder.queryType({
fields: (t) => ({
animals: t.field({
type: [Animal],
resolve: () => [new Giraffe(5.2), new Lion(true)],
}),
}),
});
```
Another option is to return data with a `__typename` property containing the GraphQL object type's
name, such as `'Giraffe'`. GraphQL uses this property when the interface has no `resolveType`
function. If an object also defines `isTypeOf`, that check must pass as well.
## Interfaces implementing interfaces [#interfaces-implementing-interfaces]
Interfaces can also implement other interfaces using the `interfaces` option. See the
[SchemaBuilder API](../api/schema-builder#interfacetypeoptions) for the available options.
# Objects
URL: /docs/guide/objects
Define GraphQL object types from your application's data.
We'll define a GraphQL type for a giraffe, using a TypeScript interface to describe its data.
### Defining an Object type [#defining-an-object-type]
Create `schema.ts` with a builder and a reference for the `Giraffe` type:
```typescript playground example="objects"
import SchemaBuilder from '@pothos/core';
interface Giraffe {
name: string;
birthday: Date;
heightInMeters: number;
}
const builder = new SchemaBuilder({});
const GiraffeRef = builder.objectRef('Giraffe');
```
The interface describes the **backing model**: the data returned by resolvers for this type.
`objectRef` associates that model with the GraphQL name `Giraffe`. We can use the ref when defining
fields that return a giraffe.
### Add some fields [#add-some-fields]
Next, call `implement` to define the object's fields. It accepts the options for the object type,
including an optional description:
```typescript playground example="objects"
GiraffeRef.implement({
description: 'A giraffe in the zoo.',
fields: (t) => ({
name: t.exposeString('name'),
height: t.exposeFloat('heightInMeters'),
birthYear: t.int({
resolve: (giraffe) => giraffe.birthday.getUTCFullYear(),
}),
}),
});
```
The `fields` function receives a field builder, usually named `t`. Here, `t.exposeString('name')`
reads the `name` property from the backing model. The `height` field reads `heightInMeters`, giving
that property a different name in the GraphQL schema.
For `birthYear`, we write a resolver that computes an integer from the giraffe's birthday. Pothos
infers the resolver's first argument as `Giraffe` from the ref.
The backing model and GraphQL fields can have different shapes. Clients can query `name`, `height`,
and `birthYear`; `birthday` is available to resolvers but isn't exposed as a field. We can add
computed fields without adding properties to the backing model. The [fields guide](./fields)
covers more field options.
For a smaller example of mapping data to a different API shape, try
[converting a stored height in meters to feet](/playground?example=foundations-overview).
## Add a query [#add-a-query]
Add a field to the root `Query` type that returns a giraffe, then build the schema:
```typescript playground example="objects"
builder.queryType({
fields: (t) => ({
giraffe: t.field({
type: GiraffeRef,
resolve: () => ({
name: 'James',
birthday: new Date(Date.UTC(2012, 11, 12)),
heightInMeters: 5.2,
}),
}),
}),
});
export const schema = builder.toSchema();
```
The `type: GiraffeRef` option tells Pothos to check that the resolver returns data matching the
`Giraffe` interface. That returned object becomes the first argument to the resolvers for `name`,
`height`, and `birthYear`.
You can run this schema immediately with a Playground action above. To run it in your own
application, add the server below.
### Create a server [#create-a-server]
`builder.toSchema()` builds a standard `GraphQLSchema`. To serve it with `graphql-yoga`, install
`graphql-yoga` and save this as `server.ts` alongside `schema.ts`:
```typescript
import { createServer } from 'node:http';
import { createYoga } from 'graphql-yoga';
import { schema } from './schema';
const yoga = createYoga({ schema });
const server = createServer(yoga);
server.listen(3000);
```
### Query your data [#query-your-data]
Run `npx tsx server.ts`, then open [http://localhost:3000/graphql](http://localhost:3000/graphql)
and run this query:
```graphql playground example="objects"
query {
giraffe {
name
birthYear
height
}
}
```
The result is:
```json
{
"data": {
"giraffe": {
"name": "James",
"birthYear": 2012,
"height": 5.2
}
}
}
```
## Different ways to define Object types [#different-ways-to-define-object-types]
This guide uses object refs with a TypeScript interface. You can also use an existing class or
register backing models by name on the builder. These forms can be used together in the same schema.
Object refs
Classes
SchemaTypes
```typescript playground example="objects"
import SchemaBuilder from '@pothos/core';
interface Giraffe {
name: string;
birthday: Date;
heightInMeters: number;
}
const builder = new SchemaBuilder({});
const GiraffeRef = builder.objectRef('Giraffe');
GiraffeRef.implement({
description: 'A giraffe in the zoo.',
fields: (t) => ({
name: t.exposeString('name'),
height: t.exposeFloat('heightInMeters'),
birthYear: t.int({
resolve: (giraffe) => giraffe.birthday.getUTCFullYear(),
}),
}),
});
```
```typescript playground example="objects-variant-classes"
class Giraffe {
constructor(
public name: string,
public birthday: Date,
public heightInMeters: number,
) {}
}
builder.objectType(Giraffe, {
name: 'Giraffe',
fields: (t) => ({
name: t.exposeString('name'),
height: t.exposeFloat('heightInMeters'),
birthYear: t.int({
resolve: (giraffe) => giraffe.birthday.getUTCFullYear(),
}),
}),
});
```
```typescript playground example="objects-variant-builder-types"
interface SchemaTypes {
Objects: { Giraffe: Giraffe };
}
const builder = new SchemaBuilder({});
builder.objectType('Giraffe', {
fields: (t) => ({
name: t.exposeString('name'),
height: t.exposeFloat('heightInMeters'),
birthYear: t.int({
resolve: (giraffe) => giraffe.birthday.getUTCFullYear(),
}),
}),
});
```
### Using Refs [#using-refs]
You can create and implement a ref in one expression:
```typescript playground example="objects-variant-inline-refs"
const GiraffeRef = builder.objectRef('Giraffe').implement({
description: 'A giraffe in the zoo.',
fields: (t) => ({
name: t.exposeString('name'),
height: t.exposeFloat('heightInMeters'),
birthYear: t.int({
resolve: (giraffe) => giraffe.birthday.getUTCFullYear(),
}),
}),
});
```
For types that reference each other, TypeScript may need ref creation and `implement` to be separate
statements, as in the walkthrough above. See [circular references](./circular-references) for details.
You can also pass a ref to `builder.objectType(GiraffeRef, { ... })` with the same options.
### Using classes [#using-classes]
If your app already represents its data with classes, prefer using those classes as backing models.
Pass the class to `builder.objectType`: its instance type becomes the backing model, and the class
itself can be used as a field's `type`:
The Classes tab shows this definition. Its playground uses `type: Giraffe` in the query field
and returns a `new Giraffe(...)` instance.
The `name` option supplies the GraphQL type name. When resolving an
[interface](./interfaces) or [union](./unions), you can also use an explicit
`isTypeOf: (value) => value instanceof Giraffe` option to identify instances of the class.
### Using SchemaTypes [#using-schematypes]
You can declare backing models in `SchemaTypes` and pass it to `SchemaBuilder`. This lets you
reference object types by their GraphQL names instead of importing refs. Using the `Giraffe`
interface from above:
The SchemaTypes tab shows the registration. Its playground uses `type: 'Giraffe'` in the query field.
The `Objects` mapping tells Pothos which backing model belongs to each name. Here, it types the
`Giraffe` field resolvers and checks the object returned by the `giraffe` query. You still define
the GraphQL fields with `builder.objectType`; declaring a backing model doesn't expose its properties.
The [SchemaBuilder guide](./schema-builder) covers the other type settings.
# Patterns
URL: /docs/guide/patterns
Guide for using common patterns in Pothos
## Sharing fields between types [#sharing-fields-between-types]
If you have common fields or arguments that are shared across multiple types (but you don't want to
use an interface to share the common logic) you can write helper functions to generate these fields
for you.
### Objects and Interfaces [#objects-and-interfaces]
The shared `idLength` resolver computes the length of each object's ID.
```typescript playground example="architecture-shared-fields"
import type { ObjectRef } from '@pothos/core';
import SchemaBuilder from '@pothos/core';
const builder = new SchemaBuilder({});
type BuilderTypes = typeof builder.$inferSchemaTypes;
function addCommonFields(refs: ObjectRef[]) {
for (const ref of refs) {
builder.objectFields(ref, (t) => ({
id: t.exposeID('id', {}),
idLength: t.int({
resolve: (parent) => parent.id.length,
}),
}));
}
}
const WithCommonFields1 = builder.objectRef<{ id: string }>('WithCommonFields1').implement({});
const WithCommonFields2 = builder.objectRef<{ id: string }>('WithCommonFields2').implement({});
addCommonFields([WithCommonFields1, WithCommonFields2]);
```
This will apply the `id` and `idLength` fields to both of the object types. The `ObjectRef` type is
what is returned when creating an object (or when calling `builder.objectRef`). It takes three generic
parameters: the builder's SchemaTypes, the shape a resolver can return for that type, and the shape
of the parent argument when defining fields. The parent shape defaults to the resolver shape.
The resolver and parent shapes are generally the same, but can differ for some special cases (like with `loadableObject` from the dataloader plugin,
which allows resolvers to resolve to an `ID` rather than the actual object). In this case,
the helper requires an `id` on the parent shape because its field resolvers read that property.
If you want to define fields on an interface, you can use `InterfaceRef` instead. If your helper
accepts both, you can differentiate the refs by using `ref.kind` which will be either `Object` or
`Interface`. Call `builder.interfaceFields` for interface refs.
### Args [#args]
The example mutation passes a reason to one field and omits it from the other.
Args are a little more complicated than fields on objects and interfaces. Pothos infers the shape of args
for your resolvers, so you can't just add on more args later. Instead, we can define a helper that
returns a set of args to apply to your field. To make this work, we need to get a few extra types:
```typescript playground example="architecture-shared-args"
import SchemaBuilder, { type ArgBuilder } from '@pothos/core';
const builder = new SchemaBuilder({});
type BuilderTypes = typeof builder.$inferSchemaTypes;
function createCommonArgs(arg: ArgBuilder) {
return {
id: arg.id({}),
reason: arg({ type: 'String', required: false }),
};
}
builder.mutationType({
fields: (t) => ({
mutation1: t.boolean({
args: {
...createCommonArgs(t.arg),
},
resolve: (_parent, args) => !!args.reason,
}),
mutation2: t.boolean({
args: {
...createCommonArgs(t.arg),
},
resolve: (_parent, args) => !!args.reason,
}),
}),
});
```
`typeof builder.$inferSchemaTypes` includes the defaults Pothos applies to the builder's settings.
Use it to type the helper's `ArgBuilder` parameter, then spread the returned arguments into each
field. The resolver's `args` type includes those arguments. If you already export the builder's
settings as a `SchemaTypes` interface,
`PothosSchemaTypes.ExtendDefaultTypes` is another way to obtain the extended types.
For a shared set of args tied to one builder, you can also use
`builder.args`. See [Inferring Types](./inferring-types) for a helper
that accepts different builders.
### Input fields [#input-fields]
Both input types use the helper to define their `reason` field.
Input fields are similar to args, and also all need to be present when the type is defined so that
Pothos can infer the correct types.
```typescript playground example="architecture-shared-inputs"
import type { InputFieldBuilder } from '@pothos/core';
import SchemaBuilder from '@pothos/core';
const builder = new SchemaBuilder({});
type BuilderTypes = typeof builder.$inferSchemaTypes;
function createInputFields(t: InputFieldBuilder) {
return {
id: t.id({}),
reason: t.field({ type: 'String', required: false }),
};
}
const FirstInput = builder.inputType('InputWithCommonFields1', {
fields: (t) => ({
...createInputFields(t),
}),
});
const SecondInput = builder.inputType('InputWithCommonFields2', {
fields: (t) => ({
...createInputFields(t),
}),
});
```
# Using the playground
URL: /docs/guide/playground
Run a Pothos schema, edit its source, and use variables to explore your data.
The playground runs Pothos schemas in your browser. You can edit the TypeScript source, inspect the generated GraphQL schema, and execute queries without starting a server.
## Run and edit a greeting [#run-and-edit-a-greeting]
[Open the greeting example](/playground?example=playground-tour\&step=1) and click **Run**. The response contains `"Hello, Sam!"`.
Change `Sam` in the query to your name and run again. This example has one source file and one query.
The resolver in `schema.ts` uses the `name` argument to build the greeting:
```typescript playground example="playground-tour-step-1"
builder.queryType({
fields: (t) => ({
greeting: t.string({
nullable: false,
args: { name: t.arg.string({ required: true }) },
resolve: (_parent, args) => `Hello, ${args.name}!`,
}),
}),
});
```
In `schema.ts`, change `Hello` to `Welcome`. Run the query again to see the edited response. **Reset example** restores the original source and query.
## Explore fields and arguments [#explore-fields-and-arguments]
Choose **Explore a schema** in the step controls, or [open step 2](/playground?example=playground-tour\&step=2). Each step loads a complete example; edits from the previous step are not carried forward.
Run **01-Tall giraffes**. With `minimumHeight` set to `5`, the response includes James. Run **02-All giraffes** to include Lucy too. Change `minimumHeight` in **Variables** and run again to try the filter.
```typescript playground example="playground-tour-step-2"
builder.queryType({
fields: (t) => ({
giraffes: t.field({
type: [Giraffe],
nullable: false,
args: { minimumHeight: t.arg.float({ required: true }) },
resolve: (_parent, args) =>
giraffes.filter((giraffe) => giraffe.height >= args.minimumHeight),
}),
}),
});
```
Open `models/giraffe.ts` in **Files** to change the backing data. Open `schema.graphql` to inspect the generated field and argument types. The schema explorer also lets you browse the types. Resolver logs and errors appear in **Console**.
## Share your changes [#share-your-changes]
Use **Share** to copy a link containing your edited source and operations. A reader opening shared code must review it and choose **Trust and build schema** before running it. **Read the guide** returns to this page; **Reset example** discards edits and reloads the original example.
The playground executes queries and mutations locally. It does not start an HTTP server, provision a database, or run live subscriptions. Examples that require those services need their documented local setup.
# Printing Schemas
URL: /docs/guide/printing-schemas
Guide for printing a Pothos schema to an SDL schema file
The [playground example](/playground?example=architecture-printing) runs the schema below and shows its SDL in `schema.graphql`. Field descriptions appear in the SDL. Writing the result to a file is a Node.js task.
Sometimes it's useful to have an SDL version of your schema. To do this, you can use some tools from
the `graphql` package to write your schema out as SDL to a file.
Save the schema as `schema.ts`:
```typescript playground example="architecture-printing"
import SchemaBuilder from '@pothos/core';
const builder = new SchemaBuilder({});
builder.queryType({
fields: (t) => ({
hello: t.string({
args: {
name: t.arg.string(),
},
resolve: (_parent, { name }) => `hello, ${name || 'World'}`,
}),
}),
});
export const schema = builder.toSchema();
```
Write its SDL from a separate Node.js script:
```typescript
// print-schema.ts
import { writeFileSync } from 'node:fs';
import { printSchema } from 'graphql';
import { schema } from './schema';
const schemaAsString = printSchema(schema);
writeFileSync('./schema.graphql', `${schemaAsString}\n`);
```
`builder.toSchema()` sorts the schema by default.
Save the script as `print-schema.ts` and run it with a TypeScript runner:
npm
pnpm
yarn
bun
```bash
npm install --save-dev tsx
```
```bash
pnpm add --save-dev tsx
```
```bash
yarn add --dev tsx
```
```bash
bun add --dev tsx
```
```sh
npx tsx print-schema.ts
```
In an existing application, import your exported `schema` instead of creating another builder. Keep
schema construction separate from starting your server so this script does not open a listening port.
The generated SDL can be checked into source control for schema reviews or read by client tooling.
`printSchema` prints type definitions, not resolver functions or backing data. It also does not
preserve applied custom directives; use a printer that supports those directives if downstream tools
need them, such as when exporting a federation subgraph.
## Using graphql-code-generator [#using-graphql-code-generator]
An alternative to printing your schema directly is to generate your schema file using
graphql-code-generator.
You can add the `schema-ast` plugin to have graphql-code-generator generate your schema file for
you.
See [Generating Client Types](./generating-client-types) for more details
# Queries, Mutations and Subscriptions
URL: /docs/guide/queries-mutations-and-subscriptions
Guide for adding queries, mutations and subscriptions to your schema
Queries, mutations, and subscriptions are fields on the schema's root types. Each root type has
methods for defining the type, adding one field, and adding several fields.
## Queries [#queries]
Use `builder.queryType()` to define the `Query` type. This example returns plain backing data through
an [object reference](./objects):
```typescript playground example="foundations-root-operations"
import SchemaBuilder from '@pothos/core';
type Giraffe = { name: string };
const builder = new SchemaBuilder({});
const giraffes: Giraffe[] = [{ name: 'James' }];
const GiraffeRef = builder.objectRef('Giraffe').implement({
fields: (t) => ({
name: t.exposeString('name'),
}),
});
builder.queryType({
fields: (t) => ({
hello: t.string({
resolve: () => 'hello, world!',
}),
giraffes: t.field({
type: [GiraffeRef],
resolve: () => giraffes,
}),
}),
});
```
Define the `Query` type once. To split its fields across files, use `builder.queryField()` or
`builder.queryFields()`. The following is an alternative to the `builder.queryType()` call above,
using the same builder, object reference, and data:
```typescript playground example="foundations-root-operations-variant-separate"
builder.queryType({});
builder.queryField('hello', (t) =>
t.string({
resolve: () => 'hello, world!',
}),
);
builder.queryFields((t) => ({
giraffes: t.field({
type: [GiraffeRef],
resolve: () => giraffes,
}),
}));
```
## Mutations [#mutations]
Use `builder.mutationType()`, `builder.mutationField()`, and `builder.mutationFields()` in the same
way. Continuing the query example, this mutation adds a giraffe to the array returned by `giraffes`:
```typescript playground example="foundations-root-operations"
builder.mutationType({});
builder.mutationField('createGiraffe', (t) =>
t.field({
type: GiraffeRef,
args: {
name: t.arg.string({ required: true }),
},
resolve: (_parent, args) => {
const giraffe = { name: args.name };
giraffes.push(giraffe);
return giraffe;
},
}),
);
export const schema = builder.toSchema();
```
The required argument provides a `string` for the backing model's `name`. This example stores data
in memory; an application can perform its database write in the resolver instead.
```graphql playground example="foundations-root-operations"
mutation CreateGiraffe {
createGiraffe(name: "Morgan") {
name
}
}
```
After this mutation, `{ giraffes { name } }` returns both James and Morgan. Each mutation adds
another entry to the in-memory store.
## Subscriptions [#subscriptions]
Live subscription delivery requires a GraphQL server and event provider. The playground executes
queries and mutations locally, so run the subscription example below in your server rather than
the playground.
Use `builder.subscriptionType()`, `builder.subscriptionField()`, and `builder.subscriptionFields()`
to define subscription fields. Each field has a `subscribe` function that returns an async iterable
of events, and a `resolve` function that converts each event into the field's result.
This separate example defines the context contract for a counter and its event provider. Supply a
shared counter and provider through your server's context so the mutation publishes to the same
stream that subscriptions consume. The provider's `subscribe()` method must register the listener
when called and release it when the returned iterator is closed.
```typescript
import SchemaBuilder from '@pothos/core';
type Context = {
count: { value: number };
counterEvents: {
publish: (value: number) => void;
subscribe: () => AsyncIterable;
};
};
const builder = new SchemaBuilder<{ Context: Context }>({});
builder.queryType({
fields: (t) => ({
count: t.int({
resolve: (_parent, _args, context) => context.count.value,
}),
}),
});
builder.mutationType({
fields: (t) => ({
incrementCount: t.int({
resolve: (_parent, _args, context) => {
context.count.value += 1;
context.counterEvents.publish(context.count.value);
return context.count.value;
},
}),
}),
});
builder.subscriptionType({
fields: (t) => ({
incrementedCount: t.int({
subscribe: (_parent, _args, context) => context.counterEvents.subscribe(),
resolve: (count) => count,
}),
}),
});
const schema = builder.toSchema();
```
Start a subscription before calling the mutation to receive its event:
```graphql
subscription {
incrementedCount
}
```
```graphql
mutation {
incrementCount
}
```
With an initial count of `0`, the mutation returns `1` and the subscription emits
`{ "data": { "incrementedCount": 1 } }`. Configure a subscription transport in your GraphQL server
to deliver these results to clients.
Define `subscribe` before `resolve` so TypeScript can infer the resolver's event type.
# Scalars
URL: /docs/guide/scalars
Register custom scalars and define their input and output behavior.
Pothos includes GraphQL's `String`, `Boolean`, `Int`, `Float`, and `ID` scalars. Use their names as
field and argument types, or the shorthand builders shown in [Fields](./fields).
A custom scalar needs both a TypeScript declaration in `SchemaTypes` and a runtime implementation.
The declaration describes the values your resolvers work with:
* `Input` is the value a resolver receives after GraphQL parses an argument or input field.
* `Output` is the value a resolver returns, before GraphQL serializes it for the response.
These are application types, not necessarily the JSON types sent by clients. A date scalar can
receive a string from a client, parse it into a `Date`, and serialize a resolver's `Date` back to a
string. In that case both `Input` and `Output` are `Date`.
## Defining a scalar [#defining-a-scalar]
This scalar accepts positive integers and checks both input values and resolver results:
```typescript playground example="types-scalars"
import SchemaBuilder from '@pothos/core';
import { GraphQLError, Kind } from 'graphql';
const builder = new SchemaBuilder<{
Scalars: {
PositiveInt: { Input: number; Output: number };
};
}>({});
function positiveInt(value: unknown): number {
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0) {
throw new GraphQLError('PositiveInt must be a positive safe integer');
}
return value;
}
builder.scalarType('PositiveInt', {
serialize: positiveInt,
parseValue: positiveInt,
parseLiteral: (node) => {
if (node.kind !== Kind.INT) {
throw new GraphQLError('PositiveInt must be an integer literal');
}
return positiveInt(Number(node.value));
},
});
builder.queryType({
fields: (t) => ({
double: t.field({
type: 'PositiveInt',
args: { value: t.arg({ type: 'PositiveInt', required: true }) },
resolve: (_parent, { value }) => value * 2,
}),
}),
});
export const schema = builder.toSchema();
```
`parseValue` handles variables, `parseLiteral` handles inline GraphQL literals, and `serialize`
handles output. Validate each path: TypeScript declarations alone do not validate values at runtime.
This example explicitly rejects string and float literals, even when they represent whole numbers.
```graphql playground example="types-scalars"
query Scalars {
double(value: 3)
}
```
```json
{
"data": {
"double": 6
}
}
```
Doubling the largest safe integer passes input parsing, but output serialization rejects the result.
Passing `0`, `-1`, `1.5`, or `"3"` is an error. An output outside the scalar's accepted range is
also an error; GraphQL applies the field's nullability rules when reporting it.
## Adding an existing scalar [#adding-an-existing-scalar]
Use `builder.addScalarType` to register an existing graphql-js `GraphQLScalarType`. Declare its
application types in the builder first. For example, with `graphql-scalars` installed:
```typescript playground example="types-existing-scalars"
import SchemaBuilder from '@pothos/core';
import { DateResolver, JSONResolver } from 'graphql-scalars';
const builder = new SchemaBuilder<{
Scalars: {
JSON: { Input: unknown; Output: unknown };
Date: { Input: Date; Output: Date };
};
}>({});
builder.addScalarType('JSON', JSONResolver);
builder.addScalarType('Date', DateResolver);
builder.queryType({
fields: (t) => ({
date: t.field({ type: 'Date', resolve: () => new Date('2026-01-01T00:00:00Z') }),
json: t.field({
type: 'JSON',
args: { value: t.arg({ type: 'JSON', required: true }) },
resolve: (_parent, { value }) => value,
}),
}),
});
export const schema = builder.toSchema();
```
The same method accepts your own `GraphQLScalarType` instance. Check an implementation's parsing
and serialization contracts when choosing its `Input` and `Output` types; Pothos does not infer
those types from the scalar instance. The two types can differ, and an implementation may accept
more output types than your application needs to return.
For a general JSON scalar, `unknown` lets resolvers check an input's structure before using it.
The global TypeScript `JSON` type describes the `JSON.parse` and `JSON.stringify` API, not JSON data.
## Customizing built-in scalar types [#customizing-built-in-scalar-types]
You can override the TypeScript types of built-in scalars through `Scalars` too. This changes
resolver checking; it does not replace GraphQL's parsing or serialization behavior. For example,
to restrict your application to string IDs:
```typescript playground example="types-scalar-id"
const builder = new SchemaBuilder<{
Scalars: {
ID: { Input: string; Output: string };
};
}>({});
```
The example queries `echo(id: 42)` and reports the parsed argument's JavaScript type with
`inputType`. Both show that GraphQL accepts integer ID inputs and parses them to strings. Use `scalarType` or
`addScalarType` if you also need to replace a scalar's runtime implementation.
## GraphQL 17 coercion hooks [#graphql-17-coercion-hooks]
Starting with Pothos 4.13, `scalarType` also accepts GraphQL 17's coercion hooks:
| Hook | Purpose |
| -------------------- | --------------------------------------------------------------- |
| `coerceOutputValue` | Serialize a resolver result. |
| `coerceInputValue` | Parse an external input value. |
| `coerceInputLiteral` | Parse a constant AST value, with variables already substituted. |
| `valueToLiteral` | Convert an external value to a constant AST value. |
These hooks are used on GraphQL 17. GraphQL 16 ignores them, so keep `serialize`, `parseValue`, and
`parseLiteral` for implementations that also run on GraphQL 16. The earlier example works on both
versions.
On GraphQL 17, you can provide `coerceOutputValue` instead of `serialize`. If both are present,
GraphQL uses `serialize`. A `coerceInputLiteral` implementation must be paired with
`coerceInputValue`; otherwise schema construction throws. `addScalarType` preserves these hooks
when registering an existing scalar that provides them.
# SchemaBuilder
URL: /docs/guide/schema-builder
Create a builder, configure TypeScript types, and build a GraphQL schema.
A `SchemaBuilder` registers your types and fields, then builds a GraphQL schema. Create one builder
for each schema, and share it between the modules that define that schema.
## Creating a builder [#creating-a-builder]
```typescript playground example="types-schema-builder"
import SchemaBuilder from '@pothos/core';
const builder = new SchemaBuilder<{
Context: { requestId: string };
}>({});
builder.queryType({
fields: (t) => ({
requestId: t.string({
resolve: (_parent, _args, context) => context.requestId,
}),
}),
});
export const schema = builder.toSchema();
```
The generic parameter is an object Pothos calls `SchemaTypes`. Its entries configure TypeScript
checking; they do not create runtime values. `Context` above types the third argument of each
resolver. Your server must supply that object when executing a request; see [Context](./context).
If you do not need to configure any types, use `new SchemaBuilder({})`.
The constructor's options object configures runtime behavior. It can include plugins and their
options, as described in [Using plugins](./using-plugins), and
[default nullability](./changing-default-nullability) settings.
The `requestId` resolver returns the value supplied in the runtime context. Declaring its
TypeScript type does not supply that value.
## Building the schema [#building-the-schema]
Call `builder.toSchema()` after registering your types and fields. It returns a standard
`GraphQLSchema` that you can pass to a GraphQL server or execute with graphql-js. A schema needs a
Query root, even when it also defines mutations or subscriptions.
By default, Pothos sorts the schema lexicographically. Use `builder.toSchema({ sortSchema: false })`
to keep definition order. See the [SchemaBuilder API](../api/schema-builder) for build options and
[App layout](./app-layout) for registering types from multiple files before building the schema.
## Backing models [#backing-models]
A backing model is the TypeScript type of the data a resolver returns for an object or interface.
It also determines the `parent` type of that object's field resolvers. It can contain properties
that are not exposed in GraphQL, while GraphQL fields can compute or load values absent from the
backing model.
Pothos associates backing models with GraphQL types in three ways:
* **Type references:** `builder.objectRef('Name')` or `builder.interfaceRef('Name')` carries
the backing type `T`. Methods that define types also return references you can use elsewhere.
* **Classes:** Register a class directly when your application already uses class instances as
backing data. Pothos uses the class's instance type for resolver checking.
* **SchemaTypes:** Declare backing models in the builder's `Objects`, `Interfaces`, or `Inputs`
maps to reference those types by their string names. These declarations still need corresponding
runtime type definitions.
The [Objects](./objects), [Interfaces](./interfaces), and [Input objects](./inputs) guides show
these alternatives. Using a reference or class does not require adding its backing model to
`SchemaTypes`.
## Other SchemaTypes entries [#other-schematypes-entries]
The generic can also configure:
* `Scalars`: the values resolvers receive and return for each scalar; see [Scalars](./scalars).
* `DefaultFieldNullability` and `DefaultInputFieldRequiredness`: defaults for output and input
fields. Set their matching constructor options too; see [Default nullability](./changing-default-nullability).
* `StrictResolveType`: when `true`, unions list their members as a record (or by name), and their
`resolveType` must return a member; see [Unions](./unions#strict-resolvetype).
* Plugin-specific types, such as the context-dependent scopes used by the scope auth plugin.
Each entry is optional unless a plugin requires it. For the complete list, see the
[SchemaBuilder reference](../api/schema-builder).
# Troubleshooting
URL: /docs/guide/troubleshooting
Guide for troubleshooting common Pothos issues
Start with the section that matches the error. If the issue persists, include the error message,
package versions, and a minimal reproduction when [reporting it](https://github.com/hayes/pothos/issues).
## Type issues [#type-issues]
1. Ensure that TypeScript is using `strict` mode
2. Move your builder types to a separate named interface
* This will make many type errors significantly more readable
```typescript playground example="architecture-troubleshooting"
import SchemaBuilder from '@pothos/core';
interface PothosTypes {
Context: {
user: {
id: string;
};
};
}
const builder = new SchemaBuilder({});
```
The example query reads `user.id` from the request context. Adapt the named interface and query into a small reproduction of your own issue. The playground uses strict TypeScript settings; it does not reproduce your application's package installation or editor configuration.
## Slow VS Code or TypeScript performance [#slow-vs-code-or-typescript-performance]
1. Ensure you are not including any very complex objects in your `Context` type. See
[https://github.com/microsoft/TypeScript/issues/45405](https://github.com/microsoft/TypeScript/issues/45405)
## Runtime issues [#runtime-issues]
### Plugin methods are not defined [#plugin-methods-are-not-defined]
Check that the plugin is imported and configured as its documentation describes. Pothos plugins
extend classes imported from `@pothos/core`; the application and plugin must resolve the same core
instance. Duplicate installations or incompatible package versions can leave methods on a different
copy of the class.
Inspect the dependency tree with your package manager:
```sh
npm ls @pothos/core
# or
pnpm why @pothos/core
```
Align the package versions and dependency resolution if they load different copies. The packages do
not have to live in a particular root `node_modules` directory.
### Received multiple implementations for plugin [#received-multiple-implementations-for-plugin]
By default, Pothos doesn't allow multiple plugin registrations with the same name. During
development, it can be helpful to disable this check by setting
`SchemaBuilder.allowPluginReRegistration = true`.
Keep in mind that this not only allows plugins to be updated when for example, using HMR but can
also lead to unexpected behavior with plugins using the same name.
### Refs are undefined [#refs-are-undefined]
If refs are undefined, a circular import may be reading them before initialization. Check these
boundaries:
1. The `builder` is defined in a file that does not import any files that use the builder (or
indirectly import it).
2. `builder.toSchema()` is called in a file that is not imported by any files that use the builder.
This is generally done by having a simple `builder.ts` that initializes the builder to export. This
file can also define some core parts of your schema (the query object, scalars etc). The rest of the
schema can then import the builder from `builder.ts`. A `schema.ts` file can then import all files
that define parts of the schema. `schema.ts` can then call `builder.toSchema()` and export the
result for use by the server.
These boundaries let deferred field callbacks read initialized refs. Options evaluated immediately
can still fail in an import cycle. See [Circular References](./circular-references) for the distinction
between module initialization errors and recursive TypeScript inference, and [App Layout](./app-layout)
for an example of these modules.
# Unions
URL: /docs/guide/unions
Guide for defining Union types in Pothos
A union lets a field return one of several object types. Unlike an [interface](./interfaces), a union
has no fields of its own, and its members don't need to share any fields.
## Defining Union Types [#defining-union-types]
This example uses two kinds of giraffe facts. Each has a backing model and an
[object reference](./objects):
```typescript playground example="types-unions"
import SchemaBuilder from '@pothos/core';
const builder = new SchemaBuilder({});
const GiraffeStringFactRef = builder
.objectRef<{ factKind: 'string'; fact: string }>('GiraffeStringFact')
.implement({
fields: (t) => ({
fact: t.exposeString('fact'),
}),
});
const GiraffeNumericFactRef = builder
.objectRef<{ factKind: 'number'; fact: string; value: number }>('GiraffeNumericFact')
.implement({
fields: (t) => ({
fact: t.exposeString('fact'),
value: t.exposeFloat('value'),
}),
});
const GiraffeFactRef = builder.unionType('GiraffeFact', {
types: [GiraffeStringFactRef, GiraffeNumericFactRef],
resolveType: (fact) => {
switch (fact.factKind) {
case 'string':
return GiraffeStringFactRef;
case 'number':
return GiraffeNumericFactRef;
}
},
});
```
The `types` array lists the union's object types. Pothos infers the union's backing type from these
members, so `fact` in `resolveType` can have either backing shape.
The `resolveType` function selects a member object type for each returned value. Here, the
`factKind` property distinguishes the two shapes. It is part of the backing data, but isn't exposed
as a GraphQL field. You can return an object reference or the member's GraphQL type name.
You can also use registered classes or object type names declared in `SchemaTypes` in the `types`
array. See the [Objects guide](./objects#different-ways-to-define-object-types) for these alternatives.
### Strict resolveType [#strict-resolvetype]
By default, `resolveType` accepts any string, and a name that isn't a member only fails at runtime.
Refs that share a backing type are also the same TypeScript type, so returning `Dog` from a union
of `Cat` alone type-checks when both are backed by the same model.
Set `StrictResolveType: true` in `SchemaTypes` to rule both out, and give `types` as a record.
`resolveType` then returns one of the record's keys, which TypeScript checks even when members
share a backing type:
```ts
const builder = new SchemaBuilder<{ StrictResolveType: true }>({});
const Cat = builder.objectRef('Cat');
const Dog = builder.objectRef('Dog');
builder.unionType('Pet', {
types: { cat: Cat, dog: Dog },
resolveType: (pet) => pet.type, // must be 'cat' | 'dog'
});
```
Several keys can point at the same member, so the keys can match a discriminant in your data
directly. With the flag on, an array of refs in `types` is a type error. An array of object type
names declared in `SchemaTypes` also works, with `resolveType` returning one of those names. The
dataloader plugin's `loadableUnion` is checked the same way and takes names.
The record form also works without the flag. The flag doesn't change interfaces, or unions whose
`resolveType` a plugin provides, such as the errors plugin's error unions.
## Using Union Types [#using-union-types]
Use the union reference as a field's return type. A list can contain values for any of its members.
`$inferType` gives us the union's backing type for the data array:
```typescript playground example="types-unions"
const facts: (typeof GiraffeFactRef.$inferType)[] = [
{
factKind: 'string',
fact: 'Each giraffe has a unique pattern of spots.',
},
{
factKind: 'number',
fact: 'Top speed (MPH)',
value: 35,
},
];
builder.queryType({
fields: (t) => ({
giraffeFacts: t.field({
type: [GiraffeFactRef],
resolve: () => facts,
}),
}),
});
export const schema = builder.toSchema();
```
## Querying Union Types [#querying-union-types]
Use inline fragments to select fields from each member. Even though both types define `fact`, it
must be selected through a fragment because the union itself defines no fields. You can select
`__typename` directly on the union to identify each result's object type:
```graphql playground example="types-unions"
query Unions {
giraffeFacts {
__typename
... on GiraffeStringFact {
fact
}
... on GiraffeNumericFact {
fact
value
}
}
}
```
```json
{
"data": {
"giraffeFacts": [
{
"__typename": "GiraffeStringFact",
"fact": "Each giraffe has a unique pattern of spots."
},
{
"__typename": "GiraffeNumericFact",
"fact": "Top speed (MPH)",
"value": 35
}
]
}
}
```
## Other ways to resolve a union [#other-ways-to-resolve-a-union]
Without a `resolveType` function, GraphQL can use a `__typename` property on the returned data or
an `isTypeOf` check on the member object types, just as it does for
[interfaces](./interfaces#other-ways-to-resolve-an-interface). If your application uses classes,
set `isTypeOf: (value) => value instanceof YourClass` on each member object type and return class
instances. Registering a class does not add this check automatically. If an object defines
`isTypeOf`, that check must pass even when its type was selected by `resolveType` or `__typename`.
# Using plugins
URL: /docs/guide/using-plugins
Guide for using plugins with Pothos
Using plugins with Pothos is fairly easy, but works a little differently than other plugin systems
you may be familiar with. One of the most important things to note is that importing plugins may
have some side effects on the Schema builder, and it is recommended to only import the plugins you
are actually using.
The reason for this is that Pothos's plugin system was designed to allow plugins to contribute
features in a way that feels like they are built into the core API, and allow the plugins to take
full advantage of the type system. This means that plugins can extend the core types in Pothos with
their own properties, which happens as soon as the plugin is imported.
## Setup [#setup]
Each plugin should have setup instructions, but should work in a similar way.
First install the plugin:
npm
pnpm
yarn
bun
```bash
npm install --save @pothos/plugin-scope-auth
```
```bash
pnpm add @pothos/plugin-scope-auth
```
```bash
yarn add @pothos/plugin-scope-auth
```
```bash
bun add @pothos/plugin-scope-auth
```
Next import the plugin's default export (which should just be the name of the plugin), and pass it
when you create your schema builder.
```typescript playground example="architecture-plugin-setup"
import SchemaBuilder from '@pothos/core';
import ScopeAuthPlugin from '@pothos/plugin-scope-auth';
const builder = new SchemaBuilder({
plugins: [ScopeAuthPlugin],
scopeAuth: {
authScopes: () => ({}),
},
});
```
Some plugins may allow you to use your own types for one of their features. This is done by passing
types in through the Generic SchemaTypes used by the Schema builder:
```typescript playground example="architecture-plugin-scopes"
import SchemaBuilder from '@pothos/core';
import ScopeAuthPlugin from '@pothos/plugin-scope-auth';
const builder = new SchemaBuilder<{
AuthScopes: {
example: string;
};
}>({
plugins: [ScopeAuthPlugin],
scopeAuth: {
authScopes: () => ({
example: (value) => value === 'allowed',
}),
},
});
```
The second example permits the `allowed` field and denies the `denied` field according to their scopes.
These types can then be used in other parts of the API (eg. defining the scopes on a field), but
the details of how these types are used will be specific to each plugin, and should be covered in
the documentation for the plugin.
## Ordering [#ordering]
Config hooks, resolver wrappers, and `afterBuild` are applied in reverse plugin order.
`beforeBuild` runs in list order. This is done to ensure that the most important
(first) plugins are applied after all other effects have been applied. For plugins that wrap
resolvers, because the first plugins are applied last, they will be the outermost layer of wrapping
and executed first. This means it is important to have plugins like `scope-auth` listed
before other less critical plugins in your SchemaBuilder.
# Writing plugins
URL: /docs/guide/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 [#the-type-system]
Pothos has 2 main pieces to its type system:
1. `PothosSchemaTypes`: A global namespace for shared types
2. `SchemaTypes`: A collection of types passed around through Generics specific to each instance of
`SchemaBuilder`
### `PothosSchemaTypes` [#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` [#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 [#getting-started]
The best place to start is by looking through the
[example plugin](https://github.com/hayes/pothos/tree/main/packages/plugin-example).
The general structure of a plugin has 3 main parts:
1. `index.ts` which contains a plugin's actual implementation
2. `global-types.ts` which contains any additions to Pothos's built in types.
3. `types.ts` which should contain any types that do NOT belong to the global `PothosSchemaTypes`
namespace.
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:
1. The plugin name in `index.ts`
2. The name of the Plugin class in `index.ts`
3. The name key/name for the plugin in the `Plugins` interface in `global-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-typests]
`global-types.ts` must contain the following:
1. A declaration of the `PothosSchemaTypes` namespace
```typescript
declare global {
export namespace PothosSchemaTypes {}
}
```
2. An addition to the `Plugins` interface that maps the plugin name, to the plugin type (this needs
to be inside the `PothosSchemaTypes` namespace)
```typescript
export interface Plugins {
example: PothosExamplePlugin;
}
```
`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.
1. [`type-options.ts`](https://github.com/hayes/pothos/blob/main/packages/core/src/types/global/type-options.ts):
Contains the interfaces that define the options objects for the various types (Object,
Interface, Enum, etc).
2. [`field-options.ts`](https://github.com/hayes/pothos/blob/main/packages/core/src/types/global/field-options.ts):
Contains the interfaces that define the options objects for creating fields
3. [`schema-types.ts`](https://github.com/hayes/pothos/blob/main/packages/core/src/types/global/schema-types.ts):
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.
4. [`classes.ts`](https://github.com/hayes/pothos/blob/main/packages/core/src/types/global/classes.ts):
Contains interfaces that describe the classes used by Pothos, include `SchemaBuilder` and 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` [#indexts]
`index.ts` must contain the following:
1. A bare import of the global types (`import './global-types';`)
2. The plugins name, which should be typed as a string literal rather than as a generic string:
`const pluginName = 'example'`
3. A default export of the plugin name `export default pluginName`
4. A class that extends BasePlugin:
`export class PothosExamplePlugin extends BasePlugin {}`
`BasePlugin` and `SchemaTypes` can both be imported from `@pothos/core`
5. A call to register the plugin: `SchemaBuilder.registerPlugin(pluginName, PothosExamplePlugin);`
`SchemaBuilder` can also be imported from `@pothos/core`
### Run a complete plugin [#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.
```typescript playground example="architecture-writing-plugin"
export class ObserverPlugin extends BasePlugin<
Types,
{ resolveCount: number }
> {
createRequestData(_context: Types['Context']) {
return { resolveCount: 0 };
}
wrapResolve(
resolver: GraphQLFieldResolver,
_fieldConfig: PothosOutputFieldConfig,
): GraphQLFieldResolver {
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:
```typescript playground example="architecture-writing-plugin"
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 [#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 the `Subscriptions` object.
* `wrapArgMappers`: Wraps outside argument mapping for resolvers and subscription setup; see
[Argument mapping and errors](#argument-mapping-and-errors).
* `wrapResolveType`: Invoked for each Union and Interface.
* `wrapIsTypeOf`: Invoked for each Object, including objects without an `isTypeOf` function.
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:
```typescript playground example="architecture-plugin-hooks"
beforeBuild() {
this.runUnique('architecture-added-field', () => {
console.log('Preparing schema once per builder');
});
}
```
## Use cases [#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 [#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.
```typescript
export interface SchemaBuilderOptions {
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:
```typescript playground example="architecture-plugin-hooks"
const label = this.builder.options.nestedOptionsObject?.exampleOption;
const rootOption = this.builder.options.optionInRootOfConfig;
```
### Adding options when building a schema (`toSchema`) [#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:
```typescript
export interface BuildSchemaOptions {
customBuildTimeOptions?: boolean;
}
```
These options can be accessed through `this.options` in your plugin:
```typescript playground example="architecture-plugin-hooks"
const buildOption = this.options.customBuildTimeOptions;
```
### Adding options to types [#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:
```typescript
export interface ObjectTypeOptions {
optionOnObject?: boolean;
}
```
These options can then be accessed in your plugin when you receive the config for the type:
```typescript playground example="architecture-plugin-hooks"
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 `kind`s 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 [#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:
```typescript
export interface MutationFieldOptions<
Types extends SchemaTypes,
Type extends TypeParam,
Nullable extends FieldNullability,
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.
```typescript playground example="architecture-plugin-hooks"
onOutputFieldConfig(fieldConfig: PothosOutputFieldConfig) {
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-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.
```typescript
export interface SchemaBuilder {
buildCustomObject: () => ObjectRef;
}
```
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:
```typescript playground example="architecture-plugin-hooks"
const schemaBuilderProto = SchemaBuilder.prototype as PothosSchemaTypes.SchemaBuilder;
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 [#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)[]>`. So be careful when using a
wrapper that introspected the return value of a resolve function. Plugins should only wrap resolvers
when absolutely necessary.
```typescript playground example="architecture-writing-plugin"
export class ObserverPlugin extends BasePlugin<
Types,
{ resolveCount: number }
> {
createRequestData(_context: Types['Context']) {
return { resolveCount: 0 };
}
wrapResolve(
resolver: GraphQLFieldResolver,
_fieldConfig: PothosOutputFieldConfig,
): GraphQLFieldResolver {
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 [#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`:
```typescript playground example="architecture-plugin-hooks"
afterBuild(schema: GraphQLSchema) {
return new GraphQLSchema({
...schema.toConfig(),
description: 'Schema transformed after build',
});
}
```
### Using SchemaTypes [#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.
```typescript
export interface SchemaBuilderOptions {
exampleSetupFn?: (context: Types['Context']) => ExamplePluginSetupConfig;
}
```
### Using user defined types [#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.
```typescript
export interface UserSchemaTypes {
NewExampleTypes: Record;
}
export interface ExtendDefaultTypes> {
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 [#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.
```typescript playground example="architecture-writing-plugin"
export class ObserverPlugin extends BasePlugin<
Types,
{ resolveCount: number }
> {
createRequestData(_context: Types['Context']) {
return { resolveCount: 0 };
}
wrapResolve(
resolver: GraphQLFieldResolver,
_fieldConfig: PothosOutputFieldConfig,
): GraphQLFieldResolver {
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 [#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 configuration
* `createInputValueMapper`: Creates a mapping function that uses the result of `mapInputFields` to
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:
```typescript playground example="architecture-plugin-inputs"
export class TrimPlugin extends BasePlugin {
// Safe to share: selection depends only on input-field extensions, not its enclosing output field.
private mappingCache = new Map>();
wrapResolve(
resolver: GraphQLFieldResolver,
fieldConfig: PothosOutputFieldConfig,
): GraphQLFieldResolver {
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:
```typescript
interface InputFieldMapping {
kind: 'Enum' | 'Scalar' | 'InputObject';
isList: boolean;
listDepth: number;
config: PothosInputFieldConfig;
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:
```typescript
interface InputTypeFieldsMapping {
configs: Record>;
map: Map> | 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 [#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`; `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:
1. `wrapArgMappers` wrappers, which see the arguments before mapping.
2. The field's `argMappers`.
3. `wrapResolve` wrappers, which see the mapped arguments.
4. 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.
```typescript playground example="architecture-arg-mappers"
import SchemaBuilder, {
BasePlugin,
type PothosOutputFieldConfig,
type SchemaTypes,
} from '@pothos/core';
import { GraphQLError, type GraphQLFieldResolver } from 'graphql';
declare global {
namespace PothosSchemaTypes {
interface Plugins {
architectureArgMappers: NameInputPlugin;
}
}
}
class NameInputError extends Error {}
export class NameInputPlugin extends BasePlugin {
override onOutputFieldConfig(fieldConfig: PothosOutputFieldConfig) {
if (!fieldConfig.extensions?.normalizeName) {
return fieldConfig;
}
return {
...fieldConfig,
argMappers: [
...fieldConfig.argMappers,
(args: Record) => {
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) => {
// 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 | undefined,
fieldConfig: PothosOutputFieldConfig,
): GraphQLFieldResolver | 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`:
```typescript playground example="architecture-arg-mappers"
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 [#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:
```typescript playground example="architecture-plugin-hooks"
onOutputFieldConfig(fieldConfig: PothosOutputFieldConfig) {
if (fieldConfig.name === 'removeMe') {
return null;
}
if (fieldConfig.kind === 'Mutation' && fieldConfig.pothosOptions.customMutationFieldOption) {
return { ...fieldConfig, description: 'Configured mutation' };
}
return fieldConfig;
}
```
```typescript playground example="architecture-plugin-hooks"
onInputFieldConfig(fieldConfig: PothosInputFieldConfig) {
return fieldConfig.name === 'removeMe' ? null : fieldConfig;
}
onEnumValueConfig(valueConfig: PothosEnumValueConfig) {
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: [#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.onFirstUse` Takes a callback to invoke once the config for the field is available.
* `buildCache.getTypeConfig` Gets the config for a given type after it has been passed through any
modifications applied by plugins.
# Giraphql to Pothos
URL: /docs/migrations/giraphql-pothos
Migration guide for upgrading from GiraphQL 2.* to Pothos 3.0
# Migrating from GiraphQL to Pothos [#migrating-from-giraphql-to-pothos]
As of 3.0 GiraphQL has been renamed to Pothos. The primary motivation for this rename is to make
this library and associated projects, guides, and other content to be more discoverable. GiraphQL is
not visually distinct from GraphQL, and has often been interpreted as a typo. Search engines tend to
auto-correct the name to GraphQL, making it hard to search for.
## Changes for consumers of GiraphQL [#changes-for-consumers-of-giraphql]
* All packages have been moved from the `@giraphql/*` scope to `@pothos/*` scope.
* The `GiraphQLSchemaTypes` global typescript scope has been renamed to `PothosSchemaTypes`
* Exported types prefixed with `GiraphQL` have had that prefix replaced with `Pothos`
For the most part, the easiest way to upgrade is by doing a CASE SENSITIVE search and replace of
`giraphql` -> `pothos` and `GiraphQL` -> `Pothos`. The only non-documentation change between the
latest version of GiraphQL and the initial version of Pothos (`v3.0.0`) are renaming of types and
packages.
## Plugin specific changes [#plugin-specific-changes]
### Prisma plugin [#prisma-plugin]
* The generator/provider for prisma types has been renamed to `prisma-pothos-types`.
You will need to update your prisma schema to use the new provider:
```prisma
generator pothos {
provider = "prisma-pothos-types"
}
```
## For plugin authors [#for-plugin-authors]
* Some `extensions` fields in the build schemas have been renamed. Specifically:
* `giraphQLOptions` has been renamed to `pothosOptions`
* `giraphQLConfig` has been renamed to `pothosConfig`
# Migrations
URL: /docs/migrations
List of Pothos migration guides
* [3.\* to (4.0)](./migrations/v4)
* [GiraphQL (2.\*) to Pothos (3.0)](./migrations/giraphql-pothos)
* [1.\* to 2.0](./migrations/v2)
# Migration to Pothos from other GraphQL libraries [#migration-to-pothos-from-other-graphql-libraries]
The following tools can help migrate an existing schema. Review their generated code and adapt
the resolvers and application integration to your project.
* [Nexus to Pothos codemod](https://github.com/villesau/nexus-to-pothos-codemod)
This 3rd party code-mod aims to transform all the nexus types, queries and mutations to Pothos
equivalents. This codemod will still require some manual adjustments to get everything working
correctly, but can be a huge help in the migration process.
* [Pothos Generator](https://github.com/hayes/pothos/tree/main/packages/converter)
This is an undocumented CLI that can convert a schema into valid Pothos code. Resolvers are all
placeholders that throw errors, so this is not quite as useful as it sounds, but can be helpful,
especially for generating input types.
# v2.0
URL: /docs/migrations/v2
Migration guide for upgrading from GiraphQL 1.* to GiraphQL 2.0
The 2.0 release was mostly focused around re-designing the plugin system so it could be properly
documented, and made available for broader adoption. The previous plugin system allowed plugins to
use the FieldWrapper base class to wrap fields. Unfortunately the overhead of this wrapping strategy
was significantly higher than expected, and could not be optimized in a way that justified the
conveniences it provided.
## Breaking changes [#breaking-changes]
### Auth plugin [#auth-plugin]
The auth plugin has been replaced by a new `scope-auth` plugin. Unfortunately due to the performance
problems with the original field wrapping API, the auth plugin had to be re-designed, and
maintaining the existing API at the cost of significant performance overhead did not seem justified.
Any existing usage of the `auth` plugin will need to be replaced with the new `scope-auth` plugin.
The API of the new `scope-auth` plugin is substantially different, and the specifics of the
migration will depend on the exact usage of the original auth plugin. Documentation on the new
plugin can be found [here](../plugins/scope-auth).
### Plugin names [#plugin-names]
Plugin names have been normalized, and are now exported as the default export of the plugin
packages.
Change:
```typescript
// old
import '@pothos/plugin-simple-objects';
const builder = new SchemaBuilder({
plugins: ['PothosSimpleObjects'],
});
// new
import SimpleObjectsPlugin from '@pothos/plugin-simple-objects';
const builder = new SchemaBuilder({
plugins: [SimpleObjectsPlugin],
});
```
### Plugin Order [#plugin-order]
The old plugin API did not make strong guarantees about the order in which plugin hooks would be
executed. Config hooks and resolver wrappers are applied in reverse order; `beforeBuild` runs in list order. The most critical plugins (like
`scope-auth`) should appear first in the list of plugins. This ensures that any modifications made
by other plugins are applied first, and lets the more important plugins be at the top of the call
stack when resolving fields.
### InputFieldBuilder.bool and InputFieldBuilder.boolList [#inputfieldbuilderbool-and-inputfieldbuilderboollist]
The `bool` alias on `InputFieldBuilder` has been removed, as it was inconsistent with the other
field builders and general naming convention of other methods. Usage of this method should be
converted to the canonical `boolean` and `booleanList` methods.
Change:
```typescript
// Old
t.arg.bool({});
t.arg.boolList({});
// New
t.arg.boolean();
t.arg.booleanList();
```
### args on "exposed" fields [#args-on-exposed-fields]
Fields defined with the `expose` helpers no longer accept `args` since they also do not have a
resolver.
### Plugin API [#plugin-api]
The Plugin API has been completely re-designed and is now
[documented here](../guide/writing-plugins). new instances of plugins are now instantiated each time
`toSchema` is called on the `SchemaBuilder`, rather than being tied to the lifetime of the
`SchemaBuilder` itself.
## New features [#new-features]
* Lots of new documentation
* New scope-auth plugin
* New directives plugin
* New plugin API
* Significant performance improvements in smart-subscriptions and scope-auth plugins
# v4.0
URL: /docs/migrations/v4
Migration guide for upgrading from Pothos 3.x to Pothos 4.0
## Overview [#overview]
Migrating from Pothos 3.x to 4.0
The `4.0` release of Pothos is largely focused on updating 4 things:
1. Improving outdated defaults to be more consistent and aligned with best practices
2. Updating naming of some config options to be more consistent
3. Updating minimum versions of peer dependencies
4. Updating internal types to support some previously challenging plugin patterns
While the internals of Pothos have almost entirely been re-written, the public API surface should
have minimal changes for most users. The first 2 sets of changes will cover the majority of
changes relevant to the majority of applications. To make the upgrade as simple as
possible, some options were added to maintain the defaults and option names from `3.x` which are
described in the simple upgrade section below.
## New minimum versions [#new-minimum-versions]
* `typescript`: `5.0.2` (`5.4` since `@pothos/core` 4.16)
* `graphql`: `16.6.0`
* `node`: `18.0`
## Simple Upgrade (restore 3.0 options and defaults) [#simple-upgrade-restore-30-options-and-defaults]
You can restore the 3.x defaults by adding the Defaults versions to both the SchemaTypes and the
builder options:
```ts
const builder = new SchemaBuilder<{
Defaults: 'v3';
}>({
defaults: 'v3',
});
```
This will restore all the defaults and config options from previous Pothos versions for both core
and plugins.
If you are using `@pothos/plugin-validation`, it has been renamed to `@pothos/plugin-zod`, as part of the v4 release. The name
`@pothos/plugin-validation` now refers to the [Standard Schema validation plugin](../plugins/validation).
```diff
- import ValidationPlugin from '@pothos/plugin-validation';
+ import ZodPlugin from '@pothos/plugin-zod';
const builder = new SchemaBuilder({
- plugins: [ValidationPlugin],
+ plugins: [ZodPlugin],
});
```
## Manual update [#manual-update]
There are a number of new defaults and changes to options for various plugins. To fully upgrade to
4.0 see the full list of breaking changes below:
# Breaking API Changes: [#breaking-api-changes]
This section covers breaking API changes that can be automatically reverted by using the Simple
Upgrade process described above.
Changes to types and classes outside the main Pothos API are described in the next section. Those
changes will primarily affect other plugins and tools written for pothos, but may be relevant to
some type helpers you have created.
## `@pothos/core` [#pothoscore]
### Default field nullability [#default-field-nullability]
In previous versions of Pothos, fields were non-nullable by default. This is inconsistent with the
rest of the GraphQL ecosystem, so the default is being changed to make fields nullable by default.
To restore the previous behavior you can set the `defaultFieldNullability` option when creating your
builder:
```ts
export const builder = new SchemaBuilder<{
DefaultFieldNullability: false;
}>({
defaultFieldNullability: false,
});
```
Alternatively, fields can be updated to add `nullable: false` to the fields options.
### Default ID Scalar types [#default-id-scalar-types]
The default types for the built in `ID` Scalar has been changed to more closely match the behavior
of Javascript GraphQL server implementations:
```ts
interface IDType {
Input: string;
Output: number | string | bigint;
}
```
This will make working with IDs in arguments and input types easier by avoiding unnecessary type
checks to see if an `ID` is a `number` or `string`.
When returning an `ID` from a scalar you will be able to return a `string`, `number`, or `bigint`.
To restore the previous defaults you can customize the `ID` scalar types when creating your builder:
```ts
const builder = new SchemaBuilder<{
Scalars: {
ID: {
Input: number | string;
Output: number | string;
};
};
}>({});
```
## `@pothos/plugin-relay` [#pothosplugin-relay]
### Renamed options [#renamed-options]
The base relay plugin options have moved from `relayOptions` to `relay` to be more consistent with
options for other plugins.
```diff
const builder = new SchemaBuilder<{}>({
- relayOptions: {...}
+ relay: {...}
})
```
### New defaults [#new-defaults]
A number of the default values for relay options have changed:
* `clientMutationId`: Now defaults to `"omit"` and was previously `"required"`
* `clientMutationId` was only required in early versions of the relay client, and is no-longer
recommended.
* `cursorType`: Now defaults to `"String"` and was previously `"ID"`
* The previous defaults were inconsistent about the type of a cursor. Cursors generally should not
be treated as IDs as they are meant to indicate a position in a list, and may contain
information specific to other filters or arguments applied to the connection.
* `brandLoadedObjects`: Now defaults to `true` and was previously `false`
* This change will improve developer experience for most node implementations, as it removes the
need for `isTypeOf` to be defined for most nodes.
* `edgesFieldOptions.nullable`: Now defaults to
`{ list: options.defaultFieldNullability, items: true }` and was previously
`{ list: false, items: true }`
* `nodeFieldOptions.nullable`: Now defaults to `options.defaultFieldNullability` and was previously
`false`
* This new default is intended to align with the relay connection spec, which does not expect
connections to be NonNullable by default
To restore the previous defaults you can pass the old values when setting up the builder:
```ts
const builder = new SchemaBuilder<{
// To change edgesFieldOptions.nullable you must also update the type here
DefaultEdgesNullability: { list: false; items: true };
}>({
relay: {
clientMutationId: 'required',
brandLoadedObjects: false,
edgesFieldOptions: {
nullable: { list: false, items: true },
},
nodeFieldOptions: {
nullable: false,
},
cursorType: 'ID',
// the cursor fields on edges and pageInfo previously defaulted to `String`
// but will be overwritten by `cursorType` so you also need to explicity set them
edgeCursorType: 'String',
pageInfoCursorType: 'String',
// If you are using the new v4 nullability defaults, you may need to change the nullability of mutation fields
relayMutationFieldOptions: {
nullable: false,
},
},
});
```
## `@pothos/plugin-prisma` [#pothosplugin-prisma]
### Nullable relations [#nullable-relations]
Previously the prisma would allow t.relation to define non-nullable fields using nullable relations.
The plugin option now requires an `onNull` option to handle null relations on NonNullable fields
To restore the previous behavior you can set the `onNull` option to `'error'`, which will result in
a runtime error when the field returns null
```diff
t.relation('nullableRelation', {
+ onNull: 'error',
})
```
Alternatively you can mark the field as nullable:
```diff
t.relation('nullableRelation', {
+ nullable: true,
})
```
`onNull` can also be set to a function that returns either a record matching the type of the
relation, or a custom Error to throw when the relation is null.
```ts
t.relation('nullableRelation', {
onNull: () => loadPlaceholder(),
});
```
## `@pothos/plugin-directives` [#pothosplugin-directives]
`useGraphQLToolsUnorderedDirectives` has been nested inside a `directives` options object:
```diff
const builder = new SchemaBuilder<{}>({
- useGraphQLToolsUnorderedDirectives: true
+ directives: {
+ useGraphQLToolsUnorderedDirectives: true
+ }
})
```
## `@pothos/plugin-errors` [#pothosplugin-errors]
### Renamed options [#renamed-options-1]
The base error plugin options have moved from `errorOptions` to `errors` to be more consistent with
options for other plugins.
```diff
const builder = new SchemaBuilder<{}>({
- errorOptions: {...}
+ errors: {...}
})
```
## `@pothos/plugin-scope-auth` [#pothosplugin-scope-auth]
### Renamed options [#renamed-options-2]
The base scope-auth plugin options have moved from `scopeAuthOptions` to `scopeAuth` to be more
consistent with options for other plugins. The `authScopes` option has been moved to
`scopeAuth.authScopes` to keep all options for the plugin in one options object.
```diff
const builder = new SchemaBuilder<{}>({
- scopeAuthOptions: {...}
- authScopes: (ctx) => ({...})
+ scopeAuth: {
+ ...otherOptions,
+ authScopes: (ctx) => ({...})
+ }
})
```
## `@pothos/plugin-zod` (previously `@pothos/plugin-validation`) [#pothosplugin-zod-previously-pothosplugin-validation]
### Renamed options [#renamed-options-3]
The base validation plugin options have moved from `validationOptions` to `zod` to be more
consistent with options for other plugins.
```diff
const builder = new SchemaBuilder<{}>({
- validationOptions: {...}
+ zod: {...}
})
```
## `@pothos/plugin-authz` has been removed [#pothosplugin-authz-has-been-removed]
The `@pothos/plugin-authz` plugin has been removed, because the underlying `@graphql-authz/core` is
not actively maintained, and has left critical security vulnerabilities unaddressed.
# Plugin API and Type changes [#plugin-api-and-type-changes]
Unlike the defaults and config changes, the changes to the types and classes used throughout Pothos
can't be easily made backwards compatibility with the 3.x releases. Below is a summary of the main
changes made to the types and classes that may be used by plugins, helpers, or other libraries. Many
of these types and classes are primarily intended for internal use, and should not affect most
applications using pothos, but the changes are documented here to help upgrades for those of you
building your own plugins, or using these types in your applications.
The 4.0 release is intended to allow pothos to become more modular and extensible. This requires
Refs and many associated type helpers to propagate the SchemaTypes from the builder that originated
them, meaning most of the changes listed below are adding `Types extends SchemaTypes` as the first
generic argument to the type.
## Classes [#classes]
* `InputFieldBuilder`
* Removed the `typename` argument from the constructor
* Updated field methods to return a new `GenericInputRef`
* `InterfaceFieldBuilder`
* Removed the `typename` argument from the constructor
* `ObjectFieldBuilder`
* Removed the `typename` argument from the constructor
* `BaseTypeRef`
* Added `SchemaTypes` as a new Generic parameter
* `EnumTypeRef`
* Added `SchemaTypes` as a new Generic parameter
* `InputObjectRef`
* Added `SchemaTypes` as a new Generic parameter
* `InputRef`
* Added `SchemaTypes` as a new Generic parameter
* `OutputTypeRef`
* Added `SchemaTypes` as a new Generic parameter
* `ListRef`
* Added `SchemaTypes` as a new Generic parameter
* `InterfaceRef`
* Added `SchemaTypes` as a new Generic parameter
* `ObjectRef`
* Added `SchemaTypes` as a new Generic parameter
* `ScalarRef`
* Added `SchemaTypes` as a new Generic parameter
* `UnionRef`
* Added `SchemaTypes` as a new Generic parameter
* `FieldRef`
* Added `SchemaTypes` as a new Generic parameter
* removed the typename from constructor args
* add the builder and Field options as arguments for the constructor
* `InputFieldRef`
* Added `SchemaTypes` as a new Generic parameter
* removed the typename and kind from constructor args
* add the builder and Field options as arguments for the constructor
* split argument refs into a new `ArgumentRef` class
## Exported types [#exported-types]
* `*FieldThunk`
* Updated to return a `GenericFieldRef`
* `FieldMap`
* Updated to `Record>;`
* `InputFieldMap`
* Updated to `Record>;`
* `InputFieldsFromShape`
* Added `SchemaTypes` as a new Generic parameter
* `InputShapeFromField`
* Updated to accept a `GenericFieldRef`
## Field options [#field-options]
The global interfaces for FieldOptions no-longer include the `resolve` option, which has moved to
the `InferredFieldOptions` interface to allow plugins to replace or change the resolve functions
types globally.
This means that when extending the `FieldOptionsByKind` interface, if you previously extended one of
the built in Field option interfaces, you will need to update your types to include the `resolve`
function types as well:
```diff
export interface FieldOptionsByKind<
Types extends SchemaTypes,
ParentShape,
Type extends TypeParam,
Nullable extends FieldNullability,
Args extends InputFieldMap,
ResolveShape,
ResolveReturnShape,
> {
- CustomObjectObject: CustomOptions &
- PothosSchemaTypes.ObjectFieldOptions<
- Types,
- ParentShape,
- Type,
- Nullable,
- Args,
- ResolveReturnShape
- >;
+ CustomObjectObject: CustomOptions &
+ PothosSchemaTypes.ObjectFieldOptions<
+ Types,
+ ParentShape,
+ Type,
+ Nullable,
+ Args,
+ ResolveReturnShape
+ > &
+ InferredFieldOptionsByKind<
+ Types,
+ Types['InferredFieldOptionsKind'],
+ ParentShape,
+ Type,
+ Nullable,
+ Args,
+ ResolveReturnShape
+ >;
}
```
The `InferredFieldOptionsByKind` interface can be used to get the `resolve` option by default, but
will also work for plugins that replace the `resolve` function with a different options for
configuring how a field is resolved. Some custom object types may want to explicitly define a
`resolve` option type, or omit it entirely (eg, the SimpleObject plugin does not use resolvers).
# Add GraphQL plugin
URL: /docs/plugins/add-graphql
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 [#install]
npm
pnpm
yarn
bun
```bash
npm install --save @pothos/plugin-add-graphql
```
```bash
pnpm add @pothos/plugin-add-graphql
```
```bash
yarn add @pothos/plugin-add-graphql
```
```bash
bun add @pothos/plugin-add-graphql
```
## Import a schema [#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.
```typescript playground example="plugin-add-graphql"
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({
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 [#import-selected-types]
Use `add.types` instead of `add.schema` to import individual named types:
```typescript playground example="plugin-add-graphql-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 [#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`:
```typescript playground example="plugin-add-graphql-ref"
const builder = new SchemaBuilder({ plugins: [AddGraphQLPlugin] });
const ImportedUser = builder.addGraphQLObject(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.
| Method | Source type | Returned ref |
| ---------------------------- | ------------------------ | ------------ |
| `addGraphQLObject` | `GraphQLObjectType` | Object |
| `addGraphQLInterface` | `GraphQLInterfaceType` | Interface |
| `addGraphQLUnion` | `GraphQLUnionType` | Union |
| `addGraphQLEnum` | `GraphQLEnumType` | Enum |
| `addGraphQLInput` | `GraphQLInputObjectType` | Input 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 [#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:
```typescript playground example="plugin-add-graphql-kinds"
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({
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(ExistingNamed);
const SearchResult = builder.addGraphQLUnion(ExistingSearchResult);
const Order = builder.addGraphQLEnum<'asc' | 'desc'>(ExistingOrder);
const MemberFilter = builder.addGraphQLInput(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'`:
```graphql
query {
member {
name
}
search(filter: { name: "Leia" }, order: ASC) {
... on Member {
name
}
}
}
```
### Referencing imported interfaces and scalars by 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:
```typescript
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 [#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](/docs/guide/scalars).
# Complexity plugin
URL: /docs/plugins/complexity
Complexity plugin docs for Pothos
This plugin allows you to define complexity of fields and limit the maximum complexity, depth, and
breadth of queries.
## Usage [#usage]
### Install [#install]
npm
pnpm
yarn
bun
```bash
npm install --save @pothos/plugin-complexity
```
```bash
pnpm add @pothos/plugin-complexity
```
```bash
yarn add @pothos/plugin-complexity
```
```bash
bun add @pothos/plugin-complexity
```
### Setup [#setup]
```typescript
import SchemaBuilder from '@pothos/core';
import ComplexityPlugin from '@pothos/plugin-complexity';
const builder = new SchemaBuilder({
plugins: [ComplexityPlugin],
});
```
### Configure defaults and limits [#configure-defaults-and-limits]
To limit query complexity you can specify a maximum complexity either in the builder setup, or when
building the schema:
```typescript
const builder = new SchemaBuilder({
plugins: [ComplexityPlugin],
complexity: {
defaultComplexity: 1,
defaultListMultiplier: 10,
limit: {
complexity: 500,
depth: 10,
breadth: 50,
},
},
});
// or
const schema = builder.toSchema({
complexity: {
limit: {
complexity: 500,
depth: 10,
breadth: 50,
},
},
});
```
#### Options [#options]
* fieldComplexity: (optional,
`(args, ctx, field) => { field: number, multiplier: number } | number`): default complexity
calculation for fields. `defaultComplexity` and `defaultListMultiplier` will not be used if this
is set.
* defaultComplexity: (optional `number`) defines the default complexity for every field in the
schema
* defaultListMultiplier: (optional `number`) defines a default complexity multiplier for a list
fields sub selections
* limit: Defines limits for queries. For request-specific limits, pass a function such as
`limit: (ctx) => ({ complexity: 500, depth: 10, breadth: 50 })`.
* complexity: defines the maximum complexity allowed for queries
* depth: defines the maximum depth of selections in a query
* breadth: defines the maximum total selections in a query
* complexityError: (optional `function`) defines the error to throw when the query complexity
exceeds the limit. The function is passed the errorKind (depth, breadth, or complexity), the
result (with the depth, breadth, complexity, and max values), and a GraphQL `info` object. It should
return (or throw) an error, or an error message as a string
### How complexity is calculated [#how-complexity-is-calculated]
Complexity is calculated before resolving any root level fields (query, mutation,
subscription), and is based purely on the shape of the query before execution begins.
The complexity of a query is the sum of the complexity of each selected field. If a field has
sub-selections, the complexity of its sub-selections are multiplied by a fields multiplier, and then
added to the fields own complexity. The default multiplier for fields is 1, and 10 for list fields.
This multiplier is meant to represent the n+1 complexity of list fields.
#### Example [#example]
The following query has a complexity of `131` (assuming we are using the default options), a depth
of `3`, and a breadth of `5`:
```gql
query {
posts {
# complexity = 131 (posts + 10 * (2 + 11))
author {
# complexity = 2 (author + 1 * name)
name # complexity = 1, depth: 3
}
comments {
# complexity = 11 (comments + 10 * comment)
comment # complexity = 1, depth: 3
}
}
}
```
### Defining complexity of a field [#defining-complexity-of-a-field]
The following alternatives assume a `Post` object ref and a `loadPosts(limit)` function returning
that model's records. Set a fixed cost with a number:
```typescript
builder.queryFields((t) => ({
posts: t.field({
type: [Post],
complexity: 20,
resolve: () => loadPosts(20),
}),
}));
```
The complexity option can also set the multiplier for a field:
```typescript
builder.queryFields((t) => ({
posts: t.field({
type: [Post],
complexity: { field: 5, multiplier: 20 },
resolve: () => loadPosts(20),
}),
}));
```
A fields complexity can also be based on the fields arguments, or the context value:
```typescript
builder.queryFields((t) => ({
posts: t.field({
type: [Post],
args: {
limit: t.arg.int(),
},
// base multiplier on how many posts are being requested
complexity: (args, ctx) => ({ field: 5, multiplier: Math.max(0, args.limit ?? 5) }),
resolve: (parent, args) => {
const limit = Math.max(0, args.limit ?? 5);
return loadPosts(limit);
},
}),
}));
```
### Example: a bounded list [#example-a-bounded-list]
This configuration caps query cost at `20`, depth at `3`, and breadth at `5`:
```typescript playground example="plugin-complexity"
const builder = new SchemaBuilder({
plugins: [ComplexityPlugin],
complexity: {
defaultComplexity: 1,
defaultListMultiplier: 10,
limit: { complexity: 20, depth: 3, breadth: 5 },
},
});
```
For a `Post` object with a `title` field, the list's base cost is `5` and its `limit` argument
sets the multiplier. Here, `posts` is an array of records and `resolverCalls` is a counter used
to observe whether the resolver is invoked:
```typescript playground example="plugin-complexity"
posts: t.field({
type: [Post],
args: { limit: t.arg.int({ defaultValue: 2 }) },
complexity: (args) => ({ field: 5, multiplier: Math.max(0, args.limit ?? 2) }),
resolve: (_parent, args) => {
resolverCalls += 1;
return posts.slice(0, Math.max(0, args.limit ?? 2));
},
}),
```
The selection `{ posts(limit: 2) { title } }` costs `5 + 2 × 1 = 7`. A limit of `16` costs `21`,
so the budget rejects that query before invoking the resolver. A limit of `15` costs exactly
`20` and is accepted. The multiplier follows the requested limit even when fewer records exist;
query cost estimates the operation rather than measuring the records actually returned.
## Utilities [#utilities]
### `complexityFromQuery(query, options)` [#complexityfromqueryquery-options]
Returns the query complexity for a given GraphQL query.
```typescript
import { complexityFromQuery } from '@pothos/plugin-complexity';
const complexity = complexityFromQuery(query, {
schema: schema,
// Complexity can be calculated based on the context and arguments,
// so you may need to provide valid values for the context and arguments.
// Both are optional, and will default to empty objects.
ctx: {},
variables: {},
});
```
### `createComplexityRule(options)` [#createcomplexityruleoptions]
Use this validation rule with GraphQL's `specifiedRules` to enforce `maxComplexity`, `maxDepth`,
or `maxBreadth` during validation. Supply the request's `context` and raw `variableValues`;
operation variable defaults and input defaults are applied before complexity callbacks run.
Pass `operationName` when the request selects a named operation. Only that operation is measured,
and an unknown name or invalid variables for it produce validation errors. Without `operationName`,
all operations whose variables can be coerced are measured independently. In a multi-operation
document, variable errors for other possible operations are left to GraphQL execution to report for
the selected operation. A single operation's variable errors are reported during validation.
```typescript
import { type GraphQLSchema, parse, specifiedRules, validate } from 'graphql';
import { createComplexityRule } from '@pothos/plugin-complexity';
function validateRequest(
schema: GraphQLSchema,
query: string,
context: object,
variableValues: Record,
operationName?: string,
) {
return validate(schema, parse(query), [
...specifiedRules,
createComplexityRule({ context, variableValues, operationName, maxComplexity: 500 }),
]);
}
```
These validation results depend on the request's variables, context, and operation name. Do not
reuse them across requests based only on the query text. `complexityFromQuery` also applies variable
defaults and throws for invalid variables; it measures the first operation in the document.
# Dataloader plugin
URL: /docs/plugins/dataloader
Batch object and relation loads, preserve result order, and cache records per request.
The dataloader plugin batches loads for fields and object types. Resolvers return record keys,
and the plugin loads those records through a DataLoader shared within the request. This reduces
repeated database calls when a query selects the same relation for many objects.
## Usage [#usage]
### Install [#install]
To use the dataloader plugin you will need to install both the `dataloader` package and the Pothos
dataloader plugin:
npm
pnpm
yarn
bun
```bash
npm install --save dataloader @pothos/plugin-dataloader
```
```bash
pnpm add dataloader @pothos/plugin-dataloader
```
```bash
yarn add dataloader @pothos/plugin-dataloader
```
```bash
bun add dataloader @pothos/plugin-dataloader
```
### Setup [#setup]
```typescript
import SchemaBuilder from '@pothos/core';
import DataloaderPlugin from '@pothos/plugin-dataloader';
type UserShape = {
id: string;
username: string;
lastPostID: number;
postIDs: number[];
};
type ContextType = {
currentUser: UserShape;
loadUsersById: (ids: string[]) => Promise;
};
const builder = new SchemaBuilder<{ Context: ContextType }>({
plugins: [DataloaderPlugin],
});
```
### loadable objects [#loadable-objects]
Later examples that redefine `User` replace this definition; choose the options needed by your
application. They are not additional declarations in the same schema.
To create an object type that can be loaded with a dataloader use the new `builder.loadableObject`
method:
```typescript
const User = builder.loadableObject('User', {
// load will be called with ids of users that need to be loaded
// Note that the types for keys (and context if present) are required
load: (ids: string[], context: ContextType) => context.loadUsersById(ids),
fields: (t) => ({
id: t.exposeID('id', {}),
username: t.string({
// the shape of parent will be inferred from `loadUsersById()` above
resolve: (parent) => parent.username,
}),
}),
});
```
Return one value from `load` for each requested ID, in the same order. The order is used to map results to their IDs, and if the results are returned in
a different order, your GraphQL requests will end up with the wrong data. Correctly sorting results
returned from a database or other data source can be tricky, so this plugin has a `sort`
option (described below) to simplify the sorting process. For more details on how the load function
works, see the [dataloader docs](https://github.com/graphql/dataloader#batch-function).
Within one request, repeated keys share the same loader entry. For example, requesting
`["3", "1", "3"]` can load keys `"3"` and `"1"` once while returning three results in the
requested order. Later loads of the same key reuse the cached result. Pass a fresh context object
for each request to keep those cached records separate.
When defining fields that return `User`s, you will now be able to return either a `string` (based on the
ids param of `load`), or a User object (type based on the return type of `loadUsersById`).
```typescript
builder.queryType({
fields: (t) => ({
user: t.field({
type: User,
args: {
id: t.arg.string({ required: true }),
},
// Here we can just return the ID directly rather than loading the user ourselves
resolve: (root, args) => args.id,
}),
currentUser: t.field({
type: User,
// If we already have the user, we use it, and the dataloader will not be called
resolve: (root, args, context) => context.currentUser,
}),
users: t.field({
type: [User],
args: {
ids: t.arg.stringList({ required: true }),
},
// Mixing ids and user objects also works
resolve: (_root, args, context) => [...args.ids, context.currentUser],
}),
}),
});
```
Pothos will detect when a resolver returns `string`, `number`, or `bigint` (typescript will
constrain the allowed types to whatever is expected by the load function). If a resolver returns an
object instead, Pothos knows it can skip the dataloader for that object.
### loadable fields [#loadable-fields]
The relation examples below extend `ContextType` with the post-loading methods they call.
`loadPosts` returns one `PostShape` per post ID; `postsByUserIds` and `loadPostsByUserIds` return
one `PostShape[]` per user ID, preserving the requested ID order. Each `posts` definition is an
alternative for the same field. For the grouped variant, add
`postsForUserIds: (ids: string[]) => Promise` to `ContextType`; it returns a flat
list with `authorId` on each post.
In some cases you may need more granular dataloaders. To handle these cases there is a new
`t.loadable` method for defining fields with their own dataloaders.
```typescript
// Normal object that the fields below will load
interface PostShape {
id: number;
authorId: string;
title: string;
content: string;
}
const Post = builder.objectRef('Post').implement({
fields: (t) => ({
id: t.exposeID('id', {}),
title: t.exposeString('title', {}),
content: t.exposeString('content', {}),
}),
});
// Loading a single Post
builder.objectField(User, 'latestPost', (t) =>
t.loadable({
type: Post,
// will be called with ids of latest posts for all users in query
load: (ids: number[], context) => context.loadPosts(ids),
resolve: (user, args) => user.lastPostID,
}),
);
// Loading multiple Posts
builder.objectField(User, 'posts', (t) =>
t.loadable({
type: [Post],
// will be called with ids of posts loaded for all users in query
load: (ids: number[], context) => context.loadPosts(ids),
resolve: (user, args) => user.postIDs,
}),
);
```
### loadableList fields for one-to-many relations [#loadablelist-fields-for-one-to-many-relations]
`loadable` fields can return lists, but do not work for loading a list of records from a single id.
The `loadableList` method can be used to define loadable fields that represent this kind of
relationship.
```typescript
// Loading multiple Posts
builder.objectField(User, 'posts', (t) =>
t.loadableList({
// type is singular, but will create a list field
type: Post,
// will be called with ids of all the users, and should return `Post[][]`
load: (ids: string[], context) => context.postsByUserIds(ids),
resolve: (user, args) => user.id,
}),
);
```
### loadableGroup fields for one-to-many relations [#loadablegroup-fields-for-one-to-many-relations]
In many cases, it's easier to load a flat list in a dataloader rather than loading a list of lists.
the `loadableGroup` method simplifies this.
```typescript
// Loading multiple Posts
builder.objectField(User, 'posts', (t) =>
t.loadableGroup({
// type is singular, but will create a list field
type: Post,
// will be called with ids of all the users, and should return `Post[]`
load: (ids: string[], context) => context.postsForUserIds(ids),
// will be called with each post to determine which group it belongs to
group: (post) => post.authorId,
resolve: (user, args) => user.id,
}),
);
```
### Accessing args on loadable fields [#accessing-args-on-loadable-fields]
By default the `load` method for fields does not have access to the fields arguments. This is
because the dataloader will aggregate the calls across different selections and aliases that may not
have the same arguments. To access the arguments, you can pass `byPath: true` in the fields options.
This will cause the dataloader to only aggregate calls for the same "path" in the query, meaning all
calls share the same arguments. This will allow you to access a 3rd `args` argument on the `load`
method.
```typescript
builder.objectField(User, 'posts', (t) =>
t.loadableList({
type: Post,
byPath: true,
args: {
limit: t.arg.int({ required: true }),
},
load: (ids: string[], context, args) => context.loadPostsByUserIds(ids, args.limit),
resolve: (user, args) => user.id,
}),
);
```
### dataloader options [#dataloader-options]
You can provide additional options for your dataloaders using `loaderOptions`.
```typescript
const User = builder.loadableObject('User', {
loaderOptions: { maxBatchSize: 20 },
load: (ids: string[], context: ContextType) => context.loadUsersById(ids),
fields: (t) => ({ id: t.exposeID('id', {}) }),
});
builder.objectField(User, 'posts', (t) =>
t.loadable({
type: [Post],
loaderOptions: { maxBatchSize: 20 },
load: (ids: number[], context) => context.loadPosts(ids),
resolve: (user, args) => user.postIDs,
}),
);
```
See [dataloader docs](https://github.com/graphql/dataloader#api) for all available options.
### Manually using dataloader [#manually-using-dataloader]
Dataloaders for "loadable" objects can be accessed via their ref by passing in the context object
for the current request. Pass a fresh context object for each request so cached records are not
shared between users.
Reusing the context also reuses its loaders:
```typescript
// create loadable object
const User = builder.loadableObject('User', {
load: (ids: string[], context: ContextType) => context.loadUsersById(ids),
fields: (t) => ({
id: t.exposeID('id', {}),
}),
});
builder.queryField('user', (t) =>
t.field({
type: User,
resolve: (parent, args, context) => {
// get data loader for User type
const loader = User.getDataloader(context);
// manually load a user
return loader.load('123');
},
}),
);
```
### Errors [#errors]
Calling dataloader.loadMany will resolve to a value like `(Type | Error)[]`. Your `load` function
may also return results in that format if your loader can have partial failures. GraphQL does not
have special handling for Error objects. Instead Pothos will map these results to something like
`(Type | Promise)[]` where Errors are replaced with promises that will be rejected. This
allows the normal graphql resolver flow to correctly handle these errors.
If you are using the `loadMany` method from a dataloader manually, you can apply the same mapping
using the `rejectErrors` helper:
```typescript
import { rejectErrors } from '@pothos/plugin-dataloader';
builder.queryField('user', (t) =>
t.field({
type: [User],
resolve: (parent, args, context) => {
const loader = User.getDataloader(context);
return rejectErrors(loader.loadMany(['123', '456']));
},
}),
);
```
### (Optional) Adding loaders to context [#optional-adding-loaders-to-context]
If you want to make dataloaders accessible via the context object directly, there is some additional
setup required. Below are a few options for different ways you can load data from the context
object. You can determine which of these options works best for you or add you own helpers.
The following interface replaces `ContextType` from the setup; include its original fields as
well as the loader helpers:
```typescript
import DataLoader from 'dataloader';
import { LoadableRef } from '@pothos/plugin-dataloader';
export interface ContextType {
currentUser: UserShape;
loadUsersById: (ids: string[]) => Promise;
userLoader: DataLoader; // expose a specific loader
getLoader: (ref: LoadableRef) => DataLoader; // helper to get a loader from a ref
load: (ref: LoadableRef, id: K) => Promise; // helper for loading a single resource
loadMany: (ref: LoadableRef, ids: K[]) => Promise<(Error | V)[]>; // helper for loading many
// other context fields
}
```
Next you'll need to update your context factory function. The exact format of this depends on what
graphql server implementation you are using.
```typescript
import { initContextCache } from '@pothos/core';
import { rejectErrors } from '@pothos/plugin-dataloader';
export const createContext = (
currentUser: UserShape,
loadUsersById: ContextType['loadUsersById'],
): ContextType => ({
currentUser,
loadUsersById,
// Preserve the cache when the server copies or extends the context.
...initContextCache(),
// using getters allows us to access the context object using `this`
get userLoader() {
return User.getDataloader(this);
},
get getLoader() {
return (ref: LoadableRef) => ref.getDataloader(this);
},
get load() {
return (ref: LoadableRef, id: K) => ref.getDataloader(this).load(id);
},
get loadMany() {
return (ref: LoadableRef, ids: K[]) =>
ref.getDataloader(this).loadMany(ids);
},
});
```
Call `createContext` for each request with the authenticated user and that request's data access
function. You can then use these helpers from resolvers:
```typescript
builder.queryFields((t) => ({
fromContext1: t.field({
type: User,
resolve: (root, args, { userLoader }) => userLoader.load('123'),
}),
fromContext2: t.field({
type: User,
resolve: (root, args, { getLoader }) => getLoader(User).load('456'),
}),
fromContext3: t.field({
type: User,
resolve: (root, args, { load }) => load(User, '789'),
}),
fromContext4: t.field({
type: [User],
resolve: (root, args, { loadMany }) => {
const users = loadMany(User, ['123', '456']);
return rejectErrors(users);
},
}),
}));
```
### Using with the Relay plugin [#using-with-the-relay-plugin]
Register both `DataloaderPlugin` and `RelayPlugin` and set `relay: {}` on the builder to use
`loadableNode`. You can use this method to create `node` objects that work like other loadable objects.
```typescript
const UserNode = builder.loadableNode('UserNode', {
id: {
resolve: (user) => user.id,
},
load: (ids: string[], context: ContextType) => context.loadUsersById(ids),
fields: (t) => ({}),
});
```
#### Loadable connections [#loadable-connections]
This example adds `loadFriendsByUserIds: (ids: string[]) => Promise>`
to `ContextType`. Return an entry for every requested ID, with an empty array for users without
friends. The helper indexes that result by user ID before paginating each list.
To data-load a connection, you can use a combination of helpers:
* `builder.connectionObject` To create the connection and edge types
* `t.loadable` with the `byPath` option to create a loadable field with access to arguments
* `t.arg.connectionArgs` to add the standard connection arguments to the field
```typescript
import { resolveArrayConnection } from '@pothos/plugin-relay';
const UserFriendsConnection = builder.connectionObject({
type: User,
name: 'UserFriendsConnection',
});
builder.objectFields(User, (t) => ({
friends: t.loadable({
type: UserFriendsConnection,
byPath: true,
args: {
...t.arg.connectionArgs(),
},
load: async (ids: string[], context, args) => {
// This implementation assumes you will load all friends for each user, and then filter them with `resolveArrayConnection`.
// This may not be efficient in a large production system
const friendsById = await context.loadFriendsByUserIds(ids);
return ids.map((id) => {
return resolveArrayConnection({ args }, friendsById[id]);
});
},
resolve: (user) => user.id.toString(),
}),
}));
```
### Loadable Refs and Circular references [#loadable-refs-and-circular-references]
You may run into type errors if you define 2 loadable objects that circularly reference each other
in their definitions.
There are a some general strategies to avoid this outlined in the
[circular-references guide](../guide/circular-references).
This plugin also has methods for creating refs (similar to `builder.objectRef`) that can be used to
split the definition and implementation of your types to avoid any issues with circular references.
```typescript
const User = builder.loadableObjectRef('User', {
load: (ids: string[], context: ContextType) => context.loadUsersById(ids),
});
User.implement({
fields: (t) => ({
id: t.exposeID('id', {}),
}),
});
// Or with relay
const UserNode = builder.loadableNodeRef('UserNode', {
load: (ids: string[], context: ContextType) => context.loadUsersById(ids),
id: {
resolve: (user) => user.id,
},
});
UserNode.implement({
fields: (t) => ({}),
});
```
All the plugin specific options should be passed when defining the ref. This allows the ref to be
used by any method that accepts a ref to implement an object:
```typescript
const User = builder.loadableObjectRef('User', {
load: (ids: string[], context: ContextType) => context.loadUsersById(ids),
});
builder.objectType(User, {
fields: (t) => ({
id: t.exposeID('id', {}),
}),
});
```
The above example is not useful on its own, but this pattern will allow these refs to be used with
others that also allow you to define object types with additional behaviors.
### Caching resources loaded manually in a resolver [#caching-resources-loaded-manually-in-a-resolver]
When manually loading a resource in a resolver it is not automatically added to the dataloader
cache. If you want any resolved value to be stored in the cache in case it is used somewhere else in
the query you can use the `cacheResolved` option.
The `cacheResolved` option takes a function that converts the loaded object into its cache key:
```typescript
const User = builder.loadableObject('User', {
load: (ids: string[], context: ContextType) => context.loadUsersById(ids),
cacheResolved: user => user.id,
fields: (t) => ({
id: t.exposeID('id', {}),
}),
});
```
Whenever a resolver returns a User or list of Users, those objects will automatically be added to the
dataloaders cache, so they can be re-used in other parts of the query.
### Sorting results from your `load` function [#sorting-results-from-your-load-function]
As mentioned above, the `load` function must return results in the same order as the provided array
of IDs. Doing this correctly can be a little complicated, so this plugin includes an alternative.
For any type or field that creates a dataloader, you can also provide a `sort` option which will
correctly map your results into the correct order based on their ids. To do this, you will need to
provide a function that accepts a result object, and returns its id.
The `loadUsersById` function can return records in storage order. `sort` uses each user's ID to
put them in the requested order:
```typescript playground example="plugin-dataloader"
const User = builder.loadableObject('User', {
load: (ids: string[], context: ContextType) => context.loadUsersById(ids),
sort: user => user.id,
fields: (t) => ({
id: t.exposeID('id', {}),
}),
});
```
This also works with loadable nodes, interfaces, unions, and fields. Missing keys become `null`
after sorting. Choose field and list-item nullability to match that behavior; a missing record in
a non-null position produces a GraphQL error. Without `sort`, return one result for every key in
the same order, using `null` or an `Error` for missing records as appropriate for your loader.
When sorting, if the list of results contains an Error the error is thrown because it can not be
mapped to the correct location. This `sort` option should NOT be used for cases where the result
list is expected to contain errors.
### Shared `toKey` method. [#shared-tokey-method]
Defining multiple functions to extract the key from a loaded object can become redundant. In cases
when you are using both `cacheResolved` and `sort` you can use a `toKey` function instead:
```typescript
const User = builder.loadableObject('User', {
load: (ids: string[], context: ContextType) => context.loadUsersById(ids),
toKey: user => user.id,
cacheResolved: true,
sort: true,
fields: (t) => ({
id: t.exposeID('id', {}),
}),
});
```
### Subscriptions [#subscriptions]
Dataloaders are stored on the context object of the subscription. This means that values are cached across the full lifetime of the subscription.
To reset all data loaders for the current subscription, you can use the `clearAllDataLoaders` helper.
```typescript
import { clearAllDataLoaders } from '@pothos/plugin-dataloader';
clearAllDataLoaders(context);
```
# Directives plugin
URL: /docs/plugins/directives
Attach directive metadata to types, fields, and schemas for other tools to consume.
Attach schema directives to Pothos types and fields. The plugin records directive metadata in
GraphQL extensions and AST nodes. A schema transformer or another consumer implements the
behavior of directives such as rate limits or authorization checks.
## Install [#install]
npm
pnpm
yarn
bun
```bash
npm install --save @pothos/plugin-directives
```
```bash
pnpm add @pothos/plugin-directives
```
```bash
yarn add @pothos/plugin-directives
```
```bash
bun add @pothos/plugin-directives
```
## Declare and attach directives [#declare-and-attach-directives]
The `Directives` schema type declares each directive's allowed locations and argument shape.
This example records a `rateLimit` directive on the `hello` field:
```typescript playground example="plugin-directives"
import SchemaBuilder from '@pothos/core';
import DirectivesPlugin from '@pothos/plugin-directives';
const builder = new SchemaBuilder<{
Directives: {
rateLimit: {
locations: 'FIELD_DEFINITION';
args: { limit: number; duration: number };
};
};
}>({
plugins: [DirectivesPlugin],
});
builder.queryType({
fields: (t) => ({
hello: t.string({
directives: [{ name: 'rateLimit', args: { limit: 5, duration: 60 } }],
resolve: () => 'world',
}),
}),
});
```
The schema still returns `world` on every request until a consumer applies the rate limit.
The generic checks your Pothos definitions; it does not register a GraphQL directive definition.
## Directive formats [#directive-formats]
An array preserves order and allows a directive to occur more than once:
```typescript
builder.queryField('greeting', (t) =>
t.string({
directives: [{ name: 'rateLimit', args: { limit: 5, duration: 60 } }],
resolve: () => 'hello',
}),
);
```
For a directive that occurs once, an object is also accepted. This is an alternative definition
of the same field:
```typescript
builder.queryField('greeting', (t) =>
t.string({
directives: { rateLimit: { limit: 5, duration: 60 } },
resolve: () => 'hello',
}),
);
```
By default, extensions contain an array of `{ name, args }` entries. Set
`directives: { useGraphQLToolsUnorderedDirectives: true }` on the builder when a consumer requires
an object keyed by directive name. That output format does not preserve order across directive names.
## Locations [#locations]
Use `directives` in the options for the type, field, argument, or enum value being annotated.
The supported locations are:
* `ARGUMENT_DEFINITION`
* `ENUM_VALUE`
* `ENUM`
* `FIELD_DEFINITION`
* `INPUT_FIELD_DEFINITION`
* `INPUT_OBJECT`
* `INTERFACE`
* `OBJECT`
* `SCALAR`
* `SCHEMA`
* `UNION`
For `SCHEMA` directives, use `schemaDirectives` on `builder.toSchema()`. Declare `SCHEMA` among
that directive's allowed locations in the `Directives` generic. The separate `directives` option
on `toSchema()` accepts GraphQL directive definitions; it does not attach directives to the schema.
## Apply directive behavior [#apply-directive-behavior]
Pass the built schema to the transformer supplied by your directive library. For example, with
`graphql-rate-limit-directive` installed:
```typescript
import { rateLimitDirective } from 'graphql-rate-limit-directive';
const { rateLimitDirectiveTransformer } = rateLimitDirective();
const schema = rateLimitDirectiveTransformer(builder.toSchema());
```
Check the consumer's required extensions format and directive-definition setup when integrating
another library. Pothos stores the annotations; the consumer determines their runtime behavior.
# Errors plugin
URL: /docs/plugins/errors
Errors plugin docs for Pothos
Represent expected failures as GraphQL result unions. Register error classes as object types, then
list them in a field's `errors` option. The plugin catches matching errors and returns an error
object that clients can query with fragments. Errors that do not match still become GraphQL errors.
## Usage [#usage]
### Install [#install]
npm
pnpm
yarn
bun
```bash
npm install --save @pothos/plugin-errors
```
```bash
pnpm add @pothos/plugin-errors
```
```bash
yarn add @pothos/plugin-errors
```
```bash
bun add @pothos/plugin-errors
```
### Example Usage [#example-usage]
```typescript
import SchemaBuilder from '@pothos/core';
import ErrorsPlugin from '@pothos/plugin-errors';
const builder = new SchemaBuilder({
plugins: [ErrorsPlugin],
errors: {
defaultTypes: [],
// onResolvedError: (error) => console.error('Handled error:', error),
},
});
builder.objectType(Error, {
name: 'Error',
fields: (t) => ({
message: t.exposeString('message'),
}),
});
builder.queryType({
fields: (t) => ({
hello: t.string({
errors: {
types: [Error],
},
args: {
name: t.arg.string({ required: false }),
},
resolve: (parent, { name }) => {
if (name && name.slice(0, 1) !== name.slice(0, 1).toUpperCase()) {
throw new Error('name must be capitalized');
}
return `hello, ${name || 'World'}`;
},
}),
}),
});
```
The above example will produce a GraphQL schema that looks like:
```graphql
type Error {
message: String!
}
type Query {
hello(name: String): QueryHelloResult
}
union QueryHelloResult = Error | QueryHelloSuccess
type QueryHelloSuccess {
data: String!
}
```
This field can be queried using fragments like:
```graphql
query {
hello(name: "World") {
__typename
... on Error {
message
}
... on QueryHelloSuccess {
data
}
}
}
```
This plugin works by wrapping fields that define error options in a union type. This union consists
of an object type for each error type defined for the field, and a Success object type that wraps
the returned data. If the fields resolver throws an instance of one of the defined errors, the
errors plugin will automatically resolve to the corresponding error object type.
### Expected and unexpected errors [#expected-and-unexpected-errors]
Only errors matching a field's registered classes become typed results. With the imports and
builder setup above, this alternative query definition catches `NameTooShort` and exposes its
`message` and `minimum` fields. Successful greetings use `QueryGreetingSuccess`; a plain `Error`
remains a GraphQL error because this field does not register that class.
```typescript playground example="plugin-errors"
class NameTooShort extends Error {
minimum = 3;
constructor() {
super('Use at least three characters');
}
}
builder.objectType(NameTooShort, {
name: 'NameTooShort',
fields: (t) => ({ message: t.exposeString('message'), minimum: t.exposeInt('minimum') }),
});
builder.queryType({
fields: (t) => ({
greeting: t.string({
args: { name: t.arg.string({ required: true }), simulateFailure: t.arg.boolean() },
errors: { types: [NameTooShort] },
resolve: (_, { name, simulateFailure }) => {
if (simulateFailure) {
throw new Error('Service unavailable');
}
if (name.length < 3) {
throw new NameTooShort();
}
return `Hello, ${name}`;
},
}),
}),
});
```
### Builder options [#builder-options]
* `defaultTypes`: An array of Error classes to include in every field with error handling.
* `directResult`: Sets the default for `directResult` option on fields (only affects non-list
fields)
* `onResolvedError`: A callback function that is called when an error is handled by the plugin
* `defaultResultOptions`: Defaults for generated success object types.
* `defaultUnionOptions`: Defaults for generated result union types.
* `defaultItemResultOptions`: Defaults for per-item success object types.
* `defaultItemUnionOptions`: Defaults for per-item result union types.
* `unsafelyHandleInputErrors`: Also catch input mapping and validation errors; see
[With validation plugin](#with-validation-plugin).
The four type-options objects accept a `name` function receiving `{ parentTypeName, fieldName }`.
Use distinct names for success objects and unions:
```typescript
const builder = new SchemaBuilder({
plugins: [ErrorsPlugin],
errors: {
defaultTypes: [Error],
defaultResultOptions: {
name: ({ parentTypeName, fieldName }) => `${parentTypeName}_${fieldName}_Success`,
},
defaultUnionOptions: {
name: ({ parentTypeName, fieldName }) => `${parentTypeName}_${fieldName}_Result`,
},
},
});
```
### Options on Fields [#options-on-fields]
* `types`: An array of Error classes to catch and handle as error objects in the schema. Will be
merged with `defaultTypes` from builder.
* `union`: An options object for the union type. Can include any normal union type options, and
`name` option for setting a custom name for the union type.
* `result`: An options object for result object type. Can include any normal object type options,
and `name` option for setting a custom name for the result type.
* `dataField`: An options object for the data field on the result object. This field will be named
`data` by default, but can be renamed by passing a custom `name` option.
* `directResult`: Boolean, can only be set to true for non-list fields. This will directly include
the fields type in the union rather than creating an intermediate Result object type. This will
throw at build time if the type is not an object type.
### A shared Error interface [#a-shared-error-interface]
1. Set up an Error interface
2. Create a BaseError object type
3. Include the Error interface in any custom Error types you define
4. Include the BaseError type in the `defaultTypes` in the builder config
This pattern will allow you to consistently query your schema using a `... on Error { message }`
fragment since all Error classes extend that interface. If your client wants to query details of
more specialized error types, they can just add a fragment for the errors it cares about. This
pattern should also make it easier to make future changes without unexpected breaking changes for
your clients.
This is a separate setup from the first example:
```typescript
import SchemaBuilder from '@pothos/core';
import ErrorsPlugin from '@pothos/plugin-errors';
const builder = new SchemaBuilder({
plugins: [ErrorsPlugin],
errors: {
defaultTypes: [Error],
},
});
const ErrorInterface = builder.interfaceRef('Error').implement({
fields: (t) => ({
message: t.exposeString('message'),
}),
});
builder.objectType(Error, {
name: 'BaseError',
interfaces: [ErrorInterface],
});
class LengthError extends Error {
minLength: number;
constructor(minLength: number) {
super(`string length should be at least ${minLength}`);
this.minLength = minLength;
this.name = 'LengthError';
}
}
builder.objectType(LengthError, {
name: 'LengthError',
interfaces: [ErrorInterface],
fields: (t) => ({
minLength: t.exposeInt('minLength'),
}),
});
builder.queryType({
fields: (t) => ({
// Simple error handling just using base error class
hello: t.string({
errors: {},
args: {
name: t.arg.string({ required: true }),
},
resolve: (parent, { name }) => {
if (!name.startsWith(name.slice(0, 1).toUpperCase())) {
throw new Error('name must be capitalized');
}
return `hello, ${name || 'World'}`;
},
}),
// Handling custom errors
helloWithMinLength: t.string({
errors: {
types: [LengthError],
},
args: {
name: t.arg.string({ required: true }),
},
resolve: (parent, { name }) => {
if (name.length < 5) {
throw new LengthError(5);
}
return `hello, ${name || 'World'}`;
},
}),
}),
});
```
### With validation plugin [#with-validation-plugin]
The validation plugin runs before field resolution. Set `unsafelyHandleInputErrors: true` to
return its failures as typed results. A validation failure then returns without running field
authorization hooks, so only enable this when those error details may be returned before authorization.
This standalone example registers the validation error and its issues. Valid input reaches the
resolver; invalid input returns `InputValidationError`.
```typescript
import SchemaBuilder from '@pothos/core';
import ErrorsPlugin from '@pothos/plugin-errors';
import ValidationPlugin, {
InputValidationError,
type StandardSchemaV1,
} from '@pothos/plugin-validation';
import { z } from 'zod';
const builder = new SchemaBuilder({
plugins: [ErrorsPlugin, ValidationPlugin],
errors: { unsafelyHandleInputErrors: true },
});
const InputValidationIssue = builder
.objectRef('InputValidationIssue')
.implement({
fields: (t) => ({
message: t.exposeString('message'),
path: t.stringList({
resolve: (issue) => issue.path?.map((part) =>
String(typeof part === 'object' ? part.key : part),
) ?? [],
}),
}),
});
builder.objectType(InputValidationError, {
name: 'InputValidationError',
fields: (t) => ({
issues: t.field({
type: [InputValidationIssue],
resolve: (err) => err.issues,
}),
}),
});
builder.queryType();
builder.queryField('fieldWithValidation', (t) =>
t.boolean({
errors: {
types: [InputValidationError],
},
args: {
string: t.arg.string({
required: true,
validate: z.string().min(3, 'Too short'),
}),
},
resolve: () => true,
}),
);
```
Example query:
```graphql
query {
fieldWithValidation(string: "a") {
__typename
... on QueryFieldWithValidationSuccess {
data
}
... on InputValidationError {
issues {
message
path
}
}
}
}
```
Validation also protects writes inside a mutation resolver. This alternative builder stores
accepted names and returns structured issues for invalid input without changing the stored data.
It uses the same imports as the validation example above and requires
`@pothos/plugin-validation` and `zod` alongside the errors plugin.
```typescript playground example="plugin-validation"
// This public example deliberately exposes validation details before authorization.
const builder = new SchemaBuilder({
plugins: [ErrorsPlugin, ValidationPlugin],
errors: { unsafelyHandleInputErrors: true },
});
const names: string[] = [];
const Issue = builder.objectRef('ValidationIssue').implement({
fields: (t) => ({
message: t.exposeString('message'),
path: t.stringList({
resolve: (issue) =>
issue.path?.map((part) => String(typeof part === 'object' ? part.key : part)) ?? [],
}),
}),
});
builder.objectType(InputValidationError, {
name: 'InputValidationError',
fields: (t) => ({ issues: t.field({ type: [Issue], resolve: (error) => error.issues }) }),
});
const Registration = builder.inputType('Registration', {
fields: (t) => ({
name: t.string({ required: true }).validate(z.string().trim().min(3, 'Name is too short')),
email: t.string({ required: true, validate: z.email('Enter a valid email') }),
}),
});
builder.mutationType({
fields: (t) => ({
register: t.string({
args: { input: t.arg({ type: Registration, required: true }) },
errors: { types: [InputValidationError] },
resolve: (_, { input }) => {
names.push(input.name);
return input.name;
},
}),
}),
});
```
### With the dataloader plugin [#with-the-dataloader-plugin]
To use this in combination with the dataloader plugin, ensure that the errors plugin is listed
BEFORE the dataloader plugin in your plugin list.
If a field with `errors` returns a `loadableObject`, or `loadableNode` the errors plugin will now
catch errors thrown when loading ids returned by the `resolve` function.
For a list of loadable IDs, loading happens per item. A load failure is not a whole-field failure,
so the field's `errors` option does not catch it. Use explicit error unions when you need typed
results for individual loads; see [List item errors](#list-item-errors) for the separate
`itemErrors` handling of errors returned or thrown while iterating the resolver's list.
### With the prisma plugin [#with-the-prisma-plugin]
To use this in combination with the prisma plugin, ensure that the errors plugin is listed BEFORE
the prisma plugin in your plugin list. This will enable `errors` option to work correctly with any
field builder method from the prisma plugin.
`errors` can be configured for any field, but if there is an error pre-loading a relation the error
will always be surfaced at the field that executed the query. Because there are cases that fall back to
executing queries for relation fields, these fields may still have errors if the relation was not
pre-loaded. Detection of nested relations will continue to work if those relations use the `errors`
plugin
### List item errors [#list-item-errors]
Use `itemErrors` on a list field to wrap each item in its own result union. It accepts the same
options as `errors`. The following addition to the shared Error interface example yields one success, then throws an error. The plugin returns that error as the final item:
```typescript
builder.queryField('listWithErrors', (t) =>
t.intList({
nullable: false,
itemErrors: {},
resolve: function* () {
yield 1;
throw new Error('Boom');
},
}),
);
```
```graphql
type Query {
listWithErrors: [QueryListWithErrorsItemResult!]!
}
union QueryListWithErrorsItemResult = BaseError | QueryListWithErrorsItemSuccess
type QueryListWithErrorsItemSuccess {
data: Int!
}
```
At runtime, `itemErrors` also handles returned error instances. If the resolver's ordinary return
type does not include errors, use an explicit error union for a list that mixes returned data and errors.
The plugin also wraps sync and async iterators. A yielded error becomes an error item; a thrown
error becomes the final item and closes the iterator. Async iterable execution requires an executor
that supports it, such as GraphQL 17.
Combine `errors: {}` with `itemErrors: {}` to handle a whole-field failure as well. The outer
`QueryListWithErrorsResult` union contains `BaseError` and `QueryListWithErrorsSuccess`; the latter's
`data` field contains the list of `QueryListWithErrorsItemResult` items.
For an async source, the same field can use an async generator. Replace its resolver with:
```typescript
resolve: async function* () {
yield 1;
throw new Error('The next item could not be loaded');
},
```
This requires an executor that supports async iterables. A thrown error ends the iterator;
use an explicit error union when the source needs to return an error item and continue.
To handle both a failure before the list is returned and a failure during iteration, replace
`listWithErrors` with:
```typescript
builder.queryField('listWithErrors', (t) =>
t.intList({
nullable: false,
errors: {},
itemErrors: {},
args: { failBeforeLoad: t.arg.boolean() },
resolve: (_parent, { failBeforeLoad }) => {
if (failBeforeLoad) throw new Error('Could not load the list');
return (function* () {
yield 1;
throw new Error('Could not load the next item');
})();
},
}),
);
```
The generated result types are:
```graphql
union QueryListWithErrorsResult = BaseError | QueryListWithErrorsSuccess
type QueryListWithErrorsSuccess {
data: [QueryListWithErrorsItemResult!]!
}
union QueryListWithErrorsItemResult = BaseError | QueryListWithErrorsItemSuccess
type QueryListWithErrorsItemSuccess {
data: Int!
}
```
### Custom error union fields [#custom-error-union-fields]
Use `t.errorUnionField` and `t.errorUnionListField` to directly specify all members of the returned union type,
including multiple success types and error types.
The following additions use the `Error` class registered as `BaseError` in the shared-interface
example. Each success shape has an `isTypeOf` check so Pothos can distinguish plain objects.
The resolver bodies simulate create and update outcomes to demonstrate the union branches;
replace them with your application's persistence logic.
```typescript
const CreateResult = builder.objectRef<{ id: string; created: boolean }>('CreateResult').implement({
isTypeOf: (obj) => typeof obj === 'object' && obj !== null && 'created' in obj,
fields: (t) => ({
id: t.exposeString('id'),
created: t.exposeBoolean('created'),
}),
});
const UpdateResult = builder.objectRef<{ id: string; updated: boolean }>('UpdateResult').implement({
isTypeOf: (obj) => typeof obj === 'object' && obj !== null && 'updated' in obj,
fields: (t) => ({
id: t.exposeString('id'),
updated: t.exposeBoolean('updated'),
}),
});
builder.mutationType({
fields: (t) => ({
modifyUser: t.errorUnionField({
types: [CreateResult, UpdateResult, Error],
args: {
id: t.arg.id({ required: true }),
name: t.arg.string({ required: true }),
create: t.arg.boolean({ required: true }),
},
resolve: (parent, { id, name, create }) => {
if (name.length < 3) return new Error('Name too short');
return create ? { id: String(id), created: true } : { id: String(id), updated: true };
},
}),
processUsers: t.errorUnionListField({
types: [CreateResult, UpdateResult, Error],
args: { ids: t.arg.idList({ required: true }) },
resolve: (parent, { ids }) => ids.map((id) =>
id === 'unknown' ? new Error('User not found') : { id: String(id), updated: true },
),
}),
}),
});
```
#### Type resolution [#type-resolution]
Union members use standard Pothos type resolution. Registering an error class with
`builder.objectType` provides an `instanceof` check. Plain object types can supply `isTypeOf`, as
above, or you can pass a custom `resolveType` in the field's `union` options. Error instances matched
by the plugin are resolved before that custom callback.
For example, this alternative field resolves its success branches explicitly using the
`CreateResult` and `UpdateResult` types above:
```typescript
builder.mutationField('previewUserChange', (t) =>
t.errorUnionField({
types: [CreateResult, UpdateResult, Error],
args: { create: t.arg.boolean({ required: true }) },
union: {
resolveType: (value) => 'created' in value ? 'CreateResult' : 'UpdateResult',
},
resolve: (_parent, { create }) =>
create ? { id: '1', created: true } : { id: '1', updated: true },
}),
);
```
Matched error instances use the plugin's error map, so this callback only needs to distinguish
the success values.
### Using `builder.errorUnion` [#using-buildererrorunion]
You can use `builder.errorUnion` to manually construct an error union type that can be used with any field. Fields returning an error union will automatically handle returned or thrown errors.
This addition to the shared-interface example distinguishes a missing user from invalid input.
Both error types implement the shared `Error` interface, and validation failures expose the field
that needs correction:
```typescript
class NotFoundError extends Error {}
class ValidationError extends Error {
constructor(message: string, public field: string) {
super(message);
}
}
builder.objectType(NotFoundError, {
name: 'NotFoundError',
interfaces: [ErrorInterface],
});
builder.objectType(ValidationError, {
name: 'ValidationError',
interfaces: [ErrorInterface],
fields: (t) => ({ field: t.exposeString('field') }),
});
const User = builder.objectRef<{ id: string; name: string }>('User').implement({
isTypeOf: (obj) => typeof obj === 'object' && obj !== null && 'id' in obj && 'name' in obj,
fields: (t) => ({
id: t.exposeString('id'),
name: t.exposeString('name'),
}),
});
const UserResult = builder.errorUnion('UserResult', {
types: [User, NotFoundError, ValidationError],
});
builder.queryField('getUser', (t) =>
t.field({
type: UserResult,
args: { id: t.arg.string({ required: true }) },
resolve: (_, { id }) => {
if (!id) throw new ValidationError('ID required', 'id');
if (id === 'unknown') return new NotFoundError('User not found');
return { id, name: 'User' };
},
}),
);
```
Clients can select the common message or the details of a particular failure:
```graphql
query {
getUser(id: "") {
... on User {
id
name
}
... on Error {
message
}
... on ValidationError {
field
}
}
}
```
#### Options [#options]
* `types`: Array of member types (object refs, error classes, etc.)
* `omitDefaultTypes`: Set to `true` to exclude `defaultTypes` from the builder options (default: `false`)
* `resolveType`: Optional custom resolve function. Called after the internal error map check.
* All other standard union type options are supported
# Federation plugin
URL: /docs/plugins/federation
Federation plugin docs for Pothos
Build [Apollo Federation 2](https://www.apollographql.com/docs/federation/) subgraphs with Pothos.
Register entity keys, resolve references from other services, and extend external types.
The examples below describe separate users, inventory, and reviews services.
## Usage [#usage]
This page will describe the basics of the Pothos API for federation, but will not cover detailed
information on how federation works, or what all the terms on this page mean. For more general
information on federation, see the
[official docs](https://www.apollographql.com/docs/federation/v2/)
### Install [#install]
You will need to install the plugin, as well as the directives plugin (`@pothos/plugin-directives`)
and `@apollo/subgraph`
npm
pnpm
yarn
bun
```bash
npm install --save @pothos/plugin-federation @pothos/plugin-directives @apollo/subgraph
```
```bash
pnpm add @pothos/plugin-federation @pothos/plugin-directives @apollo/subgraph
```
```bash
yarn add @pothos/plugin-federation @pothos/plugin-directives @apollo/subgraph
```
```bash
bun add @pothos/plugin-federation @pothos/plugin-directives @apollo/subgraph
```
You will likely want to install @apollo/server as well, but it is not required if you want to use a
different server
npm
pnpm
yarn
bun
```bash
npm install --save @apollo/server
```
```bash
pnpm add @apollo/server
```
```bash
yarn add @apollo/server
```
```bash
bun add @apollo/server
```
### Setup [#setup]
```typescript
import SchemaBuilder from '@pothos/core';
import DirectivePlugin from '@pothos/plugin-directives';
import FederationPlugin from '@pothos/plugin-federation';
const builder = new SchemaBuilder({
// If you are using other plugins, the federation plugin should be listed after plugins like auth that wrap resolvers
plugins: [DirectivePlugin, FederationPlugin],
});
```
### Run the local examples [#run-the-local-examples]
The complete users, inventory, and reviews examples live in
[website/local-examples/federation](https://github.com/hayes/pothos/tree/main/website/local-examples/federation).
From a repository checkout with Node.js 22 or newer, install dependencies and run the local suites:
```sh
pnpm install --frozen-lockfile
pnpm --dir website check:local
```
This builds the required packages, installs the isolated local-example dependencies, and runs
the federation, Grafast, and smart-subscription suites. The federation check executes each `.graphql` operation with its variable sidecar and asserts the checked-in
expected response. Edit a representation in `users/02-entities.variables.json`, or the price and
weight in `inventory/query.variables.json`, then update the corresponding expectation to test the
changed result. These checks use in-process GraphQL execution. Serving each schema and composing
a gateway are separate steps below. Apollo's schema tooling needs Node.js, so these examples
run locally instead of in the browser playground.
### Defining entities [#defining-entities]
Defining entities for your schema is a 2 step process. First you will need to define an object type
as you would normally, then you can convert that object type to an entity by providing a `key` (or
`keys`), and a method to load that entity.
```typescript
type UserRecord = { id: string; name: string; username: string };
const users: UserRecord[] = [{ id: '1', name: 'Leia', username: 'leia' }];
const User = builder.objectRef('User').implement({
fields: (t) => ({
id: t.exposeID('id'),
name: t.exposeString('name'),
username: t.exposeString('username'),
}),
});
builder.asEntity(User, {
key: builder.selection<{ id: string }>('id'),
resolveReference: (user) => users.find(({ id }) => user.id === id),
});
```
Run the local users example's `Entities` operation. Its representations load Leia by ID and return `null`
for a missing user. Change the first representation's ID to `missing` to see reference resolution
change the response. This executes one subgraph's `_entities` field locally; composition,
gateway planning, and cross-service `requires`/`provides` behavior still need the local services
described below.
`keys` are defined using `builder.selection`. This method *MUST* be called with a generic argument
that defines the types for any fields that are part of the key. `key` may also be an array.
`resolveReference` will be called with the type used by the `key` selection.
Entities are Object types that may be extended with or returned by fields in other services.
`builder.asEntity` describes how the Entity will be loaded when used by another service. The `key`
select (or selection) should use the types of scalars your server will produce for inputs. For
example, Apollo server will convert all ID fields to `string`s, even if resolvers in other services
return IDs as numbers.
### Extending external entities [#extending-external-entities]
External entities can be extended by calling `builder.externalRef`, and then calling implement on
the returned ref.
`builder.externalRef` takes the name of the entity, a selection (using `builder.selection`, just
like a `key` on an entity object), and a resolve method that loads an object given a `key`. The
return type of the resolver is used as the backing type for the ref, and will be the type of the
`parent` arg when defining fields for this type. The `key` also describes what fields will be
selected from another service to use as the `parent` object in resolvers for fields added when
implementing the `externalRef`.
The inventory service uses a separate builder with the setup above:
```typescript
const inventory = [{ upc: '1', inStock: true }];
const Product = builder.externalRef(
'Product',
builder.selection<{ upc: string }>('upc'),
(entity) => {
const product = inventory.find(({ upc }) => upc === entity.upc);
// extends the entity ({upc: string}) with other product details available in this service
return product && { ...entity, ...product };
},
);
Product.implement({
// Additional external fields can be defined here which can be used by `requires` or `provides` directives
externalFields: (t) => ({
price: t.float(),
weight: t.float(),
}),
fields: (t) => ({
// exposes properties added during loading of the entity above
upc: t.exposeString('upc'),
inStock: t.exposeBoolean('inStock'),
shippingEstimate: t.float({
// fields can add a `requires` directive for any of the externalFields defined above
// which will be made available as part of the first arg in the resolver.
requires: builder.selection<{ weight?: number; price?: number }>('price weight'),
resolve: (data) => {
// free for expensive items
if ((data.price ?? 0) > 1000) {
return 0;
}
// estimate is based on weight
return (data.weight ?? 0) * 0.5;
},
}),
}),
});
```
The inventory example supplies `price` and `weight` explicitly in `_entities` representations.
Run it to compare a shipping estimate of `2` with free shipping for an expensive item. A gateway
would obtain the required fields from another service; this isolated local example does not perform that fetch.
### Selections with inline fragments [#selections-with-inline-fragments]
The template-literal type that checks selection strings can not express selections that use inline
fragments (e.g. selecting through a union or interface field). For these cases, a string can be cast
to `FieldSet` to bypass the selection string checks. The cast replaces the generic argument
of `builder.selection` — the shape is inferred from the cast, and is still used for the resolver's
`parent` type:
This independent example assumes an external `Post` reference and a `MediaUnion` whose members
are named `Image` and `Video`, both exposing a `url` field:
```typescript
import { type FieldSet } from '@pothos/plugin-federation';
type Media = { __typename: 'Image'; url: string } | { __typename: 'Video'; url: string };
Post.implement({
externalFields: (t) => ({
media: t.field({ type: [MediaUnion] }),
}),
fields: (t) => ({
mediaUrls: t.stringList({
requires: builder.selection(
'media { ... on Image { url } ... on Video { url } }' as FieldSet<{ media: Media[] }>,
),
resolve: (post) => post.media.map((media) => media.url),
}),
}),
});
```
`FieldSet` is accepted anywhere a selection string is expected, including `builder.selection` and
`ref.provides`. The selection string is not validated against the shape, so make sure the selection
matches the fields described by the generic argument.
For a reference-only entity that this service does not resolve, use this alternative key definition:
```ts
const Product = builder.externalRef(
'Product',
builder.keyDirective(builder.selection<{ upc: string }>('upc'), false),
);
```
### Adding a provides directive [#adding-a-provides-directive]
To add a `@provides` directive, you will need to implement the Parent type of the field being
provided as an external ref, and then use the `.provides` method of the returned ref when defining
the field that will have the `@provides` directive. The provided field must be listed as an
`externalField` in the external type.
The reviews service uses another separate builder. It returns a known username with a user
reference, so the gateway does not need to fetch that field from the users service for this path:
```typescript
type ReviewRecord = { id: string; body: string; authorID: string; authorUsername: string };
const reviews: ReviewRecord[] = [
{ id: '1', body: 'Useful notebook', authorID: '1', authorUsername: 'leia' },
];
const User = builder.externalRef('User', builder.selection<{ id: string }>('id')).implement({
externalFields: (t) => ({
// The field that will be provided
username: t.string(),
}),
fields: (t) => ({
id: t.exposeID('id'),
}),
});
const Review = builder.objectRef('Review');
Review.implement({
fields: (t) => ({
id: t.exposeID('id'),
body: t.exposeString('body'),
author: t.field({
// using User.provides<...>(...) instead of just User adds the provide annotations
// and ensures the resolved value includes data for the provided field
// The generic in Type.provides works the same as the `builder.selection` method.
type: User.provides<{ username: string }>('username'),
resolve: (review) => ({
id: review.authorID,
username: review.authorUsername,
}),
}),
}),
});
builder.queryType({
fields: (t) => ({
reviews: t.field({ type: [Review], resolve: () => reviews }),
}),
});
```
The reviews example returns the provided username with the reference. Run it to observe the
resolved value; proving that `@provides` saves a cross-service fetch requires a composed gateway.
### Building your schema and starting a server [#building-your-schema-and-starting-a-server]
Each service builds and serves its own schema:
```typescript
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
// Use `toSubGraphSchema` method to add subGraph specific types and queries to the schema
const schema = builder.toSubGraphSchema({
// defaults to v2.6
linkUrl: 'https://specs.apollo.dev/federation/v2.3',
// defaults to the list of directives used in your schema
federationDirectives: ['@key', '@external', '@requires', '@provides'],
});
const server = new ApolloServer({
schema,
});
startStandaloneServer(server, { listen: { port: 4000 } })
.then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
})
.catch((error) => {
throw error;
});
```
For a functional example that combines multiple graphs built with Pothos into a single schema see
[https://github.com/hayes/pothos/tree/main/packages/plugin-federation/tests/example](https://github.com/hayes/pothos/tree/main/packages/plugin-federation/tests/example)
### Printing the schema [#printing-the-schema]
If you are printing the schema as a string for any reason, and then using the printed schema for
Apollo Federation(submitting if using Managed Federation, or composing manually with `rover`), you
must use `printSubgraphSchema`(from `@apollo/subgraph`) or another compatible way of printing the
schema(that includes directives) in order for it to work.
### Field directives [#field-directives]
Several federation directives can be configured directly when defining a field includes
`@shareable`, `@tag`, `@inaccessible`, and `@override`.
```ts
t.field({
type: 'String',
shareable: true,
tag: ['someTag'],
inaccessible: true,
override: { from: 'users' },
resolve: () => 'example',
});
```
For more details on these directives, see the official Federation documentation.
### interface entities and @interfaceObject [#interface-entities-and-interfaceobject]
Federation 2.3 introduces new features for federating interface definitions.
Pass an interface to `asEntity` to define its keys. This example assumes your service already
implements `Media` with concrete types and has a `loadMediaById` function returning those models:
```ts
const Media = builder.interfaceRef<{ id: string }>('Media').implement({
fields: (t) => ({
id: t.exposeID('id'),
}),
});
builder.asEntity(Media, {
key: builder.selection<{ id: string }>('id'),
resolveReference: ({ id }) => loadMediaById(id),
});
```
In a separate service, extend that interface by creating an `interfaceObject`:
```ts
const Media = builder.objectRef<{ id: string }>('Media').implement({
fields: (t) => ({
id: t.exposeID('id'),
// add new MediaFields here that are available on all implementors of the `Media` type
}),
});
builder.asEntity(Media, {
interfaceObject: true,
key: builder.selection<{ id: string }>('id'),
resolveReference: (ref) => ref,
});
```
See federation documentation for more details on `interfaceObject`s
### composeDirective [#composedirective]
You can apply the `composeDirective` directive when building the subgraph schema:
```ts
import { DirectiveLocation, GraphQLDirective } from 'graphql';
export const schema = builder.toSubGraphSchema({
// This adds the @composeDirective directive
composeDirectives: ['@custom'],
// composeDirective requires an @link directive on the schema pointing to the url for your directive
schemaDirectives: {
link: { url: 'https://myspecs.dev/myCustomDirective/v1.0', import: ['@custom'] },
},
// You currently also need to provide an actual implementation for your Directive
directives: [
new GraphQLDirective({
locations: [DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE],
name: 'custom',
}),
],
});
```
# Grafast plugin
URL: /docs/plugins/grafast
A plugin for building schemas with Grafast plans instead of resolvers
This package is experimental.
Plugins that add runtime behavior by wrapping resolvers do not apply that behavior to Grafast plans.
## Install [#install]
npm
pnpm
yarn
bun
```bash
npm install --save @pothos/plugin-grafast grafast graphql@^16.10.0
```
```bash
pnpm add @pothos/plugin-grafast grafast graphql@^16.10.0
```
```bash
yarn add @pothos/plugin-grafast grafast graphql@^16.10.0
```
```bash
bun add @pothos/plugin-grafast grafast graphql@^16.10.0
```
Grafast 1.0 requires GraphQL 16. Use a GraphQL version supported by your Grafast release.
## Setup [#setup]
```typescript
import SchemaBuilder from '@pothos/core';
import GrafastPlugin from '@pothos/plugin-grafast';
import { get, inhibitOnNull, lambda, loadOne, type Step } from 'grafast';
type RequestContext = { requestId: string };
declare global {
namespace Grafast {
// Define the Context type used by grafast
interface Context extends RequestContext {}
}
}
type BuilderTypes = {
// This tells the builder to expect plans instead of resolvers
InferredFieldOptionsKind: 'Grafast';
Context: RequestContext;
};
const builder = new SchemaBuilder({
plugins: [GrafastPlugin],
});
```
## Usage [#usage]
For documentation on how to write plans, see the [Grafast documentation](https://grafast.org/grafast/).
### Adding plans to fields [#adding-plans-to-fields]
```typescript
builder.queryType({
fields: (t) => ({
addTwoNumbers: t.int({
args: {
a: t.arg.int({ required: true }),
b: t.arg.int({ required: true }),
},
plan: (_, { $a, $b }) => {
return lambda([$a, $b], ([a, b]) => a + b);
},
}),
}),
});
```
### Using resolvers [#using-resolvers]
As an alternative to the previous query definition, you can write a resolver,
but you will not have access to the 4th `GraphqlResolveInfo` argument:
```typescript
builder.queryType({
fields: (t) => ({
addTwoNumbers: t.int({
args: {
a: t.arg.int({ required: true }),
b: t.arg.int({ required: true }),
},
resolve: (_, { a, b }) => {
return a + b;
},
}),
}),
});
```
Resolvers should not be used to load data, but can make it easier to define a field
that would otherwise use a simple `lambda` plan.
### Abstract types [#abstract-types]
Abstract types (Unions and Interfaces) may require defining a plan to resolve to the correct type.
For more details on how polymorphic types work in Grafast, see the [Grafast documentation](https://grafast.org/grafast/polymorphism).
#### Interfaces [#interfaces]
To implement an interface, you can implement it as you normally would in Pothos, and then call the
`.withPlan` method on the interface ref to provide a plan for resolving the correct type.
```typescript
interface AnimalData {
id: string;
kind: 'Dog' | 'Cat';
}
export const Animal = builder.interfaceRef('Animal').withPlan({
planType: ($record) => ({
$__typename: get($record, 'kind'),
}),
});
export const Dog = builder.objectRef('Dog').implement({
interfaces: [Animal],
});
export const Cat = builder.objectRef('Cat').implement({
interfaces: [Animal],
});
Animal.implement({
fields: (t) => ({
id: t.exposeID('id'),
}),
});
```
You can now define a query to resolve this interface:
```typescript
export const Animals = [
{
id: '1',
kind: 'Dog',
},
{
id: '2',
kind: 'Cat',
},
] satisfies AnimalData[];
function getAnimalsById(ids: readonly string[]): (AnimalData | null)[] {
return ids.map((id) => Animals.find((entity) => entity.id === id) ?? null);
}
builder.queryFields((t) => ({
animal: t.field({
type: Animal,
args: {
id: t.arg.string({ required: true }),
},
plan: (_, $args) => loadOne($args.$id, getAnimalsById),
}),
}));
```
### Unions [#unions]
Add another object type and combine it with `Cat` and `Dog` in a union:
```typescript
interface AlienData {
id: string;
kind: 'Alien';
}
export const Alien = builder.objectRef('Alien').implement({
fields: (t) => ({
id: t.exposeID('id'),
}),
});
export const Entity = builder
.unionType('Entity', {
types: [Cat, Dog, Alien],
})
.withPlan({
planType: ($record) => ({
$__typename: get($record, 'kind'),
}),
});
```
### `planForType` [#planfortype]
When planning polymorphic types, Grafast allows you to provide a `planForType` function that
allows you to load the correct data for the current type.
This also enables changing the type of plan required for fields that return the abstract type:
`planForType` is not entirely type-safe, and will allow plans that resolve to data for the wrong type.
This replaces the previous `Entity` definition. The lookup uses the same animals and one alien:
```typescript
function getEntitiesById(ids: readonly string[]): (AnimalData | AlienData | null)[] {
const entities: (AnimalData | AlienData)[] = [...Animals, { id: '3', kind: 'Alien' }];
return ids.map((id) => entities.find((entity) => entity.id === id) ?? null);
}
export const Entity = builder
.unionType('Entity', {
types: [Cat, Dog, Alien],
})
.withPlan({
planType: (
// Provide an explicit type so that the query field only needs to return the ID
$specifier: Step,
) => {
const $record = inhibitOnNull(loadOne($specifier, getEntitiesById));
return {
$__typename: get($record, 'kind'),
planForType: () => $record,
};
},
});
builder.queryFields((t) => ({
entity: t.field({
type: Entity,
args: {
id: t.arg.string({ required: true }),
},
// Because our Entity plan loads the record, we can just return the ID here
plan: (_, $args) => $args.$id,
}),
}));
```
## Run the plans locally [#run-the-plans-locally]
The browser playground executes GraphQL resolvers with `graphql()`. This example requires the
Grafast executor to run its plans. The complete [local example](https://github.com/hayes/pothos/tree/main/website/local-examples/grafast)
combines the setup, addition query, animal interface, and `planForType` union shown above.
With Node.js 22 or newer, run from a checkout of the [Pothos repository](https://github.com/hayes/pothos):
```bash
pnpm install --frozen-lockfile
pnpm --dir website check:local
```
The local examples use the checkout's built Pothos packages. Their separate locked installation
uses GraphQL 16, as required by Grafast 1.0; it does not change the repository's GraphQL version.
`grafast/check.ts` executes the schema with `grafast()` and checks addition, both animal types,
the alien union member, and a missing entity. It also changes the arguments and checks the new sum.
To try your own values, edit the query or `variableValues` in that file and update its expected result.
The execution call has this form:
```typescript
import { grafast } from 'grafast';
import { schema } from './schema';
const result = await grafast({
schema,
source: '{ addTwoNumbers(a: 2, b: 3) }',
contextValue: { requestId: 'example' },
});
console.log(result); // { data: { addTwoNumbers: 5 } }
```
# Plugins
URL: /docs/plugins
List of plugins for Pothos
Plugins extend the builder with fields, options, and methods for specific tasks. See
[Using plugins](/docs/guide/using-plugins) for installation and builder configuration.
The [Zod plugin](/docs/plugins/zod) documents the older Zod-specific API. For new validation fields,
start with the [Validation plugin](/docs/plugins/validation).
# Mocks plugin
URL: /docs/plugins/mocks
Mocks plugin docs for Pothos
Replace selected field resolvers when building a schema for tests or local development.
Mocks are configured per `toSchema()` call, so the same builder can produce mocked and unmocked schemas.
## Usage [#usage]
### Install [#install]
npm
pnpm
yarn
bun
```bash
npm install --save @pothos/plugin-mocks
```
```bash
pnpm add @pothos/plugin-mocks
```
```bash
yarn add @pothos/plugin-mocks
```
```bash
bun add @pothos/plugin-mocks
```
### Setup [#setup]
```typescript
import SchemaBuilder from '@pothos/core';
import MocksPlugin from '@pothos/plugin-mocks';
const builder = new SchemaBuilder({
plugins: [MocksPlugin],
});
```
### Adding mocks [#adding-mocks]
You can mock any field by adding a mock in the options passed to `builder.toSchema` under
`mocks.{typeName}.{fieldName}`.
```typescript playground example="plugin-mocks"
builder.queryType({
fields: (t) => ({
greeting: t.string({
resolve: () => {
throw new Error('Not implemented');
},
}),
untouched: t.string({ resolve: () => 'Original resolver' }),
}),
});
export const schema = builder.toSchema({
mocks: {
Query: {
greeting: () => 'Mock result!',
},
},
});
```
A mock resolver receives the usual `parent`, `args`, `context`, and `info` arguments. Configured
mocks replace their field resolvers whenever those fields execute; unlisted fields keep their
original resolvers. Without a `mocks` option, the original `greeting` resolver throws its error.
A schema can be built multiple times with different mocks.
### Adding mocks for subscribe functions [#adding-mocks-for-subscribe-functions]
To add a mock for a subscriber you can nest the mocks for subscribe and resolve in an object.
Subscription execution uses GraphQL's `subscribe()` in a server or test:
```typescript
builder.subscriptionType({
fields: (t) => ({
someField: t.string({
resolve: () => {
throw new Error('Not implemented');
},
subscribe: () => {
throw new Error('Not implemented');
},
}),
}),
});
builder.toSchema({
mocks: {
Subscription: {
someField: {
resolve: (event) => event,
subscribe: async function* () {
yield 'First event';
yield 'Second event';
},
},
},
},
});
```
# Relay plugin
URL: /docs/plugins/relay
Add globally identifiable nodes, cursor pagination, and Relay mutation fields.
The Relay plugin adds globally identifiable nodes, cursor-based connections, and mutation helpers.
Use nodes when clients need to refetch an object by ID, and connections when clients need to page
through a collection. A connection can return ordinary object types; its items do not have to
implement the `Node` interface.
## Usage [#usage]
### Install [#install]
npm
pnpm
yarn
bun
```bash
npm install --save @pothos/plugin-relay
```
```bash
pnpm add @pothos/plugin-relay
```
```bash
yarn add @pothos/plugin-relay
```
```bash
bun add @pothos/plugin-relay
```
### Setup [#setup]
```typescript
import SchemaBuilder from '@pothos/core';
import RelayPlugin from '@pothos/plugin-relay';
const builder = new SchemaBuilder({
plugins: [RelayPlugin],
relay: {},
});
```
### Options [#options]
The `relay` options object passed to builder can contain the following properties:
* `idFieldName`: The name of the field that contains the global id for the node. Defaults to `id`.
* `idFieldOptions`: Options to pass to the id field.
* `clientMutationId`: `omit` (default) | `required` | `optional`. Determines if clientMutationId
fields are created on `relayMutationFields`, and if they are required.
* `relayMutationFieldOptions`: Default options for the `relayMutationField` method.
* `cursorType`: `String` | `ID`. Determines type used for cursor fields. Defaults to `String`
* `nodeQueryOptions`: Options for the `node` field on the query object, set to false to omit the
field
* `nodesQueryOptions`: Options for the `nodes` field on the query object, set to false to omit the
field
* `nodeTypeOptions`: Options for the `Node` interface type. Supports a `name` property to customize
the type name.
* `pageInfoTypeOptions`: Options for the `PageInfo` object type. Supports a `name` property to
customize the type name.
* `clientMutationIdFieldOptions`: Options for the `clientMutationId` field on mutation payloads
* `clientMutationIdInputOptions`: Options for the `clientMutationId` field on mutation inputs
* `mutationInputArgOptions`: Options for the input argument on Relay mutation fields
* `cursorFieldOptions`: Options for the `cursor` field on an edge object.
* `nodeFieldOptions`: Options for the `node` field on an edge object.
* `edgesFieldOptions`: Options for the `edges` field on a connection object.
* `pageInfoFieldOptions`: Options for the `pageInfo` field on a connection object.
* `hasNextPageFieldOptions`: Options for the `hasNextPage` field on the `PageInfo` object.
* `hasPreviousPageFieldOptions`: Options for the `hasPreviousPage` field on the `PageInfo` object.
* `startCursorFieldOptions`: Options for the `startCursor` field on the `PageInfo` object.
* `endCursorFieldOptions`: Options for the `endCursor` field on the `PageInfo` object.
* `beforeArgOptions`: Options for the `before` arg on a connection field.
* `afterArgOptions`: Options for the `after` arg on a connection field.
* `firstArgOptions`: Options for the `first` arg on a connection field.
* `lastArgOptions`: Options for the `last` arg on a connection field.
* `defaultConnectionTypeOptions`: Default options for the `Connection` Object types.
* `defaultEdgeTypeOptions`: Default options for the `Edge` Object types.
* `defaultPayloadTypeOptions`: Default options for the `Payload` Object types.
* `defaultMutationInputTypeOptions`: default options for the mutation `Input` types.
* `nodesOnConnection`: If true, the `nodes` field will be added to the `Connection` object types.
* `defaultConnectionFieldOptions`: Default options for connection fields defined with t.connection
* `brandLoadedObjects`: Defaults to `true`. This will add a hidden symbol to objects returned from
the `load` methods of Nodes that allows the default `resolveType` implementation to identify the
type of the node. When this is enabled, you will not need to implement an `isTypeOf` check for
most common patterns.
### Customizing type names [#customizing-type-names]
If you have existing `Node` or `PageInfo` types in your schema that conflict with the ones generated
by the relay plugin, you can customize the names using `nodeTypeOptions` and `pageInfoTypeOptions`:
```typescript
import SchemaBuilder from '@pothos/core';
import RelayPlugin from '@pothos/plugin-relay';
const builder = new SchemaBuilder({
plugins: [RelayPlugin],
relay: {
nodeTypeOptions: {
name: 'RelayNode',
description: 'A node in the graph',
},
pageInfoTypeOptions: {
name: 'RelayPageInfo',
description: 'Pagination information',
},
},
});
```
Both options support all standard type options including `name`, `description`, and `extensions`.
### Creating Nodes [#creating-nodes]
Defining a node adds direct lookups through `node` and `nodes`. Filters or permission checks on
other root fields do not protect these lookups. Use Scope Auth to gate the lookup fields or apply
a shared policy to the Node interface, and enforce entity-specific access where needed. See
[Relay node authorization](/docs/plugins/scope-auth/relay-nodes) for safer defaults and their limits.
To create objects that extend the `Node` interface, use `builder.node`. The `id` resolver supplies
the local key, and `loadOne` uses that key to retrieve the record:
```typescript playground example="plugin-relay-nodes"
type UserShape = { id: string; name: string };
const users: UserShape[] = [
{ id: '1', name: 'Ada' },
{ id: '2', name: 'Grace' },
{ id: '3', name: 'Katherine' },
];
const User = builder.objectRef('User');
builder.node(User, {
id: { resolve: (user) => user.id },
loadOne: (id) => users.find((user) => user.id === id) ?? null,
fields: (t) => ({ name: t.exposeString('name') }),
});
```
```typescript
builder.queryType({});
```
`builder.node` will create an object type that implements the `Node` interface. It will also create
the `Node` interface the first time it is used. The `resolve` function for `id` should return a
number or string, which will be converted to a globalID. The relay plugin adds two new query fields
`node` and `nodes` which can be used to directly fetch nodes using global IDs by calling the
provided `loadOne` or `loadMany` method. Each node will only be loaded once by id, and cached if the
same node is loaded multiple times in the same request. You can provide `loadWithoutCache` or
`loadManyWithoutCache` instead if caching is not desired, or you are already using a caching
datasource like a dataloader.
With the default encoding, Ada's local ID `"1"` becomes the global ID `"VXNlcjox"`.
A `node(id: "VXNlcjox")` query returns Ada with that same global ID, wherever she appears in the
schema. Global IDs identify records; they do not grant access. The loader must still enforce the
application's access rules when needed.
For an application that already uses classes, pass the class to `builder.node` and provide
`name: 'User'`. The default class check uses `instanceof` or the constructor on the prototype.
Use this as an alternative to the ref above, with a loader that returns class instances.
Choose one loading option: `loadOne`, `loadMany`, `loadWithoutCache`, or
`loadManyWithoutCache`. Batch loaders must return one value per ID in the same order, including
`null` for missing nodes. Use a fresh context object for every request to isolate the node cache.
By default (unless `brandLoadedObjects` is set to `false`) any nodes loaded through one of the
`load*` methods will be branded so that the default `resolveType` method can identify the GraphQL
type for the loaded object. This means `isTypeOf` is only required for `union` and `interface`
fields that return node objects that are manually loaded, where the union or interface does not have
a custom `resolveType` method that knows how to resolve the node type.
#### parsing node ids [#parsing-node-ids]
By default all node ids are parsed as string. This behavior can be customized by providing a custom
parse function for your node's ID field:
```typescript
// Alternative User definition for a data source with numeric IDs.
type UserShape = { id: number; name: string };
const users: UserShape[] = [{ id: 1, name: 'Ada' }];
const User = builder.objectRef('User');
builder.node(User, {
id: {
resolve: (user) => user.id,
parse: (id) => {
const parsed = Number(id);
if (!Number.isSafeInteger(parsed) || parsed < 1) {
throw new Error('Invalid User ID');
}
return parsed;
},
},
loadOne: (id) => users.find((user) => user.id === id) ?? null,
fields: (t) => ({ name: t.exposeString('name') }),
});
```
### Global IDs [#global-ids]
To make it easier to create globally unique ids the relay plugin adds new methods for creating
globalID fields.
```typescript
builder.queryFields((t) => ({
singleID: t.globalID({ resolve: () => ({ id: '1', type: User }) }),
listOfIDs: t.globalIDList({
resolve: () => users.map((user) => ({ id: user.id, type: User })),
}),
}));
```
The returned IDs can either be a string (which is expected to already be a globalID), or an object
with an `id` and a `type`. The type can be either the name of a type as a string, or any object
that can be used in a type parameter.
There are also new methods for adding globalIDs in arguments or fields of input types:
```typescript
builder.queryType({
fields: (t) => ({
fieldThatAcceptsGlobalID: t.boolean({
args: {
id: t.arg.globalID({
required: true,
}),
idList: t.arg.globalIDList(),
},
resolve(parent, args) {
console.log(`Get request for type ${args.id.typename} with id ${args.id.id}`);
return true;
},
}),
}),
});
```
globalIDs used in arguments expect the client to send a globalID string, but will automatically be
converted to an object with 2 properties (`id` and `typename`) before they are passed to your
resolver in the arguments object.
#### Limiting global ID args to specific types [#limiting-global-id-args-to-specific-types]
`globalID` input's can be configured to validate the type of the globalID. This is useful if you
only want to accept IDs for specific node types.
```typescript
builder.queryField('userByGlobalID', (t) =>
t.field({
type: User,
args: {
id: t.arg.globalID({ for: User, required: true }),
},
resolve: (_parent, { id }) => users.find((user) => user.id === id.id) ?? null,
}),
);
```
To accept several node types, pass an array of their refs to `for`. The plugin rejects IDs whose
type is outside that list before calling the resolver.
### Creating Connections [#creating-connections]
The `t.connection` field builder method can be used to define connections. This method will
automatically create the `Connection` and `Edge` objects used by the connection, and add `before`,
`after`, `first`, and `last` arguments. The first time this method is used, it will also create the
`PageInfo` type.
The following field continues the `User` example. `resolveArrayConnection` slices the array and
computes cursors and page information:
```typescript
import { resolveArrayConnection } from '@pothos/plugin-relay';
builder.queryField('users', (t) =>
t.connection({
type: User,
resolve: (_parent, args) => resolveArrayConnection({ args }, users),
}),
);
const schema = builder.toSchema();
```
Query `users(first: 2)`, then pass its `pageInfo.endCursor` to `users(first: 2, after: ...)`.
The first page contains Ada and Grace; the next contains Katherine. A node's `id` identifies the
object across the schema, while an edge's `cursor` identifies a position in this connection.
Pass a returned node `id` to the root `node(id: ...)` field to refetch the same user.
Treat connection cursors as opaque values. Forward pagination uses `first` with `after`;
backward pagination uses `last` with `before`. The array helper rejects negative page sizes and
returns an empty edge list when the requested page falls beyond the data.
Connections can also contain ordinary objects. This independent schema uses four users with
local IDs: Ada, Grace, Katherine, and Dorothy. Its `User` type does not implement `Node`:
```typescript playground example="plugin-relay-pagination"
import SchemaBuilder from '@pothos/core';
import RelayPlugin, { resolveArrayConnection } from '@pothos/plugin-relay';
const builder = new SchemaBuilder({ plugins: [RelayPlugin], relay: {} });
const users = [
{ id: '1', name: 'Ada' },
{ id: '2', name: 'Grace' },
{ id: '3', name: 'Katherine' },
{ id: '4', name: 'Dorothy' },
];
const User = builder.objectRef<(typeof users)[number]>('User').implement({
fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name') }),
});
builder.queryType({
fields: (t) => ({
users: t.connection({
type: User,
resolve: (_parent, args) => resolveArrayConnection({ args }, users),
}),
}),
});
export const schema = builder.toSchema();
```
`resolveArrayConnection` paginates data already in memory. The offset and cursor helpers below
let a data source fetch only the needed records from a larger collection.
The remaining connection examples are independent patterns using application types and data
sources. For a custom connection resolver, return the following shape:
```typescript
builder.queryFields((t) => ({
numbers: t.connection(
{
type: NumberThing,
resolve: (parent, { first, last, before, after }) => {
return {
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: 'abc',
endCursor: 'def',
},
edges: [
{
cursor: 'abc',
node: new NumberThing(123),
},
{
cursor: 'def',
node: new NumberThing(123),
},
],
};
},
},
{
name: 'NameOfConnectionType', // optional, will use ParentObject + capitalize(FieldName) + "Connection" as the default
fields: (tc) => ({
// define extra fields on Connection
// We need to use a new variable for the connection field builder (eg tc) to get the correct types
}),
edgesField: {}, // optional, allows customizing the edges field on the Connection Object
// Other options for connection object can be added here
},
{
// Same as above, but for the Edge Object
name: 'NameOfEdgeType', // optional, will use Connection name + "Edge" as the default
fields: (te) => ({
// define extra fields on Edge
// We need to use a new variable for the connection field builder (eg te) to get the correct types
}),
nodeField: {}, // optional, allows customizing the node field on the Edge Object
},
),
}));
```
Manually implementing connections can be cumbersome, so there are a couple of helper methods that
can make resolving connections a little easier.
For a data source that accepts an offset and limit, use `resolveOffsetConnection`. Its callback
requests one extra record to determine `hasNextPage`; honor the supplied `limit` and return records
in a stable order:
```typescript
import { resolveOffsetConnection } from '@pothos/plugin-relay';
builder.queryFields((t) => ({
things: t.connection({
type: SomeThing,
resolve: (parent, args) => {
return resolveOffsetConnection({ args }, ({ limit, offset }) => {
return getThings(offset, limit);
});
},
}),
}));
```
`resolveOffsetConnection` has a few default limits to prevent unintentionally allowing too many
records to be fetched at once. These limits can be configured using the following options:
```typescript
{
args: ConnectionArguments;
defaultSize?: number; // defaults to 20
maxSize?: number; // defaults to 100
totalCount?: number // required to support using `last` without `before`
}
```
For APIs where you have the full array available you can use `resolveArrayConnection`, which works
just like `resolveOffsetConnection` and accepts the same options.
```typescript
import { resolveArrayConnection } from '@pothos/plugin-relay';
builder.queryFields((t) => ({
things: t.connection({
type: SomeThing,
resolve: (parent, args) => {
return resolveArrayConnection({ args }, getAllTheThingsAsArray());
},
}),
}));
```
Cursor based pagination can be implemented using the `resolveCursorConnection` method. The following
example uses Prisma and assumes `createdAt` is unique. If timestamps can repeat, use a unique
cursor and matching ordering/filtering (for example, a timestamp and ID pair) so records are not
skipped between pages. The callback must honor `limit`, including the extra record requested by
the helper, and reverse the ordering when `inverted` is true.
```typescript
import { resolveCursorConnection, ResolveCursorConnectionArgs } from '@pothos/plugin-relay';
builder.queryField('posts', (t) =>
t.connection({
type: Post,
resolve: (_, args) =>
resolveCursorConnection(
{
args,
toCursor: (post) => post.createdAt.toISOString(),
},
// Manually defining the arg type here is required
// so that typescript can correctly infer the return value
({ before, after, limit, inverted }: ResolveCursorConnectionArgs) =>
prisma.post.findMany({
take: limit,
where: {
createdAt: {
lt: before,
gt: after,
},
},
orderBy: {
createdAt: inverted ? 'desc' : 'asc',
},
}),
),
}),
);
```
### Relay Mutations [#relay-mutations]
You can use the `relayMutationField` method to define relay compliant mutation fields. This method
generates a mutation field, input object, and payload object. By default it omits
`clientMutationId`; set `relay.clientMutationId` to `required` or `optional` to include matching
input and payload fields.
Example usage:
```typescript
builder.mutationType({});
const items = new Map([['1', { id: '1' }]]);
const deleteItem = builder.relayMutationField(
'deleteItem',
{
inputFields: (t) => ({
id: t.id({
required: true,
}),
}),
},
{
nullable: false, // You can optionally change the nullability of the mutation field here
resolve: async (root, args, ctx) => {
if (items.has(args.input.id)) {
items.delete(args.input.id);
return { success: true };
}
return { success: false };
},
},
{
outputFields: (t) => ({
success: t.boolean({
resolve: (result) => result.success,
}),
}),
},
);
```
Which produces the following graphql types:
```graphql
input DeleteItemInput {
id: ID!
}
type DeleteItemPayload {
success: Boolean
}
type Mutation {
deleteItem(input: DeleteItemInput!): DeleteItemPayload!
}
```
The `relayMutationField` has 4 arguments:
* `name`: Name of the mutation field
* `inputOptions`: Options for the `input` object or a ref to an existing input object
* `fieldOptions`: Options for the mutation field
* `payloadOptions`: Options for the Payload object
The `inputOptions` has a couple of non-standard options:
* `name` which can be used to set the name of the input object
* `argName` which can be used to overwrite the default arguments name (`input`).
The `payloadOptions` object also accepts a `name` property for setting the name of the payload
object.
You can also access refs for the created input and payload objects so you can re-use them in other
fields:
```typescript
const { inputType: DeleteItemInput, payloadType: DeleteItemPayload } = deleteItem;
```
### Reusing connection objects [#reusing-connection-objects]
In some cases you may want to create a connection object type that is shared by multiple fields. To
do this, you will need to create the connection object separately and then create a fields using a
ref to your connection object:
```typescript
import { resolveArrayConnection } from '@pothos/plugin-relay';
const UsersConnection = builder.connectionObject(
{ type: User, name: 'UsersConnection' },
{ name: 'UsersEdge' },
);
builder.queryFields((t) => ({
// Normal fields need explicit connection arguments.
usersAsField: t.field({
type: UsersConnection,
args: { ...t.arg.connectionArgs() },
resolve: (_parent, args) => resolveArrayConnection({ args }, users),
}),
// Connection fields add the arguments automatically.
usersAsConnection: t.connection(
{
type: User,
resolve: (_parent, args) => resolveArrayConnection({ args }, users),
},
UsersConnection,
),
}));
```
`builder.connectionObject` creates the connect object type and the associated Edge type.
`t.arg.connectionArgs()` will create the default connection args.
### Reusing edge objects [#reusing-edge-objects]
This is an alternative to the connection definition above. Create an edge ref, then pass it to
connection definitions that share its shape:
```typescript
import { resolveArrayConnection } from '@pothos/plugin-relay';
const UsersEdge = builder.edgeObject({ name: 'UsersEdge', type: User });
const UsersConnection = builder.connectionObject(
{ type: User, name: 'UsersConnection' },
UsersEdge,
);
builder.queryField('usersWithSharedEdge', (t) =>
t.connection(
{
type: User,
resolve: (_parent, args) => resolveArrayConnection({ args }, users),
},
{ name: 'SharedEdgeUsersConnection' },
UsersEdge,
),
);
```
`builder.connectionObject` creates the connect object type and the associated Edge type.
`t.arg.connectionArgs()` will create the default connection args.
### Expose nodes [#expose-nodes]
The `t.node` and `t.nodeList` methods can be used to add additional node fields. the expected return
values of `id` and `ids` fields is the same as the resolve value of `t.globalID`, and can either be
a globalID or an object with an `id` and a `type`.
Loading nodes by `id` uses a request cache, so the same node will only be loaded once per request,
even if it is used multiple times across the schema.
```typescript
builder.queryFields((t) => ({
extraNode: t.node({ id: () => ({ id: '1', type: User }) }),
moreNodes: t.nodeList({
ids: () => users.map((user) => ({ id: user.id, type: User })),
}),
}));
```
### decoding and encoding global ids [#decoding-and-encoding-global-ids]
The relay plugin exports `decodeGlobalID` and `encodeGlobalID` as helper methods for interacting
with global IDs directly. If you accept a global ID as an argument you can use the `decodeGlobalID`
function to decode it:
```typescript
import { decodeGlobalID, encodeGlobalID } from '@pothos/plugin-relay';
const adaID = encodeGlobalID('User', '1');
builder.mutationType({});
builder.mutationField('renameUser', (t) =>
t.field({
type: User,
args: {
id: t.arg.id({ required: true }),
name: t.arg.string({ required: true }),
},
resolve: (_parent, { id: globalID, name }) => {
const { typename, id } = decodeGlobalID(globalID);
if (typename !== 'User') {
throw new Error('Expected a User ID');
}
const user = users.find((candidate) => candidate.id === id);
if (!user) {
return null;
}
user.name = name;
return user;
},
}),
);
```
### Using custom encoding for global ids [#using-custom-encoding-for-global-ids]
In some cases you may want to encode global ids differently than the build in ID encoding. To do
this, you can pass a custom encoding and decoding function into the relay options of the builder:
```typescript
import SchemaBuilder from '@pothos/core';
import RelayPlugin from '@pothos/plugin-relay';
const builder = new SchemaBuilder({
plugins: [RelayPlugin],
relay: {
encodeGlobalID: (typename: string, id: string | number | bigint) => `${typename}:${id}`,
decodeGlobalID: (globalID: string) => {
const [typename, id] = globalID.split(':');
return { typename, id };
},
},
});
```
### Using custom resolve for node and or nodes field [#using-custom-resolve-for-node-and-or-nodes-field]
If you need to customize how nodes are loaded for the `node` and or `nodes` fields you can provide
custom resolve functions in the builder options for these fields. This alternative builder uses
the `users` array above; register its `User` node after creating the builder. Manually loaded
objects include `__typename` so the `Node` interface can resolve their type:
```typescript
import SchemaBuilder from '@pothos/core';
import RelayPlugin from '@pothos/plugin-relay';
function customUserLoader({ id }: { id: string }) {
const user = users.find((user) => user.id === id);
return user ? { ...user, __typename: 'User' } : null;
}
const builder = new SchemaBuilder({
plugins: [RelayPlugin],
relay: {
nodeQueryOptions: {
resolve: (root, { id }, context, info, resolveNode) => {
// use custom loading for User nodes
if (id.typename === 'User') {
return customUserLoader(id);
}
// fallback to normal loading for everything else
return resolveNode(id);
},
},
nodesQueryOptions: {
resolve: (root, { ids }, context, info, resolveNodes) => {
return ids.map(async (id) => {
if (id.typename === 'User') {
return customUserLoader(id);
}
// it would be more efficient to load all the nodes at once
// but it is important to ensure the resolver returns nodes in the right order
// we are resolving nodes one at a time here for simplicity
return (await resolveNodes([id]))[0];
});
},
},
},
});
```
### Extending all connections [#extending-all-connections]
There are 2 builder methods for adding fields to all connection objects: `builder.globalConnectionField`
and `builder.globalConnectionFields`. These methods work like many of the other methods on the builder for
adding fields to objects or interfaces.
```typescript
builder.globalConnectionField('totalCount', (t) =>
t.int({
nullable: false,
resolve: (parent) => 123,
}),
);
// Or
builder.globalConnectionFields((t) => ({
totalCount: t.int({
nullable: false,
resolve: (parent) => 123,
}),
}));
```
In the above example, we are just returning a static number for our `totalCount` field. To make this
more useful, we need to have our resolvers for each connection actually return an object that
contains a totalCount for us. To guarantee that resolvers correctly implement this behavior, we can
define custom properties that must be returned from connection resolvers when we set up our builder:
```typescript
import SchemaBuilder from '@pothos/core';
import RelayPlugin from '@pothos/plugin-relay';
const builder = new SchemaBuilder<{
Connection: {
totalCount: number;
};
}>({
plugins: [RelayPlugin],
relay: {},
});
```
Register the same `User` node and `users` array with this replacement builder. TypeScript will ensure that objects returned from each connection resolver include a totalCount
property, which we can use in our connection fields:
```typescript
builder.globalConnectionField('totalCount', (t) =>
t.int({
nullable: false,
resolve: (parent) => parent.totalCount,
}),
);
```
Note that adding additional required properties will make it harder to use the provided connection
helpers since they will not automatically return your custom properties. You will need to manually
add in any custom props after getting the result from the helpers:
```typescript
import { resolveArrayConnection } from '@pothos/plugin-relay';
builder.queryField('users', (t) =>
t.connection({
type: User,
resolve: (_parent, args) => ({
...resolveArrayConnection({ args }, users),
totalCount: users.length,
}),
}),
);
```
### Changing nullability of edges and nodes [#changing-nullability-of-edges-and-nodes]
If you want to change the nullability of the `edges` field on a `Connection` or the `node` field on
an `Edge` you can configure this in 2 ways:
#### Globally [#globally]
```typescript
import SchemaBuilder from '@pothos/core';
import RelayPlugin from '@pothos/plugin-relay';
const builder = new SchemaBuilder<{
DefaultEdgesNullability: false;
DefaultNodeNullability: true;
}>({
plugins: [RelayPlugin],
relay: {
edgesFieldOptions: {
nullable: false,
},
nodeFieldOptions: {
nullable: true,
},
},
});
```
The types provided for `DefaultEdgesNullability` and `DefaultNodeNullability` must match the values
provided in the nullable option of `edgesFieldOptions` and `nodeFieldOptions` respectively. This
will set the default nullability for all connections created by your builder.
nullability for `edges` fields defaults to `{ list: options.defaultFieldNullability, items: true }`
and the nullability of `node` fields is the same as `options.defaultFieldNullability` (which
defaults to `true`).
#### Per connection [#per-connection]
```typescript
builder.queryFields((t) => ({
things: t.connection({
type: SomeThing,
edgesNullable: {
items: true,
list: false,
},
nodeNullable: false,
resolve: (parent, args) => {
return resolveOffsetConnection({ args }, ({ limit, offset }) => {
return getThings(offset, limit);
});
},
}),
}));
// Or
const ThingsConnection = builder.connectionObject({
type: SomeThing,
name: 'ThingsConnection',
edgesNullable: {
items: true,
list: false,
},
nodeNullable: false,
});
```
### Extending the `Node` interface [#extending-the-node-interface]
Use `relay.nodeTypeOptions` to configure the interface itself, including `authScopes` when using
Scope Auth. See [Relay node authorization](/docs/plugins/scope-auth/relay-nodes#choose-a-default-policy) for how interface
scopes apply to implementing types and how they differ from authorizing the root lookup fields.
Use the `nodeInterfaceRef` method of your Builder to reference the interface.
For example, to add a field on the interface:
```ts
builder.interfaceField(builder.nodeInterfaceRef(), 'extra', (t) =>
t.string({
resolve: () => 'it works',
}),
);
```
# Simple objects plugin
URL: /docs/plugins/simple-objects
Simple objects plugin docs for Pothos
Infer an object's backing shape from its GraphQL fields. Use `simpleObject` or `simpleInterface`
when the resolver returns data in the same shape as the schema. For an existing application model,
use a core `objectRef` or `interfaceRef` with that model's type.
## Usage [#usage]
### Install [#install]
npm
pnpm
yarn
bun
```bash
npm install --save @pothos/plugin-simple-objects
```
```bash
pnpm add @pothos/plugin-simple-objects
```
```bash
yarn add @pothos/plugin-simple-objects
```
```bash
bun add @pothos/plugin-simple-objects
```
### Example [#example]
`User` inherits `id` from the simple `Node` interface and contains nested `ContactInfo` data.
Its resolver returns those fields directly. The additional `fullName` field computes a value from
`firstName` and `lastName` instead of requiring a `fullName` property on the returned object.
```typescript playground example="plugin-simple-objects"
import SchemaBuilder from '@pothos/core';
import SimpleObjectsPlugin from '@pothos/plugin-simple-objects';
const builder = new SchemaBuilder({
plugins: [SimpleObjectsPlugin],
});
const ContactInfo = builder.simpleObject('ContactInfo', {
fields: (t) => ({
email: t.string({
nullable: false,
}),
phoneNumber: t.string({
nullable: true,
}),
}),
});
const Node = builder.simpleInterface('Node', {
fields: (t) => ({
id: t.id({
nullable: false,
}),
}),
});
const User = builder.simpleObject(
'User',
{
interfaces: [Node],
fields: (t) => ({
firstName: t.string({ nullable: false }),
lastName: t.string({ nullable: false }),
contactInfo: t.field({
type: ContactInfo,
nullable: false,
}),
}),
},
// You can add additional fields with resolvers with a third fields argument
(t) => ({
fullName: t.string({
resolve: (user) => `${user.firstName} ${user.lastName}`,
}),
}),
);
builder.queryType({
fields: (t) => ({
user: t.field({
type: User,
args: {
id: t.arg.id({ required: true }),
},
resolve: (_parent, { id }) => {
return {
id: String(id),
firstName: 'Leia',
lastName: 'Organa',
contactInfo: {
email: 'leia@example.com',
phoneNumber: null,
},
};
},
}),
}),
});
```
## Extending simple objects [#extending-simple-objects]
In some cases, you may want to add more complex fields with resolvers or args where the value isn't
just passed down from the parent.
In these cases, you can either add the field in the 3rd arg (fields) as shown above, or you can add
additional fields with `builder.objectFields`. This adds a different computed field to `User`:
```typescript playground example="plugin-simple-objects"
builder.objectFields(User, (t) => ({
initials: t.string({
resolve: (user) => `${user.firstName.slice(0, 1)}${user.lastName.slice(0, 1)}`,
}),
}));
```
## Limitations [#limitations]
When using simpleObjects in combination with other plugins like authorization, those plugins may use
`unknown` as the parent type in some custom fields (eg. `parent` of a permission check function on
a field).
# Smart subscriptions plugin
URL: /docs/plugins/smart-subscriptions
Re-execute selected query fields when their data sources publish events.
Turn a query field into a subscription that sends its initial result, then updates it when relevant
data changes. Fields and object types register event names as they resolve. When an event arrives,
the plugin re-executes the affected query and sends the new result.
Use field or object refetch options to update a smaller part of the result when the rest can be reused.
## Install [#install]
npm
pnpm
yarn
bun
```bash
npm install --save @pothos/plugin-smart-subscriptions
```
```bash
pnpm add @pothos/plugin-smart-subscriptions
```
```bash
yarn add @pothos/plugin-smart-subscriptions
```
```bash
bun add @pothos/plugin-smart-subscriptions
```
## Connect an event source [#connect-an-event-source]
Provide callbacks that subscribe and unsubscribe by event name. This example uses an in-process
event emitter. The listener map belongs to one subscription operation's context, so cleaning up one
operation does not remove another operation's listeners.
```typescript
import { EventEmitter } from 'node:events';
import SchemaBuilder from '@pothos/core';
import SmartSubscriptionsPlugin from '@pothos/plugin-smart-subscriptions';
export type Context = {
listeners: Map void>;
};
export const events = new EventEmitter();
const builder = new SchemaBuilder<{ Context: Context }>({
plugins: [SmartSubscriptionsPlugin],
smartSubscriptions: {
debounceDelay: null,
subscribe: (name, context, callback) => {
const listener = (value: unknown) => callback(null, value);
context.listeners.set(name, listener);
events.on(name, listener);
},
unsubscribe: (name, context) => {
const listener = context.listeners.get(name);
if (listener) {
events.off(name, listener);
context.listeners.delete(name);
}
},
},
});
```
Pass a fresh `{ listeners: new Map() }` context for each subscription operation. A distributed
application can connect the same callbacks to its shared event service instead. Call `callback`
with an error to report a source failure, or `callback(null, value)` for an event.
`debounceDelay: null` disables debouncing. Other values enable the plugin's default debounce
window; the current implementation does not use the supplied numeric value as a custom delay. Both `subscribe` and `unsubscribe` callbacks may return promises.
### Async iterator sources [#async-iterator-sources]
If your event service already returns async iterators, use `subscribeOptionsFromIterator` as an
alternative to the manual callbacks. It manages each iterator's lifetime:
```typescript
import { subscribeOptionsFromIterator } from '@pothos/plugin-smart-subscriptions';
type IteratorContext = {
eventsFor: (name: string) => AsyncIterableIterator;
};
const iteratorBuilder = new SchemaBuilder<{ Context: IteratorContext }>({
plugins: [SmartSubscriptionsPlugin],
smartSubscriptions: {
...subscribeOptionsFromIterator((name, context) => context.eventsFor(name)),
},
});
```
The examples below continue with the event-emitter builder.
## Define a smart subscription [#define-a-smart-subscription]
The same `polls` field is available on `Query` and `Subscription`. Register collection events on the
field and per-poll events on the object type:
```typescript
type Poll = { id: string; question: string; votes: number };
const polls = new Map([['1', { id: '1', question: 'Tea or coffee?', votes: 0 }]]);
const PollType = builder.objectRef('Poll').implement({
subscribe: (subscriptions, poll) => {
subscriptions.register(`poll/${poll.id}`);
},
fields: (t) => ({
id: t.exposeID('id'),
question: t.exposeString('question'),
votes: t.exposeInt('votes'),
}),
});
builder.queryType({
fields: (t) => ({
polls: t.field({
type: [PollType],
smartSubscription: true,
subscribe: (subscriptions) => {
subscriptions.register('poll-added');
subscriptions.register('poll-deleted');
},
resolve: () => [...polls.values()],
}),
}),
});
builder.subscriptionType();
export const schema = builder.toSchema();
```
```graphql
subscription {
polls {
id
question
votes
}
}
```
The subscription first sends the current polls. After updating stored data, emit the corresponding
event to send a new result:
```typescript
export function vote(pollId: string) {
const poll = polls.get(pollId);
if (!poll) {
throw new Error('Poll not found');
}
polls.set(pollId, { ...poll, votes: poll.votes + 1 });
events.emit(`poll/${pollId}`, { kind: 'vote' });
}
```
Calling `vote('1')` updates `votes` in the next result. Adding or deleting a poll should emit
`poll-added` or `poll-deleted` after updating the map. Each execution registers the events for its
current results, and subscriptions that are no longer needed are removed.
## Refetch an object [#refetch-an-object]
Replace the `PollType` subscription callback with this version to load only the changed poll:
```typescript
subscribe: (subscriptions, poll) => {
subscriptions.register(`poll/${poll.id}`, {
refetch: () => {
const current = polls.get(poll.id);
if (!current) throw new Error('Poll not found');
return current;
},
});
},
```
A type's registration also accepts `filter: (value) => boolean` to ignore events and
`invalidateCache: (value) => void` to clear a cached value before refetching. Event values are typed
as `unknown`; narrow them before reading properties.
## Refetch a field [#refetch-a-field]
A field can register its own event and set `canRefetch: true`. Replace the `votes` field above with:
```typescript
votes: t.int({
canRefetch: true,
subscribe: (subscriptions, poll) => {
subscriptions.register(`poll/${poll.id}`);
},
resolve: (poll) => polls.get(poll.id)?.votes ?? 0,
}),
```
Remove the object-level registration when using this field-only alternative. The plugin can then
re-run `votes` without reloading the whole polls list. Field registrations support `filter` and
`invalidateCache`, just like type registrations; `canRefetch` reuses the field resolver instead of
taking a separate `refetch` callback.
## Limitations [#limitations]
Smart subscriptions do not support list fields implemented with async generators for `@stream`.
## Run a live subscription locally [#run-a-live-subscription-locally]
The browser playground runs queries and mutations; it does not consume subscription iterators.
The [complete local example](https://github.com/hayes/pothos/tree/main/website/local-examples/smart-subscriptions)
uses GraphQL's `subscribe()` with the schema and event source above. No database or external event
service is required.
With Node.js 22 or newer, run from a checkout of the [Pothos repository](https://github.com/hayes/pothos):
```bash
pnpm install --frozen-lockfile
pnpm --dir website check:local
```
The local examples use the checkout's built Pothos packages. Their separate locked installation
uses GraphQL 16, as required by Grafast 1.0; it does not change the repository's GraphQL version.
`smart-subscriptions/check.ts` opens two subscriptions with separate contexts, checks their initial
results, calls `vote('1')`, and checks both updates. It closes the first iterator and verifies that
the second still receives events, then closes the second and verifies that no listeners remain.
To change the example, adjust the poll's starting votes in `schema.ts` and update the expected results.
When using this schema in a server, pass a new listener map for each operation and arrange for the
transport to close its iterator on disconnect. The local check makes that lifecycle explicit:
```typescript
import { parse, subscribe } from 'graphql';
import { schema, vote } from './schema';
const result = await subscribe({
schema,
document: parse('subscription { polls { id votes } }'),
contextValue: { listeners: new Map() },
});
if (Symbol.asyncIterator in result) {
const iterator = result[Symbol.asyncIterator]();
try {
console.log((await iterator.next()).value); // Initial polls
const update = iterator.next();
vote('1');
console.log((await update).value); // Incremented votes
} finally {
await iterator.return?.(); // Unsubscribe this operation's listeners
}
} else {
console.error(result.errors);
}
```
# SubGraph plugin
URL: /docs/plugins/sub-graph
Build public and internal API variants from one set of types and fields.
Use one set of type definitions to build several API variants. Tag types and fields with named
sub-graphs, then pass `subGraph` to `builder.toSchema()` to select a variant. This is separate from
Apollo Federation: a sub-graph here is a filtered view of your own schema.
## Install [#install]
npm
pnpm
yarn
bun
```bash
npm install --save @pothos/plugin-sub-graph
```
```bash
pnpm add @pothos/plugin-sub-graph
```
```bash
yarn add @pothos/plugin-sub-graph
```
```bash
bun add @pothos/plugin-sub-graph
```
## Build public and internal schemas [#build-public-and-internal-schemas]
Declare the sub-graph names in `SubGraphs`. In this example, types and fields belong to both variants
by default, while `internalNotes` is available only in the internal API:
```typescript playground example="plugin-sub-graph"
import SchemaBuilder from '@pothos/core';
import SubGraphPlugin from '@pothos/plugin-sub-graph';
const builder = new SchemaBuilder<{
SubGraphs: 'Public' | 'Internal';
}>({
plugins: [SubGraphPlugin],
subGraphs: {
defaultForTypes: ['Public', 'Internal'],
fieldsInheritFromTypes: true,
},
});
const Product = builder
.objectRef<{
id: string;
name: string;
internalNotes: string;
}>('Product')
.implement({
fields: (t) => ({
id: t.exposeID('id'),
name: t.exposeString('name'),
internalNotes: t.exposeString('internalNotes', {
subGraphs: ['Internal'],
}),
}),
});
builder.queryType({
fields: (t) => ({
product: t.field({
type: Product,
resolve: () => ({ id: '1', name: 'Notebook', internalNotes: 'Restock next week' }),
}),
}),
});
```
The `subGraph` option selects which types and fields appear in the built schema:
Public
Internal
Combined
Shared
```typescript playground example="plugin-sub-graph"
export const schema = builder.toSchema({ subGraph: 'Public' });
```
```typescript playground example="plugin-sub-graph-variant-internal"
export const schema = builder.toSchema({ subGraph: 'Internal' });
```
```typescript playground example="plugin-sub-graph-variant-combined"
export const schema = builder.toSchema({ subGraph: ['Internal', 'Public'] });
```
```typescript playground example="plugin-sub-graph-variant-shared"
export const schema = builder.toSchema({ subGraph: { all: ['Internal', 'Public'] } });
```
`{ product { name } }` works in either variant. `{ product { internalNotes } }` fails GraphQL
validation against the Public variant. Calling `toSchema()` without a sub-graph retains the full schema.
## Combine variants [#combine-variants]
An array includes types and fields belonging to any listed sub-graph:
```typescript
const combinedSchema = builder.toSchema({ subGraph: ['Internal', 'Public'] });
```
The `all` form includes only membership shared by every listed sub-graph:
```typescript
const sharedSchema = builder.toSchema({ subGraph: { all: ['Internal', 'Public'] } });
```
For the example above, the combined schema includes `internalNotes`; the shared schema omits it.
## Membership options [#membership-options]
Set `subGraphs` on a type or field to override its defaults. A type without this option uses
`subGraphs.defaultForTypes` from the builder.
A field's membership is determined in this order:
1. Its own `subGraphs` option.
2. Its parent type's `defaultSubGraphsForFields` option.
3. Its parent type's membership, when `subGraphs.fieldsInheritFromTypes` is `true`.
4. The builder's `subGraphs.defaultForFields` option.
5. An empty array.
`fieldsInheritFromTypes` defaults to `false`. An explicit empty array overrides the fallback, so
`defaultSubGraphsForFields: []` on a type makes its fields opt in individually.
## Inputs and missing types [#inputs-and-missing-types]
Nullable arguments and input fields can also have `subGraphs`. Required arguments and input fields
cannot be removed from a retained field or input object: resolvers may depend on them being present.
The plugin rejects a schema that tries to remove one.
An output field is omitted when its return type is excluded. Other references can make a filtered
schema invalid, such as a retained union containing an excluded member or a required argument using
an excluded input type. Include those dependencies in the variant or exclude the referring field or
type as well. Build and validate each variant you intend to serve.
## Include unreachable types [#include-unreachable-types]
Types that remain unreachable after filtering are normally omitted. The
`subGraphs.explicitlyIncludeType` callback retains matching types that already belong to the selected
sub-graph, even when no field reaches them.
For federation entities, `hasResolvableKey` can retain an otherwise unreachable external reference.
Use this as a separate builder setup with both plugins:
```typescript
import SchemaBuilder from '@pothos/core';
import FederationPlugin, { hasResolvableKey } from '@pothos/plugin-federation';
import SubGraphPlugin from '@pothos/plugin-sub-graph';
const builder = new SchemaBuilder<{
SubGraphs: 'Public' | 'Internal';
}>({
plugins: [SubGraphPlugin, FederationPlugin],
subGraphs: {
defaultForTypes: ['Public', 'Internal'],
fieldsInheritFromTypes: true,
explicitlyIncludeType: (type) => hasResolvableKey(type),
},
});
```
# Tracing plugin
URL: /docs/plugins/tracing
Select resolvers to trace and connect them to a logger or tracing provider.
Trace selected resolver calls with a wrapper you provide. Set a field's `tracing` option to enable,
disable, or configure its trace; use `tracing.default` for fields without an explicit setting.
The `tracing.wrap` callback receives the resolver, its tracing options, and the field configuration.
The plugin measures resolver execution. Instrument your GraphQL server separately to trace an
entire operation, including parsing, validation, and execution.
## Install [#install]
npm
pnpm
yarn
bun
```bash
npm install --save @pothos/plugin-tracing
```
```bash
pnpm add @pothos/plugin-tracing
```
```bash
yarn add @pothos/plugin-tracing
```
```bash
bun add @pothos/plugin-tracing
```
## Log resolver duration [#log-resolver-duration]
This schema traces root fields by default and logs the time spent resolving `hello`:
```typescript playground example="plugin-tracing"
import SchemaBuilder from '@pothos/core';
import TracingPlugin, { isRootField, wrapResolver } from '@pothos/plugin-tracing';
const builder = new SchemaBuilder({
plugins: [TracingPlugin],
tracing: {
default: (config) => isRootField(config),
wrap: (resolver, _options, config) =>
wrapResolver(resolver, (error, duration) => {
console.log(`${config.parentType}.${config.name}: ${duration}ms`, error);
}),
},
});
builder.queryType({
fields: (t) => ({
hello: t.string({
args: { name: t.arg.string() },
resolve: (_parent, { name }) => `hello, ${name ?? 'World'}`,
}),
}),
});
export const schema = builder.toSchema();
```
`wrapResolver` calls its completion callback for a synchronous result, fulfilled promise, thrown
error, or rejected promise. It preserves the resolver's result or error. The callback receives
`null` for a successful call and the thrown value for a failure; duration is in milliseconds.
## Choose fields to trace [#choose-fields-to-trace]
Set `tracing: true` on a field to enable tracing regardless of the default, or `tracing: false` to
disable it. These settings go alongside `type`, `args`, and `resolve` in the field options.
Use the exported predicates in `tracing.default` to select fields:
| Helper | Matches |
| ------------------------ | ------------------------------------------------------------------------------------------- |
| `isRootField(config)` | Fields whose parent is named `Query`, `Mutation`, or `Subscription`. |
| `isScalarField(config)` | Scalars and lists of scalars. |
| `isEnumField(config)` | Enums and lists of enums. |
| `isExposedField(config)` | `t.expose*` fields, fields without a resolver, and fields using GraphQL's default resolver. |
Tracing every property read can create many spans. Start with root fields and add fields that perform
work you need to inspect. If you use custom root type names, match those names in your default predicate.
## Custom tracing options [#custom-tracing-options]
Declare `Tracing` in the builder's schema types to accept your own field options. This is an
alternative builder and query definition to the first example:
```typescript playground example="plugin-tracing-options"
import SchemaBuilder from '@pothos/core';
import TracingPlugin, { isRootField, wrapResolver } from '@pothos/plugin-tracing';
const builder = new SchemaBuilder<{
Tracing: boolean | { label: string };
}>({
plugins: [TracingPlugin],
tracing: {
default: (config) => isRootField(config),
wrap: (resolver, options, config) =>
wrapResolver(resolver, (error, duration) => {
const label =
typeof options === 'object' ? options.label : `${config.parentType}.${config.name}`;
console.log(`${label}: ${duration}ms`, error);
}),
},
});
builder.queryType({
fields: (t) => ({
hello: t.string({
tracing: { label: 'greeting' },
args: { name: t.arg.string() },
resolve: (_parent, { name }) => `hello, ${name ?? 'World'}`,
}),
}),
});
export const schema = builder.toSchema();
```
A field can also compute its tracing options from resolver arguments. Replace the `tracing` option
above with this expression to use a different label for named greetings:
```typescript
tracing: (parent, { name }) => ({ label: name ? 'named greeting' : 'default greeting' }),
```
With static options, `wrap` runs once per field when the schema is built. A function-valued
`tracing` option is evaluated per resolver invocation, and `wrap` runs for that invocation unless
the result is `false` or `null`. The builder's `default` callback can return a function in the same way.
If you only need resolver arguments inside the wrapper, keep the tracing options static and read
those arguments from the returned function. This avoids constructing a wrapper for each call.
## Implementing a tracer [#implementing-a-tracer]
`wrap` returns a resolver with the same arguments as the original. Use `runFunction` to run code
before calling the resolver and observe its completion, including failures. For example, this
alternative setup logs the start and end of each root resolver:
```typescript playground example="plugin-tracing-lifecycle"
import SchemaBuilder from '@pothos/core';
import TracingPlugin, { isRootField, runFunction } from '@pothos/plugin-tracing';
const builder = new SchemaBuilder({
plugins: [TracingPlugin],
tracing: {
default: (config) => isRootField(config),
wrap: (resolver) => (parent, args, context, info) => {
console.log(`Starting ${info.parentType.name}.${info.fieldName}`);
return runFunction(
() => resolver(parent, args, context, info),
(error, duration) => {
console.log(`Finished ${info.parentType.name}.${info.fieldName}: ${duration}ms`, error);
},
);
},
},
});
```
For custom span hierarchies, the plugin exports `pathToString(info)`, `getParentSpan(context, info)`,
and `createSpanWithParent(context, info, createSpan)`. The latter caches a span on the request
context and passes the closest cached parent span to your callback. Use a fresh context for each
operation so this cache belongs to that operation.
## Tracing integrations [#tracing-integrations]
Provider packages implement `tracing.wrap` for common tracing systems. Initialize the provider SDK
and its exporter in your application before executing requests. Each setup below is an alternative
builder; add your application's types and fields to it.
### OpenTelemetry [#opentelemetry]
npm
pnpm
yarn
bun
```bash
npm install --save @pothos/tracing-opentelemetry @opentelemetry/api
```
```bash
pnpm add @pothos/tracing-opentelemetry @opentelemetry/api
```
```bash
yarn add @pothos/tracing-opentelemetry @opentelemetry/api
```
```bash
bun add @pothos/tracing-opentelemetry @opentelemetry/api
```
#### Setting up a tracer [#setting-up-a-tracer]
For a local example, create `tracer.ts` with a console exporter:
npm
pnpm
yarn
bun
```bash
npm install @opentelemetry/sdk-trace-node @opentelemetry/sdk-trace-base
```
```bash
pnpm add @opentelemetry/sdk-trace-node @opentelemetry/sdk-trace-base
```
```bash
yarn add @opentelemetry/sdk-trace-node @opentelemetry/sdk-trace-base
```
```bash
bun add @opentelemetry/sdk-trace-node @opentelemetry/sdk-trace-base
```
```typescript
import { trace } from '@opentelemetry/api';
import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
export const provider = new NodeTracerProvider({
spanProcessors: [new SimpleSpanProcessor(new ConsoleSpanExporter())],
});
provider.register();
export const tracer = trace.getTracer('pothos');
```
Initialize the provider once, before handling requests. Use your application's existing provider if
it already configures OpenTelemetry. See [OpenTelemetry's Node.js instrumentation guide](https://opentelemetry.io/docs/languages/js/instrumentation/)
for SDK configuration beyond this example.
Save the following schema as `schema.ts`. Importing `tracer` connects Pothos to the configured provider:
```typescript
import SchemaBuilder from '@pothos/core';
import TracingPlugin, { isRootField } from '@pothos/plugin-tracing';
import { createOpenTelemetryWrapper } from '@pothos/tracing-opentelemetry';
import { tracer } from './tracer';
const createSpan = createOpenTelemetryWrapper(tracer);
const builder = new SchemaBuilder({
plugins: [TracingPlugin],
tracing: {
default: (config) => isRootField(config),
wrap: (resolver, options) => createSpan(resolver, options),
},
});
builder.queryType({
fields: (t) => ({
hello: t.string({ resolve: () => 'hello, World' }),
}),
});
export const schema = builder.toSchema();
```
The wrapper records field name, response path, and return type, and connects nested resolver spans
to their nearest traced ancestor. It makes the resolver span active while the resolver runs.
Options passed to `createOpenTelemetryWrapper(tracer, options)` are:
* `includeArgs`: attach serialized field arguments; defaults to `false`.
* `includeSource`: attach the selected field's GraphQL source; defaults to `false`.
* `ignoreError`: skip recording resolver exceptions; defaults to `false`.
* `onSpan`: customize the span with `(span, fieldOptions, parent, args, context, info)`.
You can also pass these options as the third argument to `createSpan` for an individual field.
Boolean options there override the defaults; both `onSpan` callbacks run when both are supplied.
### Adding custom attributes to spans [#adding-custom-attributes-to-spans]
Use the `Tracing` schema type and `onSpan` callback together. This replaces the OpenTelemetry
builder above and accepts a static category on each field:
```typescript
import SchemaBuilder from '@pothos/core';
import TracingPlugin, { isRootField } from '@pothos/plugin-tracing';
import { createOpenTelemetryWrapper } from '@pothos/tracing-opentelemetry';
import { tracer } from './tracer';
type TracingOptions = boolean | { category: string };
const createSpan = createOpenTelemetryWrapper(tracer, {
onSpan: (span, options) => {
if (typeof options === 'object') {
span.setAttribute('app.category', options.category);
}
},
});
const builder = new SchemaBuilder<{ Tracing: TracingOptions }>({
plugins: [TracingPlugin],
tracing: {
default: (config) => isRootField(config),
wrap: (resolver, options) => createSpan(resolver, options),
},
});
builder.queryType({
fields: (t) => ({
hello: t.string({
tracing: { category: 'greeting' },
resolve: () => 'hello, World',
}),
}),
});
```
### Instrumenting the execution phase [#instrumenting-the-execution-phase]
The Pothos wrapper creates resolver spans. To group them under an operation span, wrap the server's
execution call. With GraphQL Yoga, save this plugin as `tracing.ts`:
npm
pnpm
yarn
bun
```bash
npm install graphql-yoga
```
```bash
pnpm add graphql-yoga
```
```bash
yarn add graphql-yoga
```
```bash
bun add graphql-yoga
```
```typescript
import { SpanStatusCode } from '@opentelemetry/api';
import { getOperationAST, print } from 'graphql';
import type { Plugin } from 'graphql-yoga';
import { AttributeNames, SpanNames } from '@pothos/tracing-opentelemetry';
import { tracer } from './tracer';
export const tracingPlugin: Plugin = {
onExecute({ executeFn, setExecuteFn }) {
setExecuteFn((args) =>
tracer.startActiveSpan(
SpanNames.EXECUTE,
{
attributes: {
[AttributeNames.OPERATION_NAME]:
getOperationAST(args.document, args.operationName)?.name?.value ?? '',
[AttributeNames.SOURCE]: print(args.document),
},
},
async (span) => {
try {
const result = await executeFn(args);
if ('errors' in result && result.errors?.length) {
span.setStatus({ code: SpanStatusCode.ERROR });
for (const error of result.errors) span.recordException(error);
}
return result;
} catch (error) {
span.setStatus({ code: SpanStatusCode.ERROR });
span.recordException(error instanceof Error ? error : String(error));
throw error;
} finally {
span.end();
}
},
),
);
},
};
```
This hook covers queries and mutations that return a single result. It keeps the operation span
active while resolvers execute and ends it when execution finishes. Subscriptions and incremental
responses need hooks that follow the returned iterator; use an integration that supports those
lifecycles, such as the Envelop alternative below.
Register the plugin in `server.ts`:
```typescript
import { createServer } from 'node:http';
import { createYoga } from 'graphql-yoga';
import { tracingPlugin } from './tracing';
import { schema } from './schema';
const yoga = createYoga({ schema, plugins: [tracingPlugin] });
createServer(yoga).listen(4000);
```
Run the server and query `{ hello }`. The console exporter logs an execution span and its resolver
span. The New Relic, Sentry, and X-Ray hooks below can use this same server registration with their
corresponding schema and `tracing.ts`.
#### Using the Envelop OpenTelemetry plugin [#using-the-envelop-opentelemetry-plugin]
As an alternative to the custom hook, use `@envelop/opentelemetry` for operation instrumentation.
Disable its resolver tracing when Pothos already creates those spans:
npm
pnpm
yarn
bun
```bash
npm install @envelop/opentelemetry
```
```bash
pnpm add @envelop/opentelemetry
```
```bash
yarn add @envelop/opentelemetry
```
```bash
bun add @envelop/opentelemetry
```
```typescript
import { useOpenTelemetry } from '@envelop/opentelemetry';
import { provider } from './tracer';
export const tracingPlugin = useOpenTelemetry(
{ resolvers: false, variables: false, result: false },
provider,
);
```
#### Optional HTTP instrumentation [#optional-http-instrumentation]
To include HTTP request spans, install the HTTP instrumentation packages:
npm
pnpm
yarn
bun
```bash
npm install @opentelemetry/instrumentation @opentelemetry/instrumentation-http
```
```bash
pnpm add @opentelemetry/instrumentation @opentelemetry/instrumentation-http
```
```bash
yarn add @opentelemetry/instrumentation @opentelemetry/instrumentation-http
```
```bash
bun add @opentelemetry/instrumentation @opentelemetry/instrumentation-http
```
Add this to `tracer.ts` after creating `provider`. It also works with the Datadog provider below:
```typescript
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
registerInstrumentations({
tracerProvider: provider,
instrumentations: [new HttpInstrumentation()],
});
```
Load `tracer` before importing HTTP and server modules so instrumentation can attach in time.
For the JavaScript server entry point, preload it:
```bash
node --import ./tracer.js ./server.js
```
### Datadog [#datadog]
Keep the Pothos schema and execution instrumentation above. Replace `tracer.ts` with an OTLP
exporter pointed at your Datadog Agent:
npm
pnpm
yarn
bun
```bash
npm install @opentelemetry/exporter-trace-otlp-http @opentelemetry/resources
```
```bash
pnpm add @opentelemetry/exporter-trace-otlp-http @opentelemetry/resources
```
```bash
yarn add @opentelemetry/exporter-trace-otlp-http @opentelemetry/resources
```
```bash
bun add @opentelemetry/exporter-trace-otlp-http @opentelemetry/resources
```
```typescript
import { trace } from '@opentelemetry/api';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { resourceFromAttributes } from '@opentelemetry/resources';
import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
export const provider = new NodeTracerProvider({
resource: resourceFromAttributes({ 'service.name': 'pothos-api' }),
spanProcessors: [
new SimpleSpanProcessor(new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ?? 'http://localhost:4318/v1/traces',
})),
],
});
provider.register();
export const tracer = trace.getTracer('pothos');
```
Enable the Agent's OTLP HTTP receiver. For an application running on the same host:
```yaml
otlp_config:
receiver:
protocols:
http:
endpoint: localhost:4318
```
For containers or a remote Agent, configure a reachable receiver address and set
`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` to its `/v1/traces` endpoint. See
[Datadog's OTLP Agent setup](https://docs.datadoghq.com/opentelemetry/setup/otlp_ingest_in_the_agent/?tab=host)
for the matching deployment configuration.
### New Relic [#new-relic]
npm
pnpm
yarn
bun
```bash
npm install --save @pothos/tracing-newrelic newrelic
```
```bash
pnpm add @pothos/tracing-newrelic newrelic
```
```bash
yarn add @pothos/tracing-newrelic newrelic
```
```bash
bun add @pothos/tracing-newrelic newrelic
```
npm
pnpm
yarn
bun
```bash
npm install --save-dev @types/newrelic
```
```bash
pnpm add --save-dev @types/newrelic
```
```bash
yarn add --dev @types/newrelic
```
```bash
bun add --dev @types/newrelic
```
Save this schema as `schema.ts`. The operation setup below loads the New Relic agent before the application:
```typescript
import SchemaBuilder from '@pothos/core';
import TracingPlugin, { isRootField } from '@pothos/plugin-tracing';
import { createNewrelicWrapper } from '@pothos/tracing-newrelic';
const createSegment = createNewrelicWrapper();
const builder = new SchemaBuilder({
plugins: [TracingPlugin],
tracing: {
default: (config) => isRootField(config),
wrap: (resolver, options) => createSegment(resolver, options),
},
});
builder.queryType({
fields: (t) => ({
hello: t.string({ resolve: () => 'hello, World' }),
}),
});
export const schema = builder.toSchema();
```
The wrapper creates a `graphql.resolve` segment. It accepts `includeArgs` and `includeSource`, both
defaulting to `false`, either at wrapper creation or as the third argument to `createSegment`.
#### Operation attributes [#operation-attributes]
Start the server with the New Relic agent loaded before application modules so its HTTP
instrumentation creates the request transaction:
```bash
node --require newrelic ./server.js
```
Configure the agent's application name and license key through your existing New Relic setup.
Save the following Yoga plugin as `tracing.ts` to add GraphQL operation attributes to that transaction. The Pothos
wrapper above adds the individual resolver segments; this hook does not start another transaction.
```typescript
import newrelic from 'newrelic';
import { getOperationAST, print } from 'graphql';
import type { Plugin } from 'graphql-yoga';
import { AttributeNames } from '@pothos/tracing-newrelic';
export const tracingPlugin: Plugin = {
onExecute: ({ args }) => {
const operation = getOperationAST(args.document, args.operationName);
newrelic.addCustomAttributes({
[AttributeNames.OPERATION_NAME]: operation?.name?.value ?? '',
[AttributeNames.OPERATION_TYPE]: operation?.operation ?? 'unknown',
[AttributeNames.SOURCE]: print(args.document),
});
},
};
```
Register `tracingPlugin` in the shared Yoga server setup. `getOperationAST` also finds the operation
name when the document contains a single named operation but the request omits `operationName`.
Alternatively, `@envelop/newrelic` can report operation metadata. Version 10.2 requires New Relic
agent 7–11; use the direct hook above with agent 13.
For a compatible installation, replace the manual hook with:
```typescript
import { useNewRelic } from '@envelop/newrelic';
export const tracingPlugin = useNewRelic({
trackResolvers: false,
includeOperationDocument: true,
});
```
Install `@envelop/newrelic` alongside compatible `newrelic` and `@envelop/core` versions.
`trackResolvers: false` leaves resolver tracing to Pothos instead of creating duplicate resolver
segments. Use this plugin or the manual operation hook, not both.
### Sentry [#sentry]
npm
pnpm
yarn
bun
```bash
npm install --save @pothos/tracing-sentry @sentry/node
```
```bash
pnpm add @pothos/tracing-sentry @sentry/node
```
```bash
yarn add @pothos/tracing-sentry @sentry/node
```
```bash
bun add @pothos/tracing-sentry @sentry/node
```
Initialize Sentry and arrange for an active request or operation span before resolving fields.
Without an active parent span, the wrapper calls the resolver without creating a span.
Save this schema as `schema.ts`:
```typescript
import SchemaBuilder from '@pothos/core';
import TracingPlugin, { isRootField } from '@pothos/plugin-tracing';
import { createSentryWrapper } from '@pothos/tracing-sentry';
const createSpan = createSentryWrapper();
const builder = new SchemaBuilder({
plugins: [TracingPlugin],
tracing: {
default: (config) => isRootField(config),
wrap: (resolver, options) => createSpan(resolver, options),
},
});
builder.queryType({
fields: (t) => ({
hello: t.string({ resolve: () => 'hello, World' }),
}),
});
export const schema = builder.toSchema();
```
Options are `includeArgs`, `includeSource`, `ignoreError`, and
`onSpan(span, fieldOptions, parent, args, context, info)`. The boolean options default to `false`.
Pass options at wrapper creation or as the third argument to `createSpan`.
#### Operation spans [#operation-spans]
Save the initialization below as `instrumentation.ts`:
```typescript
import * as Sentry from '@sentry/node';
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 1,
});
```
For Sentry, replace the shared `server.ts` with this entry point so initialization runs before
loading the schema and tracing plugin:
```typescript
import './instrumentation';
import { createServer } from 'node:http';
import { createYoga } from 'graphql-yoga';
import { tracingPlugin } from './tracing';
import { schema } from './schema';
const yoga = createYoga({ schema, plugins: [tracingPlugin] });
createServer(yoga).listen(4000);
```
Save the following Yoga plugin as `tracing.ts`. It creates an active `graphql.execute` span. Pothos resolver spans become
children of it. This wrapper handles ordinary query and mutation results; streaming responses and
subscriptions need a lifecycle wrapper that keeps the span open while the iterator is consumed.
```typescript
import * as Sentry from '@sentry/node';
import { getOperationAST, print } from 'graphql';
import type { Plugin } from 'graphql-yoga';
import { AttributeNames, SpanNames } from '@pothos/tracing-sentry';
export const tracingPlugin: Plugin = {
onExecute: ({ setExecuteFn, executeFn }) => {
setExecuteFn((options) => {
const operation = getOperationAST(options.document, options.operationName);
const name = operation?.name?.value ?? '';
return Sentry.startSpan(
{
name,
op: SpanNames.EXECUTE,
attributes: {
[AttributeNames.OPERATION_NAME]: name,
[AttributeNames.SOURCE]: print(options.document),
},
},
async (span) => {
const result = await executeFn(options);
if ('errors' in result && result.errors?.length) {
span.setStatus({ code: 2, message: 'GraphQL execution failed' });
}
return result;
},
);
});
},
};
```
`Sentry.startSpan` keeps the operation span active during the callback and ends it when the returned
promise settles. It marks thrown execution failures as errors. GraphQL usually returns resolver
errors in the result instead of throwing, so the callback also marks those results as failed.
Register this `tracingPlugin` in the shared Yoga setup along with the schema built using the Sentry
resolver wrapper.
#### Using the Envelop Sentry plugin [#using-the-envelop-sentry-plugin]
Alternatively, keep the Sentry initialization and resolver wrapper, and replace `tracing.ts` with
`@envelop/sentry`:
npm
pnpm
yarn
bun
```bash
npm install @envelop/sentry
```
```bash
pnpm add @envelop/sentry
```
```bash
yarn add @envelop/sentry
```
```bash
bun add @envelop/sentry
```
```typescript
import { useSentry } from '@envelop/sentry';
export const tracingPlugin = useSentry({});
```
This plugin supplies an active operation span and reports execution errors. When using its error
reporting, set `ignoreError: true` in `createSentryWrapper` to avoid reporting the same resolver error
twice. Use this alternative or the custom Sentry operation hook, not both.
### AWS XRay [#aws-xray]
npm
pnpm
yarn
bun
```bash
npm install --save @pothos/tracing-xray aws-xray-sdk-core
```
```bash
pnpm add @pothos/tracing-xray aws-xray-sdk-core
```
```bash
yarn add @pothos/tracing-xray aws-xray-sdk-core
```
```bash
bun add @pothos/tracing-xray aws-xray-sdk-core
```
Configure an active X-Ray segment for the request. The wrapper creates resolver subsegments within
that segment, or calls the resolver without a subsegment when no parent segment is available.
Save this schema as `schema.ts`:
```typescript
import SchemaBuilder from '@pothos/core';
import TracingPlugin, { isRootField } from '@pothos/plugin-tracing';
import { createXRayWrapper } from '@pothos/tracing-xray';
const createSegment = createXRayWrapper();
const builder = new SchemaBuilder({
plugins: [TracingPlugin],
tracing: {
default: (config) => isRootField(config),
wrap: (resolver, options) => createSegment(resolver, options),
},
});
builder.queryType({
fields: (t) => ({
hello: t.string({ resolve: () => 'hello, World' }),
}),
});
export const schema = builder.toSchema();
```
Options are `includeArgs`, `includeSource`, and
`onSegment(segment, fieldOptions, parent, args, context, info)`. Both boolean options default to
`false`. Pass options at wrapper creation or as the third argument to `createSegment`.
#### Operation segments [#operation-segments]
Save this standalone Yoga plugin as `tracing.ts`. It creates a root segment for each GraphQL operation and an execution
subsegment within it. It makes the execution subsegment active so the Pothos wrapper can attach
resolver subsegments. The SDK uses automatic context mode by default.
```typescript
import AWSXRay from 'aws-xray-sdk-core';
import { getOperationAST, print } from 'graphql';
import type { Plugin } from 'graphql-yoga';
import { AttributeNames, SpanNames } from '@pothos/tracing-xray';
export const tracingPlugin: Plugin = {
onExecute: ({ setExecuteFn, executeFn }) => {
setExecuteFn((options) => {
const parent = new AWSXRay.Segment('graphql');
const segment = parent.addNewSubsegment(SpanNames.EXECUTE);
const operation = getOperationAST(options.document, options.operationName);
segment.addAttribute(
AttributeNames.OPERATION_NAME,
operation?.name?.value ?? '',
);
segment.addAttribute(AttributeNames.SOURCE, print(options.document));
return AWSXRay.getNamespace().runAndReturn(async () => {
AWSXRay.setSegment(segment);
try {
const result = await executeFn(options);
if ('errors' in result) {
for (const error of result.errors ?? []) segment.addError(error);
}
return result;
} catch (error) {
segment.addError(error instanceof Error ? error : String(error));
throw error;
} finally {
segment.close();
parent.close();
}
});
});
},
};
```
Register this `tracingPlugin` in the shared Yoga setup. The `finally` block closes both segments on
successful execution and on a thrown failure; returned GraphQL errors are recorded on the execution
subsegment. This recipe is for ordinary query and mutation results. For streaming responses or
subscriptions, close the segments when the returned iterator finishes or is cancelled instead.
When your HTTP middleware already owns a request segment, use that segment as `parent` and leave
closing it to the middleware. Do not create a second root segment or close a segment owned by another
integration. Configure the X-Ray daemon or collector using the SDK's application setup so closed
segments can be exported.
# Validation plugin
URL: /docs/plugins/validation
Validation plugin docs for Pothos
Validate arguments and input objects before a field resolver runs. Attach a schema from any library
that implements [Standard Schema](https://standardschema.dev), including Zod, Valibot, and ArkType.
Validation can be asynchronous, and chained validators can transform the values passed to your resolver.
If validation fails, the resolver does not run.
## Usage [#usage]
### Install [#install]
To use the validation plugin, you'll need to install the validation plugin and a compatible validation library:
npm
pnpm
yarn
bun
```bash
npm install --save @pothos/plugin-validation zod
# OR
npm install --save @pothos/plugin-validation valibot
# OR
npm install --save @pothos/plugin-validation arktype
```
```bash
pnpm add @pothos/plugin-validation zod
# OR
pnpm add @pothos/plugin-validation valibot
# OR
pnpm add @pothos/plugin-validation arktype
```
```bash
yarn add @pothos/plugin-validation zod
# OR
yarn add @pothos/plugin-validation valibot
# OR
yarn add @pothos/plugin-validation arktype
```
```bash
bun add @pothos/plugin-validation zod
# OR
bun add @pothos/plugin-validation valibot
# OR
bun add @pothos/plugin-validation arktype
```
### Setup [#setup]
```typescript
import SchemaBuilder from '@pothos/core';
import ValidationPlugin from '@pothos/plugin-validation';
import { z } from 'zod'; // or your preferred validation library
const builder = new SchemaBuilder({
plugins: [ValidationPlugin],
});
builder.queryType({
fields: (t) => ({
simple: t.boolean({
args: {
// Validate individual arguments
email: t.arg.string({
required: true,
validate: z.string().email(),
}),
},
resolve: () => true,
}),
}),
});
```
## Validation API Overview [#validation-api-overview]
The validation plugin supports validating inputs and arguments in several different ways:
* **Argument validation**: `t.arg.string({ validate: schema })` or `t.arg.string().validate(schema)` - Validate individual arguments
* **Validate all field args**: `t.field({ args, validate: schema, ... })` or `t.field({ args: t.validate(args, schema), ... })` - Validate all arguments together
* **Input type validation**: `builder.inputType('Input', { validate: schema, ... })` or `builder.inputType('Input', { ... }).validate(schema)` - Validate entire input objects
* **Input field validation**: `t.string({ validate: schema })` or `t.string().validate(schema)` - Validate individual input type fields
Each example below is independent. Reuse the setup imports and builder configuration, then use the
example's query and input definitions in place of any earlier ones.
## Validation Patterns [#validation-patterns]
### Argument Validation [#argument-validation]
Validate each field argument independently using either the object syntax or chaining API:
```typescript
builder.queryType({
fields: (t) => ({
user: t.string({
args: {
email: t.arg.string({
required: true,
validate: z.string().email(),
}),
name: t.arg.string({ required: true })
.validate(z.string().min(2).max(50)),
},
resolve: (_, args) => `User: ${args.name}`,
}),
}),
});
```
Chained `.validate(schema)` calls receive omitted (`undefined`) and explicit `null` values.
Use a nullish schema when those inputs are allowed; a transform can supply a default, and the
resolver receives its inferred output. Validators passed in the `validate` option continue to
skip nullish values. Input type validators run only when an input object is present.
#### Data Transformation with Argument Validation [#data-transformation-with-argument-validation]
When using the chaining API, you can transform data as part of the validation process:
```typescript
builder.queryType({
fields: (t) => ({
processData: t.string({
args: {
// Convert comma-separated string to array
tags: t.arg.string({ required: true })
.validate(z.string().transform(str => str.split(',').map(s => s.trim()))),
},
resolve: (_, args) => {
return `Processed ${args.tags.length} tags`;
},
}),
}),
});
```
### Validating all Field Arguments Together [#validating-all-field-arguments-together]
Pass `validate` on the output field to check its arguments together. Optional GraphQL arguments
can be omitted or explicitly `null`; use a schema that accepts both when those are valid inputs.
```typescript
builder.queryType({
fields: (t) => ({
contact: t.boolean({
args: {
email: t.arg.string(),
phone: t.arg.string(),
},
// Ensure at least one contact method is provided
validate: z
.object({
email: z.string().nullish(),
phone: z.string().nullish(),
})
.refine(
(args) => !!args.phone || !!args.email,
{ message: 'Must provide either phone or email' }
),
resolve: () => true,
}),
}),
});
```
#### With transforms [#with-transforms]
Use `t.validate(args, schema)` to transform all arguments and infer the transformed resolver arguments:
```typescript
builder.queryType({
fields: (t) => ({
user: t.string({
args: t.validate({
email: t.arg.string(),
phone: t.arg.string(),
},
z.object({
email: z.string().nullish(),
phone: z.string().nullish(),
})
.refine(
(args) => !!args.phone || !!args.email,
{ message: 'Must provide either phone or email' }
)
.transform((args) => ({
filter: {
email: args.email ? args.email.toLowerCase() : undefined,
phone: args.phone ? args.phone.replace(/\D/g, '') : undefined,
},
}))
),
resolve: (_, args) => {
// args has transformed shape:
// { filter: { email?: string, phone?: string } }
return `User filter: ${JSON.stringify(args.filter)}`;
},
}),
}),
});
```
### Input Type Validation [#input-type-validation]
Validate entire input objects with complex validation logic using either object syntax or chaining:
```typescript
// Object syntax
const UserInput = builder.inputType('UserInput', {
fields: (t) => ({
name: t.string({ required: true }),
age: t.int({ required: true }),
}),
validate: z
.object({
name: z.string(),
age: z.number(),
})
.refine((user) => user.name !== 'admin', {
message: 'Username "admin" is not allowed',
})
});
```
#### Input Type Transformation [#input-type-transformation]
Transform entire input types:
```typescript
const UserInput = builder.inputType('RawUserInput', {
fields: (t) => ({
fullName: t.string({ required: true }),
email: t.string({ required: true }),
}),
}).validate(
z.object({
fullName: z.string(),
email: z.string().email(),
}).transform(data => ({
firstName: data.fullName.split(' ')[0],
lastName: data.fullName.split(' ').slice(1).join(' '),
email: data.email.toLowerCase(),
}))
);
builder.queryType({
fields: (t) => ({
previewUser: t.string({
args: {
userData: t.arg({ type: UserInput, required: true }),
},
resolve: (_, args) => {
// args.userData has transformed shape:
// { firstName: string, lastName: string, email: string }
return `User preview: ${args.userData.firstName} ${args.userData.lastName} <${args.userData.email}>`;
},
}),
}),
});
```
### Input Field Validation [#input-field-validation]
Validate individual fields within input types:
```typescript
const UserInput = builder.inputType('UserInput', {
fields: (t) => ({
name: t.string({
required: true,
validate: z.string().min(2).refine(
(name) => name[0].toUpperCase() === name[0],
{ message: 'Name must be capitalized' }
),
})
}),
});
```
#### Input Field Transformation [#input-field-transformation]
Transform field values during validation:
```typescript
const UserInput = builder.inputType('UserInput', {
fields: (t) => ({
birthDate: t.string({ required: true })
.validate(z.iso.date())
.validate(z.string().transform(str => new Date(str))),
}),
});
```
### Validation before mutation side effects [#validation-before-mutation-side-effects]
Validation finishes before a mutation resolver runs. A rejected input therefore cannot trigger
writes placed inside that resolver. The following example trims accepted names and stores them in
an in-memory array; invalid names or email addresses produce typed validation issues.
This separate builder uses the [errors plugin integration](./errors#with-validation-plugin).
Install `@pothos/plugin-errors` alongside the validation plugin and Zod. Setting
`unsafelyHandleInputErrors: true` allows validation details to be returned before field authorization
hooks run, so only enable it when those details may be public.
```typescript
import SchemaBuilder from '@pothos/core';
import ErrorsPlugin from '@pothos/plugin-errors';
import ValidationPlugin, {
InputValidationError,
type StandardSchemaV1,
} from '@pothos/plugin-validation';
import { z } from 'zod';
```
```typescript playground example="plugin-validation"
// This public example deliberately exposes validation details before authorization.
const builder = new SchemaBuilder({
plugins: [ErrorsPlugin, ValidationPlugin],
errors: { unsafelyHandleInputErrors: true },
});
const names: string[] = [];
const Issue = builder.objectRef('ValidationIssue').implement({
fields: (t) => ({
message: t.exposeString('message'),
path: t.stringList({
resolve: (issue) =>
issue.path?.map((part) => String(typeof part === 'object' ? part.key : part)) ?? [],
}),
}),
});
builder.objectType(InputValidationError, {
name: 'InputValidationError',
fields: (t) => ({ issues: t.field({ type: [Issue], resolve: (error) => error.issues }) }),
});
const Registration = builder.inputType('Registration', {
fields: (t) => ({
name: t.string({ required: true }).validate(z.string().trim().min(3, 'Name is too short')),
email: t.string({ required: true, validate: z.email('Enter a valid email') }),
}),
});
builder.mutationType({
fields: (t) => ({
register: t.string({
args: { input: t.arg({ type: Registration, required: true }) },
errors: { types: [InputValidationError] },
resolve: (_, { input }) => {
names.push(input.name);
return input.name;
},
}),
}),
});
```
## Plugin Options [#plugin-options]
### 'validationError' [#validationerror]
By default, failed validation throws `InputValidationError`, which contains Standard Schema issues
with their messages and paths. Set `validationError` to return your own error or message.
```typescript
const builder = new SchemaBuilder({
plugins: [ValidationPlugin],
validation: {
validationError: (validationResult, args, context) => {
// validationResult contains the standard-schema validation result
return new Error(`Validation failed: ${validationResult.issues.map(i => i.message).join(', ')}`);
},
},
});
```
#### Return Values [#return-values]
Your error handler can return:
* **Error object**: Return a custom Error instance
* **String**: Return a string message (will be wrapped in a PothosValidationError)
* **Throw**: Throw an error directly
To return validation failures as typed GraphQL results, see the
[errors plugin integration](./errors#with-validation-plugin). This requires
`unsafelyHandleInputErrors`, because input validation runs before field authorization hooks.
### Validation Execution Order [#validation-execution-order]
Understanding when and how validations are executed:
1. **Input Field Validation**: Individual input fields are validated first
2. **Input Type Validation**: Whole input object validation runs after field validation passes
3. **Argument Validation**: Individual field arguments are validated
4. **Field-Level Validation**: The output field's `validate` option and `t.validate()` run last
When there are multiple validations for the same field or type, they are executed in order, so that any transforms are applied before passing to the next schema.
Validations for separate fields or arguments are executed in parallel, and their results are merged into a single set of issues.
# With-Input plugin
URL: /docs/plugins/with-input
With-Input plugin docs for Pothos
Define a field and its input object together with `t.fieldWithInput`. The plugin creates the
input type and a required `input` argument. Use a core `builder.inputType` instead when several
fields should share the same input type.
## Usage [#usage]
### Install [#install]
npm
pnpm
yarn
bun
```bash
npm install --save @pothos/plugin-with-input
```
```bash
pnpm add @pothos/plugin-with-input
```
```bash
yarn add @pothos/plugin-with-input
```
```bash
bun add @pothos/plugin-with-input
```
### Setup [#setup]
```typescript
import SchemaBuilder from '@pothos/core';
import WithInputPlugin from '@pothos/plugin-with-input';
const builder = new SchemaBuilder({
plugins: [WithInputPlugin],
// optional
withInput: {
typeOptions: {
// default options for Input object types created by this plugin
},
argOptions: {
// set required: false to override default behavior
},
},
});
```
### Defining fields with inputs [#defining-fields-with-inputs]
```typescript
builder.queryType({
fields: (t) => ({
example: t.fieldWithInput({
input: {
// Note that this uses a new t.input field builder for defining input fields
id: t.input.id({ required: true }),
},
type: 'ID',
resolve: (root, args) => args.input.id,
}),
}),
});
```
This will produce a schema like:
```graphql
type Query {
example(input: QueryExampleInput!): ID
}
input QueryExampleInput {
id: ID!
}
```
The input name will default to `${ParentType.name}${Field.name}Input`.
### Customizing your input object [#customizing-your-input-object]
Replace the previous query definition with this version to customize both names:
```typescript
builder.queryType({
fields: (t) => ({
example: t.fieldWithInput({
typeOptions: {
name: 'CustomInputTypeName',
// Additional options for the input type can be added here
},
argOptions: {
name: 'customArgName',
// Additional options for the input argument can be added here
},
input: {
id: t.input.id({ required: true }),
},
type: 'ID',
// inputs are now under `customArgName`
resolve: (root, args) => args.customArgName.id,
}),
}),
});
```
### Changing the nullability of the input arg [#changing-the-nullability-of-the-input-arg]
You can configure the global default for input args when creating the builder by providing
`WithInputArgRequired` in the builder's `SchemaTypes`, and setting `withInput.argOptions.required`.
```typescript
const builder = new SchemaBuilder<{ WithInputArgRequired: false }>({
plugins: [WithInputPlugin],
withInput: {
argOptions: {
required: false,
},
},
});
```
Alternatively, keep the original builder and set `argOptions.required` on an individual field.
```typescript
builder.queryType({
fields: (t) => ({
example: t.fieldWithInput({
type: 'Boolean',
argOptions: {
required: false,
},
input: {
someInput: t.input.boolean({}),
},
resolve: (root, args) => {
return args.input?.someInput;
},
}),
}),
});
```
### Input arguments and resolver values [#input-arguments-and-resolver-values]
These field definitions belong in a query type's `fields` callback. The default argument is named
`input`, while `argOptions.name` changes both the GraphQL argument name and the resolver's argument
property. `typeOptions.name` changes the generated input object's name.
An optional input may be omitted or explicitly `null`. The optional field uses a nullish fallback
for both cases; required inputs are rejected by GraphQL validation when omitted.
```typescript playground example="plugin-with-input"
echo: t.fieldWithInput({
type: 'ID',
input: { id: t.input.id({ required: true }) },
resolve: (_, { input }) => input.id,
}),
lookup: t.fieldWithInput({
type: 'ID',
typeOptions: { name: 'LookupInput' },
argOptions: { name: 'criteria' },
input: { id: t.input.id({ required: true }) },
resolve: (_, { criteria }) => criteria.id,
}),
optional: t.fieldWithInput({
type: 'String',
argOptions: { required: false },
input: { name: t.input.string() },
resolve: (_, { input }) => input?.name ?? 'Anonymous',
}),
```
### Prisma plugin integration [#prisma-plugin-integration]
If you are using the prisma plugin you can use `t.prismaFieldWithInput` to add prisma fields with
input objects. This example assumes a builder configured for the Prisma and With-Input plugins,
a `User` Prisma object, and a Prisma client named `prisma`:
```typescript
builder.queryField('user', (t) =>
t.prismaFieldWithInput({
type: 'User',
input: {
id: t.input.id({ required: true }),
},
resolve: (query, _, args) =>
prisma.user.findUnique({
where: {
id: Number.parseInt(args.input.id, 10),
},
...query,
}),
}),
);
```
### Customizing the default naming conventions [#customizing-the-default-naming-conventions]
If you want to customize how the default input type names are generated you can provide a name
callback in `withInput.typeOptions`:
```typescript
import WithInputPlugin from '@pothos/plugin-with-input';
const builder = new SchemaBuilder({
plugins: [WithInputPlugin],
withInput: {
typeOptions: {
name: ({ parentTypeName, fieldName }) => {
const capitalizedFieldName = `${fieldName[0].toUpperCase()}${fieldName.slice(1)}`;
// This will remove the default Query/Mutation prefix from the input type name
if (parentTypeName === 'Query' || parentTypeName === 'Mutation') {
return `${capitalizedFieldName}Input`;
}
return `${parentTypeName}${capitalizedFieldName}Input`;
},
},
},
});
```
# Zod validation plugin
URL: /docs/plugins/zod
Validate field arguments and input fields with a validate option that maps onto zod constraints.
The [validation plugin](./validation) is now the recommended way to validate a
schema. It supports zod alongside several other validation libraries. Use this page to maintain schemas that already use the zod plugin.
The zod plugin validates field arguments and input fields with [zod](https://github.com/colinhacks/zod). You attach a `validate` option wherever you accept input (a single argument, a whole field's args, an input object, or one of its fields) and the plugin builds a zod validator that runs before your resolver. It does not re-export zod; instead `validate` takes a small options object whose keys map onto the zod methods you already know (`min`, `max`, `email`, `regex`, and so on), or an actual zod schema when you want the full API.
## Install [#install]
npm
pnpm
yarn
bun
```bash
npm install --save zod @pothos/plugin-zod
```
```bash
pnpm add zod @pothos/plugin-zod
```
```bash
yarn add zod @pothos/plugin-zod
```
```bash
bun add zod @pothos/plugin-zod
```
## Setup [#setup]
Add the plugin, then optionally hand it a `validationError` callback to shape what clients see when validation fails.
```typescript
import SchemaBuilder from '@pothos/core';
import ZodPlugin from '@pothos/plugin-zod';
const builder = new SchemaBuilder({
plugins: [ZodPlugin],
zod: {
// Runs when validation fails. Return a string or Error, or throw your own.
// The default is to throw the raw ZodError.
validationError: (zodError, _args, _context, _info) => zodError.message,
},
});
```
`validationError` receives the `ZodError`, the field's `args`, the `context`, and the GraphQL `info`. Return a `string` (thrown as a `PothosValidationError`), return an `Error` instance (thrown as-is), or throw directly. Skip it to surface the raw zod error.
Examples below are separate additions to this setup; query and mutation examples are alternatives.
## Validating a single argument [#validating-a-single-argument]
Add `validate` to any argument. The keys you pass are constraints for that argument's type; here an email string capped at 254 characters.
```typescript
builder.queryType({
fields: (t) => ({
playerByEmail: t.boolean({
args: {
email: t.arg.string({
required: true,
validate: {
email: true,
maxLength: 254,
},
}),
},
resolve: () => true,
}),
}),
});
```
## Validating all arguments together [#validating-all-arguments-together]
Cross-field rules ("at least one of these," "start before end") belong on the field's own `validate`, which receives the whole args object. It can be a function, or a `[function, options]` pair when you want a message.
```typescript
builder.mutationType({
fields: (t) => ({
inviteToTeam: t.boolean({
args: {
email: t.arg.string({ validate: { email: true } }),
phone: t.arg.string(),
},
// Require at least one contact method across the two args.
validate: [
(args) => !!args.email || !!args.phone,
{ message: 'Provide either an email address or a phone number' },
],
resolve: () => true,
}),
}),
});
```
## Custom messages [#custom-messages]
Every constraint accepts either a bare value or a `[value, { message }]` pair. The pair form is a `Constraint`; the options object is passed straight to the underlying zod method, so anything zod's method accepts (a `message`, and a `path` for object refinements) works.
```typescript
// Inside a field builder callback:
t.arg.int({
validate: {
min: [0, { message: 'jersey number cannot be negative' }],
max: [99, { message: 'jersey number must be under 100' }],
int: true,
},
});
```
## Lists [#lists]
List arguments validate the list and its items in one options object. Constraints like `minLength` / `maxLength` / `length` apply to the array; `items` carries the constraints for each element.
The following resolver replaces an in-memory roster only after both the list length and each
email address pass validation. A failed constraint leaves the previous roster unchanged.
```typescript
let emails: string[] = [];
```
```typescript playground example="plugin-zod"
builder.mutationType({
fields: (t) => ({
setRoster: t.stringList({
args: {
emails: t.arg.stringList({
required: true,
validate: {
maxLength: [2, { message: 'Roster is too large' }],
items: { email: [true, { message: 'Enter a valid email' }] },
},
}),
},
resolve: (_, args) => {
emails = args.emails;
return emails;
},
}),
}),
});
```
`items` also accepts a refinement function or a `[function, { message }]` pair. For example,
inside a field's arguments:
```typescript
t.arg.stringList({
validate: {
items: [(value) => value.trim().length > 0, { message: 'Items must not be blank' }],
},
});
```
## Input objects [#input-objects]
`validate` works the same on an input type and on its fields. Put per-field rules on each field, and cross-field rules on the input type itself, where the callback receives the whole object.
```typescript
const RegisterTeamInput = builder.inputType('RegisterTeamInput', {
fields: (t) => ({
name: t.string({ required: true, validate: { minLength: 3, maxLength: 40 } }),
contactEmail: t.string({ required: true, validate: { email: true } }),
backupEmail: t.string({ required: false, validate: { email: true } }),
}),
// Runs against the assembled input object.
validate: [
(input) => input.contactEmail !== input.backupEmail,
{ message: 'backup email must differ from contact email' },
],
});
```
## Bring your own zod schema [#bring-your-own-zod-schema]
Pass an existing zod schema with the `schema` key to reuse its rules on an argument:
```typescript
import { z } from 'zod';
t.arg.int({
validate: {
schema: z.number().int().max(5),
},
});
```
...or on the whole field, validating every argument at once:
```typescript
builder.queryType({
fields: (t) => ({
validateCredentials: t.boolean({
args: {
email: t.arg.string({ required: true }),
password: t.arg.string({ required: true }),
},
validate: {
schema: z.object({
email: z.string().email(),
password: z.string().min(8),
}),
},
resolve: () => true,
}),
}),
});
```
You can combine `schema` with the constraint keys. The plugin pipes your schema into the generated validator, so both run. Validation uses `parseAsync` before the resolver, so refinements may return a `Promise`. The parsed value is passed to the resolver. This plugin does not infer transformed argument types; use the [validation plugin](./validation) when transforms change the resolver argument shape.
## Constraint reference [#constraint-reference]
`validate` accepts a bare refinement function, an array of them, or an options object. The options object always allows these keys:
| Key | Type | Purpose |
| -------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `type` | `'number' \| 'bigint' \| 'boolean' \| 'date' \| 'string' \| 'object' \| 'array'` | Pin the base zod type. See [How it works](#how-it-works) for why this matters. |
| `refine` | function or `[function, { message?, path? }]`, or an array of either | A predicate handed to zod's `refine`. Receives the validated value; returns `boolean` or `Promise`. |
| `schema` | `ZodType` | A zod schema piped into the generated validator. |
The remaining keys depend on the field's type. Each value is a `Constraint`, either a bare value or `[value, { message }]`:
| Type | Additional keys |
| ------- | -------------------------------------------------------------------------------------------------------- |
| Number | `min`, `max`, `int`, `positive`, `nonnegative`, `negative`, `nonpositive` |
| String | `minLength`, `maxLength`, `length`, `email`, `url`, `uuid`, `regex` |
| Array | `minLength`, `maxLength`, `length`, `items` (nested validation options or a refinement for each element) |
| BigInt | *(base keys only)* |
| Boolean | *(base keys only)* |
| Date | *(base keys only)* |
| Object | *(base keys only)* |
## How it works [#how-it-works]
Each argument and input field builds its own zod validator from its `validate` options. At runtime the plugin has no access to the JavaScript type behind a GraphQL type, so when you pass plain constraints it builds a union of every base type that could satisfy them. A lone `maxLength`, for example, fits both strings and arrays:
```typescript
z.union([z.string().max(5), z.array(z.unknown()).max(5)]);
```
An `email` constraint only fits strings, so the union collapses to one member. When the argument is not required, the whole validator is wrapped `.optional().nullable()` rather than folded into the union.
Set `type` to skip the guessing and pin one base type:
```typescript
// { validate: { type: 'string', maxLength: 5 } } builds:
z.string().max(5);
```
Three cases sidestep the union entirely:
* **Input object** args and fields always validate with `z.looseObject(...)`.
* **List** args and fields always validate with `.array()`.
* **Refinement-only** validators (a bare function, or an options object with just `refine` and/or `schema`) validate against `z.unknown()`, since no constraint narrows the type.
A `schema` is piped into whatever the plugin generates (`yourSchema.pipe(generated)`), so your schema and the constraints both run.
## Sharing schemas with client code [#sharing-schemas-with-client-code]
To reuse a validator on the client, write it as an ordinary zod schema in a shared module, then attach it with `schema`:
```typescript
// shared/validators.ts
import { z } from 'zod';
export const jerseyNumber = z.number().int().min(0).max(99);
```
```typescript
// server
import { jerseyNumber } from './shared/validators';
t.arg.int({
validate: { schema: jerseyNumber },
});
```
```typescript
// client
import { jerseyNumber } from './shared/validators';
jerseyNumber.parse(23); // pass
jerseyNumber.parse(100); // throws
```
If you would rather share the constraint options object, the plugin exports `createZodSchema` to turn one into a zod schema on demand. Type the options with the exported `ValidationOptions`:
```typescript
// shared/validators.ts
import type { ValidationOptions } from '@pothos/plugin-zod';
export const jerseyNumberOptions: ValidationOptions = {
min: 0,
max: 99,
int: true,
};
```
```typescript
// server
import { jerseyNumberOptions } from './shared/validators';
t.arg.int({ validate: jerseyNumberOptions });
```
```typescript
// client
import { createZodSchema } from '@pothos/plugin-zod';
import { jerseyNumberOptions } from './shared/validators';
const validator = createZodSchema(jerseyNumberOptions);
validator.parse(23); // pass
validator.parse(100); // throws
```
# Async selections
URL: /docs/plugins/drizzle/async-selections
Planning selections that depend on asynchronous request data
Pothos selections are synchronous by default. Set `AsyncSelections: true` when a selection or
relation query must await request data before returning its query options.
## Async selections [#async-selections]
Use the client and relations configured in [Setup](./setup). The context method below supplies
a preview size loaded asynchronously, for example from account settings:
```ts
const builder = new SchemaBuilder<{
DrizzleRelations: typeof relations;
AsyncSelections: true;
Context: {
previewSize: () => Promise;
};
}>({
plugins: [DrizzlePlugin],
drizzle: {
client: db,
getTableConfig,
relations,
},
});
```
With the opt-in, `select` functions, relation `query` callbacks, `relatedCount` `where` callbacks,
and the `select` and `query` callbacks of `drizzleConnectionHelpers` may be async. Without it they
are typed as synchronous, and an async callback is a type error.
Pothos waits for these callbacks before executing the planned query. `t.relation`, `t.relatedCount`,
`t.drizzleField`, `t.drizzleConnection` and `t.relatedConnection` settle their plan before the
resolver runs, and need no changes.
```ts
builder.drizzleObject('users', {
name: 'User',
fields: (t) => ({
previewPosts: t.field({
type: ['posts'],
select: async (args, ctx, nestedSelection) => {
return {
with: {
posts: await nestedSelection({
where: { published: true },
orderBy: { id: 'asc' },
limit: await ctx.previewSize(),
}),
},
};
},
resolve: (user) => user.posts,
}),
}),
});
```
## Await nested selections [#await-nested-selections]
`await` what `nestedSelection` returns before putting it in the selection, and the same for the
`nestedQuery` passed as the fourth argument of a `t.relatedField` `select`. A selection that
contains the promise itself will throw, and so will a `select` that returns while a nested
selection it started is still pending. Include the nested selection in the returned query to
load its data.
## Connection helpers [#connection-helpers]
Pass `awaitSelections: true` to `drizzleConnectionHelpers(...).getQuery`, and `await` the query it
returns. Without it, an async selection beneath the field throws, and whether there is one
depends on the incoming document rather than on the callback you wrote. A connection helper also
throws when its own `select` or `query` is async, whatever the document asked for:
```ts
select: async (args, ctx, nestedSelection) => {
return {
with: {
attachments: await attachments.getQuery(args, ctx, nestedSelection, {
awaitSelections: true,
}),
},
};
},
```
`awaitSelections` is a per-call option, and is available whether or not the schema sets
`AsyncSelections`.
When `AsyncSelections: true`, also `await` a connection helper's `resolve()` before inspecting or
spreading its result: an async `query` callback makes resolution asynchronous. Returning its
result directly from a GraphQL resolver remains supported.
The connection-helper excerpt uses the Post attachments relation and `attachments` helper from
[Connection helpers](./connection-helpers). Request-context
methods must be supplied by the application; they are not created by declaring the Context type.
# Connection helpers
URL: /docs/plugins/drizzle/connection-helpers
Paginate attachment rows while returning Media nodes and custom edge fields
An image can appear in several posts, with a different caption in each. The Media row describes
the image; the PostMedia row describes its attachment to a post. A connection can expose the
image as its node and the attachment's caption as an edge field:
```graphql
query PostAttachments {
author(id: 1) {
postsConnection(first: 1) {
nodes {
title
attachments(first: 2) {
edges {
caption
node {
url
uploadedBy {
fullName
}
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
}
}
```
`t.relatedConnection` handles a relation whose rows are the connection's nodes. Use
`drizzleConnectionHelpers` with `t.connection` when the rows being paginated differ from those
nodes, or when you need to load the rows yourself. The helper supplies the selection, cursor,
and result handling that a normal Relay connection field does not have.
## Select the nodes through the attachment [#select-the-nodes-through-the-attachment]
Import `drizzleConnectionHelpers` from `@pothos/plugin-drizzle` and use the builder configured
with the Drizzle and Relay plugins. The helper targets `postMedia`. Its `select` uses
`nestedSelection()` to request the Media fields
selected beneath the connection's nodes; `resolveNode` returns that related Media row:
```typescript playground example="plugin-drizzle" query="{ author(id: 1) { postsConnection(first: 1) { nodes { title attachments(first: 2) { edges { caption node { url uploadedBy { fullName } } } pageInfo { endCursor hasNextPage } } } } } }"
const attachmentArgs = builder.args((t) => ({
hasCaption: t.boolean({ defaultValue: false }),
}));
const attachments = drizzleConnectionHelpers(builder, 'postMedia', {
args: () => attachmentArgs,
query: (args) => ({
orderBy: { id: 'asc' },
where: args.hasCaption ? { caption: { isNotNull: true } } : {},
}),
select: (nestedSelection) => {
return {
columns: { caption: true },
with: { media: nestedSelection() },
};
},
resolveNode: (attachment) => attachment.media,
});
```
The connection orders and creates cursors from attachment IDs, not Media IDs. An image shared
by two posts can therefore have a different caption and connection position in each post.
The nested uploader selection is still planned from the requested Media fields.
The helper's `args` and `query` options define a `hasCaption` argument. When it is true, the
connection includes only attachments with a caption. Its default, false, includes all attachments.
`getArgs()` exposes that argument on the field, and `getQuery()` combines the filter and ordering
with the requested page and node selection.
## Add the connection and its edge fields [#add-the-connection-and-its-edge-fields]
`Media` below is the object ref returned by `builder.drizzleObject('media', ...)`.
A normal `t.connection` needs an explicit `select` to load the helper's query on the parent.
Its resolver passes the selected attachment rows to the helper's `resolve` method:
```typescript playground example="plugin-drizzle" query="{ author(id: 1) { postsConnection(first: 1) { nodes { title attachments(first: 2, hasCaption: true) { edges { caption node { url } } pageInfo { endCursor hasNextPage } } } } } }"
builder.drizzleObjectField('posts', 'attachments', (t) =>
t.connection(
{
type: Media,
args: attachments.getArgs(),
select: (args, ctx, nestedSelection) => {
return {
with: {
attachments: attachments.getQuery(args, ctx, nestedSelection),
},
};
},
resolve: (post, args, ctx) => {
return attachments.resolve(post.attachments, args, ctx);
},
},
{},
{
fields: (edge) => ({
caption: edge.string({
nullable: true,
resolve: (attachment) => attachment.caption,
}),
}),
},
),
);
```
The third argument to `t.connection` configures the Edge type. Each edge retains the selected
attachment fields, so its `caption` resolver reads the attachment's caption, while `node` returns
the Media selected by `resolveNode`. A missing caption returns null.
The post has three attachments. Its first unfiltered page of two has `hasNextPage: true`;
filtering to captioned attachments returns two and has no next page. Pagination uses the same
`first`/`after` and `last`/`before` arguments as [other connections](./connections#page-through-published-posts).
## Querying the rows in a resolver [#querying-the-rows-in-a-resolver]
The same helper can build a query for a resolver that fetches the attachment rows itself.
Pass GraphQL resolve `info` to `getQuery`, merge any additional filter with its cursor filter,
and pass the result to `resolve`:
```ts
// Alternative resolver for the Post.attachments connection above:
resolve: async (post, args, ctx, info) => {
const query = attachments.getQuery(args, ctx, info);
const attachmentRows = await db.query.postMedia.findMany({
...query,
where: {
AND: [query.where ?? {}, { postId: post.id }],
},
});
return attachments.resolve(attachmentRows, args, ctx);
},
```
This alternative needs `db` imported from the application's database module and `id` selected
on Post. Remove the field's relation `select` when using it, so the rows are not also loaded
through the parent query. Replacing the helper's `where` rather than combining it would discard
pagination constraints.
If the rows and nodes are the same type, omit `resolveNode` and the node-mapping `select` when
creating the helper. Its `ref` provides the node type for `t.connection`. Prefer
`t.relatedConnection` when you can expose that relation directly without custom loading.
# Connections
URL: /docs/plugins/drizzle/connections
Paginate Drizzle queries and relations using GraphQL selections
Connection fields combine cursor pagination with Pothos's selection planning. Define the rows a
field may return; Pothos adds pagination constraints and selects the columns and relations
requested under each node. The same field supports forward and backward pagination.
Use `t.drizzleConnection` when your resolver queries the rows and `t.relatedConnection` for a
Drizzle relation. Both require the [Relay plugin](./setup#integration-with-other-plugins).
## Page through published posts [#page-through-published-posts]
The feed returns published posts ordered by creation time. Its resolver passes the result of
`query()` to Drizzle, just as a `t.drizzleField` does:
```typescript playground example="plugin-drizzle" query="{ posts(first: 2) { totalCount edges { cursor node { title author { fullName } } } pageInfo { endCursor hasNextPage } } }"
posts: t.drizzleConnection({
type: 'posts',
totalCount: () => {
return db.$count(posts, eq(posts.published, true));
},
resolve: (query) => {
return db.query.posts.findMany(
query({
where: { published: true },
// Three posts share this timestamp. Pothos adds the primary key
// to the cursor ordering, so traversing pages still visits each once.
orderBy: { createdAt: 'desc' },
}),
);
},
}),
```
Pothos uses the selected node fields to plan the database query, including the nested author.
It also adds the ordering columns needed for cursors, even when the client does not select them.
When creation times tie, Pothos appends a primary-key tie breaker so posts have distinct positions.
See [Ordering and cursors](./ordering-and-cursors) for other orderings and column requirements.
A client requests the first page with `first`:
```graphql
query PostFeed($after: String) {
posts(first: 2, after: $after) {
totalCount
edges {
cursor
node {
title
author {
fullName
}
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
```
For the next page, pass the previous `endCursor` as `after`. To paginate backward, use `last`
and `before`, taking `before` from a page's `startCursor`:
```graphql
query PreviousPosts($before: String) {
posts(last: 2, before: $before) {
nodes {
title
}
pageInfo {
startCursor
hasPreviousPage
}
}
}
```
Backward pagination retains the connection's declared ordering; it selects the preceding rows
rather than reversing the response. `nodes` is a convenience field enabled by
`relay: { nodesOnConnection: true }`. With the default Relay configuration, use `edges { node }`.
## Related connections [#related-connections]
An author's `postsConnection` applies the same publication filter as the feed. `t.relatedConnection`
plans that relation from the parent selection without a custom resolver:
```typescript playground example="plugin-drizzle" query="{ author(id: 1) { postsConnection(first: 1) { totalCount nodes { title } pageInfo { hasNextPage endCursor } } } }"
postsConnection: t.relatedConnection('posts', {
query: { where: { published: true }, orderBy: { createdAt: 'desc' } },
totalCount: true,
}),
```
The `query` option accepts an object or a callback receiving field arguments and context, as it
does on `t.relation`. Connection `orderBy` accepts column names with `'asc'` or `'desc'`, or a
column or array of columns for ascending order. Pothos can invert these orderings when fetching
a backward page. The default ordering uses the table's primary key.
Both connection methods create Connection and Edge types. Pass additional options to customize
those types, as with the [Relay plugin's connection fields](../relay).
## Connection totalCount [#connection-totalcount]
`totalCount` describes the matching rows across all pages. The author's `totalCount: true`
counts both published posts even when `first: 1` returns only one. Pothos loads the count only
when it is selected; a query for only `totalCount` does not load the related posts.
For `t.relatedConnection`, Pothos derives the count from the relation and its filter. For a root
`t.drizzleConnection`, supply a `totalCount` resolver, as the feed does above. Its filter must
match the rows returned by the connection. This resolver receives the usual
`parent`, `args`, `context`, and `info` arguments. A count-only root query skips the main resolver.
By default, a related count includes the `where` returned by the connection's `query`. Setting
`filterConnectionTotalCount: false` in the builder's `drizzle` options ignores that field filter
for counts. A filter on the Drizzle relation itself still applies. Keep the default for the
public author field: counting drafts would reveal data excluded by its publication filter.
For a many-to-many relation defined with `through`, the count matches the rows being paginated.
If two junction rows reach the same target, that target appears and is counted twice. This differs
from [`t.relatedCount`](./relations#related-count), which counts distinct related targets.
## Connections with custom edges [#connections-with-custom-edges]
A direct many-to-many relation works with `t.relatedConnection`. When the attachment itself has
fields, such as a caption, the edge needs data from the junction row while its node represents
Media. [Connection helpers](./connection-helpers) shows how to combine that mapping with selection
planning, pagination, and custom edge fields in one connection.
# Drizzle plugin
URL: /docs/plugins/drizzle
A plugin to support efficient queries through drizzles relational query builder API
This plugin uses drizzle's [relational query builder v2](https://orm.drizzle.team/docs/rqb-v2),
which ships in `drizzle-orm` 1.0. That release is still a release candidate, and npm's `latest`
tag is the 0.x line, so install drizzle with the `rc` tag. Its API can still change before 1.0 is
final.
If you are upgrading from an older version of this plugin, read drizzle's [relations v1 to v2
guide](https://orm.drizzle.team/docs/relations-v1-v2), then this package's changelog for the
Pothos specific changes.
The Drizzle plugin maps database tables to GraphQL types and plans database queries from the
fields a client selects. It supports relations, computed fields, and Relay nodes and connections.
The examples share a small author-and-post schema. The
[publishing playground](/playground?example=plugin-drizzle) includes its database definitions
and initialization, and executes queries against SQLite in the browser.
## Getting started [#getting-started]
[Setup](/docs/plugins/drizzle/setup) connects your Drizzle client to Pothos.
[Objects](/docs/plugins/drizzle/objects) covers type definitions and fields such as this lookup:
```typescript playground example="plugin-drizzle"
builder.queryType({
fields: (t) => ({
author: t.drizzleField({
type: 'users',
nullable: true,
args: { id: t.arg.int({ required: true }) },
resolve: (query, _root, args) => {
return db.query.users.findFirst(
query({
where: { id: args.id },
}),
);
},
}),
}),
});
```
```graphql
query AuthorPage {
author(id: 1) {
fullName
posts {
title
}
}
}
```
The resolver passes Pothos's selection plan to Drizzle, loading the requested posts with their
author. Adding GraphQL fields does not require rewriting this root resolver.
[Relations](/docs/plugins/drizzle/relations) covers filtering and counts; [Selections](/docs/plugins/drizzle/selections) explains computed
fields and profiles; [Connections](/docs/plugins/drizzle/connections) handles pagination. [Variants](/docs/plugins/drizzle/variants) and
[Interfaces](/docs/plugins/drizzle/interfaces) keep private account data separate from public authors. For more
specialized schemas, [Connection helpers](/docs/plugins/drizzle/connection-helpers) and [Async selections](/docs/plugins/drizzle/async-selections)
cover custom pagination and asynchronous planning.
# Interfaces
URL: /docs/plugins/drizzle/interfaces
Creating interfaces for drizzle tables that can be shared by variants
## Interfaces [#interfaces]
`builder.drizzleInterface` works like `builder.drizzleObject`. It can define the primary type
or a variant of a table. Here Viewer is a variant, leaving User as the public primary type.
## Define the interface [#define-the-interface]
```typescript playground example="plugin-drizzle" query="{ me { __typename email drafts { title } } }"
export const Viewer = builder.drizzleInterface('users', {
variant: 'Viewer',
select: { columns: { id: true, role: true } },
resolveType: (user) => (user.role === 'editor' ? 'EditorViewer' : 'AuthorViewer'),
fields: (t) => ({
user: t.variant('users'),
email: t.exposeString('email'),
drafts: t.relation('posts', {
query: { where: { published: false }, orderBy: { id: 'asc' } },
}),
}),
});
```
`resolveType` uses the selected `role` discriminator and returns GraphQL type names. Returning
names avoids a circular reference between the interface and its implementations.
## Selecting a viewer implementation [#selecting-a-viewer-implementation]
```typescript playground example="plugin-drizzle" query="{ me { __typename drafts { title } ... on EditorViewer { canReviewSubmissions } } }"
builder.drizzleObject('users', {
variant: 'EditorViewer',
interfaces: [Viewer],
select: { columns: { id: true, role: true } },
fields: (t) => ({ canReviewSubmissions: t.boolean({ resolve: () => true }) }),
});
builder.drizzleObject('users', {
variant: 'AuthorViewer',
interfaces: [Viewer],
select: { columns: { id: true, role: true } },
});
```
The interface's `select` is planned whenever a field returns the interface. An implementation's
own selection is planned when a fragment narrows to it. Selections are not inherited; put data
required by an implementation in its selection as well. An implementation may select additional
columns or expressions for fields that do not belong on the interface.
Maya (`userId: 1`) resolves to EditorViewer with `canReviewSubmissions: true`. Leo (`userId: 2`)
resolves to AuthorViewer, so the editor fragment contributes no field. The interface describes
those result shapes; the authenticated root lookup enforces whose drafts are returned.
## Extending an interface [#extending-an-interface]
Fields can be added to an interface later with `builder.drizzleInterfaceField` and
`builder.drizzleInterfaceFields`, which take the interface ref (or the table name) like their
`drizzleObjectField(s)` counterparts:
```ts
builder.drizzleInterfaceFields(Viewer, (t) => ({
publishedPosts: t.relatedConnection('posts', {
query: { where: { published: true } },
}),
}));
```
An object type implementing a drizzle interface must be based on the same table. A plain object
type that implements one is planned with the interface's table, so fragments on it will select the
relations it inherits.
# Drizzle Objects
URL: /docs/plugins/drizzle/objects
Defining GraphQL object types based on drizzle tables
Use the [configured builder](./setup) to map a Drizzle table to a GraphQL type. Defining a type
registers its fields; a root lookup makes them queryable.
## Defining Objects [#defining-objects]
The `builder.drizzleObject` method can be used to define GraphQL Object types based on a drizzle
table:
```ts
const User = builder.drizzleObject('users', {
name: 'User',
fields: (t) => ({
firstName: t.exposeString('firstName'),
lastName: t.exposeString('lastName'),
}),
});
```
You will be able to "expose" any column in the table, and GraphQL fields do not need to match the
names of the columns in your database. The returned `User` can be used like any other `ObjectRef`
in Pothos.
The example uses `builder.drizzleNode`, which adds a Relay ID and node refetching to the
same object API. See [Relay](./relay) for node IDs and refetching.
## Custom fields [#custom-fields]
A GraphQL field can combine several columns. The author’s full name declares both columns in
its selection and computes the string from the loaded row:
```typescript playground example="plugin-drizzle" query="{ author(id: 1) { fullName } }"
fullName: t.string({
select: { columns: { firstName: true, lastName: true } },
resolve: (user) => `${user.firstName} ${user.lastName}`,
}),
```
This field belongs in User's `fields` callback. With the default type selection, all scalar
columns are available; with `select: {}`, computed fields must declare what they read.
[Selections](./selections) explains when those columns and relations are loaded.
## Drizzle Fields [#drizzle-fields]
`t.drizzleField` can return a Drizzle object from Query or from any other object type. Its
resolver receives a `query` function before the usual resolver arguments. Call that function and
pass its result to `findFirst` or `findMany`: it merges your filters and ordering with the
requirements of the GraphQL selection.
### A nullable author lookup [#a-nullable-author-lookup]
The author page accepts an integer ID. A missing author returns null, matching the result of
`findFirst` and the field's `nullable: true` option:
```typescript playground example="plugin-drizzle"
builder.queryType({
fields: (t) => ({
author: t.drizzleField({
type: 'users',
nullable: true,
args: { id: t.arg.int({ required: true }) },
resolve: (query, _root, args) => {
return db.query.users.findFirst(
query({
where: { id: args.id },
}),
);
},
}),
}),
});
```
`author(id: 1) { fullName }` returns Maya Chen. `author(id: 999) { fullName }` returns null.
Adding `posts { title }` loads the public posts through the same root resolver.
For a list field, use `type: ['users']` and `db.query.users.findMany(query())`. A plain lookup
without arguments can pass no options to `query`; fields with filters pass those options to it.
### `drizzleFieldWithInput` [#drizzlefieldwithinput]
With the [with-input plugin](https://pothos-graphql.dev/docs/plugins/with-input),
`t.drizzleFieldWithInput` combines `t.drizzleField` with `t.fieldWithInput`. The `input` fields
become an input object argument, and the resolver still receives the `query` function as its first
argument. This alternative adds an `authorWithInput` lookup to the same public User type.
Install and register WithInputPlugin as described in [Setup](./setup#integration-with-other-plugins):
```ts
builder.queryFields((t) => ({
authorWithInput: t.drizzleFieldWithInput({
type: 'users',
nullable: true,
input: {
id: t.input.int({ required: true }),
},
resolve: (query, root, args, ctx) => {
return db.query.users.findFirst(
query({
where: { id: args.input.id },
}),
);
},
}),
}));
```
## Building a query from resolve info [#building-a-query-from-resolve-info]
Use `t.drizzleQueryFromInfo` inside a regular field resolver to combine its GraphQL selection
with Drizzle query options. Pass a table name or Drizzle object/interface ref, along with the
resolver's `context` and `info`. Pass the result directly to Drizzle:
```ts
builder.queryFields((t) => ({
user: t.field({
type: User,
nullable: true,
args: { id: t.arg.int({ required: true }) },
resolve: (_root, args, context, info) =>
db.query.users.findFirst(
t.drizzleQueryFromInfo('users', {
context,
info,
columns: { id: true },
where: { id: args.id },
}),
),
}),
}));
```
Pass `path: ['user']` when the resolver returns a payload containing a `user` field. `paths`
accepts multiple paths. Use a Drizzle ref instead of a table name to select a specific variant
of that table's GraphQL type.
The field builder checks the context and query options against its schema and table. Explicit
columns and relations retain their types; omitted columns are not statically guaranteed, even
if the GraphQL selection loads them. Put filters, selections, ordering, and limits inside the
helper call so they are merged with the planned selection.
With `AsyncSelections: true`, await the result before passing it to Drizzle:
```ts
resolve: async (_root, args, context, info) =>
db.query.users.findFirst(
await t.drizzleQueryFromInfo('users', {
context,
info,
where: { id: args.id },
}),
),
```
Without `AsyncSelections`, the helper is typed as synchronous, consistent with the schema's
selection callbacks. There is no per-call async option.
# Ordering and cursors
URL: /docs/plugins/drizzle/ordering-and-cursors
How a connection orders rows, and the cursors that ordering produces
Connections page with a cursor that records where in the ordering the previous page ended. This
applies to `t.relatedConnection`, `t.drizzleConnection`, and `drizzleConnectionHelpers`.
## Tied timestamps in a feed [#tied-timestamps-in-a-feed]
The [post connection](./connections#page-through-published-posts) orders by `createdAt`, which
can be identical for several posts. Pothos adds a primary-key tie breaker so each post has a
distinct position in the connection.
```typescript playground example="plugin-drizzle" query="{ posts(first: 2) { nodes { title } pageInfo { endCursor hasNextPage } } }"
posts: t.drizzleConnection({
type: 'posts',
totalCount: () => {
return db.$count(posts, eq(posts.published, true));
},
resolve: (query) => {
return db.query.posts.findMany(
query({
where: { published: true },
// Three posts share this timestamp. Pothos adds the primary key
// to the cursor ordering, so traversing pages still visits each once.
orderBy: { createdAt: 'desc' },
}),
);
},
}),
```
### Unique orderings [#unique-orderings]
`orderBy: { createdAt: 'desc' }` does not describe a unique ordering. Rows with the same timestamp
can be returned in a different order on each query, so paging through the connection may return a row
twice, or skip it.
Pothos adds the primary key to the ordering when the columns you provide are not already unique, so
this:
```ts
orderBy: { createdAt: 'desc' }
```
is queried as `createdAt desc, id desc`. You can add the tie breaker yourself if you want it in a
different position or direction:
```ts
orderBy: { createdAt: 'desc', id: 'asc' }
```
Orderings that already contain the primary key, or a unique column or constraint whose columns are
all marked `notNull()`, are used as provided. Nullable unique columns are not treated as unique,
because rows containing a `null` are still tied with each other. Uniqueness declared with
`uniqueIndex()` is not detected, so those orderings still get the primary key appended.
Pothos also recognizes composite primary keys declared with `primaryKey({ columns: [...] })`.
Primary-key columns are treated as non-null for ordering; ensure your database enforces that
constraint.
Nothing is appended when Pothos cannot find a key it can rely on, which means tables with no primary
key, and tables whose only unique column is nullable. Those connections keep the ordering you wrote,
so give them a tie breaker yourself.
### Nullable ordering columns [#nullable-ordering-columns]
A row whose ordering value is `null` cannot be paged past. `null` is not greater or less than
anything in SQL, so no row compares as coming after it and the next page comes back empty. The same
applies to an ordering expression that can evaluate to `null`.
Order by columns that are `notNull()`, or give the expression a real value to fall back to with
`coalesce`.
### Cursor values and precision [#cursor-values-and-precision]
A cursor stores the values your driver returned to JavaScript, and those values are compared against
the column when the next page is requested. If the value in the cursor is not the value the database
has, the comparison will not match the rows you expect.
This comes up with timestamps. `timestamp({ mode: 'date' })` returns a JavaScript `Date`, which only
holds milliseconds, while a Postgres `timestamptz` column stores microseconds. A row stored at
`.086068` produces a cursor containing `.086`, and `created_at < '.086'` does not match the other
rows in that millisecond, so they are never returned on any page.
There are two ways to avoid this.
The first is to map the column so the full value reaches JavaScript. `mode: 'string'` returns the
timestamp as text, with the microseconds intact:
```ts
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).notNull(),
});
```
The cursor stores that text, and Postgres parses it back to the value it stored. The column is still
what gets ordered and compared, so an index on it is still used. The field is typed as a `string`
rather than a `Date`.
The second is to order by an expression that returns the full value, which lets you keep the `Date`
mapping:
```ts
builder.queryFields((t) => ({
posts: t.drizzleConnection({
type: 'posts',
resolve: (query) => {
return db.query.posts.findMany(
query({
extras: {
createdAtExact: (table) =>
sql`to_char(${table.createdAt} AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')`,
},
orderBy: { createdAtExact: 'desc' },
}),
);
},
}),
}));
```
### Ordering by an expression [#ordering-by-an-expression]
`orderBy` accepts the name of any extra declared in the same query. Pothos orders by the expression,
builds the cursor from the value it returns, and compares the expression when paging:
```ts
query({
extras: { titleLength: (table) => sql`length(${table.title})` },
orderBy: { titleLength: 'asc' },
})
```
Extras used this way should be written as a callback. Drizzle aliases the table it queries and passes
that alias to the callback, so an expression built from the imported table object will reference a
name the query does not have.
With `drizzleConnectionHelpers`, declare the extra in `query` rather than in `select`. Only `query`
is read when the ordering is resolved.
The expression also needs to sort in the same order its values compare. A timestamp formatted as
fixed width UTC text sorts the same way it does chronologically, but a local time or variable width
format does not, and will skip rows.
# Query planning
URL: /docs/plugins/drizzle/query-planning
How the plugin turns a GraphQL query into drizzle queries
This page describes how the plugin turns a GraphQL query into drizzle queries, which is worth
knowing when a schema issues more queries than you expect.
## How fields get their data [#how-fields-get-their-data]
A field either reads its data from a row that has already been loaded, or runs a query of its own.
A field's `select` is planned into the query of the nearest ancestor that runs one: a
`t.drizzleField`, a `t.relation`, a connection, or a [fallback query](./relations#fallback-queries). The field
then reads what it needs off the loaded row, without a query of its own. A field runs its own query
when its `resolve` queries drizzle directly, and when the plugin issues a fallback query for a
relation that is missing from the row.
A field can do both. A `t.drizzleField`, or any other field with a `select`, nested under one of
those ancestors has its `select` planned into the parent's row, and still runs its own query when
it resolves.
A field-level `select` is merged into the same query as its siblings and the type-level selection,
rather than being kept separate for that field. Two selections of the same relation share a place
in that query only when their arguments (`where`, `orderBy`, `limit`, ...) match. When they differ,
the first one planned wins, and the other is loaded with a query of its own. A type-level `select`
is planned before any field's selection, no matter where they appear in the document, and fields
are planned in the order they are selected.
## Compare two orderings of one relation [#compare-two-orderings-of-one-relation]
A client may need both the newest and oldest published posts on one author page:
```graphql
query CompareOrderings {
author(id: 1) {
newest: posts {
title
}
oldest: posts(oldestFirst: true) {
title
}
}
}
```
The [published-posts field](./relations#published-posts-and-profiles) translates those arguments
into different database orderings. The two fields cannot reuse the same loaded
relation because their orderings differ. The additional ordering is loaded through a fallback
query.
In this example, selecting one ordering executes one SQL statement; selecting both executes
two. The number of GraphQL field resolvers does not determine the number of database queries.
```typescript playground example="plugin-drizzle" query="{ author(id: 1) { newest: posts { title } oldest: posts(oldestFirst: true) { title } } }"
posts: t.relation('posts', {
args: { oldestFirst: t.arg.boolean() },
query: (args) => ({
where: { published: true },
orderBy: {
createdAt: args.oldestFirst ? 'asc' : 'desc',
id: args.oldestFirst ? 'asc' : 'desc',
},
}),
}),
```
## Nested selections [#nested-selections]
A `select` function receives `nestedSelection`, which plans the selection beneath the field for the
field's own type. It returns the query config for that table, or a `many` config for a list field.
Any keys you pass in are kept as they were given, so `columns` passed to `nestedSelection` will
still narrow the parent shape:
```ts
builder.drizzleObject('users', {
name: 'User',
fields: (t) => ({
previewPosts: t.field({
type: ['posts'],
select: (args, ctx, nestedSelection) => {
return {
// what the query selects on the posts, limited to one
with: {
posts: nestedSelection({
where: { published: true },
orderBy: { id: 'asc' },
limit: 1,
}),
},
};
},
resolve: (user) => user.posts,
}),
}),
});
```
`nestedSelection(query, path, type)` takes two more arguments:
* `path`: a list of field names, for selecting a field nested under the field's type. A segment can
also be written as `{ name, type }` to pin the implementation the field must be found under.
* `type`: a member of an interface or union, to read the selection as that type.
The `nestedSelection` function also carries the `path` and `segments` of the field being planned,
which are described under [Relation queries](./relations#relation-queries).
## Async selections [#async-selections]
When a selection depends on asynchronous request data, enable `AsyncSelections: true` and await
nested plans. [Async selections](./async-selections) covers the builder configuration, supported
callbacks, helper options, and errors caused by unsettled promises.
## Conflicting selections between variants [#conflicting-selections-between-variants]
When a query selects two variants of one table for the same row, either with a fragment on each
under one field, or through a `t.variant` field, the request fails if their `select` options ask
for the same relation with different arguments. The client sees `Unexpected error.`, and the
error's `cause` explains the conflict:
```
PothosValidationError: Type-level selections of Viewer and Admin conflict on relation "posts".
Move the relation arguments to a field-level select on one of the types.
```
The same applies to two variants defining the same `extras` key with different functions
(`... conflict on extra "lowercaseName"`). Both variants describe one row, so their `select`
options are merged into a single query.
To fix this, move the relation with its arguments, or the extra, into the `select` of the field
that needs it on one of the variants. A field-level selection that conflicts with what the row
already holds falls back to a query for that field, rather than failing the request.
# Relations
URL: /docs/plugins/drizzle/relations
Adding relations to drizzle objects
`t.relation` turns a Drizzle relation into a GraphQL field with its target type and cardinality.
Register the target GraphQL type as well as the relation.
## Published posts and profiles [#published-posts-and-profiles]
The public author page returns published posts. Drafts belong on the [private Viewer](./variants),
so the filter belongs on the relation wherever User can be reached:
```typescript playground example="plugin-drizzle"
posts: t.relation('posts', {
args: { oldestFirst: t.arg.boolean() },
query: (args) => ({
where: { published: true },
orderBy: {
createdAt: args.oldestFirst ? 'asc' : 'desc',
id: args.oldestFirst ? 'asc' : 'desc',
},
}),
}),
```
The filter applies whenever this User type is queried. An author with no matching posts returns
an empty list. The `oldestFirst` argument selects the ordering.
To return the Profile object, register its GraphQL type and use `t.relation('profile')`. To
return only its nullable biography, use a [field selection](./selections#only-load-a-profile-when-requested).
## Relation queries [#relation-queries]
`query` accepts a static object or a callback that converts field arguments into Drizzle query
options. Alongside filters and ordering, a relation may use `limit` and `offset`. For example,
this alternative adds offset pagination to the public posts field:
```ts
posts: t.relation('posts', {
args: { limit: t.arg.int(), offset: t.arg.int() },
query: (args) => ({
limit: args.limit ?? 10,
offset: args.offset ?? 0,
where: { published: true },
orderBy: { createdAt: 'desc', id: 'desc' },
}),
}),
```
The query API enables you to define args and convert them into parameters that will be passed into
the relational query builder. The `query` callback receives `(args, ctx, pathInfo)` where `pathInfo`
describes where in the GraphQL query the relation is being loaded:
* `path`: a list of `ParentType.fieldName` strings, from the root field down to the field being
resolved (eg. `['Query.user', 'User.posts']`).
* `segments`: one object per entry in `path`, with `field` (the field name), `alias` (the alias used
in the query, or the field name if none), `parentType` (the name of the type the field is defined
on), and `isList` (whether the field returns a list).
You can read more about the relation query builder api
[here](https://orm.drizzle.team/docs/rqb-v2)
## Fallback queries [#fallback-queries]
A field whose data is not on the row it resolves from is loaded with a fallback query. This happens
when:
* The parent row was not loaded through a `t.drizzleField`, `t.relation`, or connection. This
covers rows a resolver queried itself, and rows that came from somewhere else entirely.
* A `drizzleField` resolver did not pass the result of `query()` to drizzle.
* A relation's arguments conflict with a sibling selection of the same relation that was planned
first.
Fallback queries are batched. Every row of a table that needs the same selection in the same tick
is loaded with one `findMany` filtered on the primary key, or on the first unique column for a
table that has no primary key, and the rows are matched back to their parents. If the query does
not return a row for a parent, because it was deleted since it was loaded, or never came from the
table, that field rejects with `Model users(1) not found`, where the value in parentheses is the
key that was looked up.
The [alias query](./query-planning#compare-two-orderings-of-one-relation) demonstrates
this fallback: newest and oldest cannot share one loaded posts list.
## Related count [#related-count]
An author’s count must use the same publication filter as the list. Otherwise the number can
reveal that unpublished posts exist:
```typescript playground example="plugin-drizzle" query="{ author(id: 1) { postCount posts { title } } empty: author(id: 3) { postCount } }"
postCount: t.relatedCount('posts', { where: eq(posts.published, true) }),
```
`t.relatedCount` returns the number of related rows without loading them. Without a `where`,
it counts all rows in the relation.
The `where` option accepts either a static SQL filter or a function that receives the field arguments
and context:
```ts
import { and, eq } from 'drizzle-orm';
import { posts } from './tables';
// In the User fields callback:
publishedPostsCount: t.relatedCount('posts', {
args: {
title: t.arg.string(),
},
where: (args, ctx) => {
if (args.title) {
return and(eq(posts.published, true), eq(posts.title, args.title));
}
return eq(posts.published, true);
},
}),
```
For a many-to-many relation (one defined with `.through(...)`), `t.relatedCount` counts distinct
related rows, so a row reachable through two junction rows counts once. A `t.relatedConnection`'s
`totalCount` counts the rows the connection pages over instead, which is one per junction row,
since that is what the relational query builder returns for the relation.
## Many-to-many relations [#many-to-many-relations]
A post exposes attached Media objects even though the database stores an attachment row. The
`through` relation handles that join; Pothos exposes its target like any other relation:
```typescript playground example="plugin-drizzle" query="{ author(id: 1) { posts { title media { url uploadedBy { fullName } } mediaConnection(first: 2) { totalCount nodes { url } } } } }"
builder.drizzleObject('posts', {
name: 'Post',
select: {},
fields: (t) => ({
id: t.exposeID('id'),
title: t.exposeString('title'),
author: t.relation('author'),
media: t.relation('media'),
mediaConnection: t.relatedConnection('media', {
query: { orderBy: { id: 'asc' } },
totalCount: true,
}),
}),
});
export const Media = builder.drizzleObject('media', {
name: 'Media',
fields: (t) => ({ url: t.exposeString('url'), uploadedBy: t.relation('uploadedBy') }),
});
```
`t.relation` and `t.relatedConnection` expose the Media type directly; no GraphQL type for the
junction table is needed.
## Related field [#related-field]
The `t.relatedField` method allows you to define a field based on a relation that uses custom
selections, including aggregations like counts. This is useful when you want to expose derived data
from a relation without loading the full related records.
For a simple count, prefer `t.relatedCount`. Its equivalent using `t.relatedField` illustrates
how `buildFilter` restricts an expression to the parent's related rows:
```ts
import { and, eq } from 'drizzle-orm';
import { posts } from './tables';
builder.drizzleNode('users', {
name: 'User',
id: { column: (user) => user.id },
fields: (t) => ({
firstName: t.exposeString('firstName'),
// Count only this author’s published posts
postsCount: t.relatedField('posts', {
type: 'Int',
// buildFilter creates the correct WHERE clause for the relation
select: (buildFilter) => {
return {
extras: {
postsCount: (parent) => {
return db.$count(
posts,
and(buildFilter(parent), eq(posts.published, true)),
);
},
},
};
},
resolve: (user) => user.postsCount,
}),
}),
});
```
`buildFilter(parent)` includes the relation's join conditions and any filter defined on the
relation. Combine it with the conditions your field needs, then return the selected value from
`resolve`. Use this helper for custom expressions that `t.relatedCount` does not cover.
`t.relatedField` also accepts the normal field options (`description`, `deprecationReason`,
`extensions`, and options added by other plugins like `authScopes`). Its `resolve` may be async,
and receives the resolve `info` as its fourth argument.
# Relay nodes
URL: /docs/plugins/drizzle/relay
Defining refetchable nodes with the Drizzle plugin
The Relay plugin supplies node IDs and cursor connections. Register it with Drizzle as shown in
[Setup](./setup#integration-with-other-plugins); [Connections](./connections) covers pagination.
Defining a node creates a direct lookup through `node` and `nodes`. These lookups bypass custom
root resolvers, so permission checks or visibility filters on a list or parent field do not protect
node refetches. An encoded global ID is an identifier, not proof of permission.
Apply an access policy to node loading, the type, or its fields as appropriate. See
[Authorizing Relay nodes](/docs/plugins/scope-auth/relay-nodes) for scope patterns and their limits.
## Define a node [#define-a-node]
`builder.drizzleNode` uses the same fields and selections as `builder.drizzleObject`, and adds
an ID and a root node lookup. The example User uses its integer primary key:
```ts
builder.drizzleNode('users', {
name: 'User',
id: { column: (user) => user.id },
fields: (t) => ({ firstName: t.exposeString('firstName') }),
});
```
This minimal alternative shows the node options. The example User adds full name, profile,
and published posts to the same type. Other ID field options can be passed alongside `column`;
for a composite primary key, `column` can return a list of columns.
## Refetch a public author [#refetch-a-public-author]
The ID returned by User can be passed back to
`node` to fetch the author with a new selection:
```graphql
query RefetchAuthor {
node(id: "VXNlcjox") {
... on User {
fullName
posts {
title
}
}
}
}
```
The [refetch operation](/playground?example=plugin-drizzle\&op=7) uses the ID of User 1. The node
returns the same public fields as the author lookup, including only published posts.
## Authorize the node load path [#authorize-the-node-load-path]
The public User type here can be refetched by anyone. Its
[published-posts relation](./relations#published-posts-and-profiles) applies the publication filter
wherever User is reached, including through `node`. Post stays an ordinary Drizzle object;
defining a public feed does not also make individual drafts globally refetchable.
# Selections
URL: /docs/plugins/drizzle/selections
Selecting the columns and relations your fields need
Selections describe the database data a GraphQL field needs. The example User starts with
`select: {}` so requesting one field does not load unrelated columns or a profile.
## Field selections [#field-selections]
An exposed column, such as `t.exposeString('firstName')`, contributes its own selection. A custom
resolver must declare the columns it reads:
```typescript playground example="plugin-drizzle" query="{ author(id: 1) { fullName } }"
fullName: t.string({
select: { columns: { firstName: true, lastName: true } },
resolve: (user) => `${user.firstName} ${user.lastName}`,
}),
```
## Only load a profile when requested [#only-load-a-profile-when-requested]
A biography is a string in GraphQL, but it comes from the optional profile relation. The field
selects that relation only when requested:
```typescript playground example="plugin-drizzle" query="{ author(id: 1) { fullName bio } missingProfile: author(id: 3) { fullName bio } }"
bio: t.string({
nullable: true,
select: { with: { profile: true } },
resolve: (user) => user.profile?.bio,
}),
```
A missing profile returns null. A name-only query does not load the profile; adding `bio` adds
the relation to the database query.
## Computed SQL values [#computed-sql-values]
The same mechanism supports SQL expressions through `extras`. For example, a field can select
`lowercaseName` only when the GraphQL operation requests it:
```typescript playground example="plugin-drizzle" query="{ author(id: 1) { fullName lowercaseName } }"
lowercaseName: t.string({
select: {
extras: {
lowercaseName: (users, { sql }) => sql`lower(${users.firstName})`,
},
},
resolve: (user) => user.lowercaseName,
}),
```
## Type selections [#type-selections]
By default, a `drizzleObject` gives its resolvers access to all columns of the table. A type-level
`select` replaces that default and makes its selected data available to every field resolver.
This alternative always loads the name, profile, and expression:
```ts
const User = builder.drizzleObject('users', {
name: 'User',
select: {
columns: {
firstName: true,
lastName: true,
},
with: {
profile: true,
},
extras: {
lowercaseName: (users, { sql }) => sql`lower(${users.firstName})`
},
},
fields: (t) => {
return {
fullName: t.string({
resolve: (user, args, ctx, info) => `${user.firstName} ${user.lastName}`,
}),
bio: t.string({
nullable: true,
resolve: (user) => user.profile?.bio,
}),
lowercaseName: t.string({
resolve: (user) => user.lowercaseName,
}),
};
},
});
```
Any selections added to the type will be available to consume in all resolvers. Columns that are not
selected can still be exposed as before.
Use type selections for data required whenever the type is loaded, and field selections for data
needed by a particular field. Both are merged into the parent query; they do not make each field
an independent database query. See [Query planning](./query-planning).
With `ScopeAuthPlugin`, scope checks that read the parent see what its type loads (its `select`, or
every column), even for a row returned from a plain `t.field`. A row missing that data is reloaded,
so a row that no longer exists fails the field with `Unexpected error.`. To return a deleted row,
load it with a `t.drizzleField` query before deleting it. Only top-level keys are checked, so a row
with a partly loaded relation isn't reloaded.
# Setup
URL: /docs/plugins/drizzle/setup
Setting up the Drizzle plugin
Pass your existing Drizzle client and relations to the plugin. These guides assume familiarity
with Drizzle; use its [driver setup](https://orm.drizzle.team/docs/connect-overview) and
[relations documentation](https://orm.drizzle.team/docs/relations-v2) to configure the database.
## Installing [#installing]
npm
pnpm
yarn
bun
```bash
npm install --save @pothos/plugin-drizzle
```
```bash
pnpm add @pothos/plugin-drizzle
```
```bash
yarn add @pothos/plugin-drizzle
```
```bash
bun add @pothos/plugin-drizzle
```
The plugin requires Drizzle's relational query builder v2. See the
[version requirements](/docs/plugins/drizzle) before installing or upgrading Drizzle.
## Configure the builder [#configure-the-builder]
```ts
// builder.ts
import SchemaBuilder from '@pothos/core';
import DrizzlePlugin from '@pothos/plugin-drizzle';
import { getTableConfig } from 'drizzle-orm/sqlite-core';
import { db } from './database';
import { relations } from './tables';
export interface PothosTypes {
DrizzleRelations: typeof relations;
Context: { userId: number };
}
export const builder = new SchemaBuilder({
plugins: [DrizzlePlugin],
drizzle: { client: db, getTableConfig, relations },
});
```
`DrizzleRelations` provides the table and relation types used by Pothos. Import `getTableConfig`
from the dialect used by your client (`sqlite-core` above, or `pg-core` / `mysql-core`). `client`
may also be a function `(ctx) => db` for a request-specific client. The examples use
[request context](/docs/guide/context) to supply the current `userId`.
Use this builder to [define GraphQL types and fields](./objects), then call `builder.toSchema()`.
## Integration with other plugins [#integration-with-other-plugins]
Install and register `@pothos/plugin-relay` to use `drizzleNode` and connection fields:
```ts
import RelayPlugin from '@pothos/plugin-relay';
// In the builder options:
plugins: [RelayPlugin, DrizzlePlugin],
relay: { nodesOnConnection: true },
```
`nodesOnConnection` adds the `nodes` convenience field used in these guides. Without it,
select `edges { node { ... } }` instead.
The [with-input plugin](../with-input) is another optional integration. To use
`t.drizzleFieldWithInput`, install `@pothos/plugin-with-input`, import it, and add it to the
builder's plugins:
```ts
import WithInputPlugin from '@pothos/plugin-with-input';
// In the builder options:
plugins: [RelayPlugin, WithInputPlugin, DrizzlePlugin],
```
## Plugin options [#plugin-options]
* `client`: the drizzle client, or a function returning one from the request context.
* `getTableConfig`: the `getTableConfig` of your dialect, used to read primary keys and unique
constraints.
* `relations`: the relations passed to `drizzle()`.
* `defaultConnectionSize` / `maxConnectionSize`: the page size a connection uses when the query
does not ask for one (defaults to 20), and the largest size it will accept (defaults to 100).
Both can also be set per field with `defaultSize` and `maxSize`.
* `filterConnectionTotalCount`: see [Connection totalCount](./connections#connection-totalcount).
* `skipDeferredFragments`: selections inside a `@defer` fragment are left out of the planned query
by default, and the fragment's fields are loaded through [fallback queries](./relations#fallback-queries)
when it resolves. Set this to `false` to plan deferred selections with the rest of the query.
`skipDeferredFragments` controls query planning; it does not enable incremental execution. The application
must register the defer directive and use an executor, server transport, and client that support
the same incremental delivery protocol. With GraphQL.js 17, ordinary `execute` rejects schemas
containing `@defer` or `@stream`; incremental execution uses `experimentalExecuteIncrementally`.
For a server integration, see [GraphQL Yoga's defer and stream setup](https://the-guild.dev/graphql/yoga-server/docs/features/defer-stream),
which uses `@graphql-yoga/plugin-defer-stream`. Check the integration's supported versions when
choosing an executor and client; adding directives or changing `skipDeferredFragments` alone is
not sufficient.
# Type variants
URL: /docs/plugins/drizzle/variants
How to define multiple GraphQL types based on the same drizzle table
A variant gives another GraphQL representation to the same database row. The publishing API
uses public User fields for author pages and a private Viewer for the signed-in account.
The API has three parts:
* `variant` registers an additional type instead of naming the primary type with `name`.
* `t.variant` returns another representation of the same row.
* A relation's `type` option can select a variant using its returned object ref.
## A single object variant [#a-single-object-variant]
Use `variant` to define Viewer alongside the primary User type. `t.variant('users')` exposes
the public representation of the same row:
```ts
const Viewer = builder.drizzleObject('users', {
variant: 'Viewer',
select: {},
fields: (t) => {
return {
id: t.exposeID('id'),
user: t.variant('users'),
drafts: t.relation('posts', {
query: {
where: { published: false },
orderBy: { createdAt: 'desc', id: 'desc' },
},
}),
};
},
});
```
## A viewer with role-specific fields [#a-viewer-with-role-specific-fields]
When viewer fields differ by role, Viewer can instead be an interface with object variants
for each role. The following definition uses this structure; [Interfaces](./interfaces) explains
its implementations:
```typescript playground example="plugin-drizzle" query="{ me { __typename email user { fullName } drafts { title } } }"
export const Viewer = builder.drizzleInterface('users', {
variant: 'Viewer',
select: { columns: { id: true, role: true } },
resolveType: (user) => (user.role === 'editor' ? 'EditorViewer' : 'AuthorViewer'),
fields: (t) => ({
user: t.variant('users'),
email: t.exposeString('email'),
drafts: t.relation('posts', {
query: { where: { published: false }, orderBy: { id: 'asc' } },
}),
}),
});
```
The root lookup uses the authenticated context ID. It does not accept an arbitrary author's ID:
```typescript playground example="plugin-drizzle" query="{ me { __typename email drafts { title } } }"
me: t.drizzleField({
type: Viewer,
nullable: true,
resolve: (query, _root, _args, ctx) => {
return db.query.users.findFirst(
query({
where: { id: ctx.userId },
}),
);
},
}),
```
Maya (`userId: 1`) sees “Planning the spring exchange”; Leo (`userId: 2`) sees “Saving rainwater.”
The interface and variants describe the shapes; the root lookup restricts ownership. Public
author fields and post connections return published posts only.
## Conditionally expose another variant [#conditionally-expose-another-variant]
The publishing User also exposes its Viewer conditionally. Define that field after Viewer is registered,
which avoids a circular reference between their definitions:
```typescript playground example="plugin-drizzle" query="{ own: author(id: 1) { viewer { email drafts { title } } } other: author(id: 2) { viewer { email } } }"
builder.drizzleObjectField('users', 'viewer', (t) =>
t.variant(Viewer, {
select: { columns: { id: true } },
isNull: (user, _args, ctx) => user.id !== ctx.userId,
}),
);
```
The ownership check is essential: without it an arbitrary author's public row could reveal that
author's drafts. `isNull` makes the variant field null when its parent is another account.
A variant field accepts a `select` just like other fields. Here it loads `id` for the ownership
check; the target variant contributes the selections required by its own fields.
## Relation variants [#relation-variants]
To use a different representation of a related row, pass its object ref to `t.relation`'s `type`
option. The variant must represent the relation's target table. For example, this alternative
adds a compact PostSummary representation and selects it for a public author's posts:
```ts
const PostSummary = builder.drizzleObject('posts', {
variant: 'PostSummary',
fields: (t) => ({ title: t.exposeString('title') }),
});
builder.drizzleObjectField('users', 'postSummaries', (t) =>
t.relation('posts', {
type: PostSummary,
query: {
where: { published: true },
orderBy: { id: 'asc' },
},
}),
);
```
Two variants selected for the same row have their type-level selections merged. Conflicting
relation arguments or extras can fail; see
[Conflicting selections between variants](./query-planning#conflicting-selections-between-variants).
# Connections
URL: /docs/plugins/prisma/connections
Creating relay connections with the Prisma plugin
These examples use the [Prisma builder setup](./setup) with the [Relay plugin](./relay).
Register the Prisma object types used by each field. Later examples replace earlier definitions of
the same field or helper.
### `prismaConnection` [#prismaconnection]
The `prismaConnection` method on a field builder can be used to create a relay `connection` field
that also pre-loads all the data nested inside that connection.
```typescript
builder.queryType({
fields: (t) => ({
posts: t.prismaConnection(
{
type: 'Post',
cursor: 'id',
resolve: (query, parent, args, context, info) => {
return prisma.post.findMany({
...query,
});
},
},
{}, // optional options for the Connection type
{}, // optional options for the Edge type),
),
}),
});
```
#### options [#options]
* `type`: the name of the prisma model being connected to
* `cursor`: a `@unique` column of the model being connected to. This is used as the `cursor` option
passed to prisma.
* `defaultSize`: (default: 20) The default page size to use if `first` and `last` are not provided.
* `maxSize`: (default: 100) The maximum number of nodes returned for a connection.
* `resolve`: Like the resolver for `prismaField`, the first argument is a `query` object that should
be spread into your prisma query. The `resolve` function should return an array of nodes for the
connection. The `query` will contain the correct `take`, `skip`, and `cursor` options based on the
connection arguments (`before`, `after`, `first`, `last`), along with `include` options for nested
selections.
* `totalCount`: A function for loading the total count for the connection. This will add a
`totalCount` field to the connection object. The `totalCount` method will receive (`connection`,
`args`, `context`, `info`) as arguments. Note that this will not work when using a shared
connection object (see details below)
The created connection queries currently support the following combinations of connection arguments:
* `first`, `last`, `before`, or `after` on their own
* `first` and `after`
* `last` and `before`
The following combinations are not supported:
* `before` and `after`
* `first` and `before`
* `last` and `after`
Queries for these combinations are not as useful, and generally requiring loading all records
between 2 cursors, or between a cursor and the end of the set. Generating query options for these
cases is more complex and likely very inefficient, so they will currently throw an Error indicating
the argument combinations are not supported.
The `maxSize` and `defaultSize` can also be configured globally using `maxConnectionSize` and
`defaultConnectionSize` options in the `prisma` plugin options.
### `relatedConnection` [#relatedconnection]
The `relatedConnection` method can be used to create a relay `connection` field based on a relation
of the current model.
```typescript
builder.prismaNode('User', {
id: { field: 'id' },
fields: (t) => ({
// Connections can be very simple to define
simplePosts: t.relatedConnection('posts', {
cursor: 'id',
}),
// Or they can include custom arguments, and other options
posts: t.relatedConnection(
'posts',
{
cursor: 'id',
args: {
oldestFirst: t.arg.boolean(),
},
query: (args, context) => ({
orderBy: {
createdAt: args.oldestFirst ? 'asc' : 'desc',
},
}),
},
{}, // optional options for the Connection type
{}, // optional options for the Edge type),
),
}),
});
```
#### options [#options-1]
* `cursor`: a `@unique` column of the model being connected to. This is used as the `cursor` option
passed to prisma.
* `defaultSize`: (default: 20) The default page size to use if `first` and `last` are not provided.
* `maxSize`: (default: 100) The maximum number of nodes returned for a connection.
* `query`: A method that accepts the `args` and `context` for the connection field, and returns
filtering and sorting logic that will be merged into the query for the relation.
* `totalCount`: when set to true, this will add a `totalCount` field to the connection object. see
[`relationCount`](./relations#relationcount) for more details. Note that this will not work when
using a shared connection object (see details below)
### Indirect relations as connections [#indirect-relations-as-connections]
`prismaConnectionHelpers` connects join rows to a different GraphQL node type. The example uses
Post.media → PostMedia.media → Media: pagination follows attachment IDs, while each node is an
image. A caption belongs to the attachment, so the same image can have different captions on two
posts. Import the helper from `@pothos/plugin-prisma` and register Post and Media first.
```typescript
const mediaConnectionHelpers = prismaConnectionHelpers(builder, 'PostMedia', {
cursor: 'id',
query: { orderBy: { id: 'asc' } },
select: (nodeSelection) => ({
caption: true,
media: nodeSelection({ select: { id: true } }),
}),
resolveNode: (attachment) => attachment.media,
});
```
`select` adds the join data used by the edge and plans the Media fields requested beneath `node`.
`resolveNode` maps each attachment to its selected image. The helper also accepts `defaultSize`
and `maxSize` (defaults 20 and 100). Selecting the node ID also keeps the Prisma selection nonempty
when the operation requests only an edge caption or connection count. Other default node fields
can be added through the same `nodeSelection` argument.
Use `t.connection` to expose the result. It does not load the relation automatically: its field
selection includes `getQuery`, and its resolver passes the loaded attachments to the helper:
```typescript
builder.prismaObjectField('Post', 'mediaConnection', (t) =>
t.connection(
{
type: Media,
select: (args, ctx, nestedSelection) => ({
_count: { select: { media: true } },
media: mediaConnectionHelpers.getQuery(args, ctx, nestedSelection),
}),
resolve: (post, args, ctx) => {
return {
...mediaConnectionHelpers.resolve(post.media, args, ctx),
totalCount: post._count.media,
};
},
},
{
fields: (connection) => ({
totalCount: connection.int({ resolve: (result) => result.totalCount }),
}),
},
{
fields: (edge) => ({
caption: edge.string({ resolve: (attachment) => attachment.caption }),
}),
},
),
);
```
The parent selection counts attachments separately from the page. The second configuration
argument adds `totalCount` to the connection; the third adds fields to its edges. A page containing
one attachment can therefore report two total attachments.
```graphql
query Attachments {
author(id: 1) {
posts(oldestFirst: true) {
title
mediaConnection(first: 1) {
totalCount
edges {
caption
node {
url
uploadedBy {
name
}
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
}
```
Both posts return the same first image, but with different captions. “Starting a seed library”
has two attachments and a next page; the cursor advances through PostMedia rows, not Media IDs.
The second page contains the seed-packets image. An attachment-free post has an empty edge list
and a zero count. The [local example](./setup#run-the-publishing-api) includes this operation.
`prismaConnectionHelpers` can also be used to manually create a connection where the edge and
connections share the same model, and pagination happens directly on a relation to nodes type (even
if that relation is nested).
```ts
const commentConnectionHelpers = prismaConnectionHelpers(builder, 'Comment', {
cursor: 'id',
});
const SelectPost = builder.prismaObject('Post', {
fields: (t) => ({
title: t.exposeString('title'),
comments: t.connection({
type: commentConnectionHelpers.ref,
select: (args, ctx, nestedSelection) => ({
comments: commentConnectionHelpers.getQuery(args, ctx, nestedSelection),
}),
resolve: (parent, args, ctx) => {
return commentConnectionHelpers.resolve(parent.comments, args, ctx);
},
}),
}),
});
```
To add arguments for a connection defined with a helper, it is often easiest to define the arguments
on the connection field rather than the connection helper. This allows connection helpers to be
shared between fields that may not share the same arguments:
```ts
const mediaConnectionHelpers = prismaConnectionHelpers(builder, 'PostMedia', {
cursor: 'id',
select: (nodeSelection) => ({
media: nodeSelection({}),
}),
resolveNode: (postMedia) => postMedia.media,
});
builder.prismaObjectField('Post', 'mediaConnection', (t) =>
t.connection({
type: Media,
args: {
inverted: t.arg.boolean(),
},
select: (args, ctx, nestedSelection) => ({
media: {
...mediaConnectionHelpers.getQuery(args, ctx, nestedSelection),
orderBy: {
post: {
createdAt: args.inverted ? 'desc' : 'asc',
},
},
},
}),
resolve: (post, args, ctx) => {
return mediaConnectionHelpers.resolve(post.media, args, ctx);
},
}),
);
```
Arguments, ordering and filtering can also be defined on the helpers themselves:
```ts
const mediaConnectionHelpers = prismaConnectionHelpers(builder, 'PostMedia', {
cursor: 'id',
// define arguments for the connection helper, these will be available as the second argument of `select`
args: (t) => ({
inverted: t.arg.boolean(),
}),
select: (nodeSelection, args) => ({
media: nodeSelection({}),
}),
query: (args) => ({
// Custom filtering with a where clause
where: {
post: {
published: true,
},
},
// custom ordering including use of args
orderBy: {
post: {
createdAt: args.inverted ? 'desc' : 'asc',
},
},
}),
resolveNode: (postMedia) => postMedia.media,
});
builder.prismaObjectField('Post', 'mediaConnection', (t) =>
t.connection({
type: Media,
// add the args from the connection helper to the field
args: mediaConnectionHelpers.getArgs(),
select: (args, ctx, nestedSelection) => ({
media: mediaConnectionHelpers.getQuery(args, ctx, nestedSelection),
}),
resolve: (post, args, ctx) => {
return mediaConnectionHelpers.resolve(post.media, args, ctx);
},
}),
);
```
### Sharing Connections objects [#sharing-connections-objects]
You can create reusable connection objects by using `builder.connectionObject`.
These connection objects can be used with `t.prismaConnection`, `t.relatedConnection`, or
`t.connection`
Shared edges can also be created using `builder.edgeObject`
```typescript
const CommentConnection = builder.connectionObject({
type: commentConnectionHelpers.ref,
name: 'CommentConnection',
});
builder.prismaObject('Post', {
fields: (t) => ({
id: t.exposeID('id'),
commentsConnection: t.relatedConnection(
'comments',
{ cursor: 'id' },
// The connection object ref can be passed in place of the connection object options
CommentConnection
),
}),
});
```
### Extending connection edges [#extending-connection-edges]
Edge fields can read data from the row paginated by the helper. In the attachment connection,
`select` loads `caption` from PostMedia and the third `t.connection` argument exposes it:
```typescript
fields: (edge) => ({
caption: edge.string({ resolve: (attachment) => attachment.caption }),
}),
```
The edge parent is inferred from the helper's resolved rows. Its caption describes the attachment;
`node.url` describes the shared Media record.
For a timestamp on the join model instead, select `createdAt: true` in the helper and add this
edge field. This alternative requires a `PostMedia.createdAt` column and a registered `DateTime`
scalar whose output is `Date`:
```ts
createdAt: edge.field({
type: 'DateTime',
resolve: (attachment) => attachment.createdAt,
}),
```
### Total count on shared connection objects [#total-count-on-shared-connection-objects]
If you set the `totalCount: true` on a `prismaConnection` or `relatedConnection` field, and are
using a custom connection object, you will need to add the `totalCount` field to the
connection object manually. The parent object on the connection will have a `totalCount` property
that is either the totalCount, or a function that will return the totalCount.
```typescript
const CommentConnection = builder.connectionObject({
type: commentConnectionHelpers.ref,
name: 'CommentConnection',
fields: (t) => ({
totalCount: t.int({
resolve: (connection) => {
const { totalCount } = connection as {
totalCount?: number | (() => number | Promise);
};
return typeof totalCount === 'function' ? totalCount() : totalCount;
},
}),
}),
});
```
If you want to add a global `totalCount` field, you can do something similar using
`builder.globalConnectionField`:
```typescript
export const builder = new SchemaBuilder<{
PrismaTypes: PrismaTypes;
Connection: {
totalCount: number | (() => number | Promise);
};
}>({
plugins: [PrismaPlugin, RelayPlugin],
prisma: {
client: prisma,
dmmf: getDatamodel(),
},
});
builder.globalConnectionField('totalCount', (t) =>
t.int({
nullable: false,
resolve: (parent) => {
return typeof parent.totalCount === 'function' ? parent.totalCount() : parent.totalCount;
},
}),
);
```
### `parsePrismaCursor` and `formatPrismaCursor` [#parseprismacursor-and-formatprismacursor]
These functions can be used to manually parse and format cursors that are compatible with prisma
connections.
Parsing a cursor will return the value from the column used for the cursor (often the `id`), this
value may be an array or object when a compound index is used as the cursor. Similarly, to format a
cursor, you must provide the column(s) that make up the cursor.
## Page through published posts [#page-through-published-posts]
These queries use `nodes` on connections. Enable `relay: { nodesOnConnection: true }` in the
builder options, or select `edges { node { title } }` with the default Relay configuration.
A public post connection applies the same `published` filter as author pages. The publishing
schema uses this root connection alongside its private viewer:
```typescript
builder.queryFields((t) => ({
me: t.prismaField({
type: Viewer,
resolve: (query, _root, _args, ctx) => {
return prisma.user.findUniqueOrThrow({
...query,
where: { id: ctx.userId },
});
},
}),
posts: t.prismaConnection({
type: 'Post',
cursor: 'id',
resolve: (query) => {
return prisma.post.findMany({
...query,
where: { published: true },
orderBy: { id: 'asc' },
});
},
totalCount: () => {
return prisma.post.count({
where: { published: true },
});
},
}),
searchPosts: t.prismaField({
type: ['Post'],
args: { where: t.arg({ type: PostWhere }), orderBy: t.arg({ type: PostOrderBy }) },
resolve: (query, _root, args) => {
return prisma.post.findMany({
...query,
// Caller filters can narrow this scope, but cannot expose drafts.
where: { AND: [{ published: true }, args.where ?? {}] },
orderBy: args.orderBy ? [args.orderBy, { id: 'asc' }] : { id: 'asc' },
});
},
}),
}));
```
```graphql
query PublishedPosts {
posts(first: 2) {
nodes {
title
}
pageInfo {
endCursor
hasNextPage
}
}
}
```
There are three published posts and two drafts in the seed data. The first page contains two
published posts and has a next page. Passing its `endCursor` as `after` returns the remaining
published post. The related author connection uses the same filter for its nodes and `totalCount`,
so Maya's count is two, including when the client requests only the count.
# Prisma plugin
URL: /docs/plugins/prisma
Prisma plugin docs for Pothos
This plugin supports Prisma ORM 7 only; install `prisma@^7`. For Prisma ORM 8, see
[Prisma Next](/docs/plugins/prisma-next).
The Prisma plugin defines GraphQL types from Prisma models and builds selections for the data
requested by a GraphQL query. It supports relations, counts, type variants, and Relay nodes and
connections. GraphQL fields can have different names and shapes from the underlying models.
You can also [use Prisma with plain object refs](/docs/plugins/prisma/without-a-plugin).
The guides also use a publishing API with public author pages, private drafts, and shared media.
Its [local setup](/docs/plugins/prisma/setup#run-the-publishing-api) runs the real Prisma client against a temporary
SQLite database. Prisma Utils filters and mutations reuse the same schema and data.
## Example [#example]
This example exposes users and their posts. See [Objects](/docs/plugins/prisma/objects), [Relations](/docs/plugins/prisma/relations), and [Connections](/docs/plugins/prisma/connections).
Use the [builder setup](/docs/plugins/prisma/setup), including its `userId` context, and add the
[Relay plugin](/docs/plugins/relay) for nodes and connections. The Prisma schema has User, Post, and Profile
models with the fields and relations used below.
```typescript
// Create an object type based on a prisma model
// without providing any custom type information
builder.prismaObject('User', {
fields: (t) => ({
// expose fields from the database
id: t.exposeID('id'),
email: t.exposeString('email'),
bio: t.string({
// the profile relation is nullable, so this field is too
nullable: true,
// automatically load the bio from the profile
// when this field is queried
select: {
profile: {
select: {
bio: true,
},
},
},
// user will be typed correctly to include the
// selected fields from above
resolve: (user) => user.profile?.bio,
}),
// Load posts as list field.
posts: t.relation('posts', {
args: {
oldestFirst: t.arg.boolean(),
},
// Define custom query options that are applied when
// loading the post relation
query: (args, context) => ({
orderBy: {
createdAt: args.oldestFirst ? 'asc' : 'desc',
},
}),
}),
// creates relay connection that handles pagination
// using prisma's built in cursor based pagination
postsConnection: t.relatedConnection('posts', {
cursor: 'id',
}),
}),
});
// Create a relay node based a prisma model
builder.prismaNode('Post', {
id: { field: 'id' },
fields: (t) => ({
title: t.exposeString('title'),
author: t.relation('author'),
}),
});
builder.queryType({
fields: (t) => ({
// Define a field that issues an optimized prisma query
me: t.prismaField({
type: 'User',
resolve: async (query, root, args, ctx, info) => {
return prisma.user.findUniqueOrThrow({
// the `query` argument will add in `include`s or `select`s to
// resolve as much of the request in a single query as possible
...query,
where: { id: ctx.userId },
});
},
}),
}),
});
```
Given this schema, you would be able to resolve a query like the following with a single prisma
query (which will still result in a few optimized SQL queries).
```graphql
query {
me {
email
posts {
title
author {
id
}
}
}
}
```
A query like
```graphql
query {
me {
email
posts {
title
author {
id
}
}
oldPosts: posts(oldestFirst: true) {
title
author {
id
}
}
}
}
```
Will result in 2 calls to prisma, one to resolve everything except `oldPosts`, and a second to
resolve everything inside `oldPosts`. Prisma can only resolve each relation once in a single query,
so we need a separate query to handle the second `posts` relation.
# Indirect relations
URL: /docs/plugins/prisma/indirect-relations
Indirect relations and join tables
## Selecting fields from a nested GraphQL field [#selecting-fields-from-a-nested-graphql-field]
By default, the `nestedSelection` function will return selections based on the type of the current
field. `nestedSelection` can also be used to get a selection from a field nested deeper inside other
fields. This is useful if the field returns a type that is not a `prismaObject`, but a field nested
inside the returned type is.
```typescript
import type { Post } from '../lib/prisma/client';
const PostRef = builder.prismaObject('Post', {
fields: (t) => ({
title: t.exposeString('title'),
content: t.exposeString('content', { nullable: true }),
author: t.relation('author'),
}),
});
const PostPreview = builder.objectRef('PostPreview').implement({
fields: (t) => ({
post: t.field({
type: PostRef,
resolve: (post) => post,
}),
preview: t.string({
nullable: true,
resolve: (post) => post.content?.slice(0, 10),
}),
}),
});
builder.prismaObject('User', {
fields: (t) => ({
id: t.exposeID('id'),
postPreviews: t.field({
select: (args, ctx, nestedSelection) => ({
posts: nestedSelection(
{
// limit the number of postPreviews to load
take: 2,
},
// Look at the selections in postPreviews.post to determine what relations/fields to select
['post'],
// (optional) If the field returns a union or interface, you can pass a typeName to get selections for a specific object type
'Post',
),
}),
type: [PostPreview],
resolve: (user) => user.posts,
}),
}),
});
```
`nestedSelection` returns the relation query for the type it selected, which for a `Post` is
`{ select?, include?, where?, orderBy?, take?, skip?, cursor? }`. Any keys you pass in are kept as
they were given, so a `select` passed to `nestedSelection` will still narrow the parent's shape.
With no argument, or with `true`, it returns the planned selection on its own.
### Pinning a type in the path [#pinning-a-type-in-the-path]
The `path` is followed through fragments, so a segment is found whether the field is selected
directly, or under a fragment on an implementation of the field's type. When several
implementations share the same field name, a segment can be written as `{ name, type }` to name the
implementation the field must be found under. Only selections of that field under a fragment on
that type, or one of its subtypes, will be planned:
```typescript
builder.prismaObject('User', {
fields: (t) => ({
entries: t.field({
type: [Entry],
select: (args, ctx, nestedSelection) => ({
// Plan what `post` selects under `... on PostEntry`, not under other implementations
posts: nestedSelection({ take: 2 }, [{ name: 'post', type: 'PostEntry' }]),
}),
resolve: (user) => {
return user.posts.map((post) => ({ kind: 'post', post }));
},
}),
}),
});
```
The same segments can be used in `queryFromInfo`'s `path` and `paths` options. The type is exported
as `PathSegment`.
### Selecting as a specific type [#selecting-as-a-specific-type]
When the field returns an interface or union, the third argument names the object type the
selection should be read as. Its type-level selection and the fields selected under a fragment on
it are planned, and fragments on other types are left out. With an empty path, this applies to the
field's own return type:
```typescript
// Activity is a union of Post and Comment
builder.prismaObject('User', {
fields: (t) => ({
recentActivity: t.field({
type: [Activity],
select: (args, ctx, nestedSelection) => ({
// What the query selects under `... on Post`, as a query for the posts relation
posts: nestedSelection({ take: 5 }, [], 'Post'),
// and under `... on Comment`, for the comments relation
comments: nestedSelection({ take: 5 }, [], 'Comment'),
}),
resolve: (user) => [...user.posts, ...user.comments],
}),
}),
});
```
## Indirect relations (eg. Join tables) [#indirect-relations-eg-join-tables]
If you want to define a GraphQL field that directly exposes data from a nested relationship (many to
many relations using a custom join table is a common example of this) you can use the
`nestedSelection` function passed to `select`.
Given a prisma schema like the following:
```
model Post {
id Int @id @default(autoincrement())
title String
content String
media PostMedia[]
}
model Media {
id Int @id @default(autoincrement())
url String
posts PostMedia[]
uploadedBy User @relation(fields: [uploadedById], references: [id])
uploadedById Int
}
model PostMedia {
id Int @id @default(autoincrement())
post Post @relation(fields: [postId], references: [id])
media Media @relation(fields: [mediaId], references: [id])
postId Int
mediaId Int
}
```
You can define a media field that can pre-load the correct relations based on the graphql query:
```typescript
const PostWithMedia = builder.prismaObject('Post', {
fields: (t) => ({
title: t.exposeString('title'),
media: t.field({
select: (args, ctx, nestedSelection) => ({
media: {
select: {
// This will look at what fields are queried on Media
// and automatically select uploadedBy if that relation is requested
media: nestedSelection(
// This argument is the default query for the media relation
// It could be something like: `{ select: { id: true } }` instead
true,
),
},
},
}),
type: [Media],
resolve: (post) => {
return post.media.map(({ media }) => media);
},
}),
}),
});
const Media = builder.prismaObject('Media', {
select: {
id: true,
},
fields: (t) => ({
url: t.exposeString('url'),
uploadedBy: t.relation('uploadedBy'),
}),
});
```
## Shared media on published posts [#shared-media-on-published-posts]
The publishing schema stores attachment rows in PostMedia, but clients request Media objects.
`nestedSelection` follows the media field's selection through that join, including the uploader
when requested:
```typescript
const Post = builder.prismaNode('Post', {
nullable: true,
id: { field: 'id' },
select: { id: true },
// Node refetches must apply the same visibility rule as root fields.
findUnique: (id, context) => ({
id: Number(id),
OR: [{ published: true }, { authorId: context.userId }],
}),
fields: (t) => ({
title: t.exposeString('title'),
published: t.exposeBoolean('published'),
author: t.relation('author'),
comments: t.relation('comments', { query: { orderBy: { id: 'asc' } } }),
media: t.field({
type: [Media],
select: (_args, _ctx, nestedSelection) => ({
media: { orderBy: { id: 'asc' }, select: { media: nestedSelection(true) } },
}),
resolve: (post) => {
return post.media.map(({ media }) => media);
},
}),
}),
});
```
Both “Starting a seed library” and “A guide to composting” attach the same image, uploaded by Leo.
Querying `posts { title media { url uploadedBy { name } } }` through Maya's author page returns
that uploader for both posts. The GraphQL shape need not expose the join table just because the
database uses one.
# Interfaces
URL: /docs/plugins/prisma/interfaces
Creating interfaces for prisma models that can be shared by variants
`builder.prismaInterface` works just like builder.prismaObject and can be used to define either the
primary type or a variant for a model.
The following example creates a `User` interface, and 2 variants Admin and Member. The `resolveType`
method returns the typenames as strings to avoid issues with circular references.
```typescript
const User = builder.prismaInterface('User', {
name: 'User',
fields: (t) => ({
id: t.exposeID('id'),
email: t.exposeString('email'),
}),
resolveType: (user) => {
return user.isAdmin ? 'Admin' : 'Member';
},
});
builder.prismaObject('User', {
variant: 'Admin',
interfaces: [User],
fields: (t) => ({
isAdmin: t.exposeBoolean('isAdmin'),
}),
});
builder.prismaObject('User', {
variant: 'Member',
interfaces: [User],
fields: (t) => ({
bio: t.exposeString('bio'),
}),
});
```
When using select mode, it's recommended to add selections to both the interface and the object
types that implement them. Selections are not inherited and will fallback to the default selection
which includes all scalar columns.
You will not be able to extend an interface for a different prisma model, doing so will result in an
error at build time.
## Selecting a viewer implementation [#selecting-a-viewer-implementation]
The [writing desk](./variants#the-authors-writing-desk) uses a Viewer interface for the signed-in
author and two object variants for account capabilities. Its type-level selection includes the
discriminator (`isAdmin`) used by `resolveType`. Each implementation also selects the fields
required by that interface; configuring a selection on the interface does not replace the
implementation's selection.
```graphql
query WritingDesk {
me {
__typename
drafts {
title
}
... on EditorViewer {
canReviewSubmissions
}
}
}
```
The editor result includes `canReviewSubmissions: true`. An author result has no field from that
fragment, while retaining the interface's `drafts` field. This keeps the public User type separate
from account-specific capabilities.
# Prisma Objects
URL: /docs/plugins/prisma/objects
Prisma plugin docs for Pothos
## Creating types with `builder.prismaObject` [#creating-types-with-builderprismaobject]
`builder.prismaObject` takes 2 arguments:
1. `name`: The name of the prisma model this new type represents
2. `options`: options for the type being created, this is very similar to the options for any other
object type
```typescript
builder.prismaObject('User', {
// Optional name for the object, defaults to the name of the prisma model
name: 'PostAuthor',
fields: (t) => ({
id: t.exposeID('id'),
email: t.exposeString('email'),
}),
});
builder.prismaObject('Post', {
fields: (t) => ({
id: t.exposeID('id'),
title: t.exposeString('title'),
}),
});
```
So far, this is just creating some simple object types. They work just like any other object type in
Pothos. The main advantage of this is that we get the type information without using object refs, or
needing imports from prisma client.
## Adding prisma fields to non-prisma objects (including Query and Mutation) [#adding-prisma-fields-to-non-prisma-objects-including-query-and-mutation]
`t.prismaField` defines fields that return Prisma objects. This example uses the client and
`userId` context from [Setup](./setup):
```typescript
builder.queryType({
fields: (t) => ({
me: t.prismaField({
type: 'User',
resolve: async (query, root, args, ctx, info) => {
return prisma.user.findUniqueOrThrow({
...query,
where: { id: ctx.userId },
});
},
}),
}),
});
```
This method works just like the normal `t.field` method with a couple of differences:
1. The `type` option must contain the name of the prisma model (eg. `User` or `[User]` for a list
field).
2. The `resolve` function has a new first argument `query` which should be spread into your prisma
query. This will be used to load data for nested relationships.
You do not need to use this method, and the `builder.prismaObject` method returns an object ref that
can be used like any other object ref (with `t.field`), but using `t.prismaField` will allow you to
take advantage of more efficient queries.
The `query` object will contain an object with `include` or `select` options to pre-load data needed
to resolve nested parts of the current query. The included/selected fields are based on which fields
are being queried, and the options provided when defining those fields and types.
### `prismaFieldWithInput` [#prismafieldwithinput]
With the [with-input plugin](https://pothos-graphql.dev/docs/plugins/with-input),
`t.prismaFieldWithInput` combines `t.prismaField` with `t.fieldWithInput`. The `input` fields become
an input object argument, and the resolver still receives the `query` to spread as its first
argument.
```typescript
builder.mutationType({
fields: (t) => ({
createPost: t.prismaFieldWithInput({
type: 'Post',
input: {
title: t.input.string({ required: true }),
authorId: t.input.id({ required: true }),
},
resolve: (query, root, args, ctx) => {
return prisma.post.create({
...query,
data: {
title: args.input.title,
authorId: Number.parseInt(args.input.authorId, 10),
},
});
},
}),
}),
});
```
## Extending prisma objects [#extending-prisma-objects]
The normal `builder.objectField(s)` methods can be used to extend prisma objects, but do not support
using selections, or exposing fields not in the default selection. To use these features, you can
use
`builder.prismaObjectField` or `builder.prismaObjectFields` instead.
## A nullable author lookup [#a-nullable-author-lookup]
An author page should return null when the requested author does not exist. A `t.prismaField`
passes the requested selections into `findUnique`; its GraphQL nullability matches that method's
result. This field uses the publishing schema described in [Setup](./setup#run-the-publishing-api):
```typescript
builder.queryType({
fields: (t) => ({
author: t.prismaField({
type: 'User',
nullable: true,
args: { id: t.arg.int({ required: true }) },
resolve: (query, _root, args) => {
return prisma.user.findUnique({
...query,
where: { id: args.id },
});
},
}),
}),
});
```
`author(id: 1) { name }` returns Maya Chen. `author(id: 999) { name }` returns null.
The type's relations can be requested through the same field without changing the resolver.
# Prisma Utils
URL: /docs/plugins/prisma/prisma-utils
Prisma utils for creating input types
This package is highly experimental and not recommended for production use
The plugin adds new helpers for creating prisma compatible input types. Use it alongside the Prisma plugin when you want these input helpers.
## Setup [#setup]
To use this plugin, you will need to enable prismaUtils option in the generator in your
schema.prisma:
```prisma
generator client {
provider = "prisma-client"
output = "../lib/prisma"
}
generator pothos {
provider = "prisma-pothos-types"
clientOutput = "./prisma" // relative path from pothos output to prisma client
output = "../lib/pothos-prisma-types.ts"
// Enable prismaUtils feature
prismaUtils = true
}
```
Once this is enabled, add the plugin alongside the Prisma plugin. This example also uses
`graphql-scalars` to register the `DateTime` scalar used by the input helpers:
npm
pnpm
yarn
bun
```bash
npm install @pothos/plugin-prisma-utils graphql-scalars @prisma/adapter-better-sqlite3
```
```bash
pnpm add @pothos/plugin-prisma-utils graphql-scalars @prisma/adapter-better-sqlite3
```
```bash
yarn add @pothos/plugin-prisma-utils graphql-scalars @prisma/adapter-better-sqlite3
```
```bash
bun add @pothos/plugin-prisma-utils graphql-scalars @prisma/adapter-better-sqlite3
```
```ts
import SchemaBuilder from '@pothos/core';
import { DateTimeResolver } from 'graphql-scalars';
import { PrismaClient } from '../lib/prisma/client';
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3';
import type PrismaTypes from '../lib/pothos-prisma-types';
import { getDatamodel } from '../lib/pothos-prisma-types';
import PrismaPlugin from '@pothos/plugin-prisma';
import PrismaUtils from '@pothos/plugin-prisma-utils';
export const prisma = new PrismaClient({
adapter: new PrismaBetterSqlite3({ url: 'file:./dev.db' }),
});
const builder = new SchemaBuilder<{
Scalars: {
DateTime: {
Input: Date;
Output: Date;
};
};
PrismaTypes: PrismaTypes;
}>({
plugins: [PrismaPlugin, PrismaUtils],
prisma: {
client: prisma,
dmmf: getDatamodel(),
},
});
builder.addScalarType('DateTime', DateTimeResolver, {});
```
## What can you do with this plugin [#what-can-you-do-with-this-plugin]
The helpers build filters, ordering, create inputs, and update inputs using the generated Prisma
types. You choose which fields and operations your API exposes.
## What is supported so far [#what-is-supported-so-far]
### Creating filter types for scalars and enums [#creating-filter-types-for-scalars-and-enums]
```typescript
const StringFilter = builder.prismaFilter('String', {
ops: ['contains', 'equals', 'startsWith', 'not'],
});
export const IDFilter = builder.prismaFilter('Int', {
ops: ['equals', 'not'],
});
builder.enumType(MyEnum, { name: 'MyEnum' });
const MyEnumFilter = builder.prismaFilter(MyEnum, {
ops: ['not', 'equals'],
});
```
### Creating filters for Prisma objects (compatible with a "where" clause) [#creating-filters-for-prisma-objects-compatible-with-a-where-clause]
```typescript
const UserWhere = builder.prismaWhere('User', {
fields: {
id: IDFilter,
},
});
const PostFilter = builder.prismaWhere('Post', {
fields: (t) => ({
// You can use either filters
id: IDFilter,
// or scalar types to only support equality
title: 'String',
createdAt: 'DateTime',
// Relations are supported by referencing other scalars
author: UserWhere,
// use t.field to provide other field options
authorId: t.field({ type: IDFilter, description: 'filter by author id' }),
}),
});
```
### Creating list filters for scalars [#creating-list-filters-for-scalars]
```typescript
export const StringListFilter = builder.prismaScalarListFilter('String', {
name: 'StringListFilter',
ops: ['has', 'hasSome', 'hasEvery', 'isEmpty', 'equals'],
});
```
### Creating list filters for Prisma objects [#creating-list-filters-for-prisma-objects]
```typescript
const UserListFilter = builder.prismaListFilter(UserWhere, {
ops: ['every', 'some', 'none'],
});
```
### Creating OrderBy input types [#creating-orderby-input-types]
```typescript
const UserOrderBy = builder.prismaOrderBy('User', {
fields: {
name: true,
},
});
export const PostOrderBy = builder.prismaOrderBy('Post', {
fields: () => ({
id: true,
title: true,
createdAt: true,
author: UserOrderBy,
}),
});
```
### Inputs for create mutations [#inputs-for-create-mutations]
You can use `builder.prismaCreate` to create input types for create mutations.
To get these types to work correctly for circular references, it is recommended to add explicit type
annotations, but for simple types that do not have circular references the explicit types can be
omitted.
```ts
import { InputObjectRef } from '@pothos/core';
import type { Prisma } from '../lib/prisma/client';
export const UserCreate: InputObjectRef = builder.prismaCreate('User', {
name: 'UserCreate',
fields: () => ({
// scalars
id: 'Int',
email: 'String',
name: 'String',
// inputs for relations need to be defined separately as shown below
profile: UserCreateProfile,
// create fields for list relations are defined just like normal relations.
// Pothos will automatically handle making the inputs lists
posts: UserCreatePosts,
}),
});
export const UserCreateProfile = builder.prismaCreateRelation('User', 'profile', {
fields: () => ({
// created with builder.prismaCreate as shown above for User
create: ProfileCreateWithoutUser,
// created with builder.prismaWhere
connect: ProfileUniqueFilter,
}),
});
export const UserCreatePosts = builder.prismaCreateRelation('User', 'posts', {
fields: () => ({
// created with builder.prismaCreate as shown above for User
create: PostCreateWithoutAuthor,
// created with builder.prismaWhere
connect: PostUniqueFilter,
}),
});
```
### Inputs for update mutations [#inputs-for-update-mutations]
You can use `builder.prismaUpdate` to create input types for update mutations.
To get these types to work correctly for circular references, it is recommended to add explicit type
annotations, but for simple types that do not have circular references the explicit types can be
omitted.
```ts
export const UserUpdate: InputObjectRef = builder.prismaUpdate(
'User',
{
name: 'UserUpdate',
fields: () => ({
id: 'Int',
email: 'String',
name: 'String',
// inputs for relations need to be defined separately as shown below
profile: UserUpdateProfile,
posts: UserUpdatePosts,
}),
},
);
export const UserUpdateProfile = builder.prismaUpdateRelation('User', 'profile', {
fields: () => ({
// created with builder.prismaCreate
create: ProfileCreateWithoutUser,
// created with builder.prismaUpdate
update: ProfileUpdateWithoutUser,
// created with builder.prismaWhereUnique
connect: ProfileUniqueFilter,
}),
});
export const UserUpdatePosts = builder.prismaUpdateRelation('User', 'posts', {
fields: () => ({
// Not all update methods need to be defined
// created with builder.prismaCreate
create: PostCreateWithoutAuthor,
// created with builder.prismaCreateMany
createMany: {
skipDuplicates: 'Boolean',
data: PostCreateManyWithoutAuthor,
},
// created with builder.prismaWhereUnique
set: PostUniqueFilter,
// created with builder.prismaWhereUnique
disconnect: PostUniqueFilter,
delete: PostUniqueFilter,
connect: PostUniqueFilter,
update: {
// created with builder.prismaWhereUnique
where: PostUniqueFilter,
// created with builder.prismaUpdate
data: PostUpdateWithoutAuthor,
},
updateMany: {
// created with builder.prismaWhere
where: PostWithoutAuthorFilter,
// created with builder.prismaUpdate
data: PostUpdateWithoutAuthor,
},
// created with builder.prismaWhere
deleteMany: PostWithoutAuthorFilter,
}),
});
```
#### Atomic Int Update operations [#atomic-int-update-operations]
```ts
const IntUpdate = builder.prismaIntAtomicUpdate();
// or with options
const IntUpdate = builder.prismaIntAtomicUpdate({
name: 'IntUpdate',
ops: ['increment', 'decrement'],
});
export const PostUpdate = builder.prismaUpdate('Post', {
name: 'PostUpdate',
fields: () => ({
title: 'String',
views: IntUpdate,
}),
});
```
## Generators [#generators]
Manually defining all the different input types shown above for a large number of tables can become
very repetitive. These utilities are designed to be building blocks for generators or utility
functions, so that you don't need to hand write these types yourself.
Pothos does not currently ship an official generator for prisma types, but there are a couple of
example generators that can be copied and modified to suit your needs. These are intentionally
somewhat limited in functionality and not written to be easily exported because they will be updated
with breaking changes as these utilities are developed further. They are only intended as building
blocks for you to build your own generators.
There are 2 main approaches:
1. Static Generation: Types are generated and written as a typescript file which can be imported
from as part of your schema
2. Dynamic Generation: Types are generated dynamically at runtime through helpers imported from your
App
### Static generator [#static-generator]
You can find an
[example static generator here](https://github.com/hayes/pothos/blob/main/packages/plugin-prisma-utils/tests/examples/codegen/generator.ts)
This generator will generate a file with input types for every table in your schema as shown
[here](https://github.com/hayes/pothos/blob/main/packages/plugin-prisma-utils/tests/examples/codegen/schema/prisma-inputs.ts)
These generated types can be used in your schema as shown
[here](https://github.com/hayes/pothos/blob/main/packages/plugin-prisma-utils/tests/examples/codegen/schema/index.ts)
### Dynamic generator [#dynamic-generator]
You can find an example
[dynamic generator here](https://github.com/hayes/pothos/blob/main/packages/plugin-prisma-utils/tests/examples/crud/generator.ts)
This generator exports a class that can be used to dynamically create input types for your builder
as shown
[here](https://github.com/hayes/pothos/blob/main/packages/plugin-prisma-utils/tests/examples/crud/schema/index.ts#L9-L20)
## Filters and draft inputs in one API [#filters-and-draft-inputs-in-one-api]
The publishing schema uses these helpers to expose a small, intentional input surface. A title
filter and ordering input support public post search:
```typescript
const TitleFilter = builder.prismaFilter('String', { ops: ['contains', 'equals'] });
const PostWhere = builder.prismaWhere('Post', {
fields: { title: TitleFilter },
});
const PostOrderBy = builder.prismaOrderBy('Post', { fields: { title: true, id: true } });
```
The `searchPosts` resolver combines the caller's filter with `published: true` using `AND`.
A caller can narrow the public result set, but cannot expand it to drafts. The selected ordering
also gets an ID tie breaker for deterministic results.
Draft creation and editing expose only title and content. The resolver supplies ownership from
the authenticated context and keeps publication state outside the input:
```typescript
const DraftInput = builder.prismaCreate('Post', {
name: 'DraftInput',
fields: { title: 'String', content: 'String' },
});
const DraftUpdate = builder.prismaUpdate('Post', {
name: 'DraftUpdate',
fields: { title: 'String', content: 'String' },
});
const CreateDraftResult = builder.objectRef<{ post: PostRow }>('CreateDraftResult').implement({
fields: (t) => ({ post: t.field({ type: Post, resolve: (result) => result.post }) }),
});
builder.mutationType({
fields: (t) => ({
createDraft: t.prismaField({
type: 'Post',
args: { input: t.arg({ type: DraftInput, required: true }) },
resolve: (query, _root, args, ctx) => {
return prisma.post.create({
...query,
data: { ...args.input, author: { connect: { id: ctx.userId } } },
});
},
}),
updateDraft: t.prismaField({
type: 'Post',
args: {
id: t.arg.int({ required: true }),
input: t.arg({ type: DraftUpdate, required: true }),
},
resolve: (query, _root, args, ctx) => {
return prisma.post.update({
...query,
where: { id: args.id, authorId: ctx.userId, published: false },
data: args.input,
});
},
}),
createDraftWithPayload: t.field({
type: CreateDraftResult,
args: {
title: t.arg.string({ required: true }),
content: t.arg.string({ required: true }),
},
resolve: async (_root, args, context, info) => {
return {
post: await prisma.post.create({
...queryFromInfo({ context, info, path: ['post'] }),
data: { ...args, authorId: context.userId },
}),
};
},
}),
}),
});
```
Creating “Mulching paths” returns an unpublished post owned by the current author, including its
requested author relation. Updating another author's draft fails and leaves its title unchanged.
`createDraftWithPayload` returns a post inside a result object, so `queryFromInfo` uses `path: ['post']` to plan the nested selection. These are application decisions; input generation does not
provide authorization by itself.
The [local setup](./setup#run-the-publishing-api) generates Prisma Utils types alongside the client
and runs these operations against SQLite. Scalar-list filters remain an alternative for databases
that support Prisma scalar lists.
# Query planning
URL: /docs/plugins/prisma/query-planning
How the plugin turns a GraphQL query into prisma queries
This page describes how the plugin turns a GraphQL query into prisma queries, which is worth
knowing when a schema issues more queries than you expect.
## How fields get their data [#how-fields-get-their-data]
A field either reads its data from a row that has already been loaded, or runs a query of its own.
A field's `select` is planned into the query of the nearest ancestor that runs one: a
`t.prismaField`, a `t.relation`, a connection, or a
[fallback query](./relations#fallback-queries). The field then reads what it needs off the loaded
row, without a query of its own. A field runs its own query when its `resolve` queries prisma
directly, and when the plugin issues a fallback query for a `t.relation` that is missing from the
row.
A field can do both. A `t.prismaField`, or any other field with a `select`, nested under one of
those ancestors has its `select` planned into the parent's row, and still runs its own query when
it resolves.
A field-level `select` is merged into the same query as its siblings and the type-level selection,
rather than getting a copy of the row for that field alone. Two selections of the same relation
share a place in that query only when their arguments (`where`, `orderBy`, `take`, ...) match. When
they differ, the first one planned wins, and the other is loaded with a query of its own. A
type-level `select` or `include` is planned before any field's selection, no matter where they
appear in the document, and fields are planned in the order they are selected.
## Async selections [#async-selections]
Selections are synchronous unless the schema opts in with `AsyncSelections: true`. This example
uses request context methods that asynchronously return a user ID and a preview limit:
```typescript
const builder = new SchemaBuilder<{
PrismaTypes: PrismaTypes;
AsyncSelections: true;
Context: {
currentUserId: () => Promise;
previewSize: () => Promise;
};
}>({
plugins: [PrismaPlugin],
prisma: {
client: prisma,
dmmf: getDatamodel(),
},
});
```
With the opt-in, `select` functions, relation `query` callbacks, `relationCount` `where` callbacks,
and the `select` and `query` callbacks of `prismaConnectionHelpers` may be async. Without it they
are typed as synchronous, and an async callback is a type error.
The plugin still builds a single query. It waits for the callbacks, and merges what they return
after every synchronous selection, in document order. `t.relation`, `t.relationCount`,
`t.prismaField`, `t.prismaConnection` and `t.relatedConnection` settle their plan before the
resolver runs, and need no changes. The following example uses a registered `Comment` Prisma
object ref and its Post.comments relation:
```typescript
builder.prismaObject('Post', {
fields: (t) => ({
comments: t.relation('comments', {
query: async (args, ctx) => ({ where: { authorId: await ctx.currentUserId() } }),
}),
previewComments: t.field({
type: [Comment],
select: async (args, ctx, nestedSelection) => ({
comments: await nestedSelection({ take: await ctx.previewSize() }),
}),
resolve: (post) => post.comments,
}),
}),
});
```
`await` what `nestedSelection` returns before putting it in the selection. A selection that
contains the promise itself will throw, and so will a `select` that returns while a nested
selection it started is still pending. Calling `nestedSelection` and discarding a synchronous
result is not detected, and the nested selection will not be loaded with the parent, so the field
falls back to its own query.
Pass `awaitSelections: true` to `queryFromInfo` and `prismaConnectionHelpers(...).getQuery`, and
`await` the query they return. Without it, an async selection beneath the field throws, and
whether there is one depends on the incoming document rather than on the callback you wrote. A
connection helper also throws when its own `select` or `query` is async, whatever the document
asked for:
```typescript
const post = await prisma.post.findUniqueOrThrow({
where: { id: args.id },
...(await queryFromInfo({ context, info, awaitSelections: true })),
});
```
`awaitSelections` is a per-call option, and is available whether or not the schema sets
`AsyncSelections`.
## Optimized queries without `t.prismaField` [#optimized-queries-without-tprismafield]
In some cases, it may be useful to get an optimized query for fields where you can't use
`t.prismaField`.
This may be required for combining with other plugins, or because your query does not directly
return a `PrismaObject`. In these cases, you can use the `queryFromInfo` helper. An example of this
might be a mutation that wraps the prisma object in a result type.
The example assumes a Post model with `id`, `title`, and `authorId` columns, an existing User
record for `context.userId`, and the [builder setup](./setup) with that context type.
```typescript
import type { Post as PostRow } from '../lib/prisma/client';
import { queryFromInfo } from '@pothos/plugin-prisma';
const Post = builder.prismaObject('Post', {
fields: (t) => ({
title: t.exposeString('title'),
}),
});
const CreatePostResult = builder.objectRef<{
success: boolean;
post: PostRow | null;
}>('CreatePostResult').implement({
fields: (t) => ({
success: t.exposeBoolean('success'),
post: t.field({
type: Post,
nullable: true,
resolve: (result) => result.post,
}),
}),
});
builder.mutationType({
fields: (t) => ({
createPost: t.field({
type: CreatePostResult,
args: {
title: t.arg.string({ required: true }),
},
resolve: async (parent, args, context, info) => {
if (!args.title.trim()) {
return { success: false, post: null };
}
const post = await prisma.post.create({
...(await queryFromInfo({
context,
info,
path: ['post'],
awaitSelections: true,
})),
data: {
title: args.title,
authorId: context.userId,
},
});
return { success: true, post };
},
}),
}),
});
```
The columns and relations the query selected come back on the rows, along with anything you passed
in as `select`, and the rows are typed to match.
To require data even when the client does not request it, pass an initial `select` (or `include`)
to `queryFromInfo`. For example, add `select: { id: true }` beside `path` in the call above to
ensure the returned post includes its ID. When nothing is selected at `path`, the helper returns
that initial selection unchanged, or an empty query object if no initial selection was supplied.
The `path` is followed through fragments in the query, including inline fragments and fragment
spreads that narrow an interface or union to one of its implementations. Every selection of the
field that is found is merged into the query. If several implementations share a field name,
matches whose field returns a different Prisma model are ignored. When you need to target a
specific implementation, a segment can be written as `{ name, type }`. The field then only matches
when it is selected directly, or under a fragment on that type or one of its subtypes:
```typescript
const user = await prisma.user.findUniqueOrThrow({
where: { id: args.id },
...queryFromInfo({
context,
info,
typeName: 'User',
// only match `appointment` when selected inside `... on AppointmentEntry`
path: [{ name: 'appointment', type: 'AppointmentEntry' }],
}),
});
// user is loaded with the selections from `... on AppointmentEntry`,
// and nothing from an `appointment` field on another implementation
```
## Conflicting selections between variants [#conflicting-selections-between-variants]
When a query selects two variants of one model for the same row, either with a fragment on each
under one field, or through a `t.variant` field, the request fails if their type-level
`select`/`include` ask for the same relation with different arguments. The client sees
`Unexpected error.`, and the error's `cause` explains the conflict:
```
PothosValidationError: Type-level selections of Viewer and Admin conflict on relation "posts".
Move the relation arguments to a field-level select on one of the types.
```
Both variants describe one row, so their type-level selections are merged into a single query.
To fix this, keep the relation with its arguments in the `select` of the field that needs it, on
one of the variants. A field-level selection that conflicts with what the row already holds falls
back to a query of its own, rather than failing the request.
## Compare two orderings of one relation [#compare-two-orderings-of-one-relation]
A client may need both the newest and oldest published posts on one author page:
```graphql
query CompareOrderings {
author(id: 1) {
newest: posts {
title
}
oldest: posts(oldestFirst: true) {
title
}
}
}
```
The [published-posts field](./relations#published-posts-and-profiles) translates those arguments
into different database orderings. Both results must retain their own order: “A guide to
composting” comes first in `newest`, and “Starting a seed library” comes first in `oldest`.
They cannot reuse the same loaded relation. The additional ordering is loaded through a fallback
query. The publishing example checks both results and the emitted SQL; a response alone would not
establish how the relation was loaded.
Prisma client calls and SQL statements are different measures: a single Prisma query can issue
several SQL statements to load related tables. Compare the emitted SQL without assuming one
statement per Prisma call.
# Relations
URL: /docs/plugins/prisma/relations
Adding relations to prisma objects
Use `t.relation` to expose relations between models. This example uses the client and authenticated
`userId` context from [Setup](./setup):
```typescript
builder.queryType({
fields: (t) => ({
me: t.prismaField({
type: 'User',
resolve: async (query, root, args, ctx, info) => {
return prisma.user.findUniqueOrThrow({
...query,
where: { id: ctx.userId },
});
},
}),
}),
});
builder.prismaObject('User', {
fields: (t) => ({
id: t.exposeID('id'),
email: t.exposeString('email'),
posts: t.relation('posts'),
}),
});
builder.prismaObject('Post', {
fields: (t) => ({
id: t.exposeID('id'),
title: t.exposeString('title'),
author: t.relation('author'),
}),
});
```
`t.relation` defines a field that can be pre-loaded by a parent resolver. This will create something
like `{ include: { author: true }}` that will be passed as part of the `query` argument of a
`prismaField` resolver. If the parent is another `relation` field, the includes will become nested,
and the full relation chain will be passed to the `prismaField` that started the chain.
For example the query:
```graphql
query {
me {
posts {
author {
id
}
}
}
}
```
the `me` `prismaField` would receive something like the following as its query parameter:
```typescript
{
include: {
posts: {
include: {
author: true;
}
}
}
}
```
When selections cannot share a Prisma query, Pothos loads the missing data with fallback queries.
### Fallback queries [#fallback-queries]
There are some cases where data can not be pre-loaded by a prisma field. In these cases, pothos will
issue a `findUnique` query for the parent of any fields that were not pre-loaded, and select the
missing relations so those fields can be resolved with the correct data. These queries should be
very efficient, are batched by pothos to combine requirements for multiple fields into one query,
and batched by Prisma to combine multiple queries (in an n+1 situation) to a single sql query.
The following are some edge cases that could cause an additional query to be necessary:
* The parent object was not loaded through a field defined with `t.prismaField`, or `t.relation`
* The root `prismaField` did not correctly spread the `query` arguments in its prisma call.
* The query selects multiple fields that use the same relation with different filters, sorting, or
limits
* The query contains multiple aliases for the same relation field with different arguments in a way
that results in different query options for the relation.
* A relation field has a query that is incompatible with the default includes of the parent object
A fallback query loads the parent row again by its primary key, or by the first required unique
field or index when the model has no primary key, selecting what the missing fields need. To load
it some other way, add a `findUnique` option to the type that returns the `where` for
`prisma..findUnique`:
```typescript
builder.prismaObject('User', {
findUnique: (user, ctx) => ({ email: user.email }),
fields: (t) => ({
id: t.exposeID('id'),
posts: t.relation('posts'),
}),
});
```
A type in include mode can also opt out of fallback queries with `findUnique: null`. A field that
would need one will throw `Missing findUnique for User` instead of querying.
With `ScopeAuthPlugin`, scope checks that read the parent see what its type loads (its `select`, or
every scalar and its `include`), even for a row returned from a plain `t.field`, unless the type
sets `findUnique: null`. A row missing that data is reloaded, so a row that no longer exists fails
the field with `Unexpected error.`. To return a deleted row, use `t.prismaField` and pass its
`query` to `delete`. Only top-level keys are checked, so a row with a partly loaded relation isn't
reloaded. When `ScopeAuthPlugin` comes before `PrismaPlugin`, a field's own `select` is loaded after
its `authScopes` run.
### Filters, Sorting, and arguments [#filters-sorting-and-arguments]
So far we have been describing very simple queries without any arguments, filtering, or sorting. For
`t.prismaField` definitions, you can add arguments to your field like normal, and pass them into
your prisma query as needed. For `t.relation` the flow is slightly different because we are not
making a prisma query directly. We do this by adding a `query` option to our field options. Query
can either be a query object, or a method that returns a query object based on the field arguments.
```typescript
builder.prismaObject('User', {
fields: (t) => ({
id: t.exposeID('id'),
posts: t.relation('posts', {
// We can define arguments like any other field
args: {
oldestFirst: t.arg.boolean(),
},
// Then we can generate our query conditions based on the arguments
query: (args, context) => ({
orderBy: {
createdAt: args.oldestFirst ? 'asc' : 'desc',
},
}),
}),
}),
});
```
The returned query object will be added to the include section of the `query` argument that gets
passed into the first argument of the parent `t.prismaField`, and can include things like `where`,
`skip`, `take`, and `orderBy`. The `query` function will be passed the arguments for the field, and
the context for the current request. Because it is used for pre-loading data, and solving n+1
issues, it can not be passed the `parent` object because it may not be loaded yet.
### Nullable relations and `onNull` [#nullable-relations-and-onnull]
A relation that is optional in the prisma schema (`profile Profile?`) can be exposed as a nullable
field with `nullable: true`. To expose it as non-nullable, `t.relation` requires an `onNull` option
describing what should happen when the related row is missing. Setting it to `'error'` lets GraphQL
raise the non-null error for the field. A function can return a replacement value instead, or an
`Error` to raise:
```typescript
builder.prismaObject('User', {
fields: (t) => ({
profile: t.relation('profile', { nullable: true }),
// An error when the user has no profile
requiredProfile: t.relation('profile', { nullable: false, onNull: 'error' }),
// A default when the user has no profile
profileOrDefault: t.relation('profile', {
nullable: false,
onNull: (user, args, ctx, info) => ({ id: 0, userId: user.id, bio: null }),
}),
}),
});
```
### relationCount [#relationcount]
`t.relationCount` adds a field that counts related rows without loading them. Its `where` option
filters the rows included in the count:
```typescript
builder.prismaObject('User', {
fields: (t) => ({
id: t.exposeID('id'),
postCount: t.relationCount('posts', {
where: {
published: true,
},
}),
}),
});
```
## Published posts and profiles [#published-posts-and-profiles]
In a publishing API, a public author page exposes published posts and an optional profile.
Drafts belong on the [private viewer](./variants#the-authors-writing-desk). Filtering the list and
its count consistently prevents the count from revealing unpublished posts. Sorting by both the
timestamp and ID makes the order deterministic when two posts share a timestamp.
The following type is used by the nullable `author` lookup in [Objects](./objects#a-nullable-author-lookup):
```typescript
builder.prismaNode('User', {
id: { field: 'id' },
select: { id: true },
fields: (t) => ({
name: t.exposeString('name'),
bio: t.string({
nullable: true,
select: { profile: { select: { bio: true } } },
resolve: (user) => user.profile?.bio,
}),
posts: t.relation('posts', {
args: { oldestFirst: t.arg.boolean() },
query: (args) => ({
where: { published: true },
orderBy: [
{ createdAt: args.oldestFirst ? 'asc' : 'desc' },
{ id: args.oldestFirst ? 'asc' : 'desc' },
],
}),
}),
postCount: t.relationCount('posts', { where: { published: true } }),
postsConnection: t.relatedConnection('posts', {
cursor: 'id',
query: { where: { published: true }, orderBy: { id: 'asc' } },
totalCount: true,
}),
}),
});
```
`author(id: 1) { name bio postCount posts { title } }` returns Maya's profile and two
published posts. Nora (`id: 3`) has no profile or posts, so `bio` is null, `posts` is empty, and
`postCount` is zero. A missing relation is represented as missing data, without manufacturing a
profile record.
# Relay nodes
URL: /docs/plugins/prisma/relay
Using the Prisma and Relay plugins together
`prismaNode` adds Relay IDs and root node lookups to Prisma objects. Register the
[Relay plugin](/docs/plugins/relay); see [Connections](./connections) for pagination.
Defining a node creates a direct lookup through `node` and `nodes`. These lookups bypass custom
root resolvers, so permission checks or visibility filters on a list or parent field do not protect
node refetches. An encoded global ID is an identifier, not proof of permission.
Apply an access policy to node loading, the type, or its fields as appropriate. See
[Authorizing Relay nodes](/docs/plugins/scope-auth/relay-nodes) for scope patterns and their limits.
### `prismaNode` [#prismanode]
The `prismaNode` method works just like the `prismaObject` method with a couple of small
differences:
* there is a new `id` option that mirrors the `id` option from `node` method of the relay plugin,
and must contain a resolve function that returns the id from an instance of the node. Rather than
defining a resolver for the id field, you can set the `field` option to the name of a unique
column or index.
```typescript
builder.prismaNode('Post', {
// This sets what database field to use for the nodes id field
id: { field: 'id' },
// fields work just like they do for builder.prismaObject
fields: (t) => ({
title: t.exposeString('title'),
author: t.relation('author'),
}),
});
```
If you need to customize how ids are formatted, you can add a resolver for the `id`, and provide a
`findUnique` option that can be used to load the node by its id. This is generally not necessary.
```typescript
builder.prismaNode('Post', {
id: { resolve: (post) => String(post.id) },
// The return value will be passed as the `where` of a `prisma.post.findUnique`
findUnique: (id) => ({ id: Number.parseInt(id, 10) }),
fields: (t) => ({
title: t.exposeString('title'),
author: t.relation('author'),
}),
});
```
When executing the `node(id: ID!)` query with a global ID for which prisma cannot find a record in
the database, the default behavior is to throw an error. There are some scenarios where it is
preferable to return `null` instead of throwing an error. For this you can add the `nullable: true`
option:
```typescript
builder.prismaNode('Post', {
id: { field: 'id' },
nullable: true,
fields: (t) => ({
title: t.exposeString('title'),
author: t.relation('author'),
}),
});
```
## Refetch a public author [#refetch-a-public-author]
The publishing schema defines User as a Relay node. Its returned ID can be passed back to
`node` to fetch the author with a new selection:
```graphql
query RefetchAuthor {
node(id: "VXNlcjox") {
... on User {
name
posts {
title
}
}
}
}
```
The ID identifies User 1. The node returns the same public fields as the author lookup, including
only published posts. A node lookup does not route through a custom root resolver, so any
access restrictions on an entity must also hold when it is loaded as a node.
Post node lookups additionally restrict rows to published posts or the requesting author's own
drafts. With `nullable: true`, a missing or inaccessible Post returns null. Merely hiding a draft
from the public connection would not prevent a client from refetching it by a known node ID.
# Selections
URL: /docs/plugins/prisma/selections
how to use custom includes and selections to optimize your prisma queries
## Includes on types [#includes-on-types]
In some cases, you may want to always pre-load certain relations. This can be helpful for defining
fields directly on type where the underlying data may come from a related table.
```typescript
builder.prismaObject('User', {
// This will always include the profile when a user object is loaded. Deeply nested relations can
// also be included this way.
include: {
profile: true,
},
fields: (t) => ({
id: t.exposeID('id'),
email: t.exposeString('email'),
bio: t.string({
// The profile relation is nullable, so this field is too.
nullable: true,
// The profile relation will always be loaded, and user will now be typed to include the
// profile field so you can return the bio from the nested profile relation.
resolve: (user) => user.profile?.bio,
}),
}),
});
```
## Select mode for types [#select-mode-for-types]
By default, the prisma plugin will use `include` when including relations, or generating fallback
queries. This means we are always loading all columns of a table when loading it in a
`t.prismaField` or a `t.relation`. This is usually what we want, but in some cases, you may want to
select specific columns instead. This can be useful if you have tables with either a very large
number of columns, or specific columns with large payloads you want to avoid loading.
To do this, you can add a `select` instead of an include to your `prismaObject`:
```typescript
builder.prismaObject('User', {
select: {
id: true,
},
fields: (t) => ({
id: t.exposeID('id'),
email: t.exposeString('email'),
}),
});
```
The `t.expose*` and `t.relation` methods will all automatically add selections for the exposed
fields when those fields are queried, ensuring that only the requested columns will be loaded from the
database.
In addition to the `t.expose` and `t.relation`, you can also add custom selections to other fields:
```typescript
builder.prismaObject('User', {
select: {
id: true,
},
fields: (t) => ({
id: t.exposeID('id'),
email: t.exposeString('email'),
bio: t.string({
nullable: true,
// This will select user.profile.bio when the `bio` field is queried
select: {
profile: {
select: {
bio: true,
},
},
},
resolve: (user) => user.profile?.bio,
}),
}),
});
```
A field-level `select` always adds to the row of the model the field is defined on, whatever type
the field returns. A `select` on a `t.prismaField` defined on `User` adds columns to the user its
resolver receives as the parent, not to the model the field returns.
## Using arguments or context in your selections [#using-arguments-or-context-in-your-selections]
This field selects the first comment created after the supplied date, or returns null when none
exists. It assumes a registered `Date` scalar accepting JavaScript dates:
```typescript
const Post = builder.prismaObject('Post', {
fields: (t) => ({
title: t.exposeString('title'),
commentFromDate: t.string({
nullable: true,
args: {
date: t.arg({ type: 'Date', required: true }),
},
select: (args) => ({
comments: {
take: 1,
orderBy: [{ createdAt: 'asc' }, { id: 'asc' }],
where: {
createdAt: {
gt: args.date,
},
},
},
}),
resolve: (post) => post.comments[0]?.content,
}),
}),
});
```
## Only load a profile when requested [#only-load-a-profile-when-requested]
The [public author type](./relations#published-posts-and-profiles) keeps its default selection
small. Its `bio` field declares the profile relation it needs, so a query for the author's name
alone does not load a profile. Adding `bio` adds that relation to the database query; the resolver
then reads the selected row. A nullable profile still produces a nullable biography.
This distinction matters for computed fields: a resolver accessing related data must declare that
data in its selection, even when the GraphQL field itself is just a string.
# Setup
URL: /docs/plugins/prisma/setup
Setting up the Prisma plugin
This plugin supports Prisma ORM 7 only; install `prisma@^7`. For Prisma ORM 8, see
[Prisma Next](/docs/plugins/prisma-next).
npm
pnpm
yarn
bun
```bash
npm install --save @pothos/plugin-prisma
```
```bash
pnpm add @pothos/plugin-prisma
```
```bash
yarn add @pothos/plugin-prisma
```
```bash
bun add @pothos/plugin-prisma
```
## Setup [#setup]
The Prisma plugin uses generated types to describe your models and relations. Add its generator
alongside your Prisma client generator, then pass the generated types and datamodel to the builder.
### Add the `pothos` generator to your prisma schema [#add-the-pothos-generator-to-your-prisma-schema]
```prisma
generator pothos {
provider = "prisma-pothos-types"
}
```
Now the types Pothos uses will be generated whenever you re-generate your prisma client. Run the
following command to re-generate the client and create the new types:
```sh
npx prisma generate
```
Generator options:
* `clientOutput`: Where the generated code will import the PrismaClient from. The default is the
full path of wherever the client is generated. If you are checking in the generated file,
you should specify a relative path for this import
* `output`: Where to write the generated types
Example with more options:
```prisma
generator client {
provider = "prisma-client"
output = "../lib/prisma"
}
generator pothos {
provider = "prisma-pothos-types"
clientOutput = "./prisma" // relative path from pothos output to prisma client
output = "../lib/pothos-prisma-types.ts"
}
```
If model or relation completions are missing, check the client import in the generated file.
### Set up the builder [#set-up-the-builder]
This example uses the generated client above and a SQLite database. Install
`@prisma/adapter-better-sqlite3` for this adapter; use the adapter for your database if it differs.
`exposeDescriptions` also accepts `{ models: true, fields: true }` to configure descriptions separately.
```typescript
import SchemaBuilder from '@pothos/core';
import { PrismaClient } from '../lib/prisma/client';
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3';
import PrismaPlugin from '@pothos/plugin-prisma';
import type PrismaTypes from '../lib/pothos-prisma-types'; // path to generated types, specified in your prisma.schema
import { getDatamodel } from '../lib/pothos-prisma-types';
const prisma = new PrismaClient({
adapter: new PrismaBetterSqlite3({ url: 'file:./dev.db' }),
});
const builder = new SchemaBuilder<{
PrismaTypes: PrismaTypes;
Context: { userId: number };
}>({
plugins: [PrismaPlugin],
prisma: {
client: prisma,
// This give pothos information about your tables, relations, and indexes to help it generate optimal queries at runtime.
// This used to be attached to the prisma client, but has been removed in most runtimes/modes to reduce bundle size.
dmmf: getDatamodel(),
// defaults to false, uses /// comments from prisma schema as descriptions
// for object types, relations and exposed fields.
// descriptions can be omitted by setting description to false
exposeDescriptions: false,
// use where clause from prismaRelatedConnection for totalCount (defaults to true)
filterConnectionTotalCount: true,
// warn when not using a query parameter correctly
onUnusedQuery: process.env.NODE_ENV === 'production' ? null : 'warn',
// leave selections inside @defer fragments out of the planned query (defaults to true)
skipDeferredFragments: true,
},
});
```
The examples use an authenticated request context with `userId: number`. Supply it through your
GraphQL server; see [Context](/docs/guide/context).
Pass the Prisma client through the plugin options. Including its full type in `Context` can slow
TypeScript checking; see [this TypeScript issue](https://github.com/microsoft/TypeScript/issues/45405).
You can also load or create the prisma client dynamically for each request. This can be used to
periodically re-create clients or create read-only clients for certain types of users.
Replace the builder above with the following to select between `prisma` and a second client.
This SQLite example uses `READ_ONLY_REPLICA_URL` for a replica database maintained by your application;
the adapter does not configure replication or enforce read-only access. Configure database access
permissions separately. Both clients must use the same generated Prisma client
and schema. For another database, use its driver adapter and replica connection options.
```typescript
const replicaUrl = process.env.READ_ONLY_REPLICA_URL;
if (!replicaUrl) {
throw new Error('READ_ONLY_REPLICA_URL is required');
}
const readOnlyPrisma = new PrismaClient({
adapter: new PrismaBetterSqlite3({ url: replicaUrl }),
});
const builder = new SchemaBuilder<{
Context: { user: { isAdmin: boolean } };
PrismaTypes: PrismaTypes;
}>({
plugins: [PrismaPlugin],
prisma: {
client: (ctx) => (ctx.user.isAdmin ? prisma : readOnlyPrisma),
dmmf: getDatamodel(),
},
});
```
## Detecting unused query arguments [#detecting-unused-query-arguments]
Forgetting to spread the `query` argument from `t.prismaField` or `t.prismaConnection` into your
prisma query can result in inefficient queries, or even missing data. To help catch these issues,
the plugin can warn you when you are not using the query argument correctly.
The `onUnusedQuery` option can be set to `warn` or `error` to enable this feature. When set to
`warn` it will log a warning to the console if Pothos detects that you have not properly used the
query in your resolver. Similarly if you set the option to `error` it will throw an error instead.
You can also pass a function which will receive the `info` object which can be used to log or throw
your own error.
The check tracks access to properties on the query object. If no properties are accessed on the query object before the
resolver returns, it will trigger the `onUnusedQuery` condition.
It's recommended to enable this check in development to more quickly find potential issues.
## Deferred fragments [#deferred-fragments]
`skipDeferredFragments` controls query planning; it does not enable incremental execution. The application
must register the defer directive and use an executor, server transport, and client that support
the same incremental delivery protocol. With GraphQL.js 17, ordinary `execute` rejects schemas
containing `@defer` or `@stream`; incremental execution uses `experimentalExecuteIncrementally`.
For a server integration, see [GraphQL Yoga's defer and stream setup](https://the-guild.dev/graphql/yoga-server/docs/features/defer-stream),
which uses `@graphql-yoga/plugin-defer-stream`. Check the integration's supported versions when
choosing an executor and client; adding directives or changing `skipDeferredFragments` alone is
not sufficient.
Selections inside a `@defer` fragment are left out of the planned query by default, so the initial
payload is not delayed by data the client has agreed to wait for. When the deferred fragment
resolves, its fields are loaded through [fallback queries](./relations#fallback-queries), batched as
usual.
Set `skipDeferredFragments: false` in the plugin options to plan deferred selections with the rest
of the query. `queryFromInfo` accepts the same option per call.
## Run the publishing API [#run-the-publishing-api]
The [publishing example](https://github.com/hayes/pothos/tree/main/website/local-examples/prisma)
uses this SQLite adapter and generated client with one schema for author pages, private drafts,
media attachments, and pagination. Prisma Utils adds filters and draft inputs to that same schema.
The example's `schema.prisma` defines User, Profile, Post, Comment, Media, and PostMedia, including
the foreign keys and unique constraints needed by their relations.
From a checkout of the Pothos repository, with Node.js 22 or newer:
```sh
pnpm install --frozen-lockfile
pnpm --dir website check:local
npm --prefix website/local-examples run prisma -- author
```
The setup generates both clients and SQLite DDL from the Prisma schema. Each run creates a fresh
temporary database and removes it afterward. The output contains the GraphQL response and emitted
SQL. The [example guide](https://github.com/hayes/pothos/blob/main/website/local-examples/prisma/README.md)
lists the operations and suggested edits. Applications should use Prisma migrations for their
persistent databases.
The request's `userId` comes from authentication in an application. The local runner supplies it as
a fixture. Public author fields return published posts; private viewer fields return only the
current author's drafts. Ownership and publication checks also apply to draft updates and node
refetches. Selecting a GraphQL type does not by itself enforce those restrictions.
# Type variants
URL: /docs/plugins/prisma/variants
How to define multiple GraphQL types based on the same prisma model
The prisma plugin supports defining multiple GraphQL types based on the same prisma model.
Additional types are called `variants`. Define a primary type as shown in [Objects](./objects).
The examples below are alternatives using the User and Post models, with an integer User id,
a User.posts relation, and a Boolean Post.published column. Node examples require the
[Relay plugin](./relay). Use the authenticated `userId` context from [Setup](./setup).
Define an additional variant by providing `variant` instead of `name`:
```typescript
const Viewer = builder.prismaObject('User', {
variant: 'Viewer',
fields: (t) => ({
id: t.exposeID('id'),
}),
});
```
You can define variant fields that reference one variant from another:
```typescript
const Viewer = builder.prismaObject('User', {
variant: 'Viewer',
fields: (t) => ({
id: t.exposeID('id'),
// Using the model name ('User') will reference the primary variant
user: t.variant('User'),
}),
});
const User = builder.prismaNode('User', {
id: { field: 'id' },
fields: (t) => ({
// To reference another variant, use the returned object Ref instead of the model name:
viewer: t.variant(Viewer, {
// return null for viewer if the parent User is not the current user
isNull: (user, args, ctx) => user.id !== ctx.userId,
}),
email: t.exposeString('email'),
}),
});
```
You can also use variants when defining relations by providing a `type` option:
```typescript
const PostDraft = builder.prismaNode('Post', {
variant: 'PostDraft',
// This sets what database field to use for the nodes id field
id: { field: 'id' },
// fields work just like they do for builder.prismaObject
fields: (t) => ({
title: t.exposeString('title'),
author: t.relation('author'),
}),
});
const Viewer = builder.prismaObject('User', {
variant: 'Viewer',
fields: (t) => ({
id: t.exposeID('id'),
drafts: t.relation('posts', {
// This will cause this relation to use the PostDraft variant rather than the default Post variant
type: PostDraft,
query: { where: { published: false } },
}),
}),
});
```
You may run into circular reference issues if you use 2 prisma object refs to reference each other.
To avoid this, you can split out the field definition for one of the relationships using
`builder.prismaObjectField`
```typescript
const Viewer = builder.prismaObject('User', {
variant: 'Viewer',
fields: (t) => ({
id: t.exposeID('id'),
}),
});
const User = builder.prismaNode('User', {
id: { field: 'id' },
fields: (t) => ({
email: t.exposeString('email'),
}),
});
// Add the reference after both types have been defined.
builder.prismaObjectField(Viewer, 'user', (t) => t.variant(User));
```
This same workaround applies when defining relations using variants.
Two variants of one model selected for the same row have their type-level selections merged into a
single query, which can fail if they disagree. See
[Conflicting selections between variants](./query-planning#conflicting-selections-between-variants).
## The author’s writing desk [#the-authors-writing-desk]
A public User and a private Viewer can represent the same row. The Viewer exposes the current
author's email and drafts, and its `user` field returns the public representation. The root `me`
resolver uses the authenticated context ID; it does not accept an arbitrary author's ID.
This version makes Viewer an interface so editor and author accounts can expose different fields:
```typescript
const Viewer = builder.prismaInterface('User', {
variant: 'Viewer',
select: { id: true, isAdmin: true },
resolveType: (user) => (user.isAdmin ? 'EditorViewer' : 'AuthorViewer'),
fields: (t) => ({
user: t.variant('User'),
email: t.exposeString('email'),
drafts: t.relation('posts', {
query: { where: { published: false }, orderBy: { id: 'asc' } },
}),
}),
});
builder.prismaObject('User', {
variant: 'EditorViewer',
interfaces: [Viewer],
select: { id: true, isAdmin: true },
fields: (t) => ({ canReviewSubmissions: t.boolean({ resolve: () => true }) }),
});
builder.prismaObject('User', {
variant: 'AuthorViewer',
interfaces: [Viewer],
select: { id: true, isAdmin: true },
});
```
With Maya's context (`userId: 1`), `me` is an EditorViewer with the draft “Planning the spring
exchange.” With Leo's context (`userId: 2`), it is an AuthorViewer with “Saving rainwater.”
The interface describes the result shape; the root lookup supplies the ownership restriction.
Neither public author fields nor public post connections return those drafts.
# Prisma without a plugin
URL: /docs/plugins/prisma/without-a-plugin
Using Prisma without a plugin
Use `builder.objectRef` with the generated Prisma model types and write resolvers that query your client.
This example uses a generated Prisma client with User and Post models, backed by SQLite.
Use the driver adapter for your database, and adjust the client import to your generated output.
Create a builder with an authenticated `userId` context:
```typescript
import SchemaBuilder from '@pothos/core';
import { PrismaClient, type Post, type User } from '../lib/prisma/client';
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3';
const builder = new SchemaBuilder<{ Context: { userId: number } }>({});
const db = new PrismaClient({
adapter: new PrismaBetterSqlite3({ url: 'file:./dev.db' }),
});
const UserObject = builder.objectRef('User');
const PostObject = builder.objectRef('Post');
UserObject.implement({
fields: (t) => ({
id: t.exposeID('id'),
email: t.exposeString('email'),
posts: t.field({
type: [PostObject],
resolve: (user) => {
return db.post.findMany({
where: { authorId: user.id },
});
},
}),
}),
});
PostObject.implement({
fields: (t) => ({
id: t.exposeID('id'),
title: t.exposeString('title'),
author: t.field({
type: UserObject,
resolve: (post) => {
return db.user.findUniqueOrThrow({
where: { id: post.authorId },
});
},
}),
}),
});
builder.queryType({
fields: (t) => ({
me: t.field({
type: UserObject,
resolve: (root, args, ctx) => {
return db.user.findUniqueOrThrow({
where: { id: ctx.userId },
});
},
}),
}),
});
```
This sets up User, and Post objects with a few fields, and a `me` query that returns the current
user. There are a few things to note in this setup:
1. We split up the `builder.objectRef` and the `implement` calls, rather than calling
`builder.objectRef(...).implement(...)`. This prevents typescript from getting tripped up by the
circular references between posts and users.
2. We use `findUniqueOrThrow` because those fields are not nullable. Using `findUnique`, prisma will
return a null if the object is not found. An alternative is to mark these fields as nullable.
3. The refs to our object types are called `UserObject` and `PostObject`, this is because `User` and
`Post` are the names of the types imported from prisma. We could instead alias the types when we
import them so we can name the refs to our GraphQL types after the models.
This setup is fairly simple, but it is easy to see the n+1 issues we might run into. Prisma helps
with this by batching queries together, but there are also things we can do in our implementation to
improve things.
One thing we could do if we know we will usually be loading the author any time we load a post is to
include the author in the backing type for a post. Replace the refs and implementations above with:
```typescript
const UserObject = builder.objectRef('User');
// We add the author here in the objectRef
const PostObject = builder.objectRef('Post');
UserObject.implement({
fields: (t) => ({
id: t.exposeID('id'),
email: t.exposeString('email'),
posts: t.field({
type: [PostObject],
resolve: (user) => {
return db.post.findMany({
// We now need to include the author when we query for posts
include: {
author: true,
},
where: { authorId: user.id },
});
},
}),
}),
});
PostObject.implement({
fields: (t) => ({
id: t.exposeID('id'),
title: t.exposeString('title'),
author: t.field({
type: UserObject,
// Now we can just return the author from the post instead of querying for it
resolve: (post) => post.author,
}),
}),
});
```
We may not always want to query for the author though, so we could make the author optional and fall
back to a query if the parent resolver did not include it. Replace `PostObject` and its implementation with:
```typescript
const PostObject = builder.objectRef('Post');
PostObject.implement({
fields: (t) => ({
id: t.exposeID('id'),
title: t.exposeString('title'),
author: t.field({
type: UserObject,
resolve: (post) =>
post.author ?? db.user.findUniqueOrThrow({ where: { id: post.authorId } }),
}),
}),
});
```
With this setup, a parent resolver has the option to include the author, but we have a fallback
in case it does not.
The [Dataloader plugin](/docs/plugins/dataloader) provides another way to batch loads across resolvers.
## Compare the same author page [#compare-the-same-author-page]
The publishing schema also defines the author lookup with ordinary object refs, using the same
Prisma models and seeded database. The refs describe the backing rows; each relation resolver
queries Prisma explicitly:
```typescript
const builder = new SchemaBuilder({});
const Author = builder.objectRef('Author');
const Article = builder.objectRef('Article');
Author.implement({
fields: (t) => ({
name: t.exposeString('name'),
posts: t.field({
type: [Article],
resolve: (author) => {
return prisma.post.findMany({
where: { authorId: author.id, published: true },
orderBy: [{ createdAt: 'desc' }, { id: 'desc' }],
});
},
}),
}),
});
Article.implement({
fields: (t) => ({
title: t.exposeString('title'),
author: t.field({
type: Author,
resolve: (post) => {
return prisma.user.findUniqueOrThrow({
where: { id: post.authorId },
});
},
}),
}),
});
builder.queryType({
fields: (t) => ({
author: t.field({
type: Author,
nullable: true,
args: { id: t.arg.int({ required: true }) },
resolve: (_root, args) => {
return prisma.user.findUnique({
where: { id: args.id },
});
},
}),
}),
});
```
`author(id: 1) { name posts { title author { name } } }` returns the same published posts as the
plugin-backed lookup. The plugin version plans relation selections into the root query; this
version makes those calls in its field resolvers. The selection, eager-loading, and fallback
alternatives above remain useful when choosing how to manage those calls yourself.
# Connections
URL: /docs/plugins/prisma-next/connections
Relay connections backed by Prisma Collections
Connections add cursor pagination to a list. They require the [Relay plugin](../relay). Use
`t.prismaConnection` to paginate a model's rows, and `t.relatedConnection` to paginate a relation.
## Paginate a query field [#paginate-a-query-field]
```ts
builder.queryFields((t) => ({
posts: t.prismaConnection({
type: 'Post',
query: { where: { published: 1 }, orderBy: { createdAt: 'desc' } },
totalCount: true,
}),
}));
```
Clients request pages with `first` and `after`, or `last` and `before`. For example:
```graphql
{
posts(first: 10) {
edges { node { title } }
pageInfo { endCursor hasNextPage }
totalCount
}
}
```
## Paginate a relation [#paginate-a-relation]
To paginate a relation, such as a user's posts, use `t.relatedConnection`:
```typescript playground example="plugin-prisma-next" query="{ author(id: 1) { fullName postConnection(first: 1) { totalCount edges { node { title } } pageInfo { endCursor hasNextPage } } } }"
// Pages through the relation inside the query that loads the user.
builder.prismaObjectField('User', 'postConnection', (t) =>
t.relatedConnection('posts', {
query: { where: { published: 1 } },
totalCount: true,
}),
);
```
It takes the same options as `t.prismaConnection` except `collection`. `totalCount` counts the rows
matching `query.where`, unless [`filterConnectionTotalCount`](./setup#plugin-options) is `false`.
## Configure a connection [#configure-a-connection]
The plugin orders the model's Collection by `query.orderBy` and the cursor, pages it, and adds what
the query selects. `t.prismaConnection` takes:
* `query`: `{ where, orderBy }`, or `(args, ctx) => ({ where, orderBy })`. `where` filters the
Collection, and `orderBy` orders it, by the primary key when omitted. See
[Ordering and cursors](#ordering-and-cursors).
* `defaultSize` / `maxSize`: page sizes, or `(args, ctx) => number`. Default to the plugin's
[connection size options](./setup#plugin-options).
* `totalCount`: `true` counts the rows, filtered by `query.where`, or pass
`(parent, args, ctx, info) => number`. The count only runs when `totalCount` is selected.
* `args`: extra arguments for filtering or sorting. The Relay pagination arguments are added automatically.
* `collection`: an optional starting ORM query, returned by `(parent, args, ctx, info) => Collection`.
See [Use a custom Collection](#use-a-custom-collection).
Return the starting Collection without `orderBy()` or `include()`, which are type errors, and
without `limit()`, `offset()`, `cursor()`, `distinct()` or `distinctOn()`, which fail before any SQL
runs.
Connection and edge types can be customized as with the [Relay plugin](../relay), including
`edgesNullable` and `nodeNullable`.
## Ordering and cursors [#ordering-and-cursors]
A connection's `query.orderBy` takes columns of the model, each with a direction, as a
[relation's](./relations#filtering-and-sorting) does. You can't order a connection by relation
columns, aggregates or expressions. Nullable columns sort nulls last unless you set `nulls`:
```ts
query: { orderBy: { score: { direction: 'desc', nulls: 'first' } } },
```
If the order doesn't include a unique key:
* The plugin adds the primary key to the end, in the same direction as the last column.
* If the primary key can't be ordered, it uses the first non-null unique key that can.
To let clients choose the order, pass a function of the arguments:
```typescript playground example="plugin-prisma-next" query="{ posts(first: 2, newestFirst: true) { totalCount edges { node { title } } pageInfo { endCursor hasNextPage } } }"
posts: t.prismaConnection({
type: 'Post',
totalCount: true,
args: { newestFirst: t.arg.boolean() },
// Chooses the direction per request; the plugin reverses it for backward pages.
query: (args) => ({
where: { published: 1 },
orderBy: { id: args.newestFirst ? 'desc' : 'asc' },
}),
}),
```
Cursors contain the values of the order columns, readable by clients, so don't order by columns
clients shouldn't see.
* Columns the ORM can't order, such as bytes, can't be in the order.
* A timestamp read as a JavaScript `Date` keeps only milliseconds, so you can't order by one with a
precision above 3. Bare `DateTime` is fine; on PostgreSQL, use `TimestamptzTemporal` for finer
timestamps.
A `query` object whose `orderBy` breaks these rules fails the schema build. A `query` function
fails each request that returns such an order.
## Use a custom Collection [#use-a-custom-collection]
Use `collection` for an existing ORM query or to run code before the connection loads. For example,
check access before returning the query for draft posts:
```ts
builder.queryField('drafts', (t) =>
t.prismaConnection({
type: 'Post',
collection: (_root, _args, ctx) => {
if (!ctx.isAdmin) {
throw new Error('Not allowed.');
}
return db.orm.Post.where({ published: 0 });
},
query: { orderBy: { createdAt: 'desc' } },
}),
);
```
The callback can be asynchronous. Return an unordered, unpaginated Collection; the connection
applies its ordering, page size and selections. [Queries and selections](./queries) explains the
callback arguments and how `collection` and `query` combine.
## Custom connections [#custom-connections]
Connection helpers are an escape hatch for custom resolvers: use them when the connection returns
a different model than the rows being paged, or when your resolver needs to work with loaded rows.
For ordinary model and relation pagination, use `t.prismaConnection` and `t.relatedConnection`.
For example, a post's `commenters` connection pages comments but returns each comment's author.
Use `t.connection` with `prismaConnectionHelpers` to describe that mapping. The helper takes a model
name or a Prisma type ref, plus `query`, `defaultSize`, `maxSize`, `totalCount` and `args`.
`totalCount` takes `true`, but not a function.
Here each row is a `Comment`, and each node is that comment's author. `select` lists what each
comment loads, and `nodeSelection()` marks `author` as the relation that holds the node.
Mark only one relation with `nodeSelection()` in a helper. Other relations can be selected with
`true` when your resolver needs their data.
`resolveNode` returns the node for each comment:
```ts
import { prismaConnectionHelpers } from '@pothos/plugin-prisma-next';
const commenters = prismaConnectionHelpers(builder, 'Comment', {
query: { orderBy: { createdAt: 'desc' } },
totalCount: true,
select: (nodeSelection) => ({ createdAt: true, author: nodeSelection() }),
resolveNode: (comment) => comment.author,
});
builder.prismaObjectField('Post', 'commenters', (t) =>
t.connection(
{
type: User,
select: (args, ctx) => ({
comments: commenters.getQuery(args, ctx),
}),
resolve: (post, args, ctx) => commenters.resolve(post.comments, args, ctx),
},
{
fields: (connection) => ({
totalCount: connection.int({ resolve: (page) => page.totalCount }),
}),
},
),
);
```
`getQuery` loads one page of comments with the post, and its count when the connection selects a
field named `totalCount`. `resolve` builds the page from rows `getQuery` or `load` loaded, or rows
loaded with the same order and paging. `totalCount` is set when the rows are the list `getQuery` or
`load` loaded, or when you pass a count. Without `resolveNode`, the rows are the nodes; use the
helper's `ref` as the connection type.
Call `getQuery` in the connection field's own `select`, at the top level, not inside another
relation's entry. To put the connection on another type, make that type a [variant](./variants) of
the model the field is on.
`select` and `query` also receive the field's arguments and context. Add the helper's `args` to the
field with `args: commenters.getArgs()`.
To count the rows yourself, such as on MySQL, where Prisma can't count them yet, pass the count to
`resolve`: `commenters.resolve(rows, args, ctx, { totalCount })`.
### Load a page in your resolver [#load-a-page-in-your-resolver]
On a root field, `load` pages a collection of the helper's model. It loads the page with the
nodes' selection in one statement, and the count when `totalCount` is selected. As with
`t.prismaConnection`, invalid starting collections are rejected before SQL. Put ordering in
`query.orderBy` and pagination in `first`/`last`. Pass the rows to
`resolve`. With `resolveNode`, each edge also holds its row, so edge fields can read it:
```ts
builder.queryField('postCommenters', (t) =>
t.connection(
{
type: User,
args: { ...commenters.getArgs(), postId: t.arg.id({ required: true }) },
resolve: async (_root, args, ctx, info) => {
const comments = db.orm.Comment.where({ postId: String(args.postId) });
const rows = await commenters.load(comments, args, ctx, info);
return commenters.resolve(rows, args, ctx);
},
},
{
fields: (connection) => ({
totalCount: connection.int({ resolve: (page) => page.totalCount }),
}),
},
{
fields: (edge) => ({
commentedAt: edge.string({ resolve: (comment) => comment.createdAt.toISOString() }),
}),
},
),
);
```
`load` returns one extra row to determine whether another page exists. Pass the returned list to
`resolve` to build the connection and preserve its `totalCount`; the extra row is omitted from the
response.
# Prisma Next plugin
URL: /docs/plugins/prisma-next
An experimental Pothos plugin for the Prisma ORM 8 collection client
The Prisma Next plugin integrates [Prisma ORM 8](https://github.com/prisma/orm) with Pothos to build
a strongly typed GraphQL API from your Prisma models. Expose database fields and relations, add
computed fields, and support Relay nodes and paginated connections. Your GraphQL types and fields can
have different names and shapes from the underlying models.
Declare what each field needs, and the plugin combines those requirements with the incoming
GraphQL query. Prisma loads related data together, often in a single SQL statement, so you don't
need a separate resolver or database call for every relation. Field types are inferred from your
Prisma contract.
This plugin targets Prisma ORM `8.0.0-rc.23` exactly. Its API, and Prisma's, may change in any
release. For Prisma ORM 7, use [the Prisma plugin](/docs/plugins/prisma).
The [playground example](/playground?example=plugin-prisma-next) runs the examples below against
SQLite in the browser, and its **SQL** tab shows the statements each query runs.
## Example [#example]
With a [configured builder](/docs/plugins/prisma-next/setup), define the posts your API exposes
and their author relation. These examples use the playground's `Post` and `User` models:
```typescript playground example="plugin-prisma-next"
builder.prismaObject('Post', {
fields: (t) => ({
id: t.exposeID('id'),
title: t.exposeString('title'),
author: t.relation('author'),
}),
});
```
The author's computed `fullName` declares the columns its resolver reads:
```typescript playground example="plugin-prisma-next"
builder.prismaObject('User', {
fields: (t) => ({
id: t.exposeID('id'),
email: t.exposeString('email'),
// Selects its columns only when a query asks for this field.
fullName: t.string({
select: ['firstName', 'lastName'],
resolve: (user) => `${user.firstName} ${user.lastName}`,
}),
}),
});
```
Add a recent-posts query that returns the ten latest published posts:
```typescript playground example="plugin-prisma-next" query="{ recentPosts { title author { fullName } } }"
builder.queryType({
fields: (t) => ({
recentPosts: t.prismaField({
type: ['Post'],
query: { where: { published: 1 }, orderBy: { id: 'desc' }, limit: 10 },
}),
}),
});
```
For `{ recentPosts { title author { fullName } } }`, the posts and their authors load in one SQL
statement, including the columns needed to compute each author's name. Each root field usually
runs as one statement, with its requested relations, counts and connection pages included.
## Relay connections [#relay-connections]
With the [Relay plugin](/docs/plugins/relay), `t.relatedConnection` adds a paginated list of
published posts to an author profile:
```typescript playground example="plugin-prisma-next" query="{ author(id: 1) { fullName postConnection(first: 1) { totalCount edges { node { title } } pageInfo { endCursor hasNextPage } } } }"
// Pages through the relation inside the query that loads the user.
builder.prismaObjectField('User', 'postConnection', (t) =>
t.relatedConnection('posts', {
query: { where: { published: 1 } },
totalCount: true,
}),
);
```
`t.prismaConnection` does the same for query fields. See
[Connections](/docs/plugins/prisma-next/connections).
## Next steps [#next-steps]
To get started, follow [Setup](/docs/plugins/prisma-next/setup), then read
[Queries and selections](/docs/plugins/prisma-next/queries), [Objects](/docs/plugins/prisma-next/objects) and [Relations](/docs/plugins/prisma-next/relations).
For an existing Prisma ORM 7 schema, see [Migrating from the Prisma plugin](/docs/plugins/prisma-next/migrating-from-prisma).
# Indirect relations
URL: /docs/plugins/prisma-next/indirect-relations
Loading relations for fields that aren't t.relation
Use [`t.relation`](./relations) for a direct relation, including a renamed or filtered field.
Use a custom field when the result follows a join model, introduces a GraphQL wrapper, or needs
other resolver logic. `nestedSelection` loads the related data requested beneath that field.
A field's `select` function gets a third argument, `nestedSelection`. Use it as a relation's entry,
and the relation's rows load with what the query selects on the field's type, in the parent's query:
```ts
const Post = builder.prismaObject('Post', {
fields: (t) => ({ title: t.exposeString('title') }),
});
builder.prismaObject('User', {
fields: (t) => ({
publishedPosts: t.field({
type: [Post],
select: (args, ctx, nestedSelection) => ({
posts: nestedSelection({ where: { published: 1 } }),
}),
resolve: (user) => user.posts,
}),
}),
});
```
For this filtered field alone, prefer
`publishedPosts: t.relation('posts', { query: { where: { published: 1 } } })`.
Without `nestedSelection`, `{ posts: true }` loads each post's columns but nothing the query
selects beneath the field, such as `author`, which then loads in an extra batched statement. Use `nestedSelection`
whenever the field's type has relations or selects of its own, so they load in the parent's query.
`nestedSelection` takes an optional object with what a [relation entry](./objects#selections)
takes, and `path`:
* `where`, `orderBy`, `limit`, `offset`: the relation's
[query options](./relations#filtering-and-sorting). A to-one relation takes none.
* `select`: what to load beneath the rows, besides what the query selects.
* `path`: a field nested under the field's type to read the selection from. A segment can be
written as `{ name, type }` to only read the field under a fragment on that type.
* `type`: the type to read the selection as, when the field returns an interface or union. It
takes what a field's `type` takes, or a model's name for its default type. The query and rows are
typed by that type's model, which must be the relation's target.
The rows hold the primary key and what the selection loads, not every column.
## Selecting fields from a nested GraphQL field [#selecting-fields-from-a-nested-graphql-field]
When the field returns a type without a model, pass the `path` of a field beneath it that returns
the model's type:
```ts
const PostPreview = builder.objectRef('PostPreview').implement({
fields: (t) => ({
post: t.field({ type: Post, resolve: (post) => post }),
}),
});
builder.prismaObject('User', {
fields: (t) => ({
postPreviews: t.field({
type: [PostPreview],
select: (args, ctx, nestedSelection) => ({
// What the query selects under `postPreviews.post`, for the 2 newest posts
posts: nestedSelection({ orderBy: { createdAt: 'desc' }, limit: 2, path: ['post'] }),
}),
resolve: (user) => user.posts,
}),
}),
});
```
## Selecting as a specific type [#selecting-as-a-specific-type]
When the field returns an interface or union, `type` names the object type to read the selection
as. Fragments on other types are left out:
```ts
const Comment = builder.prismaObject('Comment', {
// Loaded whenever a Comment is queried, so resolveType can read it
select: ['postId'],
fields: (t) => ({ body: t.exposeString('body') }),
});
const Activity = builder.unionType('Activity', {
types: [Post, Comment],
resolveType: (row) => ('postId' in row ? 'Comment' : 'Post'),
});
builder.prismaObject('User', {
fields: (t) => ({
recentActivity: t.field({
type: [Activity],
select: (args, ctx, nestedSelection) => ({
posts: nestedSelection({ limit: 5, type: Post }),
comments: nestedSelection({ limit: 5, type: Comment }),
}),
resolve: (user) => [...user.posts, ...user.comments],
}),
}),
});
```
## Join models [#join-models]
A relation defined with `rel.manyToMany` loads through its junction table, so `nestedSelection` and
`t.relation` work on it like any to-many relation.
For a join model of its own, give the relation entry a `select` of the join model's rows and put
`nestedSelection` beneath it. Here comments join users to the posts they commented on:
```ts
builder.prismaObject('User', {
fields: (t) => ({
commentedPosts: t.field({
type: [Post],
select: (args, ctx, nestedSelection) => ({
comments: { orderBy: { createdAt: 'desc' }, select: { post: nestedSelection() } },
}),
resolve: (user) => user.comments.map((comment) => comment.post),
}),
}),
});
```
The `select` takes the same entries as any other, one level down, and everything loads in the
parent's query. Add `type`, a ref or model name of a variant of the relation's model, to load only
that variant's rows: `{ type: Bug, select: { reviewer: true } }`.
# Model inheritance
URL: /docs/plugins/prisma-next/inheritance
Exposing inherited Prisma models through a GraphQL interface
When your Prisma contract defines inherited models, such as a `Task` that can be a `Bug` or a
`Feature`, expose their shared fields through a GraphQL interface. Each kind gets an object type,
and the plugin chooses the right type for each row automatically.
For multiple GraphQL views of a single Prisma model, use [Type variants](./variants).
## Define the interface and its objects [#define-the-interface-and-its-objects]
This example assumes a contract with `Task`, its `Bug` and `Feature` variants, and a `User` model
with a `tasks` relation. `Bug` also has an optional `reviewer` relation to `User`. Use the builder
and imported `db` from [Setup](./setup), with `User` already defined as a GraphQL type.
```ts
const Task = builder.prismaInterface('Task', {
fields: (t) => ({
id: t.exposeID('id'),
title: t.exposeString('title'),
owner: t.relation('owner'),
}),
});
const Bug = builder.prismaObject('Bug', {
interfaces: [Task],
select: { reviewer: true },
fields: (t) => ({
severity: t.exposeString('severity'),
reviewerEmail: t.string({
nullable: true,
resolve: (bug) => bug.reviewer?.email,
}),
}),
});
builder.prismaObject('Feature', {
interfaces: [Task],
fields: (t) => ({ priority: t.exposeInt('priority') }),
});
builder.queryFields((t) => ({
tasks: t.prismaField({ type: [Task] }),
bugs: t.prismaField({ type: [Bug] }),
}));
```
Define an implementing object for each variant in the contract. The plugin handles type
resolution, so no custom `resolveType` is needed. Clients can query shared fields and add
fragments for variant-specific fields:
```graphql
{
tasks {
id
title
... on Bug {
severity
reviewerEmail
}
... on Feature {
priority
}
}
}
```
A variant's `select` declares data shared by its fields and authorization checks, just as on any
other model type. Here `Bug` loads its reviewer so `reviewerEmail` can read it. Put dependencies in
a field's own `select` when only that field needs them. The interface's selected data is available
to its implementing types too.
## Query a variant [#query-a-variant]
Give a field the variant's type to return only rows of that variant. Use the ORM's predicate form
to filter variant-specific fields:
```ts
builder.queryField('highSeverityBugs', (t) =>
t.prismaField({
type: [Bug],
query: { where: (bug) => bug.severity.eq('high') },
}),
);
```
When composing a native query yourself, start with the base model narrowed to the variant, such as
`db.orm.Task.variant('bug')`. The plugin uses this form automatically when you omit `collection`.
## Expose related variants [#expose-related-variants]
For a relation to `Task`, return the interface to include every variant, or pass a variant's ref
as `type` to narrow the field:
```ts
builder.prismaObjectFields('User', (t) => ({
tasks: t.relation('tasks', { type: Task }),
bugs: t.relation('tasks', { type: Bug }),
}));
```
The plugin loads what each type needs before type routing and auth checks read the row, sometimes
with an extra batched query.
A relation targeting a variant stored in a separate table can filter and sort by that variant's
own fields, but not inherited base fields. To query both, start from the base Collection narrowed
with `variant()`, as in the previous section.
Custom resolvers that load rows can use [collectionFromInfo](./relations#returning-rows-from-other-fields)
to include the data their GraphQL result needs. Planned variant rows can also be returned through
a compatible `prismaInterface`; the plugin loads missing data needed to resolve their type.
## Mutations [#mutations]
When returning a `Bug` from `t.prismaField`, the resolver receives a Collection for
`orm.Task.variant('bug')`. Keep a filter such as `where({ id })` when changing one task:
`variant('bug')` chooses the kind of task, and `update()` or `delete()` acts on the first match.
### Return an updated record [#return-an-updated-record]
Some Prisma write methods return only the base model's columns when the variant has its own
table. For example, an updated `Feature` may be missing its `priority`. Use `updateAndCount()`
to make the change, then `first()` on the same filtered Collection to read the response.
When returning the `Task` interface, the plugin may also need another read for a relation such as
`Bug.reviewer`. Updates can load that data after the write.
### Return a deleted record [#return-a-deleted-record]
A deleted row cannot be read again. To return a deleted bug with its reviewer, return the `Bug`
object type, or prepare the query with `collectionFromInfo({ ..., typeName: 'Bug' })` before
calling `delete()`. This loads the reviewer with the delete instead of reloading it afterward.
A Collection using `variant('bug')` requires that `typeName` option with `collectionFromInfo`.
For a variant stored in its own table, Prisma's `delete()` result contains only the base model's
columns too. Return an ID or a payload that does not need the missing fields. Avoid `@defer` on a
deleted record: deferred fields may need a read after the row is gone.
### Upsert a variant [#upsert-a-variant]
For variants stored in the base model's table, `upsert()` can match a unique key belonging to a
different variant. For example, `Task.variant('bug').upsert(...)` can update a feature with that
key and return it as a bug. Check the existing task's kind before choosing whether to create or
update when your mutation must preserve it.
# Interfaces
URL: /docs/plugins/prisma-next/interfaces
GraphQL interfaces backed by contract models
An interface lets a field return different object types with common fields. Use
`builder.prismaInterface` for types backed by Prisma models; it takes the same options as
`builder.prismaObject`.
Building on [Type variants](./variants), update the `User` and `Viewer` definitions to share a
`Person` interface. Here `resolveType` chooses `Viewer` for the signed-in user's own row, and `User`
for other authors:
```ts
const Person = builder.prismaInterface('User', {
variant: 'Person',
resolveType: (user, ctx) => (user.id === ctx.userId ? 'Viewer' : 'User'),
fields: (t) => ({
id: t.exposeID('id'),
firstName: t.exposeString('firstName'),
}),
});
builder.prismaObject('User', {
interfaces: [Person],
fields: (t) => ({
posts: t.relation('posts', { query: { where: { published: 1 } } }),
}),
});
builder.prismaObject('User', {
variant: 'Viewer',
interfaces: [Person],
fields: (t) => ({
email: t.exposeString('email'),
drafts: t.relation('posts', { query: { where: { published: 0 } } }),
}),
});
builder.queryField('author', (t) =>
t.prismaField({
type: Person,
nullable: true,
args: { id: t.arg.id({ required: true }) },
query: (args) => ({ where: { id: String(args.id) } }),
}),
);
```
Implementing types must use the interface's Prisma model or one of its inherited models.
`t.prismaField`, `t.prismaConnection`, `t.relation` and `t.relatedConnection` all accept an
interface ref as `type`. Fields on an implementing type can read the interface's `select`.
Implementing objects don't accept `isTypeOf`.
`id` is available to `resolveType` automatically. If it needs other data, declare columns or
to-one relations in the interface's `select`; to-many relations aren't available to it.
To add fields to the interface from another file, use `prismaInterfaceField` or
`prismaInterfaceFields`:
```ts
builder.prismaInterfaceField(Person, 'displayName', (t) =>
t.string({
select: ['firstName', 'lastName'],
resolve: (person) => `${person.firstName} ${person.lastName}`,
}),
);
```
To return rows through `Node` or another interface from a plain field, see
[Returning rows from other fields](./relations#returning-rows-from-other-fields).
An object type from `builder.objectRef` can implement a `prismaInterface` if its shape is
`PrismaPlanned` for the model, such as
`builder.objectRef<{ id: string; email: string; firstName: string } & PrismaPlanned<'User'>>`. The
shape must include the interface's `select` columns and the columns its fields expose, since its
fields and `resolveType` read them.
## Native inheritance [#native-inheritance]
When Prisma itself defines inherited models, such as `Task`, `Bug` and `Feature`, the plugin can
choose the GraphQL type automatically. Continue with [Model inheritance](./inheritance) to expose
those models through an interface.
# Migrating from the Prisma plugin
URL: /docs/plugins/prisma-next/migrating-from-prisma
Updating an existing Pothos schema for Prisma ORM 8
Start with your Prisma ORM 8 client and contract, then update Pothos to use
`@pothos/plugin-prisma-next`. You can keep the structure of your GraphQL schema: `prismaObject`,
`t.expose*`, `t.relation`, nodes and connections still define the fields your clients use.
## Connect the new client [#connect-the-new-client]
Follow [Setup](./setup) to replace the plugin import, pass your contract as `PrismaContract`, and
configure `prisma: { client: db }`. The new client replaces the Prisma ORM 7 client and the
Pothos generator output used by `@pothos/plugin-prisma`. Use one plugin per program.
## What changed [#what-changed]
* `take` and `skip` are now `limit` and `offset`.
* `where` and `orderBy` use Prisma ORM 8's shapes, not Prisma Client's.
* The `dmmf`, `exposeDescriptions` and `onUnusedQuery` plugin options are gone.
* A `client` function also needs the base client's `context`; see
[Plugin options](./setup#plugin-options).
* `prismaObject` and `prismaNode` take no `include` or `findUnique`; use `select`.
* A connection helper's `getQuery` takes `(args, ctx)`, without `nestedSelection`.
* `@pothos/plugin-prisma-utils` has no counterpart.
## Declare data for computed fields [#declare-data-for-computed-fields]
Exposed fields and relations load their own data. If a custom resolver reads additional columns,
declare those with `select` so they are loaded whenever the field is requested:
```ts
builder.prismaObjectField('User', 'fullName', (t) =>
t.string({
select: ['firstName', 'lastName'],
resolve: (user) => `${user.firstName} ${user.lastName}`,
}),
);
```
Use a type-level `select` for data shared by every field or authorization check on that type.
[Selections](./objects#selections) covers both forms.
## Update queries and mutations [#update-queries-and-mutations]
For a read, `query` describes which rows to return. A field that previously spread the plugin's
query into `findUnique` can become:
```ts
builder.queryField('me', (t) =>
t.prismaField({
type: 'User',
nullable: true,
query: (_args, ctx) => ({ where: { id: ctx.userId } }),
}),
);
```
If you already compose a native ORM query, return it from `collection`, for example
`collection: () => db.orm.Post.where({ published: 1 })` for published posts.
For writes, `resolve` receives the selected ORM query and lets you call `create`, `update`, or
`delete` on it. See [Mutations](./objects#mutations) for complete examples and
[Transactions](./transactions) for multi-step operations.
Custom `t.field` resolvers that return database rows should use
[`collectionFromInfo`](./relations#returning-rows-from-other-fields) when loading those rows.
That ensures the result includes the relations and other data requested by the GraphQL query.
## Keep your connection queries [#keep-your-connection-queries]
GraphQL clients can keep using `first`, `after`, `last` and `before`. In the schema, replace a
connection's `cursor` option with the order you want users to see:
```ts
builder.prismaObjectField('User', 'postsConnection', (t) =>
t.relatedConnection('posts', {
query: { orderBy: { createdAt: 'desc' } },
}),
);
```
The plugin adds a unique key to break ties. See [Ordering and cursors](./connections#ordering-and-cursors)
for nullable columns and supported sort fields. Root connections no longer take a `resolve` callback.
Use [`collection`](./connections#use-a-custom-collection) for checks before loading, and
[connection helpers](./connections#custom-connections) when a custom resolver needs the loaded rows.
## Adapt custom relation fields [#adapt-custom-relation-fields]
A filtered count now takes `query: { where }`, for example
`t.relationCount('posts', { query: { where: { published: 1 } } })`.
For a relation with custom resolver logic, use `t.field` with a `select`.
[`nestedSelection`](./indirect-relations) loads the relations requested beneath that field, and
takes its options in one object, such as `nestedSelection({ path: ['post'] })`.
# Objects
URL: /docs/plugins/prisma-next/objects
Defining GraphQL types backed by contract models
`builder.prismaObject` defines a GraphQL object type for a model in your contract.
## Define a type [#define-a-type]
Pass the model name and the usual object type options. Column types come from the contract.
```ts
builder.prismaObject('User', {
fields: (t) => ({
id: t.exposeID('id'),
email: t.exposeString('email'),
}),
});
```
The GraphQL type is named after the model. Pass `name` to call it something else.
To add fields to a type elsewhere, use [`prismaObjectFields`](#adding-fields). To expose the same
model as another GraphQL type, define a [`variant`](./variants) or a
[`prismaInterface`](./interfaces).
To return these objects from a query field, use `t.prismaField`; see
[Queries and selections](./queries).
## Selections [#selections]
A field that reads columns it doesn't expose, or data from a relation, declares them with `select`.
The plugin then loads them along with the rest of the query.
```ts
builder.prismaObject('User', {
// Loaded whenever a User is queried
select: { firstName: true, lastName: true },
fields: (t) => ({
fullName: t.string({
resolve: (user) => `${user.firstName} ${user.lastName}`,
}),
// Loaded only when emailDomain is queried
emailDomain: t.string({
select: ['email'],
resolve: (user) => user.email.split('@')[1] ?? '',
}),
publishedPostCount: t.int({
select: {
posts: (posts) => ({ publishedCount: posts.where({ published: 1 }).count() }),
},
resolve: (user) => user.publishedCount,
}),
}),
});
```
A `select` on the type is loaded whenever the type is queried, and every field can read it. A
`select` on a field is loaded only when that field is queried. The primary key is always loaded.
A `select` accepts:
* Columns: `{ email: true }` or `['email']`.
* Relations: `{ posts: true }`, which loads the relation's scalar columns.
* Filtered or sorted relations: `{ posts: { where: { published: 1 }, orderBy: { createdAt: 'desc' }, limit: 5 } }`.
`where` also takes the ORM's predicate callback, and `orderBy` its selectors, as in `t.relation`'s
`query`.
* A `select` of the relation's rows, in the same form: `{ comments: { limit: 5, select: { post: true } } }`.
* `type`, a ref or model name of a [variant](./inheritance) of the relation's
target, which loads only that variant's rows: `{ tasks: { type: Bug } }`.
* In a field's `select` only, counts and aggregates of a to-many relation:
`{ posts: (posts) => ({ postCount: posts.count() }) }`. Each key (`postCount`) is added to the
row, so don't reuse a column or relation name.
A to-one relation takes only `type` and `select`. Read aggregates in a resolver; `t.expose*` only
works on columns.
The type-level `select`s of one model, including its [variants](./variants) and
[interfaces](./interfaces), must load each relation they name the same way, at any depth, or the
schema build throws. A `where` or `orderBy` function only matches itself, so share it between them.
Load another version of a relation with `t.relation` or a field's `select`.
To filter a to-one relation, select it and return `null` from the resolver. Here a comment shows its
post's title only when the post is published:
```ts
builder.prismaObject('Comment', {
fields: (t) => ({
postTitle: t.string({
nullable: true,
select: { post: true },
resolve: (comment) => (comment.post.published ? comment.post.title : null),
}),
}),
});
```
### Selecting per request [#selecting-per-request]
A field's `select` can also be a function of its arguments and context. Here the field selects
`lastName` only when the client asks for the full name. A column that only some branches select is
optional on the parent:
```ts
name: t.string({
args: { full: t.arg.boolean() },
select: (args) => (args.full ? { firstName: true, lastName: true } : { firstName: true }),
resolve: (user, args) =>
args.full && user.lastName ? `${user.firstName} ${user.lastName}` : user.firstName,
}),
```
With `AsyncSelections: true` in the builder's types, the function can be async.
Its third argument, `nestedSelection`, includes the dependencies and relations requested beneath a
custom field returning objects. Without an explicit `select`, it loads scalar columns alongside
those GraphQL requirements. An explicit `select` can start from a narrower set. See
[Indirect relations](./indirect-relations) for examples.
`select`, `query` and `where` callbacks run while planning or building a query, rather than for
each returned row. They can run more than once for reloads or aggregate planning, so keep them
free of side effects.
### Resolver data [#resolver-data]
A resolver can read the model's primary or non-null unique key, the type's shared `select`, and
its own field's `select`. Other columns aren't guaranteed to be loaded.
Rows the plugin loads carry a type-only marker, such as `PrismaPlanned<'User'>`. For returning rows
from custom resolvers, see
[Returning rows from other fields](./relations#returning-rows-from-other-fields).
### Auth checks [#auth-checks]
Auth checks on the type, and other plugins' field options such as `authScopes` and `grantScopes`,
see:
* the primary key
* the columns and relations in the `select`s of the type and its interfaces
* on a `t.expose*` field, the exposed column
Only `resolve` can read what a field's own `select` loads. If a row is missing any of this, such as
a row of a variant model with its own table, the plugin reloads it by its key before the check.
## Mutations [#mutations]
Use `resolve` for a field that writes data. It receives the model's Collection prepared for the
GraphQL query. Filter it to the row you want to change, then call Prisma's write method:
```ts
builder.mutationType({
fields: (t) => ({
renameUser: t.prismaField({
type: 'User',
nullable: true,
args: {
id: t.arg.id({ required: true }),
firstName: t.arg.string({ required: true }),
},
resolve: (users, _root, args) =>
users.where({ id: String(args.id) }).update({ firstName: args.firstName }),
}),
}),
});
```
The updated row becomes the mutation result, with the fields requested by the client.
### Return a payload [#return-a-payload]
For a result shaped like `{ user: ... }`, use `collectionFromInfo` to load the fields requested on
`user`. The `path: ['user']` option tells it where that object appears in the result:
```ts
import { collectionFromInfo } from '@pothos/plugin-prisma-next';
const RenameUserPayload = builder
.objectRef<{ user: typeof User.$inferType | null }>('RenameUserPayload')
.implement({
fields: (t) => ({ user: t.expose('user', { type: User, nullable: true }) }),
});
builder.mutationField('renameUser', (t) =>
t.field({
type: RenameUserPayload,
args: { id: t.arg.id({ required: true }), firstName: t.arg.string({ required: true }) },
resolve: async (_root, args, ctx, info) => {
const users = await collectionFromInfo({
collection: db.orm.User.where({ id: String(args.id) }),
context: ctx,
info,
path: ['user'],
});
return { user: await users.update({ firstName: args.firstName }) };
},
}),
);
```
If the payload field is an interface or union, pass `typeName` to choose the GraphQL object type
to load. For mutations on inherited models, see [Model inheritance](./inheritance#mutations).
An exported function that returns rows from `collectionFromInfo` needs a return type, such as
`Promise | null>`, for TypeScript to emit its declaration.
See [Transactions](./transactions) to run the write and the reads in one transaction.
### `prismaFieldWithInput` [#prismafieldwithinput]
With the [with-input plugin](/docs/plugins/with-input), `t.prismaFieldWithInput` combines
`t.prismaField` with `t.fieldWithInput`, and takes `collection`, `query` or `resolve` the same way.
The `input` fields become an input object argument:
```ts
builder.mutationField('updateFirstName', (t) =>
t.prismaFieldWithInput({
type: 'User',
nullable: true,
input: {
id: t.input.id({ required: true }),
firstName: t.input.string({ required: true }),
},
resolve: (users, _root, args) =>
users.where({ id: String(args.input.id) }).update({ firstName: args.input.firstName }),
}),
);
```
## Adding fields [#adding-fields]
To add fields to a type from another file, use `prismaObjectField` or `prismaObjectFields`.
`builder.objectField` won't work here because it doesn't support `select`.
```ts
builder.prismaObjectField('User', 'displayName', (t) =>
t.string({
select: ['firstName', 'lastName'],
resolve: (user) => `${user.firstName} ${user.lastName}`,
}),
);
```
If the resolver reads the type's `select`, pass the ref that `prismaObject` returns instead of the
model name. Passing a variant's ref adds fields to that [variant](./variants).
```ts
const User = builder.prismaObject('User', {
select: { firstName: true, lastName: true },
fields: (t) => ({ id: t.exposeID('id') }),
});
builder.prismaObjectField(User, 'initials', (t) =>
t.string({
resolve: (user) => `${user.firstName[0]}${user.lastName[0]}`,
}),
);
```
# Queries and selections
URL: /docs/plugins/prisma-next/queries
Combining an ORM collection with field queries and selected data
Most query fields need only a type and a `query`. Use `collection` when you want to start from a
native ORM query, and `select` on a custom field to declare the data its resolver reads.
The examples use the builder and imported `db` from [Setup](./setup), with `User` and `Post` models, including
`content`, `published`, `createdAt` and `score` fields on `Post`.
## Choose rows and declare field dependencies [#choose-rows-and-declare-field-dependencies]
```ts
builder.prismaObject('Post', {
fields: (t) => ({
title: t.exposeString('title'),
preview: t.string({
select: ['content'],
resolve: (post) => post.content.slice(0, 100),
}),
author: t.relation('author'),
}),
});
builder.queryField('publishedPosts', (t) =>
t.prismaField({
type: ['Post'],
args: { authorId: t.arg.string() },
query: (args) => ({
where: args.authorId ? { published: 1, authorId: args.authorId } : { published: 1 },
orderBy: { createdAt: 'desc' },
limit: 10,
}),
}),
);
```
For this query:
```graphql
{
publishedPosts(authorId: "u-alice") {
preview
author {
email
}
}
}
```
The plugin filters published posts by author, orders them by creation date, and loads the content
needed by `preview` plus the related author's email. `preview` is a GraphQL field,
not a database column. Its `select` tells the plugin how to supply its resolver.
| Piece | What it is | In this example |
| ----------------- | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| `collection` | An optional `(parent, args, ctx, info) => Collection` callback supplying the starting ORM query | Omitted: the default Post Collection |
| `query` | Filter, ordering and pagination options, or `(args, ctx) => options`, applied to the starting Collection | Published posts by the requested author, newest first |
| Field `select` | Column names, a dependency object, or `(args, ctx, nestedSelection) => dependencies` | `content` when `preview` is requested |
| GraphQL selection | The fields requested by the client | `preview` and the author's `email` |
`args` contains the field's GraphQL arguments, and `ctx` is the request's context. A `collection`
callback also receives the containing `parent` (the root value for a root field) and the field's
`GraphQLResolveInfo` as `info`. For connections, `args` includes pagination arguments too.
`nestedSelection` includes the dependencies and relations requested beneath a custom field returning
objects; see [Indirect relations](./indirect-relations).
`collection` runs during field resolution and may return a promise. Return the Collection without
calling `all()`; the plugin applies `query`, adds exposed columns, declared dependencies and requested
relations, then executes it. A nullable field's callback may return `null` to skip the query.
## Query fields [#query-fields]
`t.prismaField` returns a model's objects and loads the data requested beneath the field. Use
`type: ['Post']` for a list, or `type: 'Post'` for one object. `query` accepts `where`, `orderBy`,
`limit` and `offset`, either as an object or a function of the field's arguments and context.
For a single post lookup:
```ts
builder.queryField('post', (t) =>
t.prismaField({
type: 'Post',
nullable: true,
args: { id: t.arg.id({ required: true }) },
query: (args) => ({ where: { id: String(args.id) } }),
}),
);
```
A single-row field returns the first row found and requires `query`, `collection` or `resolve`.
A list field with no filters or pagination returns every row. Use `query.limit` or a connection
to bound the result.
## Paginating a list [#paginating-a-list]
For simple offset pagination, apply the field's arguments with `limit` and `offset`. Always include
an `orderBy` so pages are stable:
```ts
builder.queryFields((t) => ({
posts: t.prismaField({
type: ['Post'],
args: { limit: t.arg.int(), offset: t.arg.int() },
query: (args) => ({
orderBy: { createdAt: 'desc', id: 'desc' },
limit: args.limit ?? 10,
offset: args.offset ?? 0,
}),
}),
}));
```
For cursor-based pagination with page info and total counts, use `t.prismaConnection` instead. See
[Connections](./connections#paginate-a-query-field).
## Using a custom Collection [#using-a-custom-collection]
Rows load from the model's Collection, `orm.` from the plugin's `client`. To start from
a reusable application scope or a query built with native ORM methods, return a Collection from
`collection`. Don't call `.all()`, `.first()` or `.include()` on it; the plugin applies `query` and
what the query selects, then runs it.
```ts
builder.queryFields((t) => ({
myPosts: t.prismaField({
type: ['Post'],
collection: (_root, _args, ctx) => db.orm.Post.where({ authorId: ctx.userId }),
query: { orderBy: { createdAt: 'desc' } },
}),
}));
```
For writes or other logic that executes the query itself, use the field's `resolve` form instead.
It receives the selected Collection. See [Mutations](./objects#mutations).
## How options combine [#how-options-combine]
`query` refines the starting Collection; it does not replace it. The ORM's composition rules apply:
* A `where` in `query` is added to filters already on `collection`. Both must match.
* An `orderBy` in `query` is appended to ordering already on `collection`. It does not override it.
* For an ordinary field, `query.limit` and `query.offset` set those values on the Collection. A
single-row `prismaField` ultimately limits the result to one row.
For example, if `collection` already orders posts by title and `query` orders by creation date,
title stays the primary sort and creation date breaks ties. Put ordering in one place unless you
want that combination. For most fields, `query.orderBy` is the clearest place.
A relation's `query` works similarly, but starts from that parent's related rows. Filtering
`t.relation('posts')` does not filter the users themselves; it filters each user's posts.
## Connections [#connections]
`t.prismaConnection` owns ordering and pagination so its cursors match the rows it loads. Put
ordering in `query.orderBy` and page sizes in `first`/`last`. Incompatible starting Collections
are rejected by TypeScript or at runtime before SQL. See [Connections](./connections) for the
options, checks and cursor rules.
For the full `select` syntax, shared type dependencies and resolver guarantees, see
[Object selections](./objects#selections). Use [Indirect relations](./indirect-relations) when a
custom field returns related objects.
# Relations
URL: /docs/plugins/prisma-next/relations
Adding relations to Prisma-backed types
Use `t.relation` to expose a relation from your contract as a field. The plugin loads it with the
parent's query, so a user's posts come back with the user:
```ts
builder.prismaObject('User', {
fields: (t) => ({
id: t.exposeID('id'),
email: t.exposeString('email'),
posts: t.relation('posts'),
}),
});
builder.prismaObject('Post', {
fields: (t) => ({
id: t.exposeID('id'),
title: t.exposeString('title'),
author: t.relation('author'),
}),
});
```
Relation fields follow the builder's
[default field nullability](../../guide/changing-default-nullability), whatever the contract says, so
a database change never changes the schema. Pass `nullable` to override.
### Nullable relations and `onNull` [#nullable-relations-and-onnull]
A to-one relation can resolve to `null` when the contract makes it optional, or when a variant
`type` filters the row out. To expose one as non-null, `t.relation` requires `onNull`. Setting it
to `'error'` lets GraphQL raise the non-null error for the field. A function can return a
replacement row from `collectionFromInfo` instead, or an `Error` to raise. This example uses the
optional `Bug.reviewer` relation from [Model inheritance](./inheritance):
```ts
builder.prismaObject('Bug', {
fields: (t) => ({
reviewer: t.relation('reviewer', { nullable: true }),
// An error when the bug has no reviewer
requiredReviewer: t.relation('reviewer', { nullable: false, onNull: 'error' }),
// A different error
assignedReviewer: t.relation('reviewer', {
nullable: false,
onNull: () => new Error('Bug has no reviewer'),
}),
}),
});
```
## Filtering and sorting [#filtering-and-sorting]
Pass `query` to filter, sort, or limit the related rows. It takes `{ where, orderBy, limit, offset }`,
or a function of the field's arguments and context that returns one. `where` takes anything the
Collection's `where()` does. `orderBy` takes columns, each with a direction, and sorts nulls last
unless you set `nulls`, as in a [connection](./connections#ordering-and-cursors). Here clients
choose the sort order:
```ts
posts: t.relation('posts', {
args: { oldestFirst: t.arg.boolean() },
query: (args) => ({
orderBy: { createdAt: args.oldestFirst ? 'asc' : 'desc' },
}),
}),
```
With `AsyncSelections: true` in the builder's types, the function can be async.
To sort by an expression, pass `orderBy` what the Collection's `orderBy()` takes, one selector or an
array of them:
```ts
posts: t.relation('posts', {
query: {
orderBy: (post, { fns }) => fns.raw`abs(${post.score} - 2.5)`.returns('sqlite/real@1').asc(),
},
}),
```
A to-one relation takes no `query`. To filter one, select it in a field's `select` and return `null`
from the resolver.
### Paginating a list [#paginating-a-list]
The [offset pattern for root fields](./queries#paginating-a-list) works for relations too, through
`query`:
```ts
posts: t.relation('posts', {
args: { limit: t.arg.int(), offset: t.arg.int() },
query: (args) => ({
orderBy: { createdAt: 'desc' },
limit: args.limit ?? 10,
offset: args.offset ?? 0,
}),
}),
```
For cursor-based pagination, use [`t.relatedConnection`](./connections#paginate-a-relation).
### Aliases [#aliases]
A query can select one relation field several times under aliases with different arguments:
```graphql
{
author(id: "u-alice") {
newest: posts(oldestFirst: false) { title }
oldest: posts(oldestFirst: true) { title }
}
}
```
Each alias loads its own rows in the parent's query. Aliases with the same arguments share one
load.
## Counts and other aggregates [#counts-and-other-aggregates]
`t.relationCount` adds an `Int` field that counts a to-many relation. Pass a `query` with a `where`
to count only some rows:
```ts
builder.prismaObject('User', {
fields: (t) => ({
postCount: t.relationCount('posts'),
publishedPostCount: t.relationCount('posts', { query: { where: { published: 1 } } }),
postCountByStatus: t.relationCount('posts', {
args: { published: t.arg.int({ required: true }) },
query: (args) => ({ where: { published: args.published } }),
}),
}),
});
```
`query` is an object or a function of the arguments and context, as on `t.relation`, but takes only
`where`. `where` takes anything a relation's `query.where` takes, including a predicate:
`query: { where: (post) => post.published.eq(1) }`.
Count fields are always non-null, so if an auth check denies one, GraphQL nulls the parent instead.
To hide a count from some users, use a nullable field with a `select`, as below.
For sums and other aggregates, give the relation a function in a field's `select`. It takes the
relation's Collection and returns reducers by key, and each key is added to the parent row:
```ts
builder.prismaObject('User', {
fields: (t) => ({
totalPostScore: t.float({
nullable: true,
select: { posts: (posts) => ({ totalPostScore: posts.sum('score') }) },
resolve: (user) => user.totalPostScore,
}),
}),
});
```
Keep aggregate callbacks free of side effects, and return the same keys for the same arguments. A
field's `select` can load at most 61 aggregate keys at one level, across its relations; split
larger summaries across fields.
## Many-to-many [#many-to-many]
Relations defined with `rel.manyToMany({ through, from, to })` work with `t.relation`. To expose
columns from the junction table, define the junction as its own model and use two relations.
## Returning rows from other fields [#returning-rows-from-other-fields]
If a plain `t.field` returns a model's type, instead of `t.prismaField` or `t.relation`, it must
return rows the plugin planned: the parent, the relation rows the parent's `select` loads, or rows
from a query built with `collectionFromInfo`. These are typed `PrismaPlanned<'User'>`. A row built
by hand or loaded with a plain ORM query is a type error naming `PrismaPlanned<'User'>`.
```ts
builder.prismaObject('Post', {
select: { author: true },
fields: (t) => ({
writer: t.field({ type: User, resolve: (post) => post.author }),
}),
});
```
A relation entry's `type` can be a ref or model name of one of the relation's
[variants](./inheritance). The rows load as that variant only, typed as its rows:
```ts
const Bug = builder.prismaObject('Bug', {
fields: (t) => ({ severity: t.exposeString('severity') }),
});
builder.prismaObject('User', {
fields: (t) => ({
bugs: t.field({
type: [Bug],
select: { tasks: { type: Bug } },
resolve: (user) => user.tasks,
}),
}),
});
```
`collectionFromInfo` loads everything the query selects beneath the field, relations and counts
included, so the field loads in one query, like a `t.prismaField`. See
[Mutations](./objects#mutations) for its `path` and `typeName` options:
```ts
import { collectionFromInfo } from '@pothos/plugin-prisma-next';
builder.queryField('me', (t) =>
t.field({
type: User,
nullable: true,
resolve: async (_root, _args, ctx, info) => {
const users = await collectionFromInfo({
collection: db.orm.User.where({ id: ctx.userId }),
context: ctx,
info,
});
return users.first();
},
}),
);
```
In a subscription, call it in `resolve`, which runs per event; rows from `subscribe` load again.
To return a row through `Node`, or through an interface from `builder.interfaceRef()`:
* Brand it with the type's `addBrand`, such as `User.addBrand(row)`. Unbranded rows fail with
`Abstract type ... must resolve`.
* If you route a row yourself, with a `__typename` or a `resolveType` that returns a type name,
TypeScript doesn't check that the row is planned.
# Relay nodes
URL: /docs/plugins/prisma-next/relay
Defining Relay nodes for contract-backed models
`builder.prismaNode` defines a model type that implements the Relay `Node` interface, so clients can
fetch it by its global ID with the `node` and `nodes` query fields. You'll need the
[Relay plugin](../relay). For pagination, see [Connections](./connections).
## Define a node [#define-a-node]
`builder.prismaNode` takes the same options as `builder.prismaObject`, plus an `id`:
```ts
builder.prismaNode('Post', {
id: { field: 'id' },
fields: (t) => ({
title: t.exposeString('title'),
author: t.relation('author'),
}),
});
```
Any client can load a node directly by its ID through `node` and `nodes`, so add auth checks on
the type, such as [Scope Auth's](/docs/plugins/scope-auth) `authScopes`, for rows not everyone
should read. Filtering lookups with [`collection`](#filter-node-lookups) or a per-request `client`
isn't access control.
The ID holds the column's value, as text, or a JSON array of the values when `id.field` lists
several columns, as the [Prisma plugin's](/docs/plugins/prisma/relay) IDs do. To control the format
of the global ID, pass `resolve` and `parse`:
```ts
builder.prismaNode('Comment', {
id: {
field: 'id',
resolve: (comment) => `comment:${comment.id}`,
parse: (id) => id.slice('comment:'.length),
},
fields: (t) => ({ body: t.exposeString('body') }),
});
```
`prismaNode` adds these options:
* `id.field`: a column, or a list of columns, covering a non-null primary or unique key.
* `id.parse` / `id.resolve`: custom ID serialization. Pass both or neither.
* Other `id` options, such as `description`, are passed to the ID field.
* `collection`: the Collection nodes load from. See [Filter node lookups](#filter-node-lookups).
A timestamp in `id.field` has the same precision limit as a
[connection's order](./connections#ordering-and-cursors).
## Filter node lookups [#filter-node-lookups]
To limit which rows `node` and `nodes` can return, pass `collection`. It receives the context, so it
can filter by the current user. Here `node` only finds the current user's posts:
```ts
builder.prismaNode('Post', {
id: { field: 'id' },
collection: (ctx) => db.orm.Post.where({ authorId: ctx.userId }),
fields: (t) => ({ title: t.exposeString('title') }),
});
```
`node` and `nodes` return `null` for rows the filter excludes. Only filter the collection; don't
call `limit()`, `offset()`, `cursor()` or `include()` on it.
`collection` only applies to `node` and `nodes`. Rows reached through relations and other fields
don't pass through it.
## Several node types for one model [#several-node-types-for-one-model]
If you define more than one node type for a model, such as a `DraftPost` [variant](./variants) of
`Post`, each type has its own global IDs. When one field can return more than one of these types,
return a [`prismaInterface`](./interfaces) with a `resolveType`, or brand each row with its type's
`addBrand`:
```ts
import { collectionFromInfo } from '@pothos/plugin-prisma-next';
const DraftPost = builder.prismaNode('Post', {
variant: 'DraftPost',
id: { field: 'id' },
fields: (t) => ({ title: t.exposeString('title') }),
});
builder.queryField('latestDraft', (t) =>
t.field({
type: builder.nodeInterfaceRef(),
nullable: true,
resolve: async (_root, _args, ctx, info) => {
const drafts = await collectionFromInfo({
collection: db.orm.Post.where({ authorId: ctx.userId, published: 0 }).orderBy(
(post) => post.createdAt.desc(),
),
context: ctx,
info,
typeName: 'DraftPost',
});
const post = await drafts.first();
return post && DraftPost.addBrand(post);
},
}),
);
```
# Setup
URL: /docs/plugins/prisma-next/setup
Setting up the Prisma Next plugin
## Install [#install]
Set up Prisma ORM 8 and your contract with the
[Prisma ORM documentation](https://www.prisma.io/docs/orm), then install the plugin with its Prisma
peers:
npm
pnpm
yarn
bun
```bash
npm install --save @pothos/core @pothos/plugin-prisma-next graphql @prisma/orm-family-sql@8.0.0-rc.23 @prisma/orm-framework@8.0.0-rc.23
```
```bash
pnpm add @pothos/core @pothos/plugin-prisma-next graphql @prisma/orm-family-sql@8.0.0-rc.23 @prisma/orm-framework@8.0.0-rc.23
```
```bash
yarn add @pothos/core @pothos/plugin-prisma-next graphql @prisma/orm-family-sql@8.0.0-rc.23 @prisma/orm-framework@8.0.0-rc.23
```
```bash
bun add @pothos/core @pothos/plugin-prisma-next graphql @prisma/orm-family-sql@8.0.0-rc.23 @prisma/orm-framework@8.0.0-rc.23
```
The plugin targets Prisma ORM `8.0.0-rc.23` exactly. Install every `@prisma/orm-*` package,
including your database's (such as `@prisma/orm-postgres`), at that version. A second copy of any
of them breaks values passed between the two. The database packages also need `temporal-polyfill`,
which npm and pnpm install automatically; with Yarn, add it yourself.
Requires Node.js 22.20+ or 24.7+. This plugin and
`@pothos/plugin-prisma` can't be used in the same program.
If you use [`@pothos/plugin-scope-auth`](/docs/plugins/scope-auth), it needs to be 4.3.0 or newer.
## Configure the builder [#configure-the-builder]
Pass your Prisma client to the plugin, and your contract's type as `PrismaContract`:
```ts
import SchemaBuilder from '@pothos/core';
import PrismaNextPlugin from '@pothos/plugin-prisma-next';
import { contract, db } from './prisma/db';
const builder = new SchemaBuilder<{
PrismaContract: typeof contract;
}>({
plugins: [PrismaNextPlugin],
prisma: { client: db },
});
const User = builder.prismaObject('User', {
fields: (t) => ({
id: t.exposeID('id'),
email: t.exposeString('email'),
}),
});
const Post = builder.prismaObject('Post', {
fields: (t) => ({
id: t.exposeID('id'),
title: t.exposeString('title'),
author: t.relation('author'),
}),
});
builder.queryType({
fields: (t) => ({
recentPosts: t.prismaField({
type: ['Post'],
query: { where: { published: 1 }, orderBy: { createdAt: 'desc' }, limit: 10 },
}),
}),
});
export const schema = builder.toSchema();
```
`User` and `Post` are type refs, which later examples pass as a field's `type`; a model name such
as `'Post'` works too. Later pages add to these types, which you can also do with
[`prismaObjectFields`](./objects#adding-fields).
For nodes and connections, also install and add [`@pothos/plugin-relay`](/docs/plugins/relay).
This example uses PostgreSQL:
* The client groups models by schema (`db.orm.public.Post`). The plugin chooses that Collection
automatically for the field above.
* SQLite and MySQL work the same way with their own Prisma packages. Their clients don't group
models, and the rest of these docs use that shorter form, `db.orm.Post`.
Pass a context object, such as `contextValue: {}`, when you execute an operation. The plugin keeps
per-request state on it.
Examples using authentication assume context fields `userId: string` and `isAdmin: boolean`. Add
`Context: YourContext` alongside `PrismaContract` in the builder's type parameter, and supply those
values for each request. See [Context](../../guide/context) for configuring and typing it.
## Models in several namespaces [#models-in-several-namespaces]
If your contract has more than one namespace, refer to models by their full name, such as
`'auth.User'`. GraphQL type names leave the namespace out, so when two namespaces both have a
`User`, give one of them a `name`:
```ts
builder.prismaObject('auth.User', {
name: 'AuthUser',
fields: (t) => ({
id: t.exposeID('id'),
}),
});
```
## Plugin options [#plugin-options]
These go under `prisma` in the builder options.
* `client`: your Prisma client. For a [request-scoped client](./transactions#share-a-transaction-across-an-operation),
pass `(ctx) => client` together with the base client's `context`. Required.
* `skipDeferredFragments`: load `@defer` fragments when they execute instead of in the initial
query. Defaults to `true`.
* `defaultConnectionSize` / `maxConnectionSize`: default and maximum page sizes. Default to 20 and
100\.
* `filterConnectionTotalCount`: when `true` (the default), a `relatedConnection`'s `totalCount`
only counts rows matching its `query.where`.
## Transactions [#transactions]
For multi-step mutations and request-scoped clients, see the [Transactions guide](./transactions).
## TypeScript and Temporal [#typescript-and-temporal]
If you export refs that select Temporal columns, or have a Temporal primary key, and
declaration emit fails with TS2742 or TS2883 mentioning `temporal-spec`, add `temporal-spec` as a
direct dependency so TypeScript can name its types in your declarations:
npm
pnpm
yarn
bun
```bash
npm install --save temporal-spec
```
```bash
pnpm add temporal-spec
```
```bash
yarn add temporal-spec
```
```bash
bun add temporal-spec
```
If TypeScript reports missing `Temporal` globals, import their declarations once in your project
(for example in a `globals.d.ts` file):
```ts
import type {} from 'temporal-spec/global';
```
This provides types only. Your application still needs Temporal at runtime, either natively or
through its configured polyfill.
## Limitations [#limitations]
* On MySQL, Prisma reads rows but can't write them, count them or run aggregates yet. MongoDB isn't
supported.
* Malformed cursors fail with `Invalid cursor.`, and malformed IDs with `Invalid ID.` or the Relay
plugin's `Invalid global ID: …`. Errors the plugin detects in your runtime configuration reach clients as `Unexpected error.`, with detail in the error's
`cause`. ORM errors and errors from your code pass through unchanged and can name your models and
columns, so mask them in your GraphQL server.
* Prisma's packages are ESM-only, so use `"moduleResolution": "nodenext"` or `"bundler"`. A
CommonJS project on `node16` gets TS1541 errors unless `skipLibCheck` is on.
* graphql-jit isn't supported. Fields that load rows fail with `Unexpected error.` under it.
## Next steps [#next-steps]
Read [Queries and selections](./queries) for how `collection`, `query` and `select` work together,
then [Objects](./objects) and [Relations](./relations) for defining the rest of your schema.
# Transactions
URL: /docs/plugins/prisma-next/transactions
Running mutations and GraphQL operations in a Prisma transaction
Use Prisma's `db.transaction` when several writes must succeed or fail together. The plugin works
with your transaction client; your application controls when the transaction commits or rolls back.
The examples use the imported `db` and builder from [Setup](./setup), and the `User` ref from your
schema. They use the shorter SQLite client paths; on PostgreSQL, use `db.orm.public.User` and
`tx.orm.public.User` for models in the public schema.
## Write several rows atomically [#write-several-rows-atomically]
Pothos doesn't create transactions for you. To run a mutation's writes in one, call `transaction`
in the mutation field, then load the result once it commits:
```ts
import { randomUUID } from 'node:crypto';
builder.mutationField('signUp', (t) =>
t.prismaField({
type: 'User',
args: {
email: t.arg.string({ required: true }),
firstName: t.arg.string({ required: true }),
lastName: t.arg.string({ required: true }),
},
resolve: async (users, _root, args) => {
const id = await db.transaction(async (tx) => {
const user = await tx.orm.User.create({ ...args, id: randomUUID() });
await tx.orm.Post.create({
id: randomUUID(),
authorId: user.id,
title: 'Hello!',
content: '',
createdAt: new Date(),
});
return user.id;
});
return users.where({ id }).first();
},
}),
);
```
To read the result inside the transaction too, build the last write with
[`collectionFromInfo`](./objects#mutations) and return the row it writes:
```ts
import { collectionFromInfo } from '@pothos/plugin-prisma-next';
builder.mutationField('renameUser', (t) =>
t.field({
type: User,
nullable: true,
args: { id: t.arg.id({ required: true }), firstName: t.arg.string({ required: true }) },
resolve: (_root, args, ctx, info) =>
db.transaction(async (tx) => {
const users = await collectionFromInfo({
collection: tx.orm.User.where({ id: String(args.id) }),
context: ctx,
info,
});
return users.update({ firstName: args.firstName });
}),
}),
);
```
Don't return a `tx.orm` Collection from the callback: it runs after the transaction ends, and fails.
## Share a transaction across an operation [#share-a-transaction-across-an-operation]
To give each request its own client, such as a transaction, pass a function as `client` along with
the base client's `context`. Use this configuration before defining your types and building
the schema:
```ts
import SchemaBuilder from '@pothos/core';
import PrismaNextPlugin from '@pothos/plugin-prisma-next';
import { contract, db } from './prisma/db';
import type { PostgresQueryMembers } from '@prisma/orm-postgres/runtime';
const builder = new SchemaBuilder<{
PrismaContract: typeof contract;
Context: { db: PostgresQueryMembers };
}>({
plugins: [PrismaNextPlugin],
prisma: {
client: (ctx) => ctx.db,
context: db.context,
},
});
```
The configured client applies to queries the plugin starts itself. In your own `collection` and
`resolve` callbacks, use `ctx.db` in this mode too. Calls to the imported `db` use the base client
and run outside this transaction.
Call `graphql()` inside the transaction and pass `tx` in the GraphQL context:
```ts
const result = await db.transaction(async (tx) => {
const result = await graphql({
schema,
source,
contextValue: { db: tx },
});
// GraphQL reports errors in the result rather than rejecting, so throw to roll back.
if (result.errors?.length) {
throw result.errors[0];
}
return result;
});
```
With `@defer` or `@stream`, read every incremental result inside the callback: they load after the
initial result, and fail once the transaction has ended.
# Type variants
URL: /docs/plugins/prisma-next/variants
Multiple GraphQL types backed by the same contract model
Use `variant` to expose the same Prisma model as another GraphQL type. For example, a `Viewer`
type can show signed-in users their own email and drafts, which the public `User` type leaves out:
```ts
const Viewer = builder.prismaObject('User', {
variant: 'Viewer',
fields: (t) => ({
id: t.exposeID('id'),
email: t.exposeString('email'),
drafts: t.relation('posts', { query: { where: { published: 0 } } }),
}),
});
```
## Return a variant [#return-a-variant]
Wherever you use the model name, such as `'User'`, you get the default type. To return the variant,
pass its ref as `type`, for example from a root field:
```ts
builder.queryFields((t) => ({
viewer: t.prismaField({
type: Viewer,
nullable: true,
query: (_args, ctx) => ({ where: { id: ctx.userId } }),
}),
}));
```
`t.relation` takes a variant's ref as `type` the same way.
## Switch to another variant [#switch-to-another-variant]
`t.variant` adds a field that returns the same row as another variant, without another query. To
return `null` for some rows, pass `isNull`, which needs a nullable field. It sees the primary key,
the type's `select`, and the columns in the field's `select`. Here `User.viewer` returns the
`Viewer` type only for the signed-in user:
```ts
builder.prismaObject('User', {
fields: (t) => ({
viewer: t.variant(Viewer, {
nullable: true,
isNull: (user, _args, ctx) => user.id !== ctx.userId,
}),
}),
});
```
## Adding fields [#adding-fields]
To add fields to a variant, pass its ref to `prismaObjectField` or `prismaObjectFields`:
```ts
builder.prismaObjectField(Viewer, 'displayName', (t) =>
t.string({
select: { firstName: true, lastName: true },
resolve: (user) => `${user.firstName} ${user.lastName}`,
}),
);
```
To return either `User` or `Viewer` from one field, give them a shared
[GraphQL interface](./interfaces).
# Scope auth plugin
URL: /docs/plugins/scope-auth
Authorize fields and types with request scopes, permission loaders, and ownership checks.
The scope auth plugin checks authorization before a field resolver runs. Define named scopes for
roles, permissions, or ownership checks, then require those scopes on fields and types. Checks and
scope loaders are cached for the current request.
For schemas using Relay, see [Relay node authorization](/docs/plugins/scope-auth/relay-nodes)
for direct lookup policies and access granted through a parent field.
## Usage [#usage]
### Install [#install]
npm
pnpm
yarn
bun
```bash
npm install --save @pothos/plugin-scope-auth
```
```bash
pnpm add @pothos/plugin-scope-auth
```
```bash
yarn add @pothos/plugin-scope-auth
```
```bash
bun add @pothos/plugin-scope-auth
```
#### Plugin order [#plugin-order]
When using `scope-auth` with other plugins, the `scope-auth` plugin should generally be listed first
to ensure that other plugins that wrap resolvers do not execute before the `scope-auth` logic.
However, exceptions do exist where it is desirable for a plugin to run before `scope-auth`. For
instance, putting the [relay plugin](https://pothos-graphql.dev/docs/plugins/relay) before the
`scope-auth` plugin results in the `authScopes` function correctly receiving parsed `globalID`s.
### Setup [#setup]
```typescript
import SchemaBuilder from '@pothos/core';
import ScopeAuthPlugin from '@pothos/plugin-scope-auth';
type ArticlePermission = 'readArticle' | 'editArticle';
type Context = {
user: { id: string; employee: boolean; permissions: ArticlePermission[] } | null;
};
const builder = new SchemaBuilder<{
Context: Context;
AuthScopes: {
loggedIn: boolean;
employee: boolean;
canReadArticles: boolean;
customPerm: ArticlePermission;
};
}>({
plugins: [ScopeAuthPlugin],
scopeAuth: {
authorizeOnSubscribe: true,
authScopes: (context) => ({
loggedIn: !!context.user,
employee: context.user?.employee ?? false,
canReadArticles: () => context.user?.permissions.includes('readArticle') ?? false,
customPerm: (perm) => context.user?.permissions.includes(perm) ?? false,
}),
},
});
```
The setup defines two parts of the authorization model:
1. The `AuthScopes` type in the builder `SchemaTypes`. This is a map of types that defines the types
used by each of your scopes. We'll see how this is used in more detail below.
2. The `scope initializer` function, which is the implementation of each of the scopes defined in
the type above. This function returns a map of either booleans (indicating if the request has
the scope) or functions that load the scope (with an optional parameter).
The names of the scopes (`loggedIn`, `employee`, `canReadArticles`, and `customPerm`) are all
arbitrary, and are not part of the plugin. You can use whatever scope names you prefer, and can add
as many you need.
### Loading permissions asynchronously [#loading-permissions-asynchronously]
Scope initializers and loaders can return promises. This alternative builder uses request-scoped
`isEmployee` and `hasPermission` methods supplied by your application:
```typescript
const builder = new SchemaBuilder<{
Context: Context & {
isEmployee: () => Promise;
hasPermission: (permission: ArticlePermission) => Promise;
};
AuthScopes: {
loggedIn: boolean;
employee: boolean;
canReadArticles: boolean;
customPerm: ArticlePermission;
};
}>({
plugins: [ScopeAuthPlugin],
scopeAuth: {
authScopes: async (context) => ({
loggedIn: !!context.user,
employee: await context.isEmployee(),
canReadArticles: () => context.hasPermission('readArticle'),
customPerm: (permission) => context.hasPermission(permission),
}),
},
});
```
The initializer waits for `isEmployee` before returning its scope map. The permission loaders run
only when a field requires their scopes; their results are cached for the request and parameter.
### Using a scope on a field [#using-a-scope-on-a-field]
```typescript playground example="plugin-scope-auth"
builder.queryType({
fields: (t) => ({
message: t.string({
authScopes: {
loggedIn: true,
},
resolve: () => 'hi',
}),
}),
});
```
For this nullable field, an unauthenticated request receives `message: null` and a GraphQL error;
an authenticated request receives `"hi"`. Other fields that pass their own checks can still return
data. The application must authenticate the user before constructing the context: checking a
scope authorizes access using that context, but does not establish the user's identity.
Set `authorizeOnSubscribe: true` to check authorization when a subscription is created. Without
this option, checks run when subscription events resolve.
## Terminology [#terminology]
A lot of terms around authorization are overloaded, and can mean different things to different
people. Here is a short list of a few terms used in this document, and how they should be
interpreted:
* `scope`: A scope is the unit of authorization that can be used to authorize a request to resolve a
field.
* `scope map`: A map of scope names and scope parameters. This defines the set of scopes that will
be checked for a field or type to authorize the request the resolve a resource.
* `scope loader`: A function for dynamically loading scope given a scope parameter. Scope loaders
are ideal for integrating with a permission service, or creating scopes that can be customized
based on the field or values that they are authorizing.
* `scope parameter`: A parameter that will be passed to a scope loader. These are the values in the
authScopes objects.
* `scope initializer`: The function that creates the scopes or scope loaders for the current
request.
While this plugin uses `scopes` as the term for its authorization mechanism, this plugin can easily
be used for role or permission based schemes, and is not intended to dictate a specific philosophy
around how to authorize requests/access to resources.
## Use cases [#use-cases]
The examples below use the scopes from the setup above. Examples that introduce a different
builder are alternatives. Each `Article` definition below is also an alternative, using this
backing data and ref:
```typescript
type ArticleShape = {
id: string;
title: string;
content: string;
author: { id: string };
viewCount: number;
published: boolean;
};
const articles: ArticleShape[] = [{
id: '1',
title: 'Getting started',
content: 'Welcome',
author: { id: 'author-1' },
viewCount: 10,
published: true,
}];
const Article = builder.objectRef('Article');
```
If your application already uses an `Article` class, you can pass it to `builder.objectType`
instead, with `name: 'Article'`, and return class instances from resolvers.
### Top level auth on queries and mutations [#top-level-auth-on-queries-and-mutations]
Authorization runs before the field resolver. A denied mutation therefore cannot perform the
writes inside that resolver; an authorized mutation can. Put writes in the protected resolver,
not in the scope initializer or permission loader.
To add an auth check to root level queries or mutations, add authScopes to the field options:
```typescript
builder.queryType({
fields: (t) => ({
internalMessage: t.string({
authScopes: {
employee: true,
},
resolve: () => 'hi',
}),
}),
});
```
This will require the requests to have the `employee` scope. Adding multiple scopes to the
`authScopes` object will check all the scopes, and if the user has any of the scopes, the request
will be considered authorized for the current field. Subscription and Mutation root fields work the
same way.
### Auth on nested fields [#auth-on-nested-fields]
Fields on nested objects can be authorized the same way scopes are authorized on the root types.
```typescript
builder.objectType(Article, {
fields: (t) => ({
title: t.exposeString('title', {
authScopes: {
employee: true,
},
}),
}),
});
```
### Default auth for all fields on types [#default-auth-for-all-fields-on-types]
To apply the same scope requirements to all fields on a type, you can define an `authScopes` map in
the type options rather than on the individual fields.
```typescript
builder.objectType(Article, {
authScopes: {
loggedIn: true,
},
fields: (t) => ({
title: t.exposeString('title', {}),
content: t.exposeString('content', {}),
}),
});
```
### Overwriting default auth on field [#overwriting-default-auth-on-field]
In some cases you may want to use default auth scopes for a type, but need to change the behavior
for one specific field.
To add additional requirements for a specific field you can simply add additional scopes on the
field itself.
```typescript
builder.objectType(Article, {
authScopes: {
loggedIn: true,
},
fields: (t) => ({
title: t.exposeString('title', {}),
viewCount: t.exposeInt('viewCount', {
authScopes: {
employee: true,
},
}),
}),
});
```
To remove the type level scopes for a field, you can use the `skipTypeScopes` option:
```typescript
builder.objectType(Article, {
authScopes: {
loggedIn: true,
},
fields: (t) => ({
title: t.exposeString('title', {
skipTypeScopes: true,
}),
content: t.exposeString('content', {}),
}),
});
```
This will allow non-logged in users to resolve the title, but not the content of an Article.
`skipTypeScopes` can be used in conjunction with `authScopes` on a field to completely overwrite the
default scopes.
### Running scopes on types rather than fields [#running-scopes-on-types-rather-than-fields]
By default, all auth scopes are tested before a field resolves. This includes both scopes defined on
a type and scopes defined on a field. When scopes for a `type` fail, you will end up with an error
for each field of that type. Type level scopes are only executed once, but the errors are emitted
for each affected field.
The behavior may not be desirable for all users. You can set `runScopesOnType` to true, either on
object types, or in the `scopeAuth` options of the builder
```typescript
const builder = new SchemaBuilder<{
Context: Context;
AuthScopes: {
loggedIn: boolean;
};
}>({
scopeAuth: {
// Affects all object types (Excluding Query, Mutation, and Subscription)
runScopesOnType: true,
authScopes: async (context) => ({
loggedIn: !!context.user,
}),
},
plugins: [ScopeAuthPlugin],
});
builder.objectType(Article, {
runScopesOnType: true,
authScopes: {
loggedIn: true,
},
fields: (t) => ({
title: t.exposeString('title', {
// this will not have any effect because type scopes are not evaluated at the field level
skipTypeScopes: true,
}),
content: t.exposeString('content', {}),
}),
});
```
Enabling this has a couple of limitations:
1. This does not work with `graphql-jit`. This option uses the `isTypeOf` function, but
`graphql-jit` does not support async `isTypeOf`, and also does not correctly pass the context
object to the isTypeOf checks. Until this is resolved, this option will not work with
`graphql-jit`.
2. Fields of types that set `runScopesOnType` to true will not be able to use `skipTypeScopes` or
`skipInterfaceScopes`.
### Generalized auth functions with field specific arguments [#generalized-auth-functions-with-field-specific-arguments]
The scopes we have covered so far have all been related to information that applies to a full
request. In more complex applications it may not make sense to enumerate all the scopes a request
is authorized for ahead of time. To handle these cases you can define a scope loader which takes a
parameter and dynamically determines if a request is authorized for a scope using that parameter.
One common example of this would be a permission service that can check if a user or request has a
certain permission, and you want to specify the specific permission each field requires.
```typescript
builder.queryType({
fields: (t) => ({
articles: t.field({
type: [Article],
authScopes: {
customPerm: 'readArticle',
},
resolve: () => articles,
}),
}),
});
```
In the example above, the authScopes map uses the customPerm scope loader with a parameter of
`readArticle`. The first time a field requests this scope, the customPerm loader will be called with
`readArticle` as its argument. This scope will be cached, so that if multiple fields request the
same scope, the scope loader will still only be called once.
The types for the parameters you provide for each scope are based on the types provided to the
builder in the `AuthScopes` type.
### Customizing error messages [#customizing-error-messages]
Error messages (and error instances) can be customized either globally or on specific fields.
#### Globally [#globally]
```typescript
const builder = new SchemaBuilder<{
Context: Context;
AuthScopes: {
loggedIn: boolean;
};
}>({
scopeAuth: {
treatErrorsAsUnauthorized: true,
unauthorizedError: (parent, context, info, result) => new Error(`Not authorized`),
authScopes: async (context) => ({
loggedIn: !!context.user,
}),
},
plugins: [ScopeAuthPlugin],
});
```
The `unauthorizedError` callback will be called with the parent, context, and info object of the
unauthorized field. It will also include a 4th argument `result` that has the default message for
this type of failure, and a `failure` property with some details about what caused the field to be
unauthorized. This callback can either return an `Error` instance (or an instance of a class that
extends `Error`), or a `string`. If a string is returned, it will be converted to a
`ForbiddenError`.
The `treatErrorsAsUnauthorized` option changes how errors in authorization functions are handled. By
default errors are not caught by the plugin, and will act as if thrown directly from the resolver.
This means that thrown errors bypass the `unauthorizedError` callback, and will cause requests to
fail even when another scope in an `$any` passes.
Setting `treatErrorsAsUnauthorized` will cause errors to be caught and treated as if the scope was
not authorized.
#### Surfacing errors thrown in authorization checks [#surfacing-errors-thrown-in-authorization-checks]
When `treatErrorsAsUnauthorized` is set to true, errors are caught and attached to the `result`
object in the `unauthorizedError` callback. This allows you to surface the error to the client.
For example, if you want to re-throw errors thrown by authorization functions you could do this by
writing a custom `unauthorizedError` callback like this:
```typescript
import SchemaBuilder from '@pothos/core';
import ScopeAuthPlugin, { AuthFailure, AuthScopeFailureType } from '@pothos/plugin-scope-auth';
// Find the first error and re-throw it
function throwFirstError(failure: AuthFailure) {
// Check if the failure has an error attached to it and re-throw it
if ('error' in failure && failure.error) {
throw failure.error;
}
// Loop over any/all scopes and see if one of their children has an error to throw
if (
failure.kind === AuthScopeFailureType.AnyAuthScopes ||
failure.kind === AuthScopeFailureType.AllAuthScopes
) {
for (const child of failure.failures) {
throwFirstError(child);
}
}
}
const builder = new SchemaBuilder<{
Context: Context;
AuthScopes: {
loggedIn: boolean;
};
}>({
scopeAuth: {
authScopes: async (context) => ({
loggedIn: !!context.user,
}),
treatErrorsAsUnauthorized: true,
unauthorizedError: (parent, context, info, result) => {
// throw an error if it's found
throwFirstError(result.failure);
// throw a fallback error if no error was found
return new Error(`Not authorized`);
},
},
plugins: [ScopeAuthPlugin],
});
```
#### On individual fields [#on-individual-fields]
```typescript
builder.queryType({
fields: (t) => ({
example: t.string({
authScopes: { loggedIn: true },
unauthorizedError: (parent, args, context, info, result) =>
new Error("You must be logged in to query the 'example' field"),
resolve: () => 'example',
}),
}),
});
```
### Returning a custom value when unauthorized [#returning-a-custom-value-when-unauthorized]
In some cases you may want to return null, and empty array, throw a custom error, or return a custom
result when a user is not authorized. To do this you can add a `unauthorizedResolver` option to your
field.
```typescript
builder.queryType({
fields: (t) => ({
articles: t.field({
type: [Article],
authScopes: {
customPerm: 'readArticle',
},
resolve: () => articles,
unauthorizedResolver: () => [],
}),
}),
});
```
In the example above, if a user is not authorized they will simply receive an empty array in the
response. The `unauthorizedResolver` option takes the same arguments as a resolver, but also
receives a 5th argument that is an instance of `ForbiddenError`.
### Setting scopes that apply for a full request [#setting-scopes-that-apply-for-a-full-request]
We have already seen several examples of this. For scopes that apply to a full request like `loggedIn`
or `employee`, rather than using a scope loader, the scope initializer can simply use a boolean to
indicate if the request has the given scope. If you know ahead of time that a scope loader will
always return false for a specific request, you can do something like the following to avoid the
additional overhead of running the loader:
```typescript
const builder = new SchemaBuilder<{
Context: Context;
AuthScopes: {
humanPermission: ArticlePermission;
};
}>({
plugins: [ScopeAuthPlugin],
scopeAuth: {
authScopes: async (context) => ({
humanPermission: context.user
? (perm) => context.user?.permissions.includes(perm) ?? false
: false,
}),
},
});
```
This will ensure that if a request accesses a field that requests a `humanPermission` scope, and the
request has no signed-in user, we don't have to run the permission check at all for
those requests, since we know it would return false anyways.
### Change context types based on scopes [#change-context-types-based-on-scopes]
Sometimes you need to change your context typings depending on the applied scopes. You can provide
custom context for your defined scopes and use the `authField` method to access the custom context:
```typescript
type Context = {
user: { id: string } | null;
};
const builder = new SchemaBuilder<{
Context: Context;
AuthScopes: {
loggedIn: boolean;
};
AuthContexts: {
loggedIn: Context & { user: { id: string } };
};
}>({
plugins: [ScopeAuthPlugin],
scopeAuth: {
authScopes: async (context) => ({
loggedIn: !!context.user,
}),
},
});
builder.queryField('currentId', (t) =>
t.authField({
type: 'ID',
authScopes: {
loggedIn: true,
},
resolve: (parent, args, context) => context.user.id,
}),
);
```
`t.withAuth` returns a field builder with scopes already applied. It also works with methods
added by other plugins. With the same builder as above, an equivalent field is:
```typescript
builder.queryField('viewerId', (t) =>
t.withAuth({ loggedIn: true }).id({
resolve: (_parent, _args, context) => context.user.id,
}),
);
```
#### Using `withAuth` with Prisma fields [#using-withauth-with-prisma-fields]
`withAuth` preserves plugin-specific field methods while refining their context. This alternative
builder uses the client `prisma` and generated types from [Prisma setup](/docs/plugins/prisma/setup), with a
User model containing an integer `id`. The request context contains either the
signed-in user's ID or `null`:
```typescript
import PrismaPlugin from '@pothos/plugin-prisma';
import type PrismaTypes from '../lib/pothos-prisma-types';
import { getDatamodel } from '../lib/pothos-prisma-types';
const builder = new SchemaBuilder<{
Context: { user: { id: number } | null };
AuthScopes: { loggedIn: boolean };
AuthContexts: { loggedIn: { user: { id: number } } };
PrismaTypes: PrismaTypes;
}>({
plugins: [ScopeAuthPlugin, PrismaPlugin],
scopeAuth: {
authScopes: (context) => ({ loggedIn: !!context.user }),
},
prisma: {
client: prisma,
dmmf: getDatamodel(),
},
});
builder.prismaObject('User', {
fields: (t) => ({
id: t.exposeID('id'),
}),
});
builder.queryType();
builder.queryField('viewer', (t) =>
t.withAuth({ loggedIn: true }).prismaField({
type: 'User',
resolve: (query, _parent, _args, context) =>
prisma.user.findUniqueOrThrow({
...query,
where: { id: context.user.id },
}),
}),
);
```
The resolver receives a non-null `context.user` and Prisma's selection `query`. A denied request
does not call the resolver or issue its database query.
### Logical operations on auth scopes (any/all) [#logical-operations-on-auth-scopes-anyall]
By default the scopes in a scope map are evaluated in parallel, and if the request has any of
the requested scopes, the field will be resolved. In some cases, you may want to require multiple
scopes:
```typescript
builder.objectType(Article, {
fields: (t) => ({
title: t.exposeString('title', {}),
viewCount: t.exposeInt('viewCount', {
authScopes: {
$all: {
$any: {
employee: true,
canReadArticles: true,
},
loggedIn: true,
},
},
}),
}),
});
```
You can use the built in `$any` and `$all` scope loaders to combine requirements for scopes. The
above example requires a request to have either the `employee` or `canReadArticles` scopes, and the
`loggedIn` scope. `$any` and `$all` each take a scope map as their parameters, and can be nested
inside each other.
The same pattern can require both login and a permission:
`{ $all: { loggedIn: true, customPerm: 'editArticle' } }`. A user with only `readArticle` access
can read a field requiring that permission but cannot pass the edit check. A user with
`editArticle` access passes it only when `loggedIn` also succeeds.
You can change the default strategy used for top level auth scopes by setting the `defaultStrategy`
option in the builder (defaults to `any`):
```typescript
const builder = new SchemaBuilder<{
Context: {
user: { id: string } | null;
};
AuthScopes: {
loggedIn: boolean;
};
DefaultAuthStrategy: 'all';
}>({
plugins: [ScopeAuthPlugin],
scopeAuth: {
authScopes: async (context) => ({
loggedIn: !!context.user,
}),
defaultStrategy: 'all',
},
});
```
### Auth that depends on parent value [#auth-that-depends-on-parent-value]
For cases where the required scopes depend on the value of the requested resource you can use a
function in the `authScopes` option that returns the scope map for the field.
```typescript
builder.objectType(Article, {
fields: (t) => ({
viewCount: t.exposeInt('viewCount', {
authScopes: (article, args, context, info) => {
if (context.user?.id === article.author.id) {
// If user is author, let them see it
// returning a boolean lets you set auth without specifying other scopes to check
return true;
}
// If the user is not the author, require the employee scope
return {
employee: true,
};
},
}),
}),
});
```
authScopes functions on fields will receive the same arguments as the field resolver, and will be
called each time the resolve for the field would be called. This means the same authScopes function
could be called multiple time for the same resource if the field is requested multiple times using
an alias.
Returning a boolean from an auth scope function is an easy way to allow or disallow a request from
resolving a field without needing to evaluate additional scopes.
### Setting type level scopes based on the parent value [#setting-type-level-scopes-based-on-the-parent-value]
You can also use a function in the authScopes option for types. This function will be invoked with
the parent and the context as its arguments, and should return a scope map.
```typescript
builder.objectType(Article, {
authScopes: (parent, context) => {
if (parent.published) {
return {
loggedIn: true,
};
}
return {
employee: true,
};
},
fields: (t) => ({
title: t.exposeString('title', {}),
}),
});
```
This requires an employee for unpublished articles and a signed-in user for published articles.
### Setting scopes based on the return value of a field [#setting-scopes-based-on-the-return-value-of-a-field]
This is a use that is not currently supported. The current work around is to move those checks down
to the returned type. Combining this with `runScopesOnType` should work for most cases.
### Granting access to a resource based on how it is accessed [#granting-access-to-a-resource-based-on-how-it-is-accessed]
In some cases, you may want to grant a request scopes to access certain fields on a child type. To
do this you can use `$granted` scopes.
```typescript
builder.queryType({
fields: (t) => ({
freeArticle: t.field({
type: Article,
grantScopes: ['readArticle'],
resolve: () => articles[0],
}),
}),
});
builder.objectType(Article, {
authScopes: {
loggedIn: true,
$granted: 'readArticle',
},
fields: (t) => ({
title: t.exposeString('title', {}),
}),
});
```
In the above example, the fields of the `Article` type normally require the `loggedIn` scope granted
to logged in users, but can also be accessed with the `$granted` scope `readArticle`. This means
that if the field that returned the Article "granted" the scope, the article can be read. The
`freeArticle` field on the `Query` type grants this scope, allowing anyone querying that field to
access fields of the free article. `$granted` scopes are separate from other scopes, and do not give
a request access to normal scopes of the same name. `$granted` scopes are also not inherited by
nested children, and would need to be explicitly passed down for each field if you wanted to grant
access to nested children.
### Reusing checks for multiple, but not all fields [#reusing-checks-for-multiple-but-not-all-fields]
You may have cases where groups of fields on a type are accessible using some shared condition. This
is another case where `$granted` scopes can be helpful.
```typescript
builder.objectType(Article, {
grantScopes: (article, context) => {
if (context.user?.id === article.author.id) {
return ['author', 'readArticle'];
}
if (!article.published) {
return [];
}
return ['readArticle'];
},
fields: (t) => ({
title: t.exposeString('title', {
authScopes: {
$granted: 'readArticle',
},
}),
content: t.exposeString('content', {
authScopes: {
$granted: 'readArticle',
},
}),
viewCount: t.exposeInt('viewCount', {
authScopes: {
$granted: 'author',
},
}),
}),
});
```
In the above example, `title`, `content`, and `viewCount` each use `$granted` scopes. In this case,
rather than scopes being granted by the parent field, they are granted by the Article type
itself. This allows the access to each field to change based on some dynamic conditions (if the
request is from the author, and if the article is a draft) without having to duplicate that logic
in each individual field.
### Interfaces [#interfaces]
Interfaces can define auth scopes on their fields the same way objects do. Fields for a type will
run checks for each interface it implements separately, meaning that a request would need to satisfy
the scope requirements for each interface separately before the field is resolved.
Object types can set `skipInterfaceScopes` to `true` to skip interface checks when resolving fields
for that Object type.
Inherited fields use the object's `authScopes` and `grantScopes`, along with the policies of the
interface that declared the field and the other interfaces the object implements. All required
checks must pass, including when querying through an interface.
`skipInterfaceScopes` on the field or object skips all interface checks, including the declaring
interface's check. The object's own `authScopes` still runs. `skipTypeScopes` on the field skips the
object's and declaring interface's own checks. Neither option skips grants.
An interface's `runScopesOnType` does not bypass its policy: implementing objects enforce it on
both inherited fields and fields they declare themselves.
### Cache keys [#cache-keys]
Auth scopes by default are cached based on the identity of the scope parameter. This works great for
statically defined scopes, and scopes that take primitive values as their parameters. If you define
auth scopes that take complex objects, and create those objects in a scope function (based on
arguments, or parent values) You won't get cache hits on those checks.
To work around this, you can provide a `cacheKey` option to the builder for generating a cache key
from your scope checks.
```typescript
const builder = new SchemaBuilder<{
Context: Context;
AuthScopes: {
articlePermission: { permission: ArticlePermission; authorId: string };
};
}>({
plugins: [ScopeAuthPlugin],
scopeAuth: {
cacheKey: (value) => JSON.stringify(value),
authScopes: (context) => ({
articlePermission: ({ permission, authorId }) =>
context.user?.id === authorId ||
(context.user?.permissions.includes(permission) ?? false),
}),
},
});
builder.queryField('canEditArticle', (t) =>
t.boolean({
args: { authorId: t.arg.string({ required: true }) },
authScopes: (_parent, { authorId }) => ({
articlePermission: { permission: 'editArticle', authorId },
}),
resolve: () => true,
}),
);
```
Use a key that includes every value affecting the permission check. `JSON.stringify` works for
JSON-compatible parameters with consistent property ordering; it does not handle circular values.
For resource checks, a stable resource ID is often sufficient.
## When checks are run, and how things are cached [#when-checks-are-run-and-how-things-are-cached]
Create a new context object for every request. Reusing a context also reuses cached authorization
results, even if you change the user on it. When a server copies the context, initialize its cache
with `initContextCache()` from `@pothos/core` in the request context factory.
### Scope Initializer [#scope-initializer]
The scope initializer would be run once the first time a field protected by auth scopes is resolved,
its result will be cached for the current request.
### authScopes functions on fields [#authscopes-functions-on-fields]
When using a function for `authScopes` on a field, the function will be run each time the field is
resolved, since it has access to all the arguments passed to the resolver
### authScopes functions on types [#authscopes-functions-on-types]
When using a function for `authScopes` on a type, the function will be run once for each
instance of that type in the response. It will be run lazily when the first field for that object is
resolved, and its result will be cached and reused by all fields for that instance of the type.
### scope loaders [#scope-loaders]
Scope loaders will be run whenever a field requires the corresponding scope with a unique
parameter. The scope loader results are cached per request based on a combination of the name of the
scope, and its parameter.
### grantScope on field [#grantscope-on-field]
`grantScopes` on a field will run after the field is resolved, and is not cached
### grantScope on type [#grantscope-on-type]
`grantScopes` on a type (object or interface) will run when the first field on the type is
resolved. It's result will be cached and reused for each field of the same instance of the type.
## API [#api]
### Types [#types]
* `AuthScopes`: `extends {}`. Each property is the name of its scope, each value is the type for the
scopes parameter.
* `ScopeLoaderMap`: Object whose keys are scope names (from `AuthScopes`) and whose values are
either booleans (indicating whether or not the request has the scope) or function that take a
parameter (type from `AuthScope`) and return `MaybePromise`
* `ScopeMap`: A map of scope names to parameters. Based on `AuthScopes`, may also contain `$all`,
`$any` or `$granted`.
### Builder [#builder]
Set the initializer in `scopeAuth.authScopes`, alongside options such as `defaultStrategy`,
`authorizeOnSubscribe`, `unauthorizedError`, and `cacheKey`.
* `authScopes`: (context: Types\['Context']) => `MaybePromise>`
### Object and Interface options [#object-and-interface-options]
* `authScopes`: `ScopeMap` or `function`, accepts `parent` and `context` returns
`MaybePromise`
* `grantScopes`: `function`, accepts `parent` and `context` returns `MaybePromise`
### Field Options [#field-options]
* `authScopes`: `ScopeMap` or `function`, accepts same arguments as resolver, returns
`MaybePromise`
* `grantScopes`: `string[]` or `function`, accepts same arguments as resolver, returns
`MaybePromise`
* `skipTypeScopes`: `boolean`
* `skipInterfaceScopes`: `boolean`
### toSchema options [#toschema-options]
* `disableScopeAuth`: disable the scope auth plugin. Useful for testing.
# Relay node authorization
URL: /docs/plugins/scope-auth/relay-nodes
Protect direct node lookups and use scopes to distinguish refetchable access from access granted through a parent field.
Registering a Relay node creates another way to reach an object: `Query.node` and `Query.nodes`
can load it from its global ID. A filter or authorization check on `Query.articles`, a connection,
or a parent relationship does not run when a client uses that lookup. Global IDs identify objects;
they are not proof that a caller may read them.
This applies to `builder.node`, `builder.prismaNode`, and `builder.drizzleNode`. Decide which
objects should be refetchable and what access a direct lookup should allow. A type can participate
in a [connection](/docs/plugins/relay#creating-connections) without implementing `Node`.
## Choose a default policy [#choose-a-default-policy]
For an API where direct refetches require a signed-in user, put that requirement on **both**
generated lookup fields. These options belong in the builder's `relay` configuration, alongside
the Scope Auth setup shown below. The private-policy example combines these gates with the
Node interface and Query defaults discussed next; the original example keeps its public
featured-article path:
```typescript playground example="plugin-scope-auth-variant-private-nodes" op="1"
nodeQueryOptions: {
authScopes: { loggedIn: true },
},
nodesQueryOptions: {
authScopes: { loggedIn: true },
},
```
These field checks run before Relay's node resolver, so an anonymous request cannot trigger its
node loaders, even with a `__typename`-only selection. A successful login check is only a baseline:
keep resource-specific permissions on the returned types or in their loaders. These options do not
protect custom fields that return the same objects or use node-loading helpers themselves.
You can also put a shared policy on the `Node` interface:
```typescript playground example="plugin-scope-auth-variant-private-nodes" op="2"
nodeTypeOptions: {
authScopes: { loggedIn: true },
},
```
This applies the interface requirement to fields of its implementing objects, including objects
returned through ordinary root fields, relationships, and connections. It adds to each object's
own scopes; satisfying one does not bypass the other. For example, a signed-in reader can still
fail an article's separate `readArticle` permission check.
Interface scopes are useful for a shared baseline, but their default execution still happens at
the object's fields, after loading. They do not hide `__typename`. Use the root-field gates when
you also want to reject anonymous node lookups before loading. The two configurations can be used
together. An object or field can opt out of interface checks with `skipInterfaceScopes: true`, so
audit these exceptions as part of the policy.
An interface login requirement would also block the anonymous featured-article path later in this
guide. Its parent grant satisfies the article's own policy; it does not override a separate
interface requirement. Choose a baseline that matches which parts of your API are public.
### Make Query fields private by default [#make-query-fields-private-by-default]
If most of the API requires login, `authScopes` on `builder.queryType` also applies to the generated
`node` and `nodes` fields. Public fields can explicitly skip the Query type requirement:
```typescript playground example="plugin-scope-auth-variant-private-nodes" op="1"
builder.queryType({
authScopes: { loggedIn: true },
fields: (t) => ({
serviceName: t.string({
skipTypeScopes: true,
resolve: () => 'Publishing API',
}),
}),
});
```
This is a Query-root policy, not a schema-wide authorization default. Configure Mutation and
Subscription separately, and keep entity-level checks for access through other paths. Field scopes
still apply when `skipTypeScopes` is used; it only removes the parent type's requirement.
`scopeAuth.defaultStrategy` controls whether scopes in a map use AND or OR, not whether unannotated
fields are private.
If direct refetching is not part of the API, set both `relay.nodeQueryOptions` and
`relay.nodesQueryOptions` to `false`. Node types and their IDs can remain in the schema without
those generated root lookups; custom lookup fields still need their own policy.
## Apply a policy to the returned object [#apply-a-policy-to-the-returned-object]
Type `authScopes` let the same policy protect fields whether the object came from a root field,
a relationship, a connection, or a node lookup. Parameterized scopes are useful for checks such as
ownership, project membership, or permission to read a particular article. Their results are cached
by scope name and parameter for the request.
In this example, readers with the `readArticle` permission may read articles through any path.
Other readers can read the featured article through a field that explicitly grants access.
The builder defines a `permission` scope using the current user:
```typescript playground example="plugin-scope-auth" op="9"
export const builder = new SchemaBuilder<{
Context: Context;
AuthScopes: { loggedIn: boolean; permission: Permission };
}>({
plugins: [ScopeAuthPlugin, RelayPlugin],
scopeAuth: {
authScopes: (context) => ({
loggedIn: !!context.user,
permission: (permission) => context.user?.permissions.includes(permission) ?? false,
}),
unauthorizedError: () => new Error('Not authorized'),
},
});
```
The node accepts either the read permission or a grant from its parent field:
```typescript playground example="plugin-scope-auth" op="8"
const ArticleNode = builder.objectRef('Article');
builder.node(ArticleNode, {
id: { resolve: (article: Article) => article.id },
loadOne: (id) => {
return articles.find((article) => article.id === id) ?? null;
},
authScopes: {
$any: {
permission: 'readArticle',
$granted: 'readFeaturedArticle',
},
},
fields: (t) => ({
title: t.exposeString('title'),
}),
});
```
`$any` makes this an explicit choice between two authorization paths. For policies requiring both
membership and a resource permission, use `$all` instead. A scope parameter should identify the
resource or permission being checked; include every value that changes the decision in its cache
key when using object parameters.
The loader retrieves the article before scopes run. By default, type scopes run before the
object's individual field resolvers. Those fields are protected, but `__typename` does not use a
Pothos field resolver. The [existence-check operation](/playground?example=plugin-scope-auth\&op=10)
therefore returns `Article` even for an anonymous reader. Field-level checks do not hide existence.
For an ordinary, path-independent policy, `runScopesOnType: true` moves type checks to GraphQL
object completion, including selections containing only `__typename`. Denial then produces an error
at the field returning the object. It still does not prevent the loader from running. Review its
[execution compatibility and override limitations](/docs/plugins/scope-auth#running-scopes-on-types-rather-than-fields).
Keep the parent-grant pattern below on the default field-level execution: parent grants are not
resolved at the same response path when scopes run through `isTypeOf`.
## Grant access through a particular path [#grant-access-through-a-particular-path]
A featured-article field can deliberately allow a reader to view one selected article without
making every article available through node lookups:
```typescript playground example="plugin-scope-auth" op="8"
builder.queryField('featuredArticle', (t) =>
t.field({
type: ArticleNode,
nullable: true,
grantScopes: ['readFeaturedArticle'],
resolve: () => {
return articles[0];
},
}),
);
```
`grantScopes` applies to the returned object's response path. It does not add an ordinary scope to
the request, and it does not turn the returned ID into a transferable permission. An anonymous
reader can receive the featured article and still be denied when refetching the same ID:
```graphql
query ArticleAccessPaths {
featuredArticle {
id
title
}
node(id: "QXJ0aWNsZTox") {
... on Article {
id
title
}
}
}
```
[Run both access paths](/playground?example=plugin-scope-auth\&op=8). The featured article resolves,
while `node` is `null` with a `Not authorized` error at `node.id`. The Relay ID is non-null, so
its denied field check nulls the containing node. A reader with the `readArticle` permission can
refetch it directly. This distinction can be useful for previews or access granted by a parent
resource, but clients must not assume every ID they receive can be refetched successfully.
Grants are not inherited by nested children. A grant on a connection field does not automatically
reach objects beneath `edges.node`; put the policy on the returned entity, or deliberately propagate
the grant through the intervening fields. Likewise, a grant on one alias does not authorize another
alias or a separate `node` / `nodes` selection. The underlying object or permission result may be
cached while its response paths still have different granted scopes.
When access should follow the entity across all paths, use an ordinary scope such as `canReadArticle`
with the article ID, and use that same permission policy on the object regardless of its parent. Reserve parent-field
grants for access that is intentionally tied to that path. Field scopes can then add stricter permissions for
sensitive fields, such as an article's editorial notes, without restricting its public title.
Use a static type scope map for this parent-grant pattern, as above. An `authScopes` function on
a type caches its evaluated result for the returned object instance, so it is not suitable for
combining a path-dependent grant with an object-dependent check. Field `authScopes` functions are
another option when a check needs the current field's arguments or parent data. Avoid deriving
path-dependent permissions in a cached type callback.
## Decide what node loading may reveal [#decide-what-node-loading-may-reveal]
If an unauthorized entity should behave like a missing one, enforce visibility in the node loader
or the data-access layer it uses. Return `null` for both cases if that is the API's policy. Do not
rely on a type scope to prevent a database read, an eager relationship load, or side effects in a
loader. With `loadMany`, check every requested ID and preserve the input order, including missing
or inaccessible entries.
The database integrations supply their own node loaders. A `where` clause in an unrelated list
resolver does not configure them. See [Prisma Relay nodes](/docs/plugins/prisma/relay) and
[Drizzle Relay nodes](/docs/plugins/drizzle/relay) for integration-specific guidance.
Keep an entity as an
ordinary object if its direct lookup policy is not implemented.
A failed scope normally produces a GraphQL error; a loader returning `null` can represent absence
without an authorization error. Null propagation still follows the configured field and list-item
nullability, so a non-null node field or item can affect a larger part of the response. Returning
`null` alone is not a guarantee against every form of information disclosure, such as differences
in errors or timing.
## Keep authorization tied to the request [#keep-authorization-tied-to-the-request]
Relay's `loadOne` and `loadMany` cache node loads on the context, and Scope Auth caches scope
initialization and permission decisions for that context. Use a fresh context for each request and
keep its authenticated identity stable. A cache hit is not a new permission check; do not reuse a
context between users or expect changing its user midway through an operation to invalidate caches.
Database plugin loaders can have additional request caches of their own.
Exercise the direct path as well as the intended parent path: an allowed user, a denied user, a
missing ID, a mixed `nodes` request, and a selection containing only `__typename`. For grant-based
policies, request the same object through both a granted path and a direct lookup in one operation.
This catches accidental assumptions that receiving an ID or loading an object grants access to it.