Paginação
Cursor pagination da Zhex — autoPagingEach() no SDK ou async generator em fetch puro. Sem offset, sem skip de registros sob escrita concorrente.
A Zhex usa cursor pagination (starting_after, não offset). Listagens retornam:
{
"object": "list",
"data": [...],
"has_more": true,
"url": "/v1/payment_intents"
}Você itera até has_more: false. O SDK abstrai isso com autoPagingEach(); em fetch puro, vira um async generator equivalente.
Caminho 1 — SDK
import { zhex } from '@/lib/zhex';
for await (const intent of zhex.paymentIntents.list({ limit: 100 }).autoPagingEach()) {
await processar(intent);
}autoPagingEach() é lazy: cada iteração consome um item, e o SDK busca a próxima página debaixo automaticamente. limit: 100 é o máximo por página e reduz round-trips. Se você dar break, ele para — sem buscar páginas a mais.
Listagens com filtro nativo (cada recurso suporta um subset):
// payment_intents — filtra por customer
for await (const intent of zhex.paymentIntents.list({ customer: 'cus_…', limit: 100 }).autoPagingEach()) {
/* … */
}
// customers — filtra por email
for await (const c of zhex.customers.list({ email: 'jane@acme.com', limit: 100 }).autoPagingEach()) {
/* … */
}
// refunds — filtra por payment_intent
for await (const r of zhex.refunds.list({ payment_intent: 'pi_…', limit: 100 }).autoPagingEach()) {
/* … */
}Caminho 2 — Helper fetch
Quando você não quer SDK ou está fora de Node:
import { zhexFetch } from '@/lib/zhex-fetch'; // do guia de Setup
type ListResponse<T> = { data: T[]; has_more: boolean; url: string };
export async function* paginate<T extends { id: string }>(
path: string,
query: Record<string, string | number | boolean | undefined> = {},
): AsyncGenerator<T> {
let starting_after: string | undefined;
while (true) {
const page = await zhexFetch<ListResponse<T>>(path, {
query: { ...query, limit: 100, starting_after },
});
for (const item of page.data) yield item;
if (!page.has_more) break;
starting_after = page.data[page.data.length - 1].id;
}
}Uso
type PaymentIntent = { id: string; amount: number; status: string; created: number };
for await (const intent of paginate<PaymentIntent>('/v1/payment_intents', {
customer: 'cus_…',
})) {
await processar(intent);
}Versão sem helper (vanilla fetch)
Se você prefere não criar abstração:
async function listAll() {
let starting_after: string | undefined;
do {
const url = new URL('https://prometheus.zhex.io/v1/payment_intents');
url.searchParams.set('limit', '100');
if (starting_after) url.searchParams.set('starting_after', starting_after);
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.ZHEX_SECRET_KEY}` },
});
const page = await res.json();
for (const intent of page.data) {
await processar(intent);
}
if (!page.has_more) break;
starting_after = page.data[page.data.length - 1].id;
} while (true);
}Stream item-a-item evita carregar todos numa array gigante.
Filtros disponíveis por recurso
Cada list endpoint aceita starting_after, ending_before e limit (1–100, default 10). Filtros adicionais:
| Recurso | Filtros extras |
|---|---|
/v1/customers | email |
/v1/payment_intents | customer |
/v1/refunds | payment_intent |
/v1/payment_methods | customer (obrigatório), type |
/v1/products | status |
/v1/customer_subscriptions | customer, status |
/v1/events | type |
/v1/webhook_endpoints | — |
Casos típicos
Backfill em job cron
const lastId = (await db.kv.get('zhex:lastPaymentIntent')) ?? undefined;
let processed = 0;
const max = 5_000; // limite por execução
let lastSeen: string | undefined = lastId;
for await (const intent of zhex.paymentIntents.list({
...(lastId && { starting_after: lastId }),
limit: 100,
}).autoPagingEach()) {
await processar(intent);
lastSeen = intent.id;
if (++processed >= max) break;
}
if (lastSeen) await db.kv.set('zhex:lastPaymentIntent', lastSeen);Job idempotente: roda a cada N minutos, processa até max itens, persiste cursor.
Export para CSV
import { createWriteStream } from 'node:fs';
const csv = createWriteStream('export.csv');
csv.write('id,amount,currency,status,created\n');
for await (const intent of zhex.paymentIntents.list({ limit: 100 }).autoPagingEach()) {
csv.write(`${intent.id},${intent.amount},${intent.currency},${intent.status},${intent.created}\n`);
}
csv.end();Stream pro disco em vez de buffer em memória.
Paralelizar por filtro
Cursor é sequencial — não dá Promise.all na mesma listagem. Pra paralelizar, fatie por filtro nativo (ex.: por customer em jobs por cliente):
const customerIds = await db.customers.findMany({ select: { zhexId: true } });
await Promise.all(
customerIds.map(async ({ zhexId }) => {
for await (const intent of zhex.paymentIntents.list({
customer: zhexId,
limit: 100,
}).autoPagingEach()) {
await processar(intent);
}
}),
);Cada worker pagina dentro de um customer — sem overlap, throughput linear no número de clientes.
Boas práticas
limit: 100sempre que você não tem motivo pra menos.- Filtros antes de iterar — corte o universo no parâmetro de query, não em código.
- Persistir cursor em jobs longos (
db.kv.set('zhex:lastId', id)) pra retomar de onde parou em caso de crash. - Não
Promise.allna mesma listagem — cursor é sequencial; pra paralelizar, fatie por filtro nativo. - Stream em vez de coletar —
for awaititem-a-item; carregar tudo num array explode memória em datasets grandes.
Atualizado em