Paginação tipada¶
Problema: listar tudo de uma vez não escala. Você quer páginas — com o total de
itens e o número de páginas — e quer isso tipado, sem montar LIMIT/OFFSET e
contagens na mão toda vez.
Solução: o BaseRepository<Model> já traz paginate(...). O orderBy é uma coluna
tipada do modelo, e o resultado tem a mesma forma do BasePaginationSchema<T> do
tempest-fastapi-sdk — então o payload é idêntico entre um backend Python e um TS.
A teoria em uma frase¶
paginate faz duas queries: um COUNT(*) (com os mesmos filtros) pro total, e um
SELECT ... LIMIT pageSize OFFSET (page-1)*pageSize pros itens da página. Devolve os
dois juntos, mais os metadados que o front precisa.
Setup¶
import { Model, column, BaseRepository, createEngine } from "tempest-db-js";
class Product extends Model {
static tablename = "products";
id = column.integer().primaryKey();
name = column.text().notNull();
price = column.numeric(10, 2).notNull(); // → string (decimal exato)
active = column.boolean().notNull();
}
const engine = createEngine("sqlite:///shop.db");
const products = new BaseRepository(Product, engine.session());
Paginar¶
const page = await products.paginate({
page: 1,
pageSize: 20,
orderBy: "price", // coluna tipada — "prices" não compila
ascending: false, // mais caro primeiro
filters: { active: true, price: { gte: "10.00" } },
});
// page === {
// items: ProductRow[], // os 20 da página
// total: number, // total que casa com o filtro
// page: 1,
// pageSize: 20,
// pages: number, // Math.ceil(total / pageSize)
// }
orderBy é checado em tempo de compilação
// ❌ erro: "prices" não é coluna de Product
await products.paginate({ page: 1, pageSize: 20, orderBy: "prices", ascending: true });
Um erro de digitação no campo de ordenação vira erro de compilação, não um
500 em produção.
Servindo numa rota HTTP¶
Como o PaginationResult é JSON-safe na estrutura, dá pra devolver direto:
// pseudo-rota (veja o exemplo REST API pra um app completo)
async function listProducts(query: { page?: number; size?: number }) {
return products.paginate({
page: query.page ?? 1,
pageSize: query.size ?? 20,
orderBy: "name",
ascending: true,
filters: { active: true },
});
}
Mesma forma do SDK Python
PaginationFilter espelha o BasePaginationFilterSchema e PaginationResult o
BasePaginationSchema<T> do tempest-fastapi-sdk.
Um cliente que já consome um backend Python paginado não precisa mudar nada pra
consumir um backend TS feito com tempest-db-js.
Paginação por cursor¶
paginate usa LIMIT/OFFSET e um COUNT(*). Em tabela grande isso cobra caro (o
banco varre e descarta o offset) e desloca a fronteira: uma linha inserida enquanto
o usuário lê empurra um registro da página 2 para a 3, e ele aparece duas vezes.
cursorPaginate troca acesso aleatório por estabilidade:
let cursor: string | null = null;
do {
const page = await orders.cursorPaginate({
cursor,
limit: 50,
orderBy: "createdAt",
ascending: false,
filters: { status: "open" },
});
handle(page.items);
cursor = page.nextCursor; // null na última página
} while (cursor !== null);
paginate |
cursorPaginate |
|
|---|---|---|
| Navegação | qualquer página (page: 7) |
só a próxima |
COUNT(*) |
sim, por página | não |
| Sob insert concorrente | pode repetir/pular | estável |
| Metadados | total, pages |
nextCursor |
A chave primária entra sempre como desempate
Ordenar só por createdAt com linhas empatadas no mesmo instante faz a fronteira
da página cair no meio do empate — e aí uma linha sai da paginação sem nunca ter
sido lida. A PK é anexada como segunda chave de ordenação, então o par
(orderBy, chave) é único e a comparação é total. Chave composta entra inteira.
O cursor é opaco — e validado
É base64 do último par de ordenação da página, não um offset disfarçado. Cursor
corrompido, de outra versão, ou gerado com outro orderBy, lança
InvalidCursor em vez de gerar um WHERE silenciosamente errado:
await orders.cursorPaginate({ cursor: byCreatedAt.nextCursor, orderBy: "total" });
// InvalidCursor: it does not carry "total" — the ordering changed between pages
Não guarde cursor entre deploys que mudem a ordenação padrão da rota.
Recap¶
repository.paginate({ page, pageSize, orderBy, ascending, filters }).orderByé uma coluna tipada do modelo — typo = erro de compilação.- Resultado:
{ items, total, page, pageSize, pages }. - Estrutura idêntica ao
BasePaginationSchema<T>dotempest-fastapi-sdk. cursorPaginatepara tabela grande: semCOUNT(*), fronteira estável, cursor opaco.