@generazioneai/paginator
v0.1.0
Published
Efficient, framework-agnostic Prisma paginator — offset + cursor pagination, optional COUNT, stable tie-breaker ordering, optional class-transformer mapping.
Readme
@generazioneai/paginator
Efficient, framework-agnostic Prisma paginator: offset + cursor pagination, an
optional COUNT query, stable tie-breaker ordering, and optional class-transformer
mapping. No NestJS, no project coupling. Works directly on an authz-scoped delegate, so
pagination respects row-level scoping for free.
Install
npm i @generazioneai/paginator
# optional peers (only if you use them):
npm i @prisma/client class-transformerQuick start
import { paginate } from "@generazioneai/paginator";
const res = await paginate(prisma.user, { where: { active: true }, orderBy: { name: "asc" } }, {
page,
perPage,
orderByTieBreaker: "id", // deterministic ordering across pages
});
// res = { data, meta: { total, lastPage, currentPage, perPage, prev, next, hasPrev, hasNext } }Skip the COUNT on large tables
COUNT is the expensive part. When the frontend only needs next/prev (infinite scroll),
set withTotal: false — the paginator fetches perPage + 1 rows to derive hasNext
and issues no COUNT query. total/lastPage are then omitted.
const res = await paginate(prisma.event, { where }, { page, perPage, withTotal: false });
// res.meta = { currentPage, perPage, prev, next, hasPrev, hasNext } (no total/lastPage)Cursor pagination (O(1) deep pages)
No growing OFFSET, no COUNT — the efficient choice for large datasets / infinite scroll.
let res = await paginateCursor(prisma.post, { where }, { perPage: 20 });
// next page:
res = await paginateCursor(prisma.post, { where }, { perPage: 20, cursor: res.meta.nextCursor });
// res.meta = { perPage, nextCursor, prevCursor, hasNext, hasPrev }cursorField (default "id") must be unique & sequential; it is appended to your
orderBy so ordering is deterministic.
Mapping rows
Two ways, both optional:
// class-transformer entity (requires the optional peer):
await paginate(prisma.user, args, { page, perPage, type: UserEntity });
// → plainToInstance(UserEntity, rows, {excludeExtraneousValues, enableImplicitConversion})
// then instanceToPlain(..., {exposeUnsetFields:true})
// or a custom mapper (takes precedence, no peer needed):
await paginate(prisma.user, args, { page, perPage, transform: (rows) => rows.map(toDto) });API
| Export | Purpose |
|---|---|
| paginate(model, args, options) | Offset pagination → PaginatedResult<T> |
| paginator(options)(model, args) | Legacy-compatible factory (drop-in for the old util) |
| paginateCursor(model, args, options) | Cursor pagination → CursorPaginatedResult<T> |
| getPagination(page, perPage) | { page, perPage, skip } helper |
| getPaginatedResult({ data, pagination, count }) | Paginate an in-memory array |
| buildMeta({ page, perPage, total?, hasNext? }) | Build meta directly |
| transformWithClass(cls, rows) | The class-transformer mapping, standalone |
| PaginatorTypes | Back-compat type namespace |
model contract
Any object with count(args?) and findMany(args?) — i.e. a Prisma model delegate,
including an authz-scoped one. The library never imports @prisma/client.
Options (offset)
| Option | Default | Notes |
|---|---|---|
| page | 0 | 0/undefined ⇒ return ALL rows (no pagination, no meta) |
| perPage | 10 | |
| withTotal | true | false ⇒ no COUNT query |
| orderByTieBreaker | — | field name(s) appended asc for stable paging |
| type | — | class-transformer target class |
| transform | — | custom row mapper (wins over type) |
Migrating from the old utils/paginator
Change only the import — the paginator(...) factory, PaginatorTypes,
getPagination, and getPaginatedResult keep the same shapes:
- import { paginator, PaginatorTypes } from "src/utilities/utils/paginator";
+ import { paginator, PaginatorTypes } from "@generazioneai/paginator";License
MIT
