A Pothos plugin for more efficient integration with prisma
npm install @pothos/plugin-prismaThis 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 section below for
more details.
- 🎨 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
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({
// 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 includes or selects 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 tooldPosts
resolve everything inside . Prisma can only resolve each relation once in a single query,posts
so we need a separate to handle the second relation.
`bash`
yarn add @pothos/plugin-prisma
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.
`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@prisma/client
full path of wherever the client is generated. If you are checking in the generated file, using
is a good option.output
- : Where to write the generated types
Example with more options:
`prisma`
generator pothos {
provider = "prisma-pothos-types"
clientOutput = "@prisma/client"
output = "./pothos-types.ts"
}
`typescript
import SchemaBuilder from '@pothos/core';
import { PrismaClient } from '@prisma/client';
import PrismaPlugin from '@pothos/plugin-prisma';
// This is the default location for the generator, but this can be
// customized as described above.
// Using a type only import will help avoid issues with undeclared
// exports in esm mode
import type PrismaTypes from '@pothos/plugin-prisma/generated';
const prisma = new PrismaClient({});
const builder = new SchemaBuilder<{
PrismaTypes: PrismaTypes;
}>({
plugins: [PrismaPlugin],
prisma: {
client: prisma,
// 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',
},
});
`
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 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 '@pothos/plugin-prisma/generated';
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),
// Because the prisma client is loaded dynamically, we need to explicitly provide the some information about the prisma schema
dmmf: Prisma.dmmf,
},
});
`
builder.prismaObject takes 2 arguments:
1. name: The name of the prisma model this new type representsoptions
2. : 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.
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 listresolve
field).
2. The function has a new first argument query which should be spread into query 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 thant.field
can be used like any other object ref (with ), 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.
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{ include: { author: true }}
like that will be passed as part of the query argument of aprismaField resolver. If the parent is another relation field, the includes will become nested,prismaField
and the full relation chain will be passed to the 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
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.relationprismaField
- The root did not correctly spread the query arguments in is 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.
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 intot.relation
your prisma query as needed. For the flow is slightly different because we are notquery
making a prisma query directly. We do this by adding a 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 getst.prismaField
passed into the first argument of the parent , and can include things like where,skip, take, and orderBy. The query function will be passed the arguments for the field, andparent
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 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',
},
}),
}),
}),
});
Prisma supports querying for
relation counts
which allow including counts for relations along side other includes. Before prisma 4.2.0, thisfilteredRelationCount
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 preview
feature flag.
`typescript`
builder.prismaObject('User', {
fields: (t) => ({
id: t.exposeID('id'),
postCount: t.relationCount('posts', {
where: {
published: true,
},
}),
}),
});
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 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,
}),
}),
});
By default, the prisma plugin will use include when including relations, or generating fallbackt.prismaField
queries. This means we are always loading all columns of a table when loading it in a 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:
`typescriptbio
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 the field is queried`
select: {
profile: {
select: {
bio: true,
},
},
},
resolve: (user) => user.profile.bio,
}),
}),
});
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,
}),
}),
});
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.
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 asvariant
described above). Additional variants can be defined by providing a option instead of aname 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 set's 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,User
// referencing the in fields would cause a circular type issue`
builder.prismaObjectField(Viewer, 'user', t.variant(User));
This same workaround applies when defining relations using 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.
By default, the nestedSelection function will return selections based on the type of the currentnestedSelection
field. can also be used to get a selection from a field nested deeper inside otherprismaObject
fields. This is useful if the field returns a type that is not a , 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
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,
}),
}),
});
`
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{ select: { id: true } }
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 arument is the default query for the media relation
// It could be something like: 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'),
}),
});
`
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 towarn it will log a warning to the console if Pothos detects that you have not properly used theerror
query in your resolver. Similarly if you set the option to it will throw an error instead.info
You can also pass a function which will receive the 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.
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:
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.city.create({
...queryFromInfo({
context,
info,
// nested path where the selections for this type can be found
path: ['post']
// optionally you can pass a custom initial selection, generally you wouldn't need this
// but if the field at path is not selected, the initial selection set may be empty
select: {
comments: true,
},
}),
data: {
title: args.input.title,
...
},
});
return {
success: true,
post,
}
},
},
);
`
This plugin has extensive integration with the
relay plugin, which makes creating nodes and
connections very easy.
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,field
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 option to the name of a unique
column or index.
`typescript`
builder.prismaNode('Post', {
// This set's 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 afindUnique option that can be used to load the node by it's id. This is generally not necessary.
`typescriptwhere
builder.prismaNode('Post', {
id: { resolve: (post) => String(post.id) },
// The return value will be passed as the 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 innull
the database, the default behavior is to throw an error. There are some scenarios where it is
preferable to return 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'),
}),
});
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
- type: the name of the prisma model being connected tocursor
- : a @unique column of the model being connected to. This is used as the cursor optiondefaultSize
passed to prisma.
- : (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 shouldresolve
be spread into your prisma query. The function should return an array of nodes for thequery
connection. The will contain the correct take, skip, and cursor options based on thebefore
connection arguments (, after, first, last), along with include options for nestedtotalCount
selections.
- : A function for loading the total count for the connection. This will add atotalCount
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, or beforefirst
- and beforelast
- and after
Queries for other 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 anddefaultConnectionSize options in the prisma plugin options.
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
- cursor: a @unique column of the model being connected to. This is used as the cursor optiondefaultSize
passed to prisma.
- : (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 returnstotalCount
filtering and sorting logic that will be merged into the query for the relation.
- : when set to true, this will add a totalCount field to the connection object. seerelationCount
above for more details. Note that this will not work when using a shared
connection object (see details below)
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 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:
`tsselect
const mediaConnectionHelpers = prismaConnectionHelpers(builder, 'PostMedia', {
cursor: 'id',
// define arguments for the connection helper, these will be available as the second argument of
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),
}),
);
`
You can create reusable connection objects by using builder.connectionObject.
These connection objects can be used with t.prismaConnection, t.relatedConnection, ort.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
),
}),
});
`
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) => ({
media: mediaConnectionHelpers.getQuery(args, ctx, nestedSelection),
select: {
media: nestedSelection({}, ['edges', 'node']),
},
}),
resolve: (post, args, ctx) =>
mediaConnectionHelpers.resolve(
post.media.map(({ media }) => 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,
}),
}),
},
),
}));
`
If you are set the totalCount: true on a prismaConnection or relatedConnection field, and aretotalCount
using a custom connection object, you will need to manually add the field to thetotalCount
connection object manually. The parent object on the connection will have a property
that is either a 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 usingbuilder.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
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.
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
const PostObject = builder.objectRef
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 callingbuilder.objectRef(...).implement(...)
. This prevents typescript from getting tripped up by thefindUniqueOrThrow
circular references between posts and users.
2. We use because those fields are not nullable. Using findUnique, prisma willUserObject
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 and PostObject, this is because User andPost
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
// We add the author here in the objectRef
const PostObject = builder.objectRef
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
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.
To create input types compatible with the prisma client, you can check out the
prisma-utils plugin