Paginazione a cursore

Paginazione a cursore in avanti (§1.2) - cursori opachi, limiti di limit, marcatore di fine lista e ordinamento stabile.

5 min letto
paginationcursorlimit

Ogni endpoint di lista usa la paginazione a cursore in avanti. Invece dei numeri di pagina (che si rompono quando gli elementi vengono inseriti o eliminati), il server restituisce un nextCursor opaco che codifica il punto esatto di continuazione.

Parametri della richiesta

  • limit - numero massimo di elementi da restituire. Intero, da 1 a 100 (default 25). I valori fuori dall’intervallo restituiscono 400 VALIDATION (envelope v2 - vedi errors).
  • cursor - stringa opaca restituita dalla risposta precedente. Omettila alla prima richiesta.

Forma della risposta

{
  "data": [ /* up to `limit` items */ ],
  "nextCursor": "eyJpZCI6ImtfN2Y4YSIsIl90cyI6MTcyMzAwMDAwMH0",
  "hasMore": true
}
  • nextCursor - passalo invariato alla richiesta successiva. Non fare il parsing, non decodificarlo, non modificarlo. È base64url di uno stato interno al server e il formato può cambiare.
  • hasMore - false segnala la fine della lista; nextCursor sarà null.

Ordinamento

Tutti gli endpoint paginati ordinano per orario di creazione (dal più recente) e risolvono i pareggi per ID di documento. Gli elementi inseriti durante l’iterazione appaiono in testa e non vengono osservati da uno scan in corso. Le eliminazioni vengono saltate silenziosamente.

Esempio completo

curl

# Page 1
curl "https://<your-deployment>.convex.site/api/v1/runs?limit=50" \
  -H "Authorization: Bearer cc_live_..."

# Page 2 - use the cursor from page 1
curl "https://<your-deployment>.convex.site/api/v1/runs?limit=50&cursor=eyJpZCI6..." \
  -H "Authorization: Bearer cc_live_..."

TypeScript

async function* paginate<T>(path: string, key: string) {
  let cursor: string | null = null;
  do {
    const url = new URL(`https://<your-deployment>.convex.site${path}`);
    url.searchParams.set("limit", "100");
    if (cursor) url.searchParams.set("cursor", cursor);

    const res = await fetch(url, {
      headers: { "Authorization": `Bearer ${key}` },
    });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);

    const body = await res.json() as {
      data: T[];
      nextCursor: string | null;
      hasMore: boolean;
    };

    for (const item of body.data) yield item;
    cursor = body.hasMore ? body.nextCursor : null;
  } while (cursor);
}

// Usage
for await (const run of paginate<{ id: string }>("/api/v1/runs", key)) {
  console.log(run.id);
}

Python

import os, requests

def paginate(path: str, key: str, limit: int = 100):
    cursor = None
    base = "https://<your-deployment>.convex.site"
    while True:
        params = {"limit": limit}
        if cursor:
            params["cursor"] = cursor
        r = requests.get(
            f"{base}{path}",
            headers={"Authorization": f"Bearer {key}"},
            params=params,
            timeout=30,
        )
        r.raise_for_status()
        body = r.json()
        for item in body["data"]:
            yield item
        if not body.get("hasMore"):
            return
        cursor = body["nextCursor"]

for run in paginate("/api/v1/runs", os.environ["CC_KEY"]):
    print(run["id"])

Marcatore di fine lista

Lo scan termina quando hasMore === false. Un array data vuoto con hasMore: false significa che la collezione è vuota (o che tutti gli elementi rimanenti sono stati filtrati). Non ciclare mai fino a nextCursor === null senza controllare il flag hasMore.

Anti-pattern

  • Non decodificare i cursori per estrarre gli ID - usa GET /:id se ti serve un record specifico.
  • Non mettere in cache i cursori tra processi o attraverso le revoche delle chiavi API.
  • Non modificare la stringa del cursore; passala verbatim.

Correlati