docs

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:

RecursoFiltros extras
/v1/customersemail
/v1/payment_intentscustomer
/v1/refundspayment_intent
/v1/payment_methodscustomer (obrigatório), type
/v1/productsstatus
/v1/customer_subscriptionscustomer, status
/v1/eventstype
/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: 100 sempre 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.all na mesma listagem — cursor é sequencial; pra paralelizar, fatie por filtro nativo.
  • Stream em vez de coletarfor await item-a-item; carregar tudo num array explode memória em datasets grandes.
Esta página foi útil?

Atualizado em

Nesta página