Pagination par curseur

Pagination par curseur en avant (§1.2) - curseurs opaques, bornes de limit, marqueur de fin de liste et ordre stable.

5 min lire
paginationcursorlimit

Chaque endpoint de liste utilise la pagination par curseur en avant. Au lieu de numéros de page (qui cassent lorsque des éléments sont insérés ou supprimés), le serveur renvoie un nextCursor opaque qui encode le point de continuation exact.

Paramètres de requête

  • limit - nombre maximal d’éléments à renvoyer. Entier, 1 à 100 (défaut 25). Les valeurs hors de la plage renvoient 400 VALIDATION (enveloppe v2 - voir errors).
  • cursor - chaîne opaque renvoyée par la réponse précédente. Omettez-la à la première requête.

Forme de la réponse

{
  "data": [ /* up to `limit` items */ ],
  "nextCursor": "eyJpZCI6ImtfN2Y4YSIsIl90cyI6MTcyMzAwMDAwMH0",
  "hasMore": true
}
  • nextCursor - passez-le tel quel à la requête suivante. Ne le parsez pas, ne le décodez pas, ne le modifiez pas. C’est du base64url d’un état interne au serveur et le format peut changer.
  • hasMore - false signale la fin de la liste ; nextCursor sera null.

Ordre

Tous les endpoints paginés ordonnent par heure de création (le plus récent d’abord) et départagent les égalités par ID de document. Les éléments insérés pendant l’itération apparaissent en tête et ne sont pas observés par un scan en cours. Les suppressions sont silencieusement ignorées.

Exemple complet

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"])

Marqueur de fin de liste

Le scan se termine lorsque hasMore === false. Un tableau data vide avec hasMore: false signifie que la collection est vide (ou que tous les éléments restants ont été filtrés). Ne bouclez jamais jusqu’à nextCursor === null sans vérifier le flag hasMore.

Anti-patterns

  • Ne décodez pas les curseurs pour en extraire des IDs - utilisez GET /:id si vous avez besoin d’un enregistrement spécifique.
  • Ne mettez pas en cache les curseurs entre processus ou à travers les révocations de clé API.
  • Ne modifiez pas la chaîne du curseur ; passez-la verbatim.

Liens connexes