Objects
Defining GraphQL types backed by contract models
builder.prismaObject defines a GraphQL object type for a model in your contract.
Define a type
Pass the model name and the usual object type options. Column types come from the contract.
builder.prismaObject('User', {
fields: (t) => ({
id: t.exposeID('id'),
email: t.exposeString('email'),
}),
});The GraphQL type is named after the model. Pass name to call it something else.
To add fields to a type elsewhere, use prismaObjectFields. To expose the same
model as another GraphQL type, define a variant or a
prismaInterface.
To return these objects from a query field, use t.prismaField; see
Queries and selections.
Selections
A field that reads columns it doesn't expose, or data from a relation, declares them with select.
The plugin then loads them along with the rest of the query.
builder.prismaObject('User', {
// Loaded whenever a User is queried
select: { firstName: true, lastName: true },
fields: (t) => ({
fullName: t.string({
resolve: (user) => `${user.firstName} ${user.lastName}`,
}),
// Loaded only when emailDomain is queried
emailDomain: t.string({
select: ['email'],
resolve: (user) => user.email.split('@')[1] ?? '',
}),
publishedPostCount: t.int({
select: {
posts: (posts) => ({ publishedCount: posts.where({ published: 1 }).count() }),
},
resolve: (user) => user.publishedCount,
}),
}),
});A select on the type is loaded whenever the type is queried, and every field can read it. A
select on a field is loaded only when that field is queried. The primary key is always loaded.
A select accepts:
- Columns:
{ email: true }or['email']. - Relations:
{ posts: true }, which loads the relation's scalar columns. - Filtered or sorted relations:
{ posts: { where: { published: 1 }, orderBy: { createdAt: 'desc' }, limit: 5 } }.wherealso takes the ORM's predicate callback, andorderByits selectors, as int.relation'squery. - A
selectof the relation's rows, in the same form:{ comments: { limit: 5, select: { post: true } } }. type, a ref or model name of a variant of the relation's target, which loads only that variant's rows:{ tasks: { type: Bug } }.- In a field's
selectonly, counts and aggregates of a to-many relation:{ posts: (posts) => ({ postCount: posts.count() }) }. Each key (postCount) is added to the row, so don't reuse a column or relation name.
A to-one relation takes only type and select. Read aggregates in a resolver; t.expose* only
works on columns.
The type-level selects of one model, including its variants and
interfaces, must load each relation they name the same way, at any depth, or the
schema build throws. A where or orderBy function only matches itself, so share it between them.
Load another version of a relation with t.relation or a field's select.
To filter a to-one relation, select it and return null from the resolver. Here a comment shows its
post's title only when the post is published:
builder.prismaObject('Comment', {
fields: (t) => ({
postTitle: t.string({
nullable: true,
select: { post: true },
resolve: (comment) => (comment.post.published ? comment.post.title : null),
}),
}),
});Selecting per request
A field's select can also be a function of its arguments and context. Here the field selects
lastName only when the client asks for the full name. A column that only some branches select is
optional on the parent:
name: t.string({
args: { full: t.arg.boolean() },
select: (args) => (args.full ? { firstName: true, lastName: true } : { firstName: true }),
resolve: (user, args) =>
args.full && user.lastName ? `${user.firstName} ${user.lastName}` : user.firstName,
}),With AsyncSelections: true in the builder's types, the function can be async.
Its third argument, nestedSelection, includes the dependencies and relations requested beneath a
custom field returning objects. Without an explicit select, it loads scalar columns alongside
those GraphQL requirements. An explicit select can start from a narrower set. See
Indirect relations for examples.
select, query and where callbacks run while planning or building a query, rather than for
each returned row. They can run more than once for reloads or aggregate planning, so keep them
free of side effects.
Resolver data
A resolver can read the model's primary or non-null unique key, the type's shared select, and
its own field's select. Other columns aren't guaranteed to be loaded.
Rows the plugin loads carry a type-only marker, such as PrismaPlanned<'User'>. For returning rows
from custom resolvers, see
Returning rows from other fields.
Auth checks
Auth checks on the type, and other plugins' field options such as authScopes and grantScopes,
see:
- the primary key
- the columns and relations in the
selects of the type and its interfaces - on a
t.expose*field, the exposed column
Only resolve can read what a field's own select loads. If a row is missing any of this, such as
a row of a variant model with its own table, the plugin reloads it by its key before the check.
Mutations
Use resolve for a field that writes data. It receives the model's Collection prepared for the
GraphQL query. Filter it to the row you want to change, then call Prisma's write method:
builder.mutationType({
fields: (t) => ({
renameUser: t.prismaField({
type: 'User',
nullable: true,
args: {
id: t.arg.id({ required: true }),
firstName: t.arg.string({ required: true }),
},
resolve: (users, _root, args) =>
users.where({ id: String(args.id) }).update({ firstName: args.firstName }),
}),
}),
});The updated row becomes the mutation result, with the fields requested by the client.
Return a payload
For a result shaped like { user: ... }, use collectionFromInfo to load the fields requested on
user. The path: ['user'] option tells it where that object appears in the result:
import { collectionFromInfo } from '@pothos/plugin-prisma-next';
const RenameUserPayload = builder
.objectRef<{ user: typeof User.$inferType | null }>('RenameUserPayload')
.implement({
fields: (t) => ({ user: t.expose('user', { type: User, nullable: true }) }),
});
builder.mutationField('renameUser', (t) =>
t.field({
type: RenameUserPayload,
args: { id: t.arg.id({ required: true }), firstName: t.arg.string({ required: true }) },
resolve: async (_root, args, ctx, info) => {
const users = await collectionFromInfo({
collection: db.orm.User.where({ id: String(args.id) }),
context: ctx,
info,
path: ['user'],
});
return { user: await users.update({ firstName: args.firstName }) };
},
}),
);If the payload field is an interface or union, pass typeName to choose the GraphQL object type
to load. For mutations on inherited models, see Model inheritance.
An exported function that returns rows from collectionFromInfo needs a return type, such as
Promise<PrismaPlanned<'User'> | null>, for TypeScript to emit its declaration.
See Transactions to run the write and the reads in one transaction.
prismaFieldWithInput
With the with-input plugin, t.prismaFieldWithInput combines
t.prismaField with t.fieldWithInput, and takes collection, query or resolve the same way.
The input fields become an input object argument:
builder.mutationField('updateFirstName', (t) =>
t.prismaFieldWithInput({
type: 'User',
nullable: true,
input: {
id: t.input.id({ required: true }),
firstName: t.input.string({ required: true }),
},
resolve: (users, _root, args) =>
users.where({ id: String(args.input.id) }).update({ firstName: args.input.firstName }),
}),
);Adding fields
To add fields to a type from another file, use prismaObjectField or prismaObjectFields.
builder.objectField won't work here because it doesn't support select.
builder.prismaObjectField('User', 'displayName', (t) =>
t.string({
select: ['firstName', 'lastName'],
resolve: (user) => `${user.firstName} ${user.lastName}`,
}),
);If the resolver reads the type's select, pass the ref that prismaObject returns instead of the
model name. Passing a variant's ref adds fields to that variant.
const User = builder.prismaObject('User', {
select: { firstName: true, lastName: true },
fields: (t) => ({ id: t.exposeID('id') }),
});
builder.prismaObjectField(User, 'initials', (t) =>
t.string({
resolve: (user) => `${user.firstName[0]}${user.lastName[0]}`,
}),
);