# 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.
# Overview
URL: /docs
Build type-safe GraphQL schemas in TypeScript.
Pothos is a GraphQL schema builder for TypeScript. You define types, fields, and resolvers in
TypeScript, and Pothos checks how they fit together without generating resolver types or maintaining
a separate schema definition.
`builder.toSchema()` creates a standard graphql-js `GraphQLSchema` that you can pass to your GraphQL
server. The core package has `graphql` as its only peer dependency. Plugins add features such as
authorization, pagination, and ORM integration to the same builder.
## Build a schema from your data [#build-a-schema-from-your-data]
```typescript
import SchemaBuilder from '@pothos/core';
const builder = new SchemaBuilder({});
const Giraffe = builder.objectRef<{ name: string; heightInMeters: number }>('Giraffe').implement({
fields: (t) => ({
name: t.exposeString('name'),
heightInFeet: t.float({
resolve: (giraffe) => giraffe.heightInMeters * 3.28084,
}),
}),
});
builder.queryType({
fields: (t) => ({
giraffe: t.field({
type: Giraffe,
resolve: () => ({ name: 'Gina', heightInMeters: 5 }),
}),
}),
});
export const schema = builder.toSchema();
```
The backing data and the GraphQL type can have different shapes. Here, the resolver returns a height
in meters, while clients query `heightInFeet`. Pothos checks the data returned by `giraffe` and infers
the `giraffe` parameter's type in the field resolver.
Follow the [Guide](/docs/guide) to install Pothos, start a server, and run your first query.
[Objects](/docs/guide/objects) explains object references, classes, and registering backing types by
name. The [API reference](/docs/api/schema-builder) lists the builder's methods and options.
## Plugins that make Pothos even better [#plugins-that-make-pothos-even-better]
# 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>;
};
```
* `type`: [Type Parameter](./arg-builder#type-parameter)
* `required`: defaults to `false`, unless overwritten in SchemaBuilder see
[Changing Default Nullability](../guide/changing-default-nullability).
* `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>;
};
```
* `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).
* `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>;
};
```
* `type`: [Type Parameter](./input-field-builder#type-parameter)
* `required`: defaults to `false`, unless overwritten in SchemaBuilder. See
[Changing Default Nullability](../guide/changing-default-nullability).
* `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.
## `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 = {
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 = {
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 = {
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 = {
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 = {
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 = {
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).
## `enumType(param, options)` [#enumtypeparam-options]
* `param`: A string name of the enum or a typescript enum
* `options`: `EnumTypeOptions`
### `EnumTypeOptions` [#enumtypeoptions]
```typescript
type EnumTypeOptions = {
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
## `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 = {
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 = {
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.
## `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 schema definition AST node.
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
// src/types.ts
export interface User {
id: string;
name: string;
}
```
```typescript
// src/builder.ts
import SchemaBuilder from '@pothos/core';
export const builder = new SchemaBuilder({});
```
```typescript
// 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
// 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
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
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
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
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
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
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
query {
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
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
{
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.
# 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.
```ts
// builder.ts
import SchemaBuilder from '@pothos/core';
export const builder = new SchemaBuilder({});
```
```ts
// 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] }),
}),
});
```
```ts
// 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 }),
}),
});
```
```ts
// 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:
```ts
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
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
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
query {
currentUser {
id
username
}
}
```
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
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;
},
}),
}),
});
```
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
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
{
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"`.
## 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
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
enum Diet {
HERBIVOROUS,
CARNIVOROUS,
OMNIVOROUS,
}
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:
```typescript
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 }]),
),
});
```
Here the GraphQL names are `sedan`, `suv`, `truck`, and `motorcycle`, while resolvers use `SEDAN`,
`SUV`, `TRUCK`, and `MOTORCYCLE`. To use the object's values for both names and internal values,
replace the enum definition with:
```typescript
const VehicleTypeEnum = builder.enumType('VehicleType', {
values: Object.values(VehicleType),
});
```
To use the keys for both, use `Object.keys(VehicleType)`. TypeScript types `Object.keys` as
`string[]`, so the following assertion preserves the known keys of this local object:
```typescript
const VehicleTypeEnum = builder.enumType('VehicleType', {
values: Object.keys(VehicleType) as (keyof typeof VehicleType)[],
});
```
# 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
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
builder.queryType({
fields: (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'] }),
}),
});
```
## 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
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
const LengthUnit = builder.enumType('LengthUnit', {
values: { Feet: {}, Meters: {} },
});
builder.objectField('Giraffe', 'preferredNeckLengthUnit', (t) =>
t.field({
type: LengthUnit,
resolve: () => 'Feet',
}),
);
```
## Lists [#lists]
To create a list field, you can wrap the type in an array
```typescript
builder.queryType({
fields: (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
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
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
builder.queryType({
fields: (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
builder.queryFields((t) => ({
giraffe: 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)
## Nested Lists [#nested-lists]
You can use `t.listRef` to create a list of lists
```typescript
const Query = 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: (parent, args) => {
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
// schema.ts
// Import the builder
import builder from './builder';
// Import your type definitions
import './types/Query';
import './types/User';
import './types/Posts';
// Build and export the schema
export const schema = builder.toSchema();
```
## 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;
```
# Guide
URL: /docs/guide
Install Pothos, build a schema, and run your first GraphQL query.
Pothos builds a standard GraphQL schema from TypeScript. This guide creates a small schema, serves it
with [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server), and runs a query against it.
## 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
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
query {
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.
# 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.
```ts
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.
```ts
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.
```ts
import type { SchemaTypes } from '@pothos/core';
function createPaginationArgs(
builder: PothosSchemaTypes.SchemaBuilder,
) {
return builder.args((t) => ({
limit: t.int(),
offset: t.int(),
}));
}
builder.queryType({});
builder.queryField('getUsers', (t) =>
t.field({
type: [UserWithId],
args: {
...createPaginationArgs(builder),
},
resolve: () => [],
}),
);
```
# 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 examples through “Recursive inputs” build on the same schema. Start with a backing model and
an object ref for the mutation result:
```typescript
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
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
mutation {
createGiraffe(input: { name: "Gina", birthdate: "2020-03-15", height: 4.8 }) {
name
height
}
}
```
```json
{ "data": { "createGiraffe": { "name": "Gina", "height": 4.8 } } }
```
## 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.
Add this input and mutation field to the builder above:
```typescript
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;
},
}),
);
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
mutation {
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
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
builder.inputType('GiraffeInput', {
fields: (t) => ({
name: t.string({ required: true }),
birthdate: t.string({ required: true }),
height: t.float({ required: true }),
}),
});
```
In the `createGiraffe` mutation, replace the `input` argument's `t.arg(...)` expression with:
```typescript
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
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
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:
```typescript
builder.queryType({
fields: (t) => ({
animals: t.field({
type: [AnimalRef],
resolve: () => [
{ 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
query {
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`.
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
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
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.
## Add a query [#add-a-query]
Add a field to the root `Query` type that returns a giraffe, then build the schema:
```typescript
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`.
### 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
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.
### Using Refs [#using-refs]
You can create and implement a ref in one expression:
```typescript
const GiraffeRef = builder.objectRef('Giraffe').implement({
fields: (t) => ({
name: t.exposeString('name'),
}),
});
```
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`:
```typescript
class Giraffe {
constructor(
public name: string,
public birthday: Date,
public heightInMeters: number,
) {}
}
builder.objectType(Giraffe, {
name: 'Giraffe',
fields: (t) => ({
name: t.exposeString('name'),
}),
});
builder.queryType({
fields: (t) => ({
giraffe: t.field({
type: Giraffe,
resolve: () => new Giraffe('James', new Date(Date.UTC(2012, 11, 12)), 5.2),
}),
}),
});
```
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:
```typescript
interface SchemaTypes {
Objects: {
Giraffe: Giraffe;
};
}
const builder = new SchemaBuilder({});
builder.objectType('Giraffe', {
fields: (t) => ({
name: t.exposeString('name'),
}),
});
builder.queryType({
fields: (t) => ({
giraffe: t.field({
type: 'Giraffe',
resolve: () => ({
name: 'James',
birthday: new Date(Date.UTC(2012, 11, 12)),
heightInMeters: 5.2,
}),
}),
}),
});
```
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]
```typescript
import type { ObjectRef } from '@pothos/core';
import builder from './builder';
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]
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
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]
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
import type { InputFieldBuilder } from '@pothos/core';
import builder from './builder';
type BuilderTypes = typeof builder.$inferSchemaTypes;
function createInputFields(t: InputFieldBuilder) {
return {
id: t.id({}),
reason: t.field({ type: 'String', required: false }),
};
}
builder.inputType('InputWithCommonFields1', {
fields: (t) => ({
...createInputFields(t),
}),
});
builder.inputType('InputWithCommonFields2', {
fields: (t) => ({
...createInputFields(t),
}),
});
```
# Printing Schemas
URL: /docs/guide/printing-schemas
Guide for printing a Pothos schema to an SDL schema file
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.
```typescript
import { writeFileSync } from 'node:fs';
import { printSchema } from 'graphql';
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'}`,
}),
}),
});
const schema = builder.toSchema();
const schemaAsString = printSchema(schema);
writeFileSync('./schema.graphql', `${schemaAsString}\n`);
```
`builder.toSchema()` sorts the schema by default.
Save the example 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
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
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
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;
},
}),
);
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
mutation {
createGiraffe(name: "Morgan") {
name
}
}
```
A subsequent `{ giraffes { name } }` query returns both James and Morgan.
## Subscriptions [#subscriptions]
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
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
{
double(value: 3)
}
```
```json
{
"data": {
"double": 6
}
}
```
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
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') }),
}),
});
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
const builder = new SchemaBuilder<{
Scalars: {
ID: { Input: string; Output: string };
};
}>({});
```
GraphQL still 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
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.
## 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).
* 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
import SchemaBuilder from '@pothos/core';
interface PothosTypes {
Context: {
user: {
id: string;
};
};
}
const builder = new SchemaBuilder({});
```
## 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
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.
## 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
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
query {
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
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
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',
}),
},
});
```
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`
### 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.
* `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 }`.
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.
## 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:
```typescript
export class PothosExamplePlugin extends BasePlugin {
onTypeConfig(typeConfig: PothosTypeConfig) {
console.log(this.builder.options.optionInRootOfConfig);
return typeConfig;
}
}
```
### 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
export class PothosExamplePlugin extends BasePlugin {
onTypeConfig(typeConfig: PothosTypeConfig) {
console.log(this.options.customBuildTimeOptions);
return typeConfig;
}
}
```
### 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
export class PothosExamplePlugin extends BasePlugin {
onTypeConfig(typeConfig: PothosTypeConfig) {
if (typeConfig.kind === 'Object') {
console.log(typeConfig.pothosOptions.optionOnObject);
}
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
export class PothosExamplePlugin extends BasePlugin {
onOutputFieldConfig(fieldConfig: PothosOutputFieldConfig) {
if (fieldConfig.kind === 'Mutation') {
console.log(fieldConfig.pothosOptions.customMutationFieldOption);
}
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
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
export class PothosExamplePlugin extends BasePlugin {
wrapResolve(
resolver: GraphQLFieldResolver,
fieldConfig: PothosOutputFieldConfig,
): GraphQLFieldResolver {
return (parent, args, context, info) => {
console.log(`Resolving ${info.parentType}.${info.fieldName}`);
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.
### 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
export class PothosExamplePlugin extends BasePlugin<
Types,
{ resolveCount: number }
> {
createRequestData(context: Types['Context']): { resolveCount: number } {
return { resolveCount: 0 };
}
wrapResolve(
resolver: GraphQLFieldResolver,
fieldConfig: PothosOutputFieldConfig,
): GraphQLFieldResolver {
return (parent, args, context, info) => {
const requestData = this.requestData(context);
requestData.resolveCount += 1;
console.log(`request has resolved ${requestData.resolveCount} fields`);
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 have a method for wrapping input fields, instead, the `wrapResolve`
and `wrapSubscribe` methods can be used to modify the `args` object before passing it down to the
original resolver.
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:
```typescript
export class PothosRelayPlugin extends BasePlugin {
// Optionally create a cache for input mappings so that mappings can be reused across multiple fields
// Be sure to only provide a mapping cache if your argument mappings are not specific to the current field
private mappingCache = new Map>();
wrapResolve(
resolver: GraphQLFieldResolver,
fieldConfig: PothosOutputFieldConfig,
): GraphQLFieldResolver {
// Given the args for the this field, select the fields that are globalIds
const argMappings = mapInputFields(fieldConfig.args, this.buildCache, (inputField) => {
if (inputField.extensions?.isRelayGlobalID) {
return true;
}
// returning null means no mapping will be created for this input field
return null;
}, this.mappingCache);
// If all fields reachable through args return null for their mapping, we don't need to wrap the resolver
if (!argMappings) {
return resolver;
}
// Calls the mapping function for each value with a mapping if the value is not null or undefined
const argMapper = createInputValueMapper(argMappings, (globalID, mapping) =>
internalDecodeGlobalID(this.builder, String(globalID)),
);
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
### 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
onOutputFieldConfig(fieldConfig: PothosOutputFieldConfig) {
if (fieldConfig.name === 'removeMe') {
return null;
}
return fieldConfig;
}
onInputFieldConfig(fieldConfig: PothosInputFieldConfig) {
if (fieldConfig.name === 'removeMe') {
return null;
}
return fieldConfig;
}
onEnumValueConfig(valueConfig: PothosEnumValueConfig) {
if (valueConfig.value === 'removeMe') {
return null;
}
return valueConfig;
}
```
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`
* `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
A plugin for adding existing GraphQL types to Pothos
This plugin makes it easy to integrate GraphQL types from existing schemas into your Pothos API
It can be used for incremental migrations from nexus, graphql-tools, or any other JS/TS executable
schema.
## 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
```
## Setup [#setup]
```typescript
import AddGraphQLPlugin from '@pothos/plugin-add-graphql';
const builder = new SchemaBuilder({
plugins: [AddGraphQLPlugin],
});
```
## Usage [#usage]
There are 2 ways you can reference existing types.
* Adding types (or a whole external schema) when setting up the builder
* Adding types as Refs using new builder methods
### Adding types when creating your builder [#adding-types-when-creating-your-builder]
Adding types to the builder will automatically include the types in your schema when it's built.
Types will only be added if no existing type of the same name is added to the builder before
building the schema.
Adding types recursively adds any other types that the added type depends on in its fields,
interfaces, or union members.
```ts
import { existingSchema } from './existing-schema-location';
const builder = new SchemaBuilder({
plugins: [AddGraphQLPlugin],
add: {
// You can add individual types
// This accepts Any GraphQLNamedType (Objects, Interface, Unions, Enums, Scalars, and InputObjects)
types: [existingSchema.getType('User'), existingSchema.getType('Post')],
// Or you can add an entire external schema
schema: existingSchema,
},
});
```
Adding types by themselves isn't very useful, so you'll probably want to be able to reference them
when defining fields in your schema. To do this, you can add them to the builders generic Types.
This currently only works for `Object`, `Interface`, and `Scalar` types. For other types, use the
builder methods below to create refs to the added types.
```ts
import { existingSchema } from './existing-schema-location';
const builder = new SchemaBuilder<{
Objects: {
User: UserType;
};
Interfaces: {
ExampleInterface: { id: string };
};
Scalars: {
DateTime: {
Output: Date;
Input: Date;
};
};
}>({
plugins: [AddGraphQLPlugin],
add: {
types: [
existingSchema.getType('User'),
existingSchema.getType('ExampleInterface'),
existingSchema.getType('DateTime'),
],
},
});
builder.queryFields((t) => ({
user: t.field({ type: 'User', resolve: () => getUser() }),
exampleInterface: t.field({ type: 'ExampleInterface', resolve: () => getThings() }),
now: t.field({ type: 'DateTime', resolve: () => new Date() }),
}));
```
### Adding types using builder methods [#adding-types-using-builder-methods]
#### Objects [#objects]
```ts
// Passing in a generic type is recommended to ensure type-safety
const UserRef = builder.addGraphQLObject(
existingSchema.getType('User') as GraphQLObjectType,
{
// Optionally you can override the types name
name: 'AddedUser',
// You can also pass in any other options you can define for normal object types
description: 'This type represents Users',
},
);
const PostRef = builder.addGraphQLObject<{
id: string;
title: string;
content: string;
}>(existingSchema.getType('Post') as GraphQLObjectType, {
fields: (t) => ({
// remove existing title field from type
title: null,
// add new titleField
postTitle: t.exposeString('title'),
}),
});
```
You can then use the returned references when defining fields:
```ts
builder.queryFields((t) => ({
posts: t.field({
type: [PostRef],
resolve: () => loadPosts(),
}),
}));
```
### Interfaces [#interfaces]
```ts
const NodeRef = builder.addGraphQLInterface(
existingSchema.getType('Node') as GraphQLInterfaceType,
{
// interface options
},
);
```
### Unions [#unions]
```ts
const SearchResult = builder.addGraphQLUnion(
existingSchema.getType('SearchResult') as GraphQLUnionType,
{
// union options
},
);
```
### Enums [#enums]
```ts
const OrderBy = builder.addGraphQLEnum<'Asc' | 'Desc'>(
existingSchema.getType('OrderBy') as GraphQLEnumType,
{
// enum options
},
);
```
### Input objects [#input-objects]
```ts
const PostFilter = builder.addGraphQLInput<{ title?: string, tags? string[] }>(
existingSchema.getType('PostFilter') as GraphQLInputObjectType,
{
// input options
},
);
```
### Scalars [#scalars]
This plugin does not add a new method for scalars, because Pothos already has a method for adding
existing scalar types.
```ts
builder.addScalarType('DateTime', existingSchema.getType('DateTime') as GraphQLScalar, {
// scalar options
});
```
# 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 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
limit: (ctx) => ({
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) => { complexity: 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, passed the context object if `limit` is a function
* 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]
You can set a custom complexity value on any field:
```typescript
builder.queryFields((t) => ({
posts: t.field({
type: [Post],
complexity: 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 },
}),
}));
```
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: args.limit ?? 5 }),
}),
}));
```
## Utilities [#utilities]
### `complexityFromQuery(query, options)` [#complexityfromqueryquery-options]
Returns the query complexity for a given GraphQL query.
```typescript
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.
context: {},
variables: {},
});
```
# Dataloader plugin
URL: /docs/plugins/dataloader
Dataloader plugin docs for Pothos
This plugin makes it easy to add fields and types that are loaded through a dataloader.
## 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 DataloaderPlugin from '@pothos/plugin-dataloader';
const builder = new SchemaBuilder({
plugins: [DataloaderPlugin],
});
```
### loadable objects [#loadable-objects]
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,
}),
}),
});
```
It is **VERY IMPORTANT** to return values from `load` in an order that exactly matches the order of
the requested IDs. 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).
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]
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: string;
title: string;
content: string;
}
const Post = builder.objectRef('Post').implement({
fields: (t) => ({
id: t.exposeID('id', {}),
title: t.exposeString('title', {}),
content: t.exposeString('title', {}),
}),
});
// 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: number[], 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: number[], context) => db.posts.findMany({ where: { authorId: { in: 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.loadable({
type: [Post],
byPath: true,
args: {
limit: t.arg.int({ required: true }),
},
load: (ids: number[], 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. dataloaders are not shared across requests, so we need the context to get
the correct dataloader for the current request:
```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.
First you'll need to update the types for your context type:
```typescript
import { LoadableRef } from '@pothos/plugin-dataloader';
export interface ContextType {
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 { LoadableRef, rejectErrors } from '@pothos/plugin-dataloader';
export const createContext = (req, res): ContextType => ({
// Adding this will prevent any issues if you server implementation
// copies or extends the context object before passing it to your resolvers
...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[]) =>
rejectErrors(ref.getDataloader(this).loadMany(ids));
},
});
```
Now you can use these helpers from your context object:
```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 }) => loadMany(User, ['123', '456']),
}),
}));
```
### Using with the Relay plugin [#using-with-the-relay-plugin]
If you are using the Relay plugin, there is an additional method `loadableNode` that gets added to
the builder. 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]
To data-load a connection, you can use a combination of helpers:
* `builder.connectionObject` To create the connection and edge types
* `builder.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
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, 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({
isTypeOf: (obj) => obj instanceof User,
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.
```typescript
const User = builder.loadableObject('User', {
load: (ids: string[], context: ContextType) => context.loadUsersById(ids),
sort: user => user.id,
fields: (t) => ({
id: t.exposeID('id', {}),
...
}),
});
```
This will also work with loadable nodes, interfaces, unions, or fields.
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);
```
# Directive plugin
URL: /docs/plugins/directives
Directive plugin docs for Pothos
A plugin for using schema directives with schemas generated by Pothos.
Schema Directives are not intended to be used with code first schemas, but there is a large existing
community with several very useful directives based on them.
## Usage [#usage]
### 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
```
### Setup [#setup]
```typescript
import DirectivePlugin from '@pothos/plugin-directives';
import { rateLimitDirective } from 'graphql-rate-limit-directive';
const builder = new SchemaBuilder<{
Directives: {
rateLimit: {
locations: 'OBJECT' | 'FIELD_DEFINITION';
args: { limit: number, duration: number };
};
};
}>({
plugins: [DirectivePlugin],
directives: {
useGraphQLToolsUnorderedDirectives: true,
}
});
builder.queryType({
directives: {
rateLimit: { limit: 5, duration: 60 },
},
fields: (t) => ({
hello: t.string({ resolve: () => 'world' });
});
});
const { rateLimitDirectiveTransformer } = rateLimitDirective();
const schema = rateLimitDirectiveTransformer(builder.toSchema());
```
The directives plugin allows you to define types for the directives your schema will use the
`SchemaTypes` parameter. Each directive can define a set of locations the directive can appear, and
an object type representing the arguments the directive accepts.
The valid locations for directives are:
* `ARGUMENT_DEFINITION`
* `ENUM_VALUE`
* `ENUM`
* `FIELD_DEFINITION`
* `INPUT_FIELD_DEFINITION`
* `INPUT_OBJECT`
* `INTERFACE`
* `OBJECT`
* `SCALAR`
* `SCHEMA`
* `UNION`
Pothos does not apply the directives itself, this plugin simply adds directive information to the
extensions property of the underlying GraphQL type so that it can be consumed by other tools like
`graphql-tools`.
By default this plugin uses the format that Gatsby uses (described
[here](https://github.com/graphql/graphql-js/issues/1343#issuecomment-479871020)). This format
[was not supported by older versions of `graphql-tools`](https://github.com/ardatan/graphql-tools/issues/2534).
To support older versions of `graphql-tools` or directives that provide a schema visitor based on an
older graphql-tools version like the rate-limit directive from the example above you can set the
`useGraphQLToolsUnorderedDirectives` option. This option does not preserve the order that directives
are defined in. This will be okay for most cases, but may cause issues if your directives need to be
applied in a specific order.
To define directives on your fields or types, you can add a `directives` property in any of the
supported locations using one of the following 2 formats:
```typescript
{
directives: [
{
name: "validation",
args: {
regex: "/abc+/"
}
},
{
name: "required",
args: {},
}
],
// or
directives: {
validation: {
regex: "/abc+/"
},
required: {}
}
}
```
Each of these applies the same 2 directives. The first format is preferred, especially when using
directives that are sensitive to ordering, or can be repeated multiple times for the same location.
## Applying directives [#applying-directives]
For most locations (On fields and types) the options object for the field or type will have a
`directives` option which can be used to define directives.
To apply `SCHEMA` directives, you can use the `schemaDirectives` option on the `toSchema` method.
`directives` on `toSchema` is reserved for the Directive implementations.
# Errors plugin
URL: /docs/plugins/errors
Errors plugin docs for Pothos
A plugin for easily including error types in your GraphQL schema and hooking up error types to
resolvers
## 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
```
### Setup [#setup]
Ensure that the target in your `tsconfig.json` is set to `es6` or higher (default is `es3`).
### Example Usage [#example-usage]
```typescript
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.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.
### 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`: Sets the defaults for `result` option on fields.
* `name`: Function to generate a custom name on the generated result types.
```ts
export const builderWithCustomErrorTypeNames = new SchemaBuilder<{}>({
plugins: [ErrorPlugin, ValidationPlugin],
errors: {
defaultTypes: [Error],
defaultResultOptions: {
name: ({ parentTypeName, fieldName }) => `${fieldName}_Custom`,
},
defaultUnionOptions: {
name: ({ parentTypeName, fieldName }) => `${fieldName}_Custom`,
},
},
});
```
* `defaultUnionOptions`: Sets the defaults for `result` option on fields.
* `name`: Function to generate a custom name on the generated union types.
```ts
export const builderWithCustomErrorTypeNames = new SchemaBuilder<{}>({
plugins: [ErrorPlugin, ValidationPlugin],
errors: {
defaultTypes: [Error],
defaultResultOptions: {
name: ({ parentTypeName, fieldName }) => `${fieldName}_Custom`,
},
defaultUnionOptions: {
name: ({ parentTypeName, fieldName }) => `${fieldName}_Custom`,
},
},
});
```
### 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 written 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.
### Recommended Usage [#recommended-usage]
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.
The following is a small example of this pattern:
```typescript
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]
To handle validation errors you will need to enable the `unsafelyHandleInputErrors` option in the
errors plugin options. This will allow the errors plugin to catch errors thrown by the validation plugin.
This setting is unsafe because it wraps and catches errors at a higher level which will allow you to
bypass other plugin hooks like the `auth` plugin. This enables you to return structured error responses for
validation issues which happen BEFORE auth checks are executed, but this also means that those auth checks won't be run.
Once you enable the `unsafelyHandleInputErrors` option, you can define types for an InputValidationError
(or any custom error you use in the validation plugin), the same way you would for any other error type. Below
is a simple example of how this can be done, but the specifics of how you structure your error types are left up to you.
```typescript
const InputValidationIssue = builder
.objectRef('InputValidationIssue')
.implement({
fields: (t) => ({
message: t.exposeString('message'),
path: t.stringList({
resolve: (issue) => issue.path?.map((p) => String(p)),
}),
}),
});
builder.objectType(InputValidationError, {
name: 'InputValidationError',
interfaces: [ErrorInterface],
fields: (t) => ({
issues: t.field({
type: [InputValidationIssue],
resolve: (err) => err.issues,
}),
}),
});
builder.queryField('fieldWithValidation', (t) =>
t.boolean({
errors: {
types: [InputValidationError],
},
args: {
string: t.arg.string({
validate: z.string().min(3, 'Too short'),
}),
},
resolve: () => true,
}),
);
```
Example query:
```graphql
query {
validation(string: "a") {
__typename
... on QueryValidationSuccess {
data
}
... on InputValidationError {
issues {
message
path
}
}
}
}
```
### 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.
If the field is a `List` field, errors that occur when resolving objects from `ids` will not be
handled by the errors plugin. This is because those errors are associated with each item in the list
rather than the list field itself. In the future, the dataloader plugin may have an option to throw
an error at the field level if any items can not be loaded, which would allow the error plugin to
handle these types of errors.
### 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]
For fields that return lists, you can specify `itemErrors` to wrap the list items in a union type so
that errors can be handled per-item rather than replacing the whole list with an error.
The `itemErrors` options are exactly the same as the `errors` options, but they are applied to each
item in the list rather than the whole list.
```typescript
builder.queryType({
fields: (t) => ({
listWithErrors: t.string({
itemErrors: {},
resolve: (parent, { name }) => {
return [
1,
2,
new Error('Boom'),
3,
]
},
}),
}),
});
```
This will produce a GraphQL schema that looks like:
```graphql
type Query {
listWithErrors: [QueryListWithErrorsItemResult!]!
}
union QueryListWithErrorsItemResult = Error | QueryListWithErrorsItemSuccess
type QueryListWithErrorsItemSuccess {
data: Int!
}
```
Item errors also works with both sync and async iterators (in graphql@>=17, or other executors that support the @stream directive):
```typescript
builder.queryType({
fields: (t) => ({
asyncListWithErrors: t.string({
itemErrors: {},
resolve: async function* () {
yield 1;
yield 2;
yield new Error('Boom');
yield 4;
throw new Error('Boom');
},
}),
}),
});
```
When an error is yielded, an error result will be added into the list, if the generator throws an error,
the error will be added to the list, and no more results will be returned for that field
You can also use the `errors` and `itemErrors` options together:
```typescript
builder.queryType({
fields: (t) => ({
listWithErrors: t.string({
itemErrors: {},
errors: {},
resolve: (parent, { name }) => {
return [
1,
new Error('Boom'),
3,
]
}),
}),
});
```
This will produce a GraphQL schema that looks like:
```graphql
type Query {
listWithErrors: [QueryListWithErrorsResult!]!
}
union QueryListWithErrorsResult = Error | QueryListWithErrorsSuccess
type QueryListWithErrorsSuccess {
data: [QueryListWithErrorsItemResult!]!
}
union QueryListWithErrorsItemResult = Error | 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.
```typescript
const CreateResult = builder.objectRef<{ id: string; created: true }>('CreateResult').implement({
isTypeOf: (obj) => 'created' in obj,
fields: (t) => ({
id: t.exposeString('id'),
created: t.exposeBoolean('created'),
}),
});
const UpdateResult = builder.objectRef<{ id: string; updated: true }>('UpdateResult').implement({
isTypeOf: (obj) => 'updated' in obj,
fields: (t) => ({
id: t.exposeString('id'),
updated: t.exposeBoolean('updated'),
}),
});
builder.mutationType({
fields: (t) => ({
modifyUser: t.errorUnionField({
types: [CreateResult, UpdateResult, ValidationError],
resolve: (parent, { action, name }) => {
if (name.length < 3) return new ValidationError('Name too short');
if (action === 'create') return { id: '123', created: true };
return { id: '123', updated: true };
},
}),
processUsers: t.errorUnionListField({
types: [CreateResult, UpdateResult, ValidationError],
resolve: (parent, { operations }) =>
operations.map(op =>
op.invalid ? new ValidationError('Invalid') :
op.action === 'create' ? { id: op.id, created: true } :
{ id: op.id, updated: true }
),
}),
}),
});
```
#### Type resolution [#type-resolution]
Union members are resolved using standard Pothos type resolution. You have three options:
**Class-based types** (most error types) - automatically resolved via `instanceof` checks:
```typescript
class ValidationError extends Error { ... }
builder.objectType(ValidationError, { name: 'ValidationError', fields: ... });
// No isTypeOf needed - uses instanceof ValidationError
```
**`isTypeOf` function** - for plain object types:
```typescript
const CreateResult = builder.objectRef<{ id: string }>('CreateResult').implement({
isTypeOf: (obj) => 'created' in obj,
fields: ...
});
```
**Custom `resolveType` on union** - for complex resolution logic:
```typescript
t.errorUnionField({
types: [CreateResult, UpdateResult, ValidationError],
union: {
resolveType: (value) => {
if (value instanceof ValidationError) return 'ValidationError';
if ('created' in value) return 'CreateResult';
return 'UpdateResult';
},
},
resolve: ...
});
```
### 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.
```typescript
builder.objectType(NotFoundError, {
name: 'NotFoundError',
interfaces: [ErrorInterface],
});
builder.objectType(ValidationError, {
name: 'ValidationError',
interfaces: [ErrorInterface],
isTypeOf: (value) => value instanceof ValidationError,
fields: (t) => ({
field: t.exposeString('field'),
}),
});
const UserType = builder.objectRef<{ id: string; name: string }>('User').implement({
isTypeOf: (obj) => 'id' in obj && 'name' in obj,
fields: (t) => ({
id: t.exposeString('id'),
name: t.exposeString('name'),
}),
});
const UserResult = builder.errorUnion('UserResult', {
types: [UserType, NotFoundError, ValidationError],
});
builder.queryField('getUser', (t) =>
t.field({
type: UserResult,
args: { id: t.arg.string({ required: true }) },
resolve: (_, { id }) => {
// Handles thrown errors
if (!id) throw new ValidationError('ID required', 'id');
// Handles returned errors
if (id === 'unknown') return new NotFoundError('User not found');
return { id, name: 'User' };
},
})
);
```
#### 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
A plugin for building subGraphs that are compatible with
[Apollo Federation 2](https://www.apollographql.com/docs/federation/).
## 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 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],
});
```
### 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
const UserType = builder.objectRef('User').implement({
fields: (t) => ({
id: t.exposeID('id'),
name: t.exposeString('name'),
username: t.exposeString('username'),
}),
});
builder.asEntity(UserType, {
key: builder.selection<{ id: string }>('id'),
resolveReference: (user, users) => users.find(({ id }) => user.id === id),
});
```
`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`.
```typescript
const ProductRef = 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 };
},
);
ProductRef.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;
},
}),
}),
});
```
### 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:
```typescript
import { type FieldSet } from '@pothos/plugin-federation';
type Media = { __typename: 'Image'; url: string } | { __typename: 'Video'; url: string };
PostRef.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.
To set the `resolvable` property of an external field to `false`, can use `builder.keyDirective`:
```ts
const ProductRef = 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.
```typescript
const UserType = 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 ReviewType = builder.objectRef('Review');
ReviewType.implement({
fields: (t) => ({
id: t.exposeID('id'),
body: t.exposeString('body'),
author: t.field({
// using UserType.provides<...>(...) instead of just UserType 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: UserType.provides<{ username: string }>('username'),
resolve: (review) => ({
id: review.authorID,
username: usernames.find((username) => username.id === review.authorID)!.username,
}),
}),
product: t.field({
type: Product,
resolve: (review) => ({ upc: review.product.upc }),
}),
}),
});
```
### Building your schema and starting a server [#building-your-schema-and-starting-a-server]
```typescript
// Use new `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' },
});
```
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.
You can now pass interfaces to `asEntity` to define keys for an interface:
```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),
});
```
You can also extend interfaces from another subGraph 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
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 currently experimental and will have breaking changes in the near future.
This plugin currently does not work with MOST other Pothos plugins.
Many plugins depend on wrapping resolvers to add runtime functionality to your schema, which will not work
with grafast.
## Install [#install]
npm
pnpm
yarn
bun
```bash
npm install --save @pothos/plugin-grafast grafast@>=0.1.1-beta.24
```
```bash
pnpm add @pothos/plugin-grafast grafast@>=0.1.1-beta.24
```
```bash
yarn add @pothos/plugin-grafast grafast@>=0.1.1-beta.24
```
```bash
bun add @pothos/plugin-grafast grafast@> 0.1.1-beta.24
```
## Setup [#setup]
```typescript
import GrafastPlugin from '@pothos/plugin-grafast';
declare global {
namespace Grafast {
// Define the Context type used by grafast
interface Context extends YourContextType {}
}
}
type BuilderTypes = {
// This tells the builder to expect plans instead of resolvers
InferredFieldOptionsKind: 'Grafast';
Context: YourContextType;
};
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]
Pothos and Grafast will still allow you to write resolvers when using grafast,
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 Dog = builder.objectRef('Dog').implement({
interfaces: [Animal],
});
export const Cat = builder.objectRef('Cat').implement({
interfaces: [Animal],
});
export const Animal = builder
.interfaceRef('Animal')
.withPlan({
planType: ($record) => ({
$__typename: get($record, 'kind'),
}),
})
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]
Unions can be implemented just like interfaces:
```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 API is likely to change in the future.
```typescript
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,
}),
}));
```
# Plugins
URL: /docs/plugins
List of plugins for Pothos
# Mocks plugin
URL: /docs/plugins/mocks
Mocks plugin docs for Pothos
A simple plugin for adding resolver mocks to a GraphQL schema.
## 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 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
builder.queryType({
fields: (t) => ({
someField: t.string({
resolve: () => {
throw new Error('Not implemented');
},
}),
}),
});
builder.toSchema({
mocks: {
Query: {
someField: (parent, args, context, info) => 'Mock result!',
},
},
});
```
Mocks will replace the resolve functions any time a mocked field is executed. 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:
```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: (parent, args, context, info) => 'Mock result!',
subscribe: (parent, args, context, info) => {
/* return a mock async iterator */
},
},
},
},
});
```
# Relay plugin
URL: /docs/plugins/relay
Relay plugin docs for Pothos
The Relay plugin adds a number of builder methods and helper functions to simplify building a relay
compatible schema.
## 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 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 connection objects
* `clientMutationIdInputOptions`: Options for the `clientMutationId` input field on connections
fields
* `mutationInputArgOptions`: Options for the Input object created for each connection field
* `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 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]
To create objects that extend the `Node` interface, you can use the new `builder.node` method.
```typescript
// Using object refs
const User = builder.objectRef('User');
// Or using a class
class User {
id: string;
name: string;
}
builder.node(User, {
// define an id field
id: {
resolve: (user) => user.id,
// other options for id field can be added here
},
// Define only one of the following methods for loading nodes by id
loadOne: (id) => loadUserByID(id),
loadMany: (ids) => loadUsers(ids),
loadWithoutCache: (id) => loadUserByID(id),
loadManyWithoutCache: (ids) => loadUsers(ids),
// if using a class instead of a ref, you will need to provide a name
name: 'User',
fields: (t) => ({
name: t.exposeString('name'),
}),
});
```
`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.
Nodes may also implement an `isTypeOf` method which can be used to resolve the correct type for
lists of generic nodes. When using a class as the type parameter, the `isTypeOf` method defaults to
using an `instanceof` check, and falls back to checking the constructor property on the prototype.
That means that for many cases if you are using classes in your type parameters, and all your values
are instances of those classes, you won't need to implement an `isTypeOf` method, but it is usually
better to explicitly define that behavior.
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:
```ts
const User = builder.objectRef('User')
builder.node(User, {
// define an id field
id: {
resolve: (user) => user.id,
parse: (id) => Number.parseInt(id, 10),
},
// the ID is now a number
loadOne: (id) => loadUserByID(id),
...
});
```
### Global IDs [#global-ids]
To make it easier to create globally unique ids the relay plugin adds new methods for creating
globalID fields.
```typescript
import { encodeGlobalID } from '@pothos/plugin-relay';
builder.queryFields((t) => ({
singleID: t.globalID({
resolve: (parent, args, context) => {
return { id: 123, type: 'SomeType' };
},
}),
listOfIDs: t.globalIDList({
resolve: (parent, args, context) => {
return [{ id: 123, type: 'SomeType' }];
},
}),
}));
```
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.queryType({
fields: (t) => ({
fieldThatAcceptsGlobalID: t.boolean({
args: {
id: t.arg.globalID({
for: SomeType,
// or allow multiple types
for: [TypeOne, TypeTwo],
required: true,
}),
},
}),
}),
});
```
### 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.
```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 limit/offset based apis:
```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 configure 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: SomeThings,
resolve: (parent, args) => {
return resolveArrayConnection({ args }, getAllTheThingsAsArray());
},
}),
}));
```
Cursor based pagination can be implemented using the `resolveCursorConnection` method. The following
example uses prisma, but a similar solution should work with any data store that supports limits,
ordering, and filtering.
```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
will generate a mutation field, an input object with a `clientMutationId` field, and an output
object with the corresponding `clientMutationId`.
Example usage:
```typescript
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 (ctx.items.has(args.input.id)) {
ctx.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 {
clientMutationId: ID!
id: ID!
}
type DeleteItemPayload {
clientMutationId: ID!
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
// Using aliases when destructuring lets you name your refs rather than using the generic `inputType` and `payloadType`
const { inputType: DeleteItemInput, payloadType: DeleteItemPayload } = builder.relayMutationField(
'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 { resolveOffsetConnection } from '@pothos/plugin-relay';
const ThingsConnection = builder.connectionObject(
{
// connection options
type: SomeThing,
name: 'ThingsConnection',
},
{
// Edge options (optional)
name: 'ThingsEdge', // defaults to Appending `Edge` to the Connection name
},
);
// You can use connection object with normal fields
builder.queryFields((t) => ({
things: t.field({
type: ThingsConnection,
args: {
...t.arg.connectionArgs(),
},
resolve: (parent, args) => {
return resolveOffsetConnection({ args }, ({ limit, offset }) => {
return getThings(offset, limit);
});
},
}),
}));
// Or by providing the connection type when creating a connection field
builder.queryFields((t) => ({
things: t.connection({
resolve: (parent, args) => {
return resolveOffsetConnection({ args }, ({ limit, offset }) => {
return getThings(offset, limit);
});
},
}),
ThingsConnection,
}));
```
`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]
Similarly you can directly create and re-use edge objects
```typescript
import { resolveOffsetConnection } from '@pothos/plugin-relay';
const ThingsEdge = builder.edgeObject(
{
name: 'ThingsEdge',
type: SomeThing,
},
);
// The edge object can be used when creating a connection object
const ThingsConnection = builder.connectionObject(
{
type: SomeThing,
name: 'ThingsConnection',
},
ThingsEdge,
);
// Or when creating a connection field
builder.queryFields((t) => ({
things: t.connection({
resolve: (parent, args) => {
return resolveOffsetConnection({ args }, ({ limit, offset }) => {
return getThings(offset, limit);
});
},
}),
{
// connection options
},
ThingsEdge,
}));
```
`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.nodes` 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: () => 'TnVtYmVyOjI=',
}),
moreNodes: t.nodeList({
ids: () => ['TnVtYmVyOjI=', { id: 10, type: 'SomeType' }],
}),
}));
```
### 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
builder.mutationFields((t) => ({
updateThing: t.field({
type: Thing,
args: {
id: t.args.id({ required: true }),
update: t.args.string({ required: true }),
},
resolve(parent, args) {
const { type, id } = decodeGlobalID(args.id);
const thing = Thing.findById(id);
thing.update(args.update);
return thing;
},
}),
}));
```
### 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 RelayPlugin from '@pothos/plugin-relay';
const builder = new SchemaBuilder({
plugins: [RelayPlugin],
relayOptions: {
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:
```typescript
import RelayPlugin from '@pothos/plugin-relay';
function customUserLoader({ id, typename }: { id: string; typename: string }) {
// load user
}
const builder = new SchemaBuilder({
plugins: [RelayPlugin],
relayOptions: {
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((id) => {
if (id.typename === 'User') {
return customNodeLoader(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 resolveNodes([id]);
});
},
},
},
});
```
### Extending all connections [#extending-all-connections]
There are 2 builder methods for adding fields to all connection objects: `t.globalConnectionField`
and `t.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 RelayPlugin from '@pothos/plugin-relay';
const builder = new SchemaBuilder<{
Connection: {
totalCount: number;
};
}>({
plugins: [RelayPlugin],
relayOptions: {},
});
```
Now 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
builder.queryFields((t) => ({
posts: t.connection({
type: Post,
resolve: (parent, args, context) => {
const postsArray = context.Posts.getAll();
const result = resolveArrayConnection({ args }, postsArray);
return result && { totalCount: postsArray.length, ...result };
},
}),
}));
```
### 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 RelayPlugin from '@pothos/plugin-relay';
const builder = new SchemaBuilder<{
DefaultEdgesNullability: false;
DefaultNodeNullability: true;
}>({
plugins: [RelayPlugin],
relayOptions: {
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: SomeThings,
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 the `nodeInterfaceRef` method of your Builder.
For example, to add a new derived field on the interface:
```ts
builder.interfaceField(builder.nodeInterfaceRef(), 'extra', (t) =>
t.string({
resolve: () => 'it works',
}),
);
```
# 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.
## 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
```
#### IMPORTANT [#important]
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 MyPerms = 'readStuff' | 'updateStuff' | 'readArticle';
type Context = {
user: { id: string; employee: boolean; permissions: MyPerms[] } | null;
};
const builder = new SchemaBuilder<{
Context: Context;
AuthScopes: {
loggedIn: boolean;
employee: boolean;
deferredScope: boolean;
customPerm: MyPerms;
};
}>({
plugins: [ScopeAuthPlugin],
scopeAuth: {
authorizeOnSubscribe: true,
authScopes: (context) => ({
loggedIn: !!context.user,
employee: context.user?.employee ?? false,
deferredScope: () => context.user?.employee ?? 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`, `deferredScope`, 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.
### Using a scope on a field [#using-a-scope-on-a-field]
```typescript
builder.queryType({
fields: (t) => ({
message: t.string({
authScopes: {
loggedIn: true,
},
resolve: () => 'hi',
}),
}),
});
```
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]
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 authScope 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: MyPerms;
};
}>({
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,
}),
);
```
### 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,
deferredScope: 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 `deferredScope` scopes, and the
`loggedIn` scope. `$any` and `$all` each take a scope map as their parameters, and can be nested
inside each other.
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,
};
},
}),
}),
});
```
authScope 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 authScope 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 authScope 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.
### 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: {
loggedIn: boolean;
};
}>({
scopeAuth: {
cacheKey: (val) => JSON.stringify(val),
authScopes: async (context) => ({
loggedIn: !!context.user,
}),
},
plugins: [ScopeAuthPlugin],
});
```
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.
# Simple objects plugin
URL: /docs/plugins/simple-objects
Simple objects plugin docs for Pothos
The Simple Objects Plugin provides a way to define objects and interfaces without defining type
definitions for those objects, while still getting full type safety.
## 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
```
### Setup [#setup]
```typescript
import SimpleObjectsPlugin from '@pothos/plugin-simple-objects';
const builder = new SchemaBuilder({
plugins: [SimpleObjectsPlugin],
});
```
### Example [#example]
```typescript
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 UserType = builder.simpleObject(
'User',
{
interfaces: [Node],
fields: (t) => ({
firstName: t.string(),
lastName: t.string(),
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: UserType,
args: {
id: t.arg.id({ required: true }),
},
resolve: (parent, args, { User }) => {
return {
id: '1003',
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 to the type using methods like `builder.objectType`:
```typescript
builder.objectType(UserType, (t) => ({
fullName: t.string({
resolve: (user) => `${user.firstName} ${user.lastName}`,
}),
}));
```
## 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
Smart subscriptions plugin docs for Pothos
This plugin provides a way of turning queries into GraphQL subscriptions. Each field, Object, and
Interface in a schema can define subscriptions to be registered when that field or type is used in a
smart subscription.
The basic flow of a smart subscription is:
1. Run the query the smart subscription is based on and push the initial result of that query to the
subscription
2. As the query is resolved, register any subscriptions defined on fields or types that were used
in the query
3. When any of the subscriptions are triggered, re-execute the query and push the updated data to
the subscription.
There are additional options which will allow only the sub-tree of a field/type that triggered a
fetch to be re-resolved.
This pattern makes it easy to define subscriptions without having to worry about what parts of your
schema are accessible via the subscribe query, since any type or field can register a subscription.
## Usage [#usage]
### 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
```
### Setup [#setup]
```typescript
import SchemaBuilder from '@pothos/core';
import SmartSubscriptionsPlugin from '@pothos/plugin-smart-subscriptions';
const builder = new SchemaBuilder({
plugins: [SmartSubscriptionsPlugin],
smartSubscriptions: {
debounceDelay: number | null;
subscribe: (
name: string,
context: Context,
cb: (err: unknown, data?: unknown) => void,
) => Promise | void;
unsubscribe: (name: string, context: Context) => Promise | void;
},
});
```
#### Helper for usage with async iterators [#helper-for-usage-with-async-iterators]
```typescript
const builder = new SchemaBuilder({
smartSubscriptions: {
...subscribeOptionsFromIterator((name, { pubsub }) => {
return pubsub.asyncIterableIterator(name);
}),
},
});
```
### Creating a smart subscription [#creating-a-smart-subscription]
```typescript
builder.queryFields((t) => ({
polls: t.field({
type: ['Poll'],
smartSubscription: true,
subscribe: (subscriptions, root, args, ctx, info) => {
subscriptions.register('poll-added')
subscriptions.register('poll-deleted')
},
resolve: (root, args, ctx, info) => {
return ctx.getThings();
},
}),
})
```
Adding `smartSubscription: true` to a query field creates a field of the same name on the
`Subscriptions` type. The `subscribe` option is optional, and shows how a field can register a
subscription.
This would be queried as:
```graphql
subscription {
polls {
question
answers {
id
value
}
}
}
```
### registering subscriptions for objects [#registering-subscriptions-for-objects]
```typescript
builder.objectType('Poll', {
subscribe: (subscriptions, poll, context) => {
subscriptions.register(`poll/${poll.id}`)
},
fields: (t) => ({
question: t.exposeString('question', {}),
answers: t.field({...}),
}),
});
```
This will create a new subscription for every `Poll` that is returned in the subscription. When the
query is updated to fetch a new set of results because a subscription event fired, the subscribe
call will be called again for each poll in the new result set.
#### more options [#more-options]
```typescript
builder.objectType('Poll', {
subscribe: (subscriptions, poll, context) => {
subscriptions.register(`poll/${poll.id}`, {
filter: (value) => true | false,
invalidateCache: (value) => context.PollCache.remove(poll.id),
refetch: (): => context.Polls.fetchByID(poll.id)!),
});
},
fields: (t) => ({
...
}),
});
```
Passing a `filter` function will filter the events, and only cause a re-fetch if it returns true.
`invalidateCache` is called before refetching data, to allow any cache invalidation to happen so
that when the new data is loaded, results are not stale.
`refetch` enables directly refetching the current object. When refetch is provided and a
subscription event fires for the current object, or any of its children, other parts of the query
that are not dependents of this object will not be refetched.
### registering subscriptions for fields [#registering-subscriptions-for-fields]
```typescript
builder.objectType('Poll', {
fields: (t) => ({
question: t.exposeString('question', {}),
answers: t.field({
type: ['Answer'],
subscribe: (subscriptions, poll) => subscriptions.register(`poll-answers/${poll.id}`),
resolve: (parent, args, context, info) => {
return parent.answers;
},
}),
}),
});
```
#### more options for fields [#more-options-for-fields]
```typescript
builder.objectType('Poll', {
fields: (t) => ({
question: t.exposeString('question', {}),
answers: t.field({
type: ['Answer'],
canRefetch: true,
subscribe: (subscriptions, poll) =>
subscriptions.register(`poll-answers/${poll.id}`, {
filter: (value) => true | false,
invalidateCache: (value) => context.PollCache.remove(poll.id),
}),
resolve: (parent, args, context, info) => {
return parent.answers;
},
}),
}),
});
```
Similar to subscriptions on objects, fields can pass `filter` and `invalidateCache` functions when
registering a subscription. Rather than passing a `refetch` function, you can set `canRefetch` to
`true` in the field options. This will re-run the current resolve function to update it (and its
children) without having to re-run the rest of the query.
### Known limitations [#known-limitations]
* Currently value passed to `filter` and `invalidateCache` is typed as `unknown`. This should be
improved in the future.
* Does not work with list fields implemented with async-generators (used for `@stream` queries)
# SubGraph plugin
URL: /docs/plugins/sub-graph
SubGraph plugin docs for Pothos
A plugin for creating sub-selections of your graph. This allows you to use the same code/types for
multiple variants of your API.
One common use case for this is to share implementations between your public and internal APIs, by
only exposing a subset of your graph publicly.
## Usage [#usage]
### 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
```
### Setup [#setup]
```typescript
import SubGraphPlugin from '@pothos/plugin-sub-graph';
const builder = new SchemaBuilder<{
SubGraphs: 'Public' | 'Internal';
}>({
plugins: [SubGraphPlugin],
subGraphs: {
defaultForTypes: [],
fieldsInheritFromTypes: true,
},
});
//in another file:
const schema = builder.toSchema();
const publicSchema = builder.toSchema({ subGraph: 'Public' });
const internalSchema = builder.toSchema({ subGraph: 'Internal' });
// You can also build a graph containing multiple subgraphs:
const combinedSchema = builder.toSchema({ subGraph: ['Internal', 'Public'] });
// Or create a graph of the intersection between multiple subgraphs:
const allSchema = builder.toSchema({ subGraph: { all: ['Internal', 'Public'] } });
```
### Options on Types [#options-on-types]
* `subGraphs`: An optional array of sub-graph the type should be included in.
### Object and Interface types: [#object-and-interface-types]
* `defaultSubGraphsForFields`: Default sub-graph for fields of the type to be included in.
## Options on Fields [#options-on-fields]
* `subGraphs`: An optional array of sub-graph the field to be included in. If not provided, will
fallback to:
* `defaultForFields` if set on type
* `subGraphs` of the type if `subGraphs.fieldsInheritFromTypes` was set in the builder
* an empty array
### Options on Builder [#options-on-builder]
* `subGraphs.defaultForTypes`: Specifies what sub-graph a type is part of by default.
* `subGraphs.fieldsInheritFromTypes`: defaults to `false`. When true, fields on a type will default
to being part of the same sub-graph as their parent type. Only applies when type does not have
`defaultForFields` set.
### Usage [#usage-1]
```typescript
builder.queryType({
// Query type will be available in default, Public, and Internal schemas
subGraphs: ['Public', 'Internal'],
// Fields on the Query object will now default to not being a part of any subgraph
defaultForFields: [];
fields: (t) => ({
someField: t.string({
// someField will be in the default schema and "Internal" sub graph, but
// not present in the Public sub graph
subGraphs: ['Internal']
resolve: () => {
throw new Error('Not implemented');
},
}),
}),
});
```
### Missing types [#missing-types]
When creating a sub-graph, the plugin will only copy in types that are included in the sub-graph,
either by explicitly setting it on the type, or because the sub-graph is included in the default
list. Like types, output fields that are not included in a sub-graph will also be omitted. Arguments
and fields on Input types can not be removed because that would break assumptions about argument
types in resolvers.
If a type that is not included in the sub-graph is referenced by another part of the graph that is
included in the graph, a runtime error will be thrown when the sub graph is constructed. This can
happen in a number of cases including cases where a removed type is used in the interfaces of an
object, a member of a union, or the type of a field argument.
### Explicitly including types [#explicitly-including-types]
You can use the `explicitlyIncludeType` option to explicitly include types in a sub-graph that are
unreachable. This isn't normally required, but there are some edge cases where this may be useful.
For instance, when extending external references with the federation plugin, the externalRef may
not be reachable directly through your schema, but you may still want to include it when building the
schema. To work around this, we can explicitly include any types that have a `key` directive:
```typescript
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: {
explicitlyIncludeType: (type, subGraphs) => hasResolvableKey(type)
},
});
```
# Tracing plugin
URL: /docs/plugins/tracing
A Pothos plugin for tracing and logging resolver invocations
This plugin adds hooks for tracing and logging resolver invocations. It also comes with a few
additional packages for integrating with various tracing providers including opentelemetry, New
Relic and Sentry.
## Usage [#usage]
### 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
```
### Setup [#setup]
```typescript
import TracingPlugin, { wrapResolver, isRootField } from '@pothos/plugin-tracing';
const builder = new SchemaBuilder({
plugins: [TracingPlugin],
tracing: {
// Enable tracing for rootFields by default, other fields need to opt in
default: (config) => isRootField(config),
// Log resolver execution duration
wrap: (resolver, options, config) =>
wrapResolver(resolver, (error, duration) => {
console.log(`Executed resolver ${config.parentType}.${config.name} in ${duration}ms`);
}),
},
});
```
### Overview [#overview]
The Tracing plugin is designed to have very limited overhead, and uses a modular approach to cover a
wide variety of use cases.
The tracing plugin comes with a number of utility functions for implementing common patterns, and a
couple of provider specific modules that can be installed separately (described in more detail
below).
The primary interface to the tracing plugin consists of 3 parts:
1. A new `tracing` option is added to each field, for enabling or configuring tracing for that field
2. The `tracing.default` which is used as a fallback for any field that does not explicitly set its
`tracing` options.
3. The `tracing.wrap` function, which takes a resolver, the tracing option for a field, and a field
configuration object, and should return a wrapped/traced version of the resolver.
### Enabling tracing for a field [#enabling-tracing-for-a-field]
Enabling tracing on a field is as simple as setting the tracing option to `true`
```ts
builder.queryType({
fields: (t) => ({
hello: t.string({
args: { name: t.arg.string() },
// enable tracing
tracing: true,
resolve: (parent, { name }) => `hello, ${name || 'World'}`,
}),
}),
});
```
#### Custom tracing options [#custom-tracing-options]
For more advanced tracing setups, you may want to allow fields to provide additional tracing
options. You can do this by customizing the `Tracing` generic in the builder.
```ts
import TracingPlugin, { wrapResolver, isRootField } from '@pothos/plugin-tracing';
export const builder = new SchemaBuilder<{
// the `tracing` option can now be a boolean, or an object with a formatMessage function
Tracing: boolean | { formatMessage: (duration: number) => string };
}>({
plugins: [TracingPlugin],
tracing: {
// Using custom options in your tracer will be described below
...
},
});
builder.queryType({
fields: (t) => ({
hello: t.string({
args: { name: t.arg.string() },
// We can now use custom options when configuring tracing
tracing: { formatMessage: (duration) => `It took ${duration}ms to say hello` },
resolve: (parent, { name }) => `hello, ${name || 'World'}`,
}),
}),
});
```
### Enabling tracing by default [#enabling-tracing-by-default]
In most applications you won't want to configure tracing for each field. Instead you can use the
`tracing.default` to enable tracing for specific types of fields.
```ts
import TracingPlugin, { wrapResolver, isRootField } from '@pothos/plugin-tracing';
export const builder = new SchemaBuilder<{
Tracing: boolean | { formatMessage: (duration: number) => string };
}>({
plugins: [TracingPlugin],
tracing: {
// Here we enable tracing for root fields
default: (config) => isRootField(config)
wrap: (resolve) => resolve, // actual tracing wrappers will be described below
},
});
```
There are a number of utility functions for detecting certain types of fields. For most applications
tracing every resolver will add significant overhead with very little benefit. The following
utilities exported by the tracing plugin can be used to determine which fields should have tracing
enabled by default.
* `isRootField`: Returns true for fields of the `Query`, `Mutation`, and `Subscription` types
* `isScalarField`: Returns true for fields that return Scalars, or lists of scalars
* `isEnumField`: Returns true for fields that return an Enum or list of Enums
* `isExposedField`: Returns true for fields defined with the `t.expose*` field builder methods, or
fields that use the `defaultFieldResolver`.
### Implementing a tracer [#implementing-a-tracer]
Tracers work by wrapping the execution of resolver calls. The `tracing.wrap` function keeps this
process as minimal as possible by simply providing the resolver for a field, and expecting a wrapped
version of the resolver to be returned. Resolvers can throw errors or return promises, and correctly
handling these edge cases can be a little complicated so the tracing plugin also comes with some
helper utilities to simplify this process.
`tracing.wrap` takes 3 arguments:
1. `resolver`: the resolver for a field
2. `options`: the tracing options for the field (set either on the field, or returned by
`tracing.default`).
3. `fieldConfig`: A config object that describes the field being wrapped
```ts
export const builder = new SchemaBuilder<{
Tracing: boolean | { formatMessage: (duration: number) => string };
}>({
plugins: [TracingPlugin],
tracing: {
default: (config) => isRootField(config),
wrap: (resolver, options, config) =>
wrapResolver(resolver, (error, duration) => {
const message =
typeof options === 'object'
? options.formatMessage(duration)
: `Executed resolver ${config.parentType}.${config.name} in ${duration}ms`;
console.log(message);
}),
},
});
```
The `wrapResolver` utility takes a resolver, and a `onEnd` callback, and returns a wrapped version
of the resolver that will call the callback with an error (or null) and the duration the resolver
took to complete.
The `runFunction` helper is similar, but rather than wrapping a resolver, will immediately execute a
function with no arguments. This can be useful for more complex use cases where you need access to
other resolver arguments, or want to add your own logic before the resolver begins executing.
```ts
export const builder = new SchemaBuilder<{
Tracing: boolean | { formatMessage: (duration: number) => string };
}>({
plugins: [TracingPlugin],
tracing: {
default: (config) => isRootField(config) || (!isScalarField(config) && !isEnumField(config)),
wrap: (resolver, options) => (source, args, ctx, info) => {
doSomethingFirst(args);
return runFunction(
() => resolver(source, args, ctx, info),
(error, duration) => {
console.log(
`Executed resolver for ${info.parentType}.${info.fieldName} in ${duration}ms`,
);
},
);
},
},
});
```
### Using resolver arguments in tracers [#using-resolver-arguments-in-tracers]
When defining tracing options for a field, you may want to pass some resolver args to your tracing
logic.
The following example shows how arguments might be passed to a tracer to be attached to a span:
```ts
// Create a simple tracer that creates spans, and adds custom attributes if they are provided
export const builder = new SchemaBuilder<{
Tracing: false | { attributes?: Record };
}>({
plugins: [TracingPlugin],
tracing: {
default: (config) => {
if (isRootField(config)) {
return {};
}
return false;
},
// The `tracing` options are passed as the second argument for wrap
wrap: (resolver, options, fieldConfig) => (source, args, ctx, info) => {
const span = tracer.createSpan();
if (options.attributes) {
span.setAttributes();
}
return runFunction(
() => resolver(source, args, ctx, info),
() => {
span.end();
},
);
},
},
});
builder.queryType({
fields: (t) => ({
hello: t.string({
args: { name: t.arg.string() },
// Pass this fields args as a custom attribute
tracing: (root, args) => ({ attributes: { args } }),
resolve: (root, { name }) => `hello, ${name || 'World'}`,
}),
}),
});
```
The `default` option can also return a function to access resolver arguments:
```ts
// Create a simple tracer that creates spans, and adds custom attributes if they are provided
export const builder = new SchemaBuilder<{
Tracing: false | { attributes?: Record };
}>({
plugins: [TracingPlugin],
tracing: {
default: (config) => {
if (isRootField(config)) {
// For all root fields, add arguments as a custom attribute
return (root, args) => ({ attributes: { args }});
}
// disable tracing for exposed fields
if (isExposedField(config)) {
return false
}
// Enable tracing, but don't add any attributes
return {}
},
wrap: ...,
});
```
It is important to know that if a field uses a function to return its tracing option (either
directly on the field definition, or as a default) the behavior of the `wrap` function changes
slightly.
By default `wrap` is called for each field when the schema is built. For fields that return their
tracing option via a function, wrap will be called whenever the field is executed because the
tracing options are dependent on the resolver arguments.
For many uses cases this does not add a lot of overhead, but as a rule of thumb, it is always more
efficient to use tracing options that don't depend on the resolver value.
The above example could be re-designed slightly to improve tracing performance:
```ts
// Create a simple tracer that creates spans, and adds custom attributes if they are provided
export const builder = new SchemaBuilder<{
Tracing: false | { includeArgs?: boolean };
}>({
plugins: [TracingPlugin],
tracing: {
default: (config) => {
if (isRootField(config)) {
// For all root fields, add arguments as a custom attribute
return { includeArgs: true }
}
return false
},
// Wrap is now only called once for each field at build time
// since we don't depend on args to generate the tracing options
wrap: (resolver, options, fieldConfig) => (source, args, ctx, info) => {
const span = tracer.createSpan();
if (options.includeArgs) {
span.setAttributes({ args });
}
return runFunction(
() => resolver(source, args, ctx, info),
() => {
span.end();
},
);
},,
});
```
## Tracing integrations [#tracing-integrations]
### Opentelemetry [#opentelemetry]
#### install [#install-1]
npm
pnpm
yarn
bun
```bash
npm install --save @pothos/tracing-opentelemetry @opentelemetry/semantic-conventions @opentelemetry/api
```
```bash
pnpm add @pothos/tracing-opentelemetry @opentelemetry/semantic-conventions @opentelemetry/api
```
```bash
yarn add @pothos/tracing-opentelemetry @opentelemetry/semantic-conventions @opentelemetry/api
```
```bash
bun add @pothos/tracing-opentelemetry @opentelemetry/semantic-conventions @opentelemetry/api
```
#### Basic usage [#basic-usage]
```ts
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, {
includeSource: true,
});
export const builder = new SchemaBuilder({
plugins: [TracingPlugin],
tracing: {
default: (config) => isRootField(config),
wrap: (resolver, options) => createSpan(resolver, options),
},
});
```
#### options [#options]
* `includeArgs`: default: `false`
* `includeSource`: default: `false`
* `ignoreError`: default: `false`
* `onSpan`: `(span, tracingOptions, parent, args, context, info) => void`
#### Adding custom attributes to spans [#adding-custom-attributes-to-spans]
```ts
import { AttributeValue } from '@opentelemetry/api';
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 | { attributes?: Record };
const createSpan = createOpenTelemetryWrapper(tracer, {
includeSource: true,
onSpan: (span, options) => {
if (typeof options === 'object' && options.attributes) {
span.setAttributes(options.attributes);
}
},
});
export 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({
args: { name: t.arg.string() },
tracing: (parent, { name }) => ({ attributes: { name } }),
resolve: (parent, { name }) => `hello, ${name || 'World'}`,
}),
}),
});
```
#### Instrumenting the execution phase [#instrumenting-the-execution-phase]
The tracing plugin for Pothos only adds spans for resolvers. You may also want to capture additional
information about other parts of the graphql execution process.
This example uses GraphQL Yoga, by providing a custom envelop plugin that wraps the execution phase.
Many graphql server implementations have ways to wrap or replace the execution call, but will look
slightly different.
```ts
import { tracer } from './tracer'; // Tracer should be imported first if it handles additional instrumentation
import { print } from 'graphql';
import { createYoga, Plugin } from 'graphql-yoga';
import { createServer } from 'node:http';
import { AttributeNames, SpanNames } from '@pothos/tracing-opentelemetry';
import { schema } from './schema';
const tracingPlugin: Plugin = {
onExecute: ({ setExecuteFn, executeFn }) => {
setExecuteFn((options) =>
tracer.startActiveSpan(
SpanNames.EXECUTE,
{
attributes: {
[AttributeNames.OPERATION_NAME]: options.operationName ?? undefined,
[AttributeNames.SOURCE]: print(options.document),
},
},
async (span) => {
try {
const result = await executeFn(options);
return result;
} catch (error) {
span.recordException(error as Error);
throw error;
} finally {
span.end();
}
},
),
);
},
};
const yoga = createYoga({
schema,
plugins: [tracingPlugin],
});
const server = createServer(yoga);
```
Envelop also provides its own opentelemetry plugin which can be used instead of a custom plugin like
the one shown above. The biggest drawback to this is the current version of `@envelop/opentelemetry`
does not track the parent/child relations of spans it creates.
```ts
import { provider } from './tracer'; // Tracer should be imported first if it handles additional instrumentation
import { useOpenTelemetry } from '@envelop/opentelemetry';
import { createYoga } from 'graphql-yoga';
import { createServer } from 'node:http';
import { schema } from './schema';
const yoga = createYoga({
schema,
plugins: [
useOpenTelemetry(
{
// Disabling envelops resolver tracing is important to avoid duplicate spans
resolvers: false,
variables: false,
result: false,
},
provider,
),
],
});
const server = createServer(yoga);
```
#### Setting up a tracer [#setting-up-a-tracer]
The following setup creates a very simple opentelemetry tracer that will log spans to the console.
Real applications will need to define exporters that match the opentelemetry backend you are using.
```ts
import { diag, DiagConsoleLogger, DiagLogLevel, trace } from '@opentelemetry/api';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
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();
registerInstrumentations({
// Automatically create spans for http requests
instrumentations: [new HttpInstrumentation({})],
});
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO);
export const tracer = trace.getTracer('graphql');
```
### Datadog [#datadog]
Datadog supports opentelemetry. To report traces to datadog, you will need to instrument your
application with an opentelemetry tracer, and configure your datadog agent to collect open telemetry
traces.
#### Creating a tracer that exports to datadog [#creating-a-tracer-that-exports-to-datadog]
```ts
import { trace } from '@opentelemetry/api';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { resourceFromAttributes } from '@opentelemetry/resources';
import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
export const provider = new NodeTracerProvider({
resource: resourceFromAttributes({
[ATTR_SERVICE_NAME]: 'Pothos-OTEL-example',
}),
});
provider.addSpanProcessor(
new SimpleSpanProcessor(
new OTLPTraceExporter({
// optionally set the opentelemetry collector endpoint if you are not using the default port
// url: 'http://host:port',
}),
),
);
provider.register();
registerInstrumentations({
instrumentations: [new HttpInstrumentation({})],
});
export const tracer = trace.getTracer('graphql');
```
#### Configuring the datadog agent to collect open telemetry [#configuring-the-datadog-agent-to-collect-open-telemetry]
Add the following to your datadog agent configuration
```yaml
otlp_config:
receiver:
protocols:
http:
endpoint: 0.0.0.0:4318
```
### New Relic [#new-relic]
#### install [#install-2]
npm
pnpm
yarn
bun
```bash
npm install --save @pothos/tracing-newrelic newrelic @types/newrelic
```
```bash
pnpm add @pothos/tracing-newrelic newrelic @types/newrelic
```
```bash
yarn add @pothos/tracing-newrelic newrelic @types/newrelic
```
```bash
bun add @pothos/tracing-newrelic newrelic @types/newrelic
```
#### Basic usage [#basic-usage-1]
```ts
import SchemaBuilder from '@pothos/core';
import TracingPlugin, { isRootField } from '@pothos/plugin-tracing';
import { createNewrelicWrapper } from '@pothos/tracing-newrelic';
const wrapResolver = createNewrelicWrapper({
includeArgs: true,
includeSource: true,
});
export const builder = new SchemaBuilder({
plugins: [TracingPlugin],
tracing: {
default: (config) => isRootField(config),
wrap: (resolver) => wrapResolver(resolver),
},
});
```
#### options [#options-1]
* `includeArgs`: default: `false`
* `includeSource`: default: `false`
#### Instrumenting the execution phase [#instrumenting-the-execution-phase-1]
The tracing plugin for Pothos only adds spans for resolvers. You may also want to capture additional
information about other parts of the graphql execution process.
This example uses GraphQL Yoga, by providing a custom envelop plugin that wraps the execution phase.
Many graphql server implementations have ways to wrap or replace the execution call, but will look
slightly different.
```ts
import newrelic from 'newrelic'; // newrelic must be imported first
import { print } from 'graphql';
import { createYoga, Plugin } from 'graphql-yoga';
import { createServer } from 'node:http';
import { AttributeNames } from '@pothos/tracing-newrelic';
import { schema } from './schema';
const tracingPlugin: Plugin = {
onExecute: ({ args }) => {
newrelic.addCustomAttributes({
[AttributeNames.OPERATION_NAME]: args.operationName ?? '',
[AttributeNames.SOURCE]: print(args.document),
});
},
};
const yoga = createYoga({
schema,
plugins: [tracingPlugin],
});
const server = createServer(yoga);
```
### Using the envelop newrelic plugin [#using-the-envelop-newrelic-plugin]
Envelop has its own plugin for newrelic that can be combined with the tracing plugin:
```ts
import { useNewRelic } from '@envelop/newrelic';
import { createYoga } from 'graphql-yoga';
import { createServer } from 'node:http';
import { schema } from './schema';
const yoga = createYoga({
schema,
plugins: [
useNewRelic({
// Disable resolver tracking since this is covered by the pothos tracing plugin
// If all resolvers are being traced, you could use the New Relic envelop plug instead of the pothos tracing plugin
trackResolvers: false,
}),
],
});
const server = createServer(yoga);
```
### Sentry [#sentry]
#### install [#install-3]
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
```
#### Basic usage [#basic-usage-2]
```ts
import SchemaBuilder from '@pothos/core';
import TracingPlugin, { isRootField } from '@pothos/plugin-tracing';
import { createSentryWrapper } from '@pothos/tracing-sentry';
const traceResolver = createSentryWrapper({
includeArgs: true,
includeSource: true,
});
export const builder = new SchemaBuilder({
plugins: [TracingPlugin],
tracing: {
default: (config) => isRootField(config),
wrap: (resolver, options) => traceResolver(resolver, options),
},
});
```
#### options [#options-2]
* `includeArgs`: default: `false`
* `includeSource`: default: `false`
* `ignoreError`: default: `false`
#### Instrumenting the execution phase [#instrumenting-the-execution-phase-2]
The tracing plugin for Pothos only adds spans for resolvers. You may also want to capture additional
information about other parts of the graphql execution process.
This example uses GraphQL Yoga, by providing a custom envelop plugin that wraps the execution phase.
Many graphql server implementations have ways to wrap or replace the execution call, but will look
slightly different.
```ts
import { print } from 'graphql';
import { createYoga, Plugin } from 'graphql-yoga';
import { createServer } from 'node:http';
import { AttributeNames } from '@pothos/tracing-sentry';
import * as Sentry from '@sentry/node';
import { schema } from './schema';
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 1,
});
const tracingPlugin: Plugin = {
onExecute: ({ setExecuteFn, executeFn }) => {
setExecuteFn((options) =>
Sentry.startSpan(
{
op: 'graphql.execute',
name: options.operationName ?? '',
forceTransaction: true,
attributes: {
[AttributeNames.OPERATION_NAME]: options.operationName ?? undefined,
[AttributeNames.SOURCE]: print(options.document),
},
},
() => executeFn(options),
),
);
},
};
const yoga = createYoga({
schema,
plugins: [tracingPlugin],
});
const server = createServer(yoga);
```
### Using the envelop sentry plugin [#using-the-envelop-sentry-plugin]
Envelop has its own plugin for Sentry that can be combined with the tracing plugin:
```ts
import { useSentry } from '@envelop/sentry';
import { createYoga } from 'graphql-yoga';
import { createServer } from 'node:http';
import { schema } from './schema';
const yoga = createYoga({
schema,
plugins: [useSentry({})],
});
const server = createServer(yoga);
```
### AWS XRay [#aws-xray]
#### install [#install-4]
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
```
#### Basic usage [#basic-usage-3]
```ts
import SchemaBuilder from '@pothos/core';
import TracingPlugin, { isEnumField, isRootField, isScalarField } from '@pothos/plugin-tracing';
import { createXRayWrapper } from '@pothos/tracing-xray';
const traceResolver = createXRayWrapper({
includeArgs: true,
includeSource: true,
});
export const builder = new SchemaBuilder({
plugins: [TracingPlugin],
tracing: {
default: (config) => isRootField(config) || (!isScalarField(config) && !isEnumField(config)),
wrap: (resolver, options) => traceResolver(resolver, options),
},
});
```
#### options [#options-3]
* `includeArgs`: default: `false`
* `includeSource`: default: `false`
#### Instrumenting the execution phase [#instrumenting-the-execution-phase-3]
The tracing plugin for Pothos only adds spans for resolvers. You may also want to capture additional
information about other parts of the graphql execution process.
This example uses GraphQL Yoga, by providing a custom envelop plugin that wraps the execution phase.
Many graphql server implementations have ways to wrap or replace the execution call, but will look
slightly different.
```ts
import AWSXRay from 'aws-xray-sdk-core';
import { print } from 'graphql';
import { createYoga, Plugin } from 'graphql-yoga';
import { createServer } from 'node:http';
import { AttributeNames, SpanNames } from '@pothos/tracing-xray';
import { schema } from './schema';
const tracingPlugin: Plugin = {
onExecute: ({ setExecuteFn, executeFn }) => {
setExecuteFn(async (options) => {
const parent = new AWSXRay.Segment('parent');
return AWSXRay.getNamespace().runAndReturn(() => {
AWSXRay.setSegment(parent);
return AWSXRay.captureAsyncFunc(
SpanNames.EXECUTE,
(segment) => {
if (segment) {
segment.addAttribute(
AttributeNames.OPERATION_NAME,
options.operationName ?? '',
);
segment.addAttribute(AttributeNames.SOURCE, print(options.document));
}
return executeFn(options);
},
parent,
);
});
});
},
};
const yoga = createYoga({
schema,
plugins: [tracingPlugin],
});
const server = createServer(yoga);
```
# Validation plugin
URL: /docs/plugins/validation
Validation plugin docs for Pothos
A plugin for adding validation to field arguments, input object fields, and input types using modern validation libraries like [Zod](https://github.com/colinhacks/zod), [Valibot](https://valibot.dev), and [ArkType](https://arktype.io).
This plugin provides a library-agnostic approach to validation by supporting any validation library that implements the [standard schema](https://standardschema.dev) interface, making it flexible and future-proof.
## 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 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({
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), ... })` - Validate all arguments together
* **Input type validation**: `builder.inputType({ validate: schema, ... })` or `builder.inputType({ ... }).validate(schema)` - Validate entire input objects
* **Input field validation**: `t.string({ validate: schema })` or `t.string().validate(schema)` - Validate individual input type fields
## 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({
validate: z.string().email(),
}),
name: t.arg.string()
.validate(z.string().min(2).max(50)),
},
resolve: (_, args) => `User: ${args.name}`,
}),
}),
});
```
#### 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()
.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]
You can validate all arguments of a field together by passing a validation schema to the `t.field`
```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().optional(),
phone: z.string().optional(),
})
.refine(
(args) => !!args.phone || !!args.email,
{ message: 'Must provide either phone or email' }
),
resolve: () => true,
}),
}),
});
```
#### With transforms [#with-transforms]
To transform all arguments together, you will need to use t.validate(args):
```typescript
builder.queryType({
fields: (t) => ({
user: t.string({
args: t.validate({
email: t.arg.string(),
phone: t.arg.string(),
},
z.object({
email: z.string().optional(),
phone: z.string().optional(),
})
.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(),
age: t.int(),
}),
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(),
birthYear: t.string(),
}),
}).validate(
z.object({
fullName: z.string(),
birthYear: z.string(),
}).transform(data => ({
firstName: data.fullName.split(' ')[0],
lastName: data.fullName.split(' ').slice(1).join(' '),
age: new Date().getFullYear() - parseInt(data.birthYear),
}))
);
builder.queryType({
fields: (t) => ({
createUser: t.string({
args: {
userData: t.arg({ type: UserInput }),
},
resolve: (_, args) => {
// args.userData has transformed shape:
// { firstName: string, lastName: string, age: number }
return `Created user: ${args.userData.firstName} ${args.userData.lastName}`;
},
}),
}),
});
```
### Input Field Validation [#input-field-validation]
Validate individual fields within input types:
```typescript
const UserInput = builder.inputType('UserInput', {
fields: (t) => ({
name: t.string({
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()
.validate(z.string().regex(/^\d{4}-\d{2}-\d{2}$/))
.validate(z.string().transform(str => new Date(str))),
}),
});
```
## Supported Validation Libraries [#supported-validation-libraries]
This plugin works with multiple validation libraries, giving you the flexibility to choose the one that best fits your needs:
* **[Zod](https://zod.dev)** - TypeScript-first schema validation with static type inference
* **[Valibot](https://valibot.dev)** - The open source schema library for TypeScript with bundle size, type safety and developer experience in mind
* **[ArkType](https://arktype.io)** - TypeScript's 1:1 validator, optimized from editor to runtime
* Any library implementing the [standard schema](https://standardschema.dev) interface
## Plugin Options [#plugin-options]
### 'validationError' [#validationerror]
The `validationError` option allows you to customize how validation errors are handled and formatted. This is useful for:
* Customizing error messages for your application's needs
* Logging validation failures for monitoring
* Integrating with error tracking services
* Providing context-specific error messages
```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
### 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**: Cross-field validation with `t.validate()` runs 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
A plugin for creating fields with a single input object. This plugin adds a new `t.fieldWithInput`
method that allows you to more easily define fields with a single input type without having to
define it separately.
## 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 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]
You can customize the name of your Input object, and the name of the input argument:
```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,
},
},
});
```
arg requiredness can also be set on a per field basis by setting `argOptions.required`
```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;
},
}),
});
```
### 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:
```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
Zod plugin docs for Pothos
[The new Pothos Validation plugin](https://pothos-graphql.dev/docs/plugins/validation) is now the recommended way
to add validation to your schema, and has support for zod, as well as several other validation libraries.
This plugin should continue to work, but does not support async validation, or validations that transform data.
A plugin for adding validation for field arguments based on
[zod](https://github.com/colinhacks/zod). This plugin does not expose zod directly, but most of the
options map closely to the validations available in zod.
## Usage [#usage]
### Install [#install]
To use the zod plugin you will need to install both `zod` package and the zod plugin:
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]
```typescript
import ZodPlugin from '@pothos/plugin-zod';
const builder = new SchemaBuilder({
plugins: [ZodPlugin],
zod: {
// optionally customize how errors are formatted
validationError: (zodError, args, context, info) => {
// the default behavior is to just throw the zod error directly
return zodError;
},
},
});
builder.queryType({
fields: (t) => ({
simple: t.boolean({
args: {
// Validate individual args
email: t.arg.string({
validate: {
email: true,
},
}),
phone: t.arg.string(),
},
// Validate all args together
validate: (args) => !!args.phone || !!args.email,
resolve: () => true,
}),
}),
});
```
## Options [#options]
`validationError`: (optional) A function that will be called when validation fails. The function
will be passed the zod validation error, as well as the args, context and info objects. It can
throw an error, or return an error message or custom Error instance.
### Examples [#examples]
#### With custom message [#with-custom-message]
```typescript
builder.queryType({
fields: (t) => ({
withMessage: t.boolean({
args: {
email: t.arg.string({
validate: {
email: [true, { message: 'invalid email address' }],
},
}),
phone: t.arg.string(),
},
validate: [
(args) => !!args.phone || !!args.email,
{ message: 'Must provide either phone number or email address' },
],
resolve: () => true,
}),
}),
});
```
### Validating List [#validating-list]
```typescript
builder.queryType({
fields: (t) => ({
list: t.boolean({
args: {
list: t.arg.stringList({
validate: {
items: {
email: true,
},
maxLength: 3,
},
}),
},
resolve: () => true,
}),
}),
});
```
### Using your own zod schemas [#using-your-own-zod-schemas]
If you just want to use a zod schema defined somewhere else, rather than using the validation
options you can use the `schema` option:
```typescript
builder.queryType({
fields: (t) => ({
list: t.boolean({
args: {
max5: t.arg.int({
validate: {
schema: zod.number().int().max(5),
},
}),
},
resolve: () => true,
}),
}),
});
```
You can also validate all arguments together using a zod schema:
```typescript
builder.queryType({
fields: (t) => ({
simple: t.boolean({
args: {
email: t.arg.string(),
phone: t.arg.string(),
},
// Validate all args together using own zod schema
validate: {
schema: zod.object({
email: zod.string().email(),
phone: zod.string(),
}),
},
resolve: () => true,
}),
}),
});
```
## API [#api]
### On Object fields (for validating field arguments) [#on-object-fields-for-validating-field-arguments]
* `validate`: `Refinement` | `Refinement[]` | `ValidationOptions`.
### On InputObjects (for validating all fields of an input object) [#on-inputobjects-for-validating-all-fields-of-an-input-object]
* `validate`: `Refinement` | `Refinement[]` | `ValidationOptions`.
### On arguments or input object fields (for validating a specific input field or argument) [#on-arguments-or-input-object-fields-for-validating-a-specific-input-field-or-argument]
* `validate`: `Refinement` | `Refinement[]` | `ValidationOptions`.
### `Refinement` [#refinement]
A `Refinement` is a function that will be passed to the `zod` `refine` method. It receives the args
object, input object, or value of the specific field the refinement is defined on. It should return
a `boolean` or `Promise`.
`Refinement`s can either be just a function: `(val) => isValid(val)`, or an array with the function,
and an options object like: `[(val) => isValid(val), { message: 'field should be valid' }]`.
The options object may have a `message` property, and if the type being validated is an object, it
can also include a `path` property with an array of strings indicating the path of the field in the
object being validated. See the zod docs on `refine` for more details.
### `ValidationOptions` [#validationoptions]
The validation options available depend on the type being validated. Each property of
`ValidationOptions` can either be a value specific to the constraint, or an array with the value,
and the options passed to the underlying zod method. This options object can be used to set a custom
error message:
```typescript
{
validate: {
max: [10, { message: 'should not be more than 10' }],
int: true,
}
}
```
#### Number [#number]
* `type`?: `'number'`
* `refine`?: `Refinement | Refinement[]`
* `min`?: `Constraint`
* `max`?: `Constraint`
* `positive`?: `Constraint`
* `nonnegative`?: `Constraint`
* `negative`?: `Constraint`
* `nonpositive`?: `Constraint`
* `int`?: `Constraint`
* `schema`?: `ZodSchema`
#### BigInt [#bigint]
* `type`?: `'bigint'`
* `refine`?: `Refinement | Refinement[]`
* `schema`?: `ZodSchema`
#### Boolean [#boolean]
* `type`?: `'boolean'`
* `refine`?: `Refinement | Refinement[]`
* `schema`?: `ZodSchema`
#### Date [#date]
* `type`?: `'boolean'`
* `refine`?: `Refinement | Refinement[]`
* `schema`?: `ZodSchema`
#### String [#string]
* `type`?: `'string'`;
* `refine`?: `Refinement | Refinement[]`
* `minLength`?: `Constraint`
* `maxLength`?: `Constraint`
* `length`?: `Constraint`
* `url`?: `Constraint`
* `uuid`?: `Constraint`
* `email`?: `Constraint`
* `regex`?: `Constraint`
* `schema`?: `ZodSchema`
#### Object [#object]
* `type`?: `'object'`;
* `refine`?: `Refinement | Refinement[]`
* `schema`?: `ZodSchema`
#### Array [#array]
* `type`?: `'array'`;
* `refine`?: `Refinement | Refinement[]`
* `minLength`?: `Constraint`
* `maxLength`?: `Constraint`
* `length`?: `Constraint`
* `items`?: `ValidationOptions | Refinement`
* `schema`?: `ZodSchema`
### How it works [#how-it-works]
Each arg on an object field, and each field on an input type with validation will build its own zod
validator. These validators will be a union of all potential types that can apply the validations
defined for that field. For example, if you define an optional field with a `maxLength` validator,
it will create a zod schema that looks something like:
```typescript
zod.union([zod.null(), zod.undefined(), zod.array().maxLength(5), zod.string().maxLength(5)]);
```
If you set an `email` validation instead the schema might look like:
```typescript
zod.union([zod.null(), zod.undefined(), zod.string().email()]);
```
At runtime, we don't know anything about the types being used by your schema, we can't infer the
expected js type from the type definition, so the best we can do is limit the valid types based on
what validations they support. The `type` validation allows explicitly validating the `type` of a
field to be one of the base types supported by zod:
```typescript
// field
{
validate: {
type: 'string',
maxLength: 5
}
// generated
zod.union([zod.null(), zod.undefined(), zod.string().maxLength(5)]);
```
There are a few exceptions to the above:
1. args and input fields that are `InputObject`s always use `zod.object()` rather than creating a
union of potential types.
2. args and input fields that are list types always use `zod.array()`.
3. If you only include a `refine` validation (or just pass a function directly to validate) we will
just use `zod`s unknown validator instead:
```typescript
// field
{
validate: (val) => isValid(val),
}
// generated
zod.union([zod.null(), zod.undefined(), zod.unknown().refine((val) => isValid(val))]);
```
If the validation options include a `schema` that schema will be used as an intersection with the
generated validator:
```typescript
// field
{
validate: {
int: true,
schema: zod.number().max(10),
}
// generated
zod.union([zod.null(), zod.undefined(), zod.intersection(zod.number().max(10), zod.number().int())]);
```
### Sharing schemas with client code [#sharing-schemas-with-client-code]
The easiest way to share validators is to use them to define schemas for your fields in an external
file using the normal zod APIs, and then attaching those to your fields using the `schema` option.
```typescript
// shared
import { ValidationOptions } from '@pothos/plugin-zod';
const numberValidation = zod.number().max(5);
// server
builder.queryType({
fields: (t) => ({
example: t.boolean({
args: {
num: t.arg.int({
validate: {
schema: numberValidation,
}
}),
},
resolve: () => true,
}),
});
});
// client
numberValidator.parse(3) // pass
numberValidator.parse('3') // fail
```
You can also use the `createZodSchema` helper from the plugin directly to create zod Schemas from an
options object:
```typescript
// shared
import { ValidationOptions } from '@pothos/plugin-zod';
const numberValidation: ValidationOptions = {
max: 5,
};
// server
builder.queryType({
fields: (t) => ({
example: t.boolean({
args: {
num: t.arg.int({
validate: numberValidation,
}),
},
resolve: () => true,
}),
});
});
// client
import { createZodSchema } from '@pothos/plugin-zod';
const validator = createZodSchema(numberValidator);
validator.parse(3) // pass
validator.parse('3') // fail
```
# Connections
URL: /docs/plugins/drizzle/connections
Creating relay connections with the Drizzle plugin
## Related connections [#related-connections]
To implement a relation as a connection, you can use `t.relatedConnection` instead of `t.relation`:
```ts
builder.drizzleNode('users', {
name: 'User',
fields: (t) => ({
posts: t.relatedConnection('posts'),
}),
});
```
This will automatically define the `Connection`, and `Edge` types, and their respective fields. To
customize the Connection and Edge types, options for these types can be passed as additional
arguments to `t.relatedConnection`, just like `t.connection` from the relay plugin. See the
[relay plugin docs](https://pothos-graphql.dev/docs/plugins/relay) for more details.
You can also define a `query` like with `t.relation`. The only difference with `t.relatedConnection`
is that the `orderBy` format is slightly changed.
To comply with the relay spec and efficiently support backwards pagination, some queries need to be
performed in reverse order, which requires inverting the orderBy clause. To do this automatically,
the `t.relatedConnection` method accepts orderBy as an object keyed by column name with `'asc'` or
`'desc'` values, like `{ createdAt: 'desc' }`, rather than using the `asc(column)` and
`desc(column)` helpers from drizzle. orderBy can also be returned as a single column, or an array of
columns when ordering by multiple columns, which orders ascending.
Ordering defaults to using the table `primaryKey`, and the orderBy columns will also be used to
derive the connections cursor.
```ts
builder.drizzleNode('users', {
name: 'User',
fields: (t) => ({
posts: t.relatedConnection('posts', {
query: () => ({
where: {
published: true,
},
orderBy: {
id: 'desc',
},
}),
}),
}),
});
```
### Connection totalCount [#connection-totalcount]
You can add a `totalCount` field to your connection by setting the `totalCount` option to `true`:
```ts
builder.drizzleNode('users', {
name: 'User',
fields: (t) => ({
posts: t.relatedConnection('posts', {
totalCount: true,
query: () => ({
where: {
published: true,
},
orderBy: {
id: 'desc',
},
}),
}),
}),
});
```
This will automatically add a `totalCount` field to the connection type. The count query is only
executed when the `totalCount` field is actually requested in the GraphQL query, and it's included
as a subquery in the main database query for efficiency.
```graphql
query {
user(id: "...") {
posts(first: 10) {
totalCount
edges {
node {
id
title
}
}
}
}
}
```
The count applies the `where` returned by the field's `query`, so it counts the same rows the
connection paginates (only published posts in the example above). To count every related row
regardless of the filter, set `filterConnectionTotalCount: false` in the `drizzle` plugin options:
```ts
const builder = new SchemaBuilder({
plugins: [DrizzlePlugin],
drizzle: {
client: db,
getTableConfig,
relations,
// count every related row for totalCount, ignoring the where from query (defaults to true)
filterConnectionTotalCount: false,
},
});
```
A `where` on the relation itself always applies to the count, as does the junction table of a
many-to-many relation defined with `.through(...)`. The count joins the junction table the same way
the rows do, so a row that matches the junction twice counts twice, and appears twice in the
connection.
## Drizzle connections [#drizzle-connections]
Similar to `t.drizzleField`, `t.drizzleConnection` allows you to define a connection field that acts
as an entry point to your drizzle query. The `orderBy` in `t.drizzleConnection` works the same way
as it does for `t.relatedConnection`
```ts
builder.queryFields((t) => ({
posts: t.drizzleConnection({
type: 'posts',
resolve: (query, root, args, ctx) =>
db.query.posts.findMany(
query({
where: {
published: true,
},
orderBy: {
id: 'desc',
},
}),
),
}),
}));
```
### drizzleConnection totalCount [#drizzleconnection-totalcount]
You can add a `totalCount` field to a `drizzleConnection` by providing a `totalCount` callback function that returns the count:
```ts
builder.queryFields((t) => ({
posts: t.drizzleConnection({
type: 'posts',
// Use db.$count() for a simple count query
totalCount: () => db.$count(posts, eq(posts.published, true)),
resolve: (query, root, args, ctx) =>
db.query.posts.findMany(
query({
where: {
published: true,
},
orderBy: {
id: 'desc',
},
}),
),
}),
}));
```
The `totalCount` callback receives the same arguments as a normal resolver (`parent`, `args`, `context`, `info`), allowing you to implement custom count logic based on the query context. The example above uses `db.$count()` for a simple count, but you can use any Drizzle query approach.
When only the `totalCount` field is requested (without `edges` or `nodes`), the main query is skipped entirely and only the count query is executed for efficiency.
### Indirect relations as connections [#indirect-relations-as-connections]
In many cases, you can define many to many connections via drizzle relations, allowing the `relatedConnection` API to work across
more complex relations. In some cases you may want to define a connection for a relation not expressed directly as a relation in
your drizzle schema. For these cases, you can use the `drizzleConnectionHelpers`, which allows you to define connection with the `t.connection` API.
```typescript
// Create a drizzle object for the node type of your connection
const Role = builder.drizzleObject('roles', {
name: 'Role',
fields: (t) => ({
id: t.exposeID('id'),
name: t.exposeString('name'),
}),
});
// Create connection helpers for the media type. This will allow you
// to use the normal t.connection with a drizzle type
const rolesConnection = drizzleConnectionHelpers(builder, 'userRoles', {
// select the data needed for the nodes
select: (nestedSelection) => ({
with: {
// use nestedSelection to create the correct selection for the node
role: nestedSelection(),
},
}),
// resolve the node from the returned list item
resolveNode: (userRole) => userRole.role,
});
builder.drizzleObjectField('users', 'rolesConnection', (t) =>
t.connection({
// The type for the Node
type: Role,
// since we are not using t.relatedConnection we need to manually
// include the selections for our connection
select: (args, ctx, nestedSelection) => ({
with: {
userRoles: rolesConnection.getQuery(args, ctx, nestedSelection),
},
}),
// This helper takes a list of nodes and formats them for the connection
resolve: (user, args, ctx) => {
return rolesConnection.resolve(user.userRoles, args, ctx, user);
},
}),
);
```
The above example assumes that you are paginating a relation to a join table, where the pagination
args are applied based on the relation to that join table, but the nodes themselves are nested
deeper.
`drizzleConnectionHelpers` 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 = drizzleConnectionHelpers(builder, 'comments');
const SelectPost = builder.drizzleObject('posts', {
fields: (t) => ({
title: t.exposeString('title'),
comments: t.connection({
type: commentConnectionHelpers.ref,
select: (args, ctx, nestedSelection) => ({
with: {
comments: commentConnectionHelpers.getQuery(args, ctx, nestedSelection),
},
}),
resolve: (parent, args, ctx) => commentConnectionHelpers.resolve(parent.comments, args, ctx),
}),
}),
});
```
Arguments, ordering and filtering can also be defined in the helpers:
```ts
const rolesConnection = drizzleConnectionHelpers(builder, 'userRoles', {
// define additional arguments
args: (t) => ({}),
query: (args) => ({
// define an order
orderBy: {
roleId: 'asc',
}
// define a filter
where: {
accepted: true,
}
}),
// select the data needed for the nodes
select: (nestedSelection) => ({
with: {
// use nestedSelection to create the correct selection for the node
role: nestedSelection(),
},
}),
// resolve the node from the returned list item
resolveNode: (userRole) => userRole.role,
});
builder.drizzleObjectField('users', 'rolesConnection', (t) =>
t.connection({
type: Role,
// add the args from the connection helper to the field
args: rolesConnection.getArgs(),
select: (args, ctx, nestedSelection) => ({
with: {
userRoles: rolesConnection.getQuery(args, ctx, nestedSelection),
},
}),
resolve: (user, args, ctx) => rolesConnection.resolve(user.userRoles, args, ctx, user),
}),
);
```
### Extending connection edges [#extending-connection-edges]
In some cases you may want to expose some data from an indirect connection on the edge object.
```typescript
const rolesConnection = drizzleConnectionHelpers(builder, 'userRoles', {
select: (nestedSelection) => ({
with: {
role: nestedSelection(),
},
}),
resolveNode: (userRole) => userRole.role,
});
builder.drizzleObjectFields('users', (t) => ({
rolesConnection: t.connection(
{
type: Role,
select: (args, ctx, nestedSelection) => ({
with: {
userRoles: rolesConnection.getQuery(args, ctx, nestedSelection),
},
}),
resolve: (user, args, ctx) =>
rolesConnection.resolve(
user.userRoles,
args,
ctx,
user,
),
},
{},
// options for the edge object
{
// define the additional fields on the edge object
fields: (edge) => ({
createdAt: edge.field({
type: 'DateTime',
// the parent shape for edge fields is inferred from the connections resolve function
resolve: (role) => role.createdAt,
}),
}),
},
),
}));
```
### `drizzleConnectionHelpers` for non-relation connections [#drizzleconnectionhelpers-for-non-relation-connections]
You can also use `drizzleConnectionHelpers` for non-relation connections where you want a connection where your edges and nodes are not the same type.
Note that when doing this, you need to be careful to properly merge the `where` clause generated by the connection helper with any additional `where` clause you need to apply to your query
```typescript
const rolesConnection = drizzleConnectionHelpers(builder, 'userRoles', {
select: (nestedSelection) => ({
with: {
role: nestedSelection(),
},
}),
resolveNode: (userRole) => userRole.role,
});
builder.queryFields((t) => ({
roles: t.connection({
type: Role,
args: {
userId: t.arg.int({ required: true }),
},
nodeNullable: true,
resolve: async (_, args, ctx, info) => {
const query = rolesConnection.getQuery(args, ctx, info);
const userRoles = await db.query.userRoles.findMany({
...query,
where: {
...query.where,
userId: args.userId,
},
});
return rolesConnection.resolve(userRoles, args, ctx);
},
}),
}));
```
# 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.
This plugin provides tighter integration with drizzle, making it easier to define GraphQL object
types based on your drizzle tables, and resolving the relations between them in as few queries as
possible. It also has integrations for the relay plugin to make defining nodes and connections easy
and efficient.
## Getting started [#getting-started]
Install the plugin and add it to your builder, as described in [Setup](/docs/plugins/drizzle/setup):
npm
pnpm
yarn
bun
```bash
npm install --save @pothos/plugin-drizzle drizzle-orm@rc
```
```bash
pnpm add @pothos/plugin-drizzle drizzle-orm@rc
```
```bash
yarn add @pothos/plugin-drizzle drizzle-orm@rc
```
```bash
bun add @pothos/plugin-drizzle drizzle-orm@rc
```
Define an object type for a table, expose the columns you want in your API, and add the relations
you want to be able to query through:
```ts
const UserRef = builder.drizzleObject('users', {
name: 'User',
fields: (t) => ({
firstName: t.exposeString('firstName'),
lastName: t.exposeString('lastName'),
posts: t.relation('posts'),
}),
});
builder.queryType({
fields: (t) => ({
me: t.drizzleField({
type: 'users',
resolve: (query, root, args, ctx) =>
db.query.users.findFirst(query({ where: { id: ctx.userId } })),
}),
}),
});
```
Querying `me { firstName posts { title } }` loads the user and their posts in a single query
through drizzles relational query builder.
# Interfaces
URL: /docs/plugins/drizzle/interfaces
Creating interfaces for drizzle tables that can be shared by variants
## Interfaces [#interfaces]
`builder.drizzleInterface` works just like `builder.drizzleObject`, and can be used to define
either the primary type or a variant of a table as an interface that other variants implement. The
interface's `select` is planned whenever a field returns the interface, and an implementation's own
`select` is planned when a fragment narrows to it. Selections are not inherited, so an
implementation that exposes more columns will need a `select` of its own.
```ts
export const Viewer = builder.drizzleInterface('users', {
variant: 'Viewer',
select: { columns: { id: true, role: true } },
resolveType: (user) => (user.role === 'admin' ? 'AdminViewer' : 'MemberViewer'),
fields: (t) => ({
id: t.exposeID('id'),
user: t.variant('users'),
}),
});
builder.drizzleObject('users', {
variant: 'AdminViewer',
interfaces: [Viewer],
select: { columns: { permissions: true } },
fields: (t) => ({
permissions: t.exposeStringList('permissions'),
}),
});
```
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) => ({
posts: t.relatedConnection('posts'),
}));
```
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
## Defining Objects [#defining-objects]
The `builder.drizzleObject` method can be used to define GraphQL Object types based on a drizzle
table:
```ts
const UserRef = 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 `UserRef` can be used like any other `ObjectRef`
in Pothos.
## Custom fields [#custom-fields]
You will often want to define fields in your API that do not correspond to a specific database
column. To do this, you can define fields with a resolver like any other Pothos object type:
```ts
const UserRef = builder.drizzleObject('users', {
name: 'User',
fields: (t) => ({
fullName: t.string({
resolve: (user, args, ctx, info) => `${user.firstName} ${user.lastName}`,
}),
}),
});
```
## Drizzle Fields [#drizzle-fields]
Drizzle objects and relations allow you to define parts of your schema backed by your drizzle
schema, but don't provide a clear entry point into this Graph of data. To make your drizzle objects
queryable, we will need to add fields that return our drizzle objects. This can be done using the
`t.drizzleField` method. This can be used to define fields on the root `Query` type, or any other
object type in your schema:
```ts
builder.queryType({
fields: (t) => ({
post: t.drizzleField({
type: 'posts',
args: {
id: t.arg.id({ required: true }),
},
resolve: (query, root, args, ctx) =>
db.query.posts.findFirst(
query({
where: {
id: Number.parseInt(args.id, 10),
},
}),
),
}),
posts: t.drizzleField({
type: ['posts'],
resolve: (query, root, args, ctx) => db.query.posts.findMany(query()),
}),
}),
});
```
The `resolve` function of a `drizzleField` will be passed a `query` function that MUST be called and
passed to a drizzle `findOne` or `findMany` query. The `query` function optionally accepts any
arguments that are normally passed into the query, and will merge these options with the selection
used to resolve data for the nested GraphQL selections.
### `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:
```ts
builder.queryFields((t) => ({
user: t.drizzleFieldWithInput({
type: 'users',
input: {
id: t.input.id({ required: true }),
},
resolve: (query, root, args, ctx) =>
db.query.users.findFirst(query({ where: { id: Number.parseInt(args.input.id, 10) } })),
}),
}));
```
# Ordering and cursors
URL: /docs/plugins/drizzle/ordering-and-cursors
How a connection orders rows, and the cursors that ordering produces
## Ordering and cursors [#ordering-and-cursors]
Connections page with a cursor that records where in the ordering the previous page ended. This
applies to `t.relatedConnection`, `t.drizzleConnection`, and `drizzleConnectionHelpers`.
### 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.
Primary keys are used whether or not their columns are marked `notNull()`, since SQL makes primary
key columns non-nullable anyway. That includes composite keys declared with
`primaryKey({ columns: [...] })`.
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) =>
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.
Ordering by an expression cannot use an index on the underlying column. If the table is large enough
to need one, add an index on the expression.
# 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.
## 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) => ({
latestPosts: t.field({
type: [Post],
select: (args, ctx, nestedSelection) => ({
// what the query selects on the posts, limited to one
with: { posts: nestedSelection({ 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]
Selections are synchronous unless the schema opts in with `AsyncSelections: true`:
```ts
const builder = new SchemaBuilder<{
DrizzleRelations: typeof relations;
AsyncSelections: true;
}>({
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.
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.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) => ({
posts: t.relation('posts', {
query: async (args, ctx) => ({ where: { authorId: await ctx.currentUserId() } }),
}),
latestPosts: t.field({
type: [Post],
select: async (args, ctx, nestedSelection) => ({
with: { posts: await nestedSelection({ limit: await ctx.previewSize() }) },
}),
resolve: (user) => user.posts,
}),
}),
});
```
`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. Calling `nestedSelection` and discarding a synchronous
result is not detected, and the nested selection will not be loaded with the parent.
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) => ({
with: {
comments: await commentConnectionHelpers.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.
## 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 plugin will throw a `PothosValidationError` if
their `select` options ask for the same relation with different arguments:
```
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
## Relations [#relations]
Drizzles relational query builder allows you to define the relationships between your tables. The
`t.relation` method makes it easy to add fields to your GraphQL API that implement those
relations:
```ts
builder.drizzleObject('profiles', {
name: 'Profile',
fields: (t) => ({
bio: t.exposeString('bio'),
}),
});
builder.drizzleObject('posts', {
name: 'Post',
fields: (t) => ({
title: t.exposeString('title'),
author: t.relation('author'),
}),
});
builder.drizzleObject('users', {
name: 'User',
fields: (t) => ({
firstName: t.exposeString('firstName'),
profile: t.relation('profile'),
posts: t.relation('posts'),
}),
});
```
The relation will automatically define GraphQL fields of the appropriate type based on the relation
defined in your drizzle schema.
## Relation queries [#relation-queries]
For some cases, exposing relations as fields without any customization works great, but in some
cases you may want to apply some filtering or ordering to your relations. This can be done by
specifying a `query` option on the relation:
```ts
builder.drizzleObject('users', {
name: 'User',
fields: (t) => ({
firstName: t.exposeString('firstName'),
posts: t.relation('posts', {
args: {
limit: t.arg.int(),
offset: t.arg.int(),
},
// query callback receives (args, ctx, pathInfo)
query: (args) => ({
limit: args.limit ?? 10,
offset: args.offset ?? 0,
where: {
published: true,
},
orderBy: {
updatedAt: 'desc',
},
}),
}),
drafts: t.relation('posts', {
query: {
where: {
published: false,
},
},
}),
}),
});
```
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#querying)
## 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.
## 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.
### Count aggregations [#count-aggregations]
One common use case is adding a count field that efficiently counts related records:
```ts
import { count } from 'drizzle-orm';
builder.drizzleNode('users', {
name: 'User',
id: { column: (user) => user.id },
fields: (t) => ({
firstName: t.exposeString('firstName'),
// Add a count of related posts
postsCount: t.relatedField('posts', {
type: 'Int',
// buildFilter creates the correct WHERE clause for the relation
select: (buildFilter) => ({
extras: {
postsCount: (parent) => db.$count(posts, buildFilter(parent)),
},
}),
resolve: (user) => user.postsCount,
}),
}),
});
```
The `buildFilter` function passed to `select` generates the appropriate SQL filter based on the
relation definition. This is no different than using `t.field`, but the `buildFilter` helper makes
it easier to filter for the related records.
`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.
### SQLite many-to-many filters [#sqlite-many-to-many-filters]
On SQLite, `buildFilter` can look up related target identities through the junction join when the
target has a non-null unique key. PostgreSQL retains an `EXISTS` predicate to avoid an additional
target join.
For a custom `RAW` scope in a Drizzle relation definition, use the table supplied to the callback:
```ts
where: {
RAW: (target) => sql`${target.published} = true`,
},
```
This lets Drizzle use the target's alias inside the lookup. A static SQL scope referencing the
original target table, such as ``RAW: sql`${posts.published} = true` ``, is passed through unchanged
and can still force SQLite to scan the target table. The same applies to a callback that ignores
its table argument and references `posts` directly. Object filters such as
`where: { published: true }` also use the supplied alias.
## Related count [#related-count]
For the common case of counting related records, there's a simpler `t.relatedCount` method that handles
all the boilerplate for you:
```ts
builder.drizzleNode('users', {
name: 'User',
id: { column: (user) => user.id },
fields: (t) => ({
firstName: t.exposeString('firstName'),
// Simple count of all related comments
commentsCount: t.relatedCount('comments'),
// Count with a where filter
publishedPostsCount: t.relatedCount('posts', {
where: eq(posts.published, true),
}),
}),
});
```
The `where` option accepts either a static SQL filter or a function that receives the field arguments
and context:
```ts
publishedPostsCount: t.relatedCount('posts', {
args: {
category: t.arg.string(),
},
where: (args, ctx) => args.category
? and(eq(posts.published, true), eq(posts.category, args.category))
: 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.
# Relay
URL: /docs/plugins/drizzle/relay
Using the Drizzle and Relay plugins together
## Relay integration [#relay-integration]
Relay provides some very useful best practices that are useful for most GraphQL APIs. To make it
easy to comply with these best practices, the drizzle plugin has built in support for defining relay
`nodes` and `connections`.
## Relay Nodes [#relay-nodes]
Defining relay nodes works just like defining normal `drizzleObject`s, but requires specifying a
column to use as the nodes `id` field.
```ts
builder.drizzleNode('users', {
name: 'User',
id: {
column: (user) => user.id,
// other options for the ID field can be passed here
},
fields: (t) => ({
firstName: t.exposeString('firstName'),
lastName: t.exposeString('lastName'),
}),
});
```
The id column can also be set to a list of columns for types with a composite primary key.
# Selections
URL: /docs/plugins/drizzle/selections
Selecting the columns and relations your fields need
## Type selections [#type-selections]
By default, a `drizzleObject` gives its resolvers access to all columns of the table. For tables
with many columns, it can be more efficient to only select the needed columns. You can configure the
selected columns, and relations by passing a `select` option when defining the type:
```ts
const UserRef = builder.drizzleObject('users', {
name: 'User',
select: {
columns: {
firstName: true,
lastName: true,
},
with: {
profile: true,
},
extras: {
lowercaseName: (users, sql) => sql`lower(${users.firstName})`
},
},
fields: (t) => ({
fullName: t.string({
resolve: (user, args, ctx, info) => `${user.firstName} ${user.lastName}`,
}),
bio: t.string({
resolve: (user) => user.profile.bio,
}),
email: t.string({
resolve: (user) => `${user.lowercaseName}@example.com`,
}),
}),
});
```
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.
## Field selections [#field-selections]
The previous example allows you to control what gets selected by default, but you often want to only
select the columns that are required to fulfill a specific field. You can do this by adding the
appropriate selections on each field:
```ts
const UserRef = builder.drizzleObject('users', {
name: 'User',
select: {},
fields: (t) => ({
fullName: t.string({
select: {
columns: { firstName: true, lastName: true },
},
resolve: (user, args, ctx, info) => `${user.firstName} ${user.lastName}`,
}),
bio: t.string({
select: {
with: { profile: true },
},
resolve: (user) => user.profile.bio,
}),
email: t.string({
select: {
extras: {
lowercaseName: (users, sql) => sql`lower(${users.firstName})`
},
},
resolve: (user) => `${user.lowercaseName}@example.com`,
}),
}),
});
```
# Setup
URL: /docs/plugins/drizzle/setup
Setting up the Drizzle plugin
## Installing [#installing]
npm
pnpm
yarn
bun
```bash
npm install --save @pothos/plugin-drizzle drizzle-orm@rc
```
```bash
pnpm add @pothos/plugin-drizzle drizzle-orm@rc
```
```bash
yarn add @pothos/plugin-drizzle drizzle-orm@rc
```
```bash
bun add @pothos/plugin-drizzle drizzle-orm@rc
```
The drizzle plugin is built on top of drizzles relational query builder, and requires that you
define and configure all the relevant relations in your drizzle schema. See
[https://orm.drizzle.team/docs/relations-v2](https://orm.drizzle.team/docs/relations-v2) for detailed documentation on the relations API.
Once you have configured your drizzle schema, you can initialize your Pothos
SchemaBuilder with the drizzle plugin:
```ts
import { drizzle } from 'drizzle-orm/...';
// Import the appropriate getTableConfig for your dialect
import { getTableConfig } from 'drizzle-orm/sqlite-core';
import SchemaBuilder from '@pothos/core';
import DrizzlePlugin from '@pothos/plugin-drizzle';
import { relations } from './db/relations';
const db = drizzle({ client, relations });
type DrizzleRelations = typeof relations
export interface PothosTypes {
DrizzleRelations: DrizzleRelations;
}
const builder = new SchemaBuilder({
plugins: [DrizzlePlugin],
drizzle: {
client: db, // or (ctx) => db if you want to create a request specific client
getTableConfig,
relations,
},
});
```
### Integration with other plugins [#integration-with-other-plugins]
The drizzle plugin has integrations with several other plugins. While the `with-input` and `relay`
plugins are not required, many examples will assume these plugins have been installed:
```ts
import { drizzle } from 'drizzle-orm/...';
import SchemaBuilder from '@pothos/core';
import DrizzlePlugin from '@pothos/plugin-drizzle';
import RelayPlugin from '@pothos/plugin-relay';
import WithInputPlugin from '@pothos/plugin-with-input';
import { getTableConfig } from 'drizzle-orm/sqlite-core';
import { relations } from './db/relations';
const db = drizzle({ client, relations });
export interface PothosTypes {
DrizzleRelations: typeof relations;
}
const builder = new SchemaBuilder({
plugins: [RelayPlugin, WithInputPlugin, DrizzlePlugin],
drizzle: {
client: db,
getTableConfig,
relations,
},
});
```
### 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.
# Type variants
URL: /docs/plugins/drizzle/variants
How to define multiple GraphQL types based on the same drizzle table
## Variants [#variants]
It is often useful to be able to define multiple object types based on the same table. This can be
done using a feature called `variants`. The `variants` API consists of 3 parts:
* A `variant` option that can be passed instead of a name on `drizzleObjects`
* The ability to pass an `ObjectRef` to the `type` option of `t.relation` and other similar fields
* A `t.field` method that works similar to `t.relation`, but is used to define a GraphQL field that
references a variant of the same record.
```ts
// Viewer type representing the current user
export const Viewer = builder.drizzleObject('users', {
variant: 'Viewer',
select: {},
fields: (t) => ({
id: t.exposeID('id'),
// A reference to the normal user type so normal user fields can be queried
user: t.variant('users'),
// Adding drafts to View allows a user to fetch their own drafts without exposing it for Other Users in the API
drafts: t.relation('posts', {
query: {
where: {
published: false,
},
orderBy: {
updatedAt: 'desc',
},
},
}),
}),
});
builder.queryType({
fields: (t) => ({
me: t.drizzleField({
// We can use the ref returned by builder.drizzleObject to define our `drizzleField`
type: Viewer,
resolve: (query, root, args, ctx) =>
db.query.users.findFirst(
query({
where: {
id: ctx.user.id,
},
}),
),
}),
}),
});
builder.drizzleNode('users', {
name: 'User',
fields: (t) => ({
firstName: t.exposeString('firstName'),
// This field will resolve to the Viewer type, but be set to null if the user is not the current user
viewer: t.variant(Viewer, {
isNull: (user, args, ctx) => user.id !== ctx.user?.id,
}),
}),
});
```
A `t.variant` field can have a `select` of its own. It is planned along with the variant's
type-level selection when the variant is queried through that field:
```ts
builder.drizzleNode('users', {
name: 'User',
fields: (t) => ({
viewer: t.variant(Viewer, {
// loaded with the row when `viewer` is selected
select: { columns: { email: true } },
isNull: (user, args, ctx) => user.id !== ctx.user?.id,
}),
}),
});
```
Two variants of one table 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).
# Connections
URL: /docs/plugins/prisma/connections
Creating relay connections with the Prisma plugin
### `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) => 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]
Creating connections from indirect relations is a little more involved, but can be achieved using
`prismaConnectionHelpers` with a normal `t.connection` field.
```typescript
// Create a prisma object for the node type of your connection
const Media = builder.prismaObject('Media', {
select: {
id: true,
},
fields: (t) => ({
url: t.exposeString('url'),
}),
});
// Create connection helpers for the media type. This will allow you
// to use the normal t.connection with a prisma type
const mediaConnectionHelpers = prismaConnectionHelpers(
builder,
'PostMedia', // this should be the join table
{
cursor: 'id',
select: (nodeSelection) => ({
// select the relation to the media node using the nodeSelection function
media: nodeSelection({
// optionally specify fields to select by default for the node
select: {
id: true,
posts: true,
},
}),
}),
// resolve the node from the edge
resolveNode: (postMedia) => postMedia.media,
// additional/optional options
maxSize: 100,
defaultSize: 20,
},
);
builder.prismaObjectField('Post', 'mediaConnection', (t) =>
t.connection({
// The type for the Node
type: Media,
// since we are not using t.relatedConnection we need to manually
// include the selections for our connection
select: (args, ctx, nestedSelection) => ({
media: mediaConnectionHelpers.getQuery(args, ctx, nestedSelection),
}),
resolve: (post, args, ctx) =>
// This helper takes a list of nodes and formats them for the connection
mediaConnectionHelpers.resolve(
// map results to the list of edges
post.media,
args,
ctx,
),
}),
);
```
The above example assumes that you are paginating a relation to a join table, where the pagination
args are applied based on the relation to that join table, but the nodes themselves are nested
deeper.
`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) => 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) => 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) => 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 `t.edgeObject`
```typescript
const CommentConnection = builder.connectionObject({
type: Comment,
// or
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]
In some cases you may want to expose some data from an indirect connection on the edge object.
```typescript
const mediaConnectionHelpers = prismaConnectionHelpers(builder, 'PostMedia', {
cursor: 'id',
select: (nodeSelection) => ({
// select the relation to the media node using the nodeSelection function
media: nodeSelection({}),
// Select additional fields from the join table
createdAt: true,
}),
// resolve the node from the edge
resolveNode: (postMedia) => postMedia.media,
});
builder.prismaObjectFields('Post', (t) => ({
manualMediaConnection: t.connection(
{
type: Media,
select: (args, ctx, nestedSelection) => ({
// count the join table rows for totalCount
_count: {
select: {
media: true,
},
},
// select the join table rows, with the pagination and node selection from the helpers
media: {
...mediaConnectionHelpers.getQuery(args, ctx, nestedSelection),
},
}),
resolve: (post, args, ctx) => ({
totalCount: post._count.media,
...mediaConnectionHelpers.resolve(post.media, args, ctx),
}),
},
{},
// options for the edge object
{
// define the additional fields on the edge object
fields: (edge) => ({
createdAt: edge.field({
type: 'DateTime',
// the parent shape for edge fields is inferred from the connections resolve function
resolve: (media) => media.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: Comment,
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],
relayOptions: {},
prisma: {
client: db,
},
});
builder.globalConnectionField('totalCount', (t) =>
t.int({
nullable: false,
resolve: (parent) =>
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.
# Prisma plugin
URL: /docs/plugins/prisma
Prisma plugin docs for Pothos
This plugin provides tighter integration with prisma, making it easier to define prisma based object
types, and helps solve n+1 queries for relations. It also has integrations for the relay plugin to
make defining nodes and connections easy and efficient.
This plugin is NOT required to use prisma with Pothos, but does make things a lot easier and more
efficient. See the [Using Prisma without a plugin](#using-prisma-without-a-plugin) section below for
more details.
## Features [#features]
* 🎨 Quickly define GraphQL types based on your Prisma models
* 🦺 Strong type-safety throughout the entire API
* 🤝 Automatically resolve relationships defined in your database
* 🎣 Automatic Query optimization to efficiently load the specific data needed to resolve a query
(solves common N+1 issues)
* 💅 Types and fields in GraphQL schema are not implicitly tied to the column names or types in your
database.
* 🔀 Relay integration for defining nodes and connections that can be efficiently loaded.
* 📚 Supports multiple GraphQL models based on the same Database model
* 🧮 Count fields can easily be added to objects and connections
## Example [#example]
Here is a quick example of what an API using this plugin might look like. There is a more thorough
breakdown of what the methods and options used in the example 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) =>
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
const PostRef = builder.prismaObject('Post', {
fields: (t) => ({
title: t.exposeString('title'),
content: t.exposeString('content'),
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(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) => 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 PostDraft = 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) => post.media.map(({ media }) => media),
}),
}),
});
const Media = builder.prismaObject('Media', {
select: {
id: true,
},
fields: (t) => ({
url: t.exposeString('url'),
uploadedBy: t.relation('uploadedBy'),
}),
});
```
# 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
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.
# 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]
There is a new `t.prismaField` method which can be used to define fields that resolve to your prisma
types:
```typescript
builder.queryType({
fields: (t) => ({
me: t.prismaField({
type: 'User',
resolve: async (query, root, args, ctx, info) =>
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) =>
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.
# 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. It is NOT required to use
the normal prisma plugin.
## 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, you can add the plugin to your schema along with the normal prisma plugin:
```ts
import SchemaBuilder from '@pothos/core';
import { PrismaClient } from '@prisma/client';
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({});
export default new SchemaBuilder<{
Scalars: {
DateTime: {
Input: Date;
Output: Date;
};
};
PrismaTypes: PrismaTypes;
}>({
plugins: [PrismaPlugin, PrismaUtils],
prisma: {
client: prisma,
dmmf: getDatamodel(),
},
});
```
## What can you do with this plugin [#what-can-you-do-with-this-plugin]
Currently this plugin is focused on making it easier to define prisma compatible input types that
take advantage of the types defined in your Prisma schema.
The goal is not to generate all input types automatically, but rather to provide building blocks so
that writing your own helpers or code-generators becomes a lot easier. There are far too many
tradeoffs and choices to be made when designing input types for queries that one solution won't work
for everyone.
This plugin will eventually provide more helpers and examples that should allow anyone to quickly
set something up to automatically creates all their input types (and eventually other crud
operations).
## 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: UserFilter,
// 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 { Prisma } from '@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)
# 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`:
```typescript
const builder = new SchemaBuilder<{
PrismaTypes: PrismaTypes;
AsyncSelections: true;
}>({
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.
```typescript
builder.prismaObject('Post', {
fields: (t) => ({
comments: t.relation('comments', {
query: async (args, ctx) => ({ where: { authorId: await ctx.currentUserId() } }),
}),
latestComments: 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.
```typescript
const Post = builder.prismaObject('Post', {...});
builder.objectRef<{
success: boolean;
post?: Post
}>('CreatePostResult').implement({
fields: (t) => ({
success: t.boolean(),
post: t.field({
type: Post,
nullable: true,
resolve: (result) => result.post,
}),
}),
});
builder.mutationField(
'createPost',
{
args: (t) => ({
title: t.string({ required: true }),
...
}),
},
{
resolve: async (parent, args, context, info) => {
if (!validateCreatePostArgs(args)) {
return {
success: false,
}
}
const post = prisma.post.create({
...queryFromInfo({
context,
info,
// nested path where the selections for this type can be found
path: ['post'],
// optionally you can pass an initial selection, generally you wouldn't need this;
// when nothing is selected at `path`, it is returned as is
select: {
comments: true,
},
}),
data: {
title: args.input.title,
...
},
});
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.
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.appointments 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 plugin will throw if their type-level
`select`/`include` ask for the same relation with different arguments:
```
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.
# Relations
URL: /docs/plugins/prisma/relations
Adding relations to prisma objects
You can add fields for relations using the `t.relation` method:
```typescript
builder.queryType({
fields: (t) => ({
me: t.prismaField({
type: 'User',
resolve: async (query, root, args, ctx, info) =>
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;
}
}
}
}
```
This will work perfectly for the majority of queries. There are a number of edge cases that make it
impossible to resolve everything in a single query. When this happens Pothos will automatically
construct an additional query to ensure that everything is still loaded correctly, and split into as
few efficient queries as possible. This process is described in more detail below
### 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
All of the above should be relatively uncommon in normal usage, but the plugin ensures that these
types of edge cases are automatically handled when they do occur.
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.
### 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.
```typescript
builder.prismaObject('User', {
fields: (t) => ({
id: t.exposeID('id'),
email: t.exposeString('email'),
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',
},
}),
}),
}),
});
```
### 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]
Prisma supports querying for
[relation counts](https://www.prisma.io/docs/concepts/components/prisma-client/aggregation-grouping-summarizing#count-relations)
which allow including counts for relations along side other `includes`. Before prisma 4.2.0, this
does not support any filters on the counts, but can give a total count for a relation. Starting from
prisma 4.2.0, filters on relation count are available under the `filteredRelationCount` preview
feature flag.
```typescript
builder.prismaObject('User', {
fields: (t) => ({
id: t.exposeID('id'),
postCount: t.relationCount('posts', {
where: {
published: true,
},
}),
}),
});
```
# Relay
URL: /docs/plugins/prisma/relay
Using the Prisma and Relay plugins together
This plugin has extensive integration with the
[relay plugin](https://pothos-graphql.dev/docs/plugins/relay), which makes creating nodes and
connections very easy.
### `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: { resolve: (post) => String(post.id) },
nullable: true,
fields: (t) => ({
title: t.exposeString('title'),
author: t.relation('author'),
}),
});
```
# 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 THEY 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({
// 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]
The following is a slightly contrived example, but shows how arguments can be used when creating a
selection for a field:
```typescript
const PostDraft = builder.prismaObject('Post', {
fields: (t) => ({
title: t.exposeString('title'),
commentFromDate: t.string({
args: {
date: t.arg({ type: 'Date', required: true }),
},
select: (args) => ({
comments: {
take: 1,
where: {
createdAt: {
gt: args.date,
},
},
},
}),
resolve: (post) => post.comments[0]?.content,
}),
}),
});
```
# Setup
URL: /docs/plugins/prisma/setup
Setting up the Prisma plugin
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]
This plugin requires a little more setup than other plugins because it integrates with the prisma to
generate some types that help the plugin better understand your prisma schema. Previous versions of
this plugin used to infer all required types from the prisma client itself, but this resulted in a
poor dev experience because the complex types slowed down editors, and some more advanced use cases
could not be typed correctly.
### 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
```
additional 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 none of your auto-completions are working for prisma types and relations, make sure your generated types are importing from the correct location
### Set up the builder [#set-up-the-builder]
```typescript
import SchemaBuilder from '@pothos/core';
import { PrismaClient } from '@prisma/client';
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({});
const builder = new SchemaBuilder<{
PrismaTypes: PrismaTypes; // This gives the builder all the type information about your prisma schema
}>({
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: boolean | { models: boolean, fields: boolean },
// 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,
},
});
```
It is strongly recommended NOT to put your prisma client into `Context`. This will result in slower
type-checking and a laggy developer experience in VSCode. See
[this issue](https://github.com/microsoft/TypeScript/issues/45405) for more details.
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.
```typescript
import SchemaBuilder from '@pothos/core';
import { PrismaClient, Prisma } from '@prisma/client';
import PrismaPlugin from '@pothos/plugin-prisma';
import type PrismaTypes from '../lib/pothos-prisma-types';
import { getDatamodel } from '../lib/pothos-prisma-types';
const prisma = new PrismaClient({});
const readOnlyPrisma = new PrismaClient({
datasources: {
db: {
url: process.env.READ_ONLY_REPLICA_URL,
},
},
});
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.
This check is fairly naive and works by wrapping the properties on the query with a getter that sets
a flag if the property is accessed. 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]
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.
# 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`. You will always need to have a "Primary" variant (defined as
described above). Additional variants can be defined by providing a `variant` option instead of a
`name` option when creating the type:
```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: {
resolve: (user) => user.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.user.id,
}),
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: { draft: true } },
}),
});
});
```
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'),
user: t.variant(User),
});
});
const User = builder.prismaNode('User', {
interfaces: [Named],
id: {
resolve: (user) => user.id,
},
fields: (t) => ({
email: t.exposeString('email'),
}),
});
// Viewer references the `User` ref in its field definition,
// referencing the `User` in fields would cause a circular type issue
builder.prismaObjectField(Viewer, 'user', 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).
# Prisma without a plugin
URL: /docs/plugins/prisma/without-a-plugin
Using Prisma without a plugin
Using prisma without a plugin is relatively straight forward using the `builder.objectRef` method.
The easiest way to create types backed by prisma looks something like:
```typescript
import { Post, PrismaClient, User } from '@prisma/client';
const db = new PrismaClient();
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) =>
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) => db.user.findUniqueOrThrow({ where: { id: post.authorId } }),
}),
}),
});
builder.queryType({
fields: (t) => ({
me: t.field({
type: UserObject,
resolve: (root, args, ctx) => 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
make the author part of shape required for a post:
```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) =>
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 using a query if it was not provided by the parent resolver:
```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.findUnique({ rejectOnNotFound: true, where: { id: post.authorId } }),
}),
}),
});
```
With this setup, a parent resolver has the option to include the author, but we have a fallback
incase it does not.
There are other patterns like data loaders than can be used to reduce n+1 issues, and make your
graph more efficient, but they are too complex to describe here.