Connections
@pothos/plugin-prisma-next ships two connection helpers and a public
composable for advanced cases. All three share the same cursor-pagination
machinery and the same totalCount plumbing.
t.prismaConnection
A top-level Relay connection. Define one on Query / Mutation / any non-prisma object:
builder.queryType({
fields: (t) => ({
users: t.prismaConnection({
type: 'User',
cursor: 'id',
resolve: (_root, _args, ctx) => ctx.db.orm.User,
}),
}),
});The resolver returns the base Collection. The plugin:
- Auto-applies the selection set (descending through
edges.nodeandnodesso nested relations preload). - Applies cursor pagination (
.where(cursor predicate).orderBy(cursor).take(limit + 1)). - Materializes via
.all(). - Optionally runs a parallel
totalCountaggregate (see below). - Builds the Relay connection page (
edges/pageInfo/ optionaltotalCount).
Cursor
cursor: 'id' uses a single column as the cursor. For lexicographic
compound cursors, pass a tuple:
cursor: ['createdAt', 'id'] as const,Both columns ship into every row's SELECT automatically.
Options
| option | type | description |
|---|---|---|
type | model name or ref | the connection's node type |
cursor | string or tuple | column(s) used for cursor pagination |
args | input args | extra args for filtering / sorting |
defaultSize | number | (args, ctx) => number | default first: value |
maxSize | number | (args, ctx) => number | hard cap on page size |
totalCount | boolean | (parent, args, ctx, info) => number | enable totalCount (see below) |
resolve | (parent, args, ctx, info) => Collection | return the base collection |
defaultSize / maxSize fall back to the plugin-wide
defaultConnectionSize / maxConnectionSize options if not set.
totalCount
Three modes:
totalCount: false(default) — no totalCount field.totalCount: true— adds atotalCount: Int!field on the connection type. The plugin runs an aggregatecount()against the user-returned collection (post-filter, pre-pagination), in parallel with the rows fetch.totalCount: (parent, args, ctx, info) => number— adds the field; the user provides the count. Only invoked when the client actually selectstotalCount, so you don't pay for a count the client didn't ask for.
t.relatedConnection
A connection nested on a prisma object — pages a to-many relation
without making a separate orm-client call. Useful for queries like
user.postsConnection(first: 10).
builder.prismaObject('User', {
fields: (t) => ({
id: t.exposeID('id'),
postsConnection: t.relatedConnection('posts', {
cursor: 'id',
where: { published: 1 },
totalCount: true,
}),
}),
});The pagination happens INSIDE the .include('posts', cb => …) callback
on the parent collection — so the parent and the page rows ship as one
orm-client plan whenever prisma-next's planner can collapse it. (At
time of writing, the use of .combine for totalCount forces a
multi-query fallback; tracked upstream.)
Options
Same as t.prismaConnection, with one addition:
where: filter applied to the relation before cursor pagination. Accepts a literalShorthandWhereFilteror a callback(accessor, args, ctx) => predicate.
The where is filter-only at the type level — take/skip/orderBy
are owned by cursor pagination and rejected as options.
prismaConnectionHelpers
For custom connection fields that don't fit t.prismaConnection /
t.relatedConnection (e.g. a connection that wraps the rows in
domain-specific metadata), use prismaConnectionHelpers:
import { prismaConnectionHelpers } from '@pothos/plugin-prisma-next';
const userPostsHelpers = prismaConnectionHelpers(builder, 'Post', {
cursor: 'id',
// `args` accepts the callback form so `t.arg` is in scope.
args: (t) => ({
onlyPublished: t.boolean(),
}),
totalCount: true,
where: (p, args) => (args.onlyPublished ? p.published.eq(1) : undefined),
});
builder.prismaObject('User', {
fields: (t) => ({
posts: t.field({
type: someCustomConnectionType,
args: userPostsHelpers.getArgs(),
resolve: async (user, args, ctx, info) => {
const { collection, totalCountPromise, wrap } =
userPostsHelpers.applyPagination(
ctx.db.orm.Post.where((p) => p.authorId.eq(user.id)),
args,
info,
ctx,
);
const rows = await collection.all();
return wrap(rows, await totalCountPromise);
},
}),
}),
});The returned object exposes:
ref: the cachedPrismaNextObjectReffor the model.getArgs(): the connection args (idempotent — call once per field).applyPagination(coll, args, info, ctx): applies cursor pagination- auto-include and returns
{ collection, totalCountPromise, wrap }.
- auto-include and returns
connectionOptions(options): injects the synthetictotalCountfield onto the connection type's options.
Pass info to applyPagination whenever possible — without it, the
auto-include mapper can't descend into edges.node and nested
t.relation fields under the connection won't preload.
Mapping rows on the edge
Pass resolveNode to transform each row into a different shape (e.g.
to expose a wrapper object with extra metadata as the connection's
node):
prismaConnectionHelpers(builder, 'Post', {
cursor: 'id',
resolveNode: (row) => ({ post: row, decoratedAt: new Date() }),
});resolveNode runs AFTER buildConnectionPage builds the cursors, so
each edge's cursor is encoded from the original row columns — the
transform doesn't change cursor semantics.