Prisma
Build the where fragment and skip/take args for offset and keyset (cursor)
pagination. The adapter is pure data — it carries no @prisma/client
dependency, so it works with any Prisma version. Cursors are byte-compatible with
the Python and Rust adapters; see cross-language parity.
Install
npm i @cyblow/paginate
# @prisma/client is your own dependency
The adapter is exposed as a namespace from the main package:
import { prisma, OffsetParams, CursorParams, encodeCursor, decodeCursor } from "@cyblow/paginate";
Public API
// { skip, take } for prisma.model.findMany offset pagination
prisma.offsetArgs(params: OffsetParams): { skip: number; take: number };
// A `where` fragment selecting rows strictly after `cursorValues` along `keys`
prisma.keysetWhere(
keys: readonly { field: string; direction?: "asc" | "desc" }[],
cursorValues: readonly (string | number | boolean | null)[],
opts?: { backwards?: boolean },
): Record<string, unknown>;
Offset pagination
const params = new OffsetParams({ page: 2, limit: 20 });
const [items, total] = await Promise.all([
db.user.findMany({ where: { status: "active" }, ...prisma.offsetArgs(params) }),
db.user.count({ where: { status: "active" } }),
]);
offsetArgs returns { skip: params.offset, take: params.limit }. Pair the count
with the package's offsetMeta(page, limit, total) helper if you want
pages / hasNext metadata (see the architecture
overview).
Keyset (cursor) pagination
keysetWhere renders the core's portable keyset predicate as a nested boolean
where object. For keys: [{ field: "score", direction: "desc" }, { field: "id", direction: "desc" }] after [42, 100] it produces:
{ "OR": [ { "score": { "lt": 42 } },
{ "AND": [ { "score": 42 }, { "id": { "lt": 100 } } ] } ] }
A full forward page, over-fetching limit + 1 to detect a next page:
const keys = [
{ field: "score", direction: "desc" },
{ field: "id", direction: "desc" },
] as const;
async function page(params: CursorParams) {
const token = params.after ?? params.before;
const backwards = params.before !== null;
const cursorWhere = token
? prisma.keysetWhere(keys, decodeCursor(token), { backwards })
: undefined;
const rows = await db.post.findMany({
where: cursorWhere ? { AND: [{ published: true }, cursorWhere] } : { published: true },
orderBy: keys.map((k) => ({ [k.field]: k.direction })),
take: params.limit + 1,
});
const hasNext = rows.length > params.limit;
const items = hasNext ? rows.slice(0, params.limit) : rows;
const last = items.at(-1);
const nextCursor = hasNext && last ? encodeCursor([last.score, last.id]) : null;
return { items, nextCursor };
}
Combine keysetWhere with your own filters via { AND: [yourWhere, keysetWhere(...)] }
as shown. Pass { backwards: true } for a before cursor — it flips each key's
strict comparison; flip your orderBy and reverse the returned rows to keep
display order stable.