@chrono-os/pagination
v0.1.1
Published
Paginação offset genérica para services Chrono: normaliza page/limit, roda findMany+count em paralelo e calcula a meta (total, totalPages, hasNext, hasPrev). Zero dependências de runtime.
Maintainers
Readme
@chrono-os/pagination
Paginação offset genérica para services Chrono: normaliza page/limit vindos de query string, roda findMany+count em paralelo e calcula a meta (total, totalPages, hasNext, hasPrev). Zero dependências de runtime (Node ≥ 18.17).
Extraído do Plano 3B.10, a partir da matemática duplicada em ≥12 services (skip=(page-1)*limit + Promise.all([findMany, count])), com nairio-os-api/src/common/dto/pagination.dto.ts como ponto de partida.
Install
yarn add @chrono-os/paginationUso
import { normalizarPaginacao, paginar, metaPaginacao, formatarEnvelopeMeta } from '@chrono-os/pagination'
// 1. Saneia o que veio da query (?page=&limit=)
const { page, limit } = normalizarPaginacao(req.query)
// 2. Roda findMany+count em paralelo — passe FUNÇÕES, não o client Prisma direto
const { data, total } = await paginar(
(skip, take) => prisma.user.findMany({ where, orderBy, skip, take }),
() => prisma.user.count({ where }),
{ page, limit },
)
// 3. Monta a meta e o envelope que o SEU frontend espera
const meta = metaPaginacao(total, page, limit)
return formatarEnvelopeMeta(data, meta) // { data, meta: {...} }Se o service já recebe skip/take por fora (padrão "repositório"), use só o cálculo:
import { normalizarPaginacao, paraSkipTake } from '@chrono-os/pagination'
const params = normalizarPaginacao({ page: dto.page, limit: dto.limit })
const { skip, take } = paraSkipTake(params)
const { contacts, total } = await this.repository.findByOrg(orgId, search, skip, take)API
normalizarPaginacao(input: { page?: unknown; limit?: unknown }, opts?: { padrao?: number; maximo?: number }): { page: number; limit: number }Saneia ausente/NaN/string inválida/negativo/zero/float parapage=1elimit=opts.padrao(default 20); saturalimitemopts.maximo(default 100).paraSkipTake(params: { page: number; limit: number }): { skip: number; take: number }paginar<T>(findMany: (skip, take) => Promise<T[]>, count: () => Promise<number>, params: { page, limit }): Promise<{ data: T[]; total: number; page: number; limit: number }>Genérico e agnóstico de ORM — recebe funções, roda em paralelo viaPromise.all.metaPaginacao(total: number, page: number, limit: number): { page; limit; total; totalPages; hasNext; hasPrev }- Formatadores opcionais de envelope (ver "Origem dos envelopes" abaixo):
formatarEnvelopeMeta,formatarEnvelopePagination,formatarEnvelopeFlat.
totalPages com total=0
totalPages = Math.ceil(total / limit), sem clamp para 1. Só a fonte original (nairio-os-api) usava Math.max(1, Math.ceil(...)); as demais ≥12 duplicatas fazem Math.ceil puro. Mantive o padrão majoritário porque o clamp faria hasNext/hasPrev mentirem numa lista vazia (diria "página 1 de 1" quando não há página nenhuma). Se seu consumer depende do clamp em 1, aplique na borda do seu app.
Origem dos envelopes
Em produção há pelo menos 3 contratos de envelope vivos, cada um consumido por um frontend específico — o pacote não força um formato único:
| Formatador | Forma | Onde foi observado |
|---|---|---|
| formatarEnvelopeMeta | { data, meta: {...} } | nairio-os-api (todos os services), chrono-publisher-api (users/templates/content), publisher-nairio (content/templates/users) |
| formatarEnvelopePagination | { data, pagination: {...} } | nairio-members-backend/comments.service.ts, chat-bullq-api (conversations/notifications) |
| formatarEnvelopeFlat | { data, page, limit, total, totalPages, ... } (sem aninhar) | publisher-nairio/analytics.service.ts (ranking()) |
Duas variações reais não viraram formatador porque a chave do array não é data (contacts, messages, notifications, users — nome da entidade) ou porque usam total_pages em snake_case (nairio-members-backend/users.service.ts, enrollments.service.ts) ou pageSize em vez de limit. Nesses casos, monte o objeto à mão com metaPaginacao():
const meta = metaPaginacao(total, page, limit)
return { contacts: data, pagination: meta }Não existe evidência real de um envelope { rows, ... } no parque hoje (checado em Naírio, Chrono, Cris, Módulos) — não foi fabricado um formatador para isso.
Versionamento
Keep a Changelog + SemVer. Pacote em 0.x: minor pode trazer mudança incompatível até a 1.0.0 (ver CHANGELOG).
