Cursor-Paginierung

Forward-Cursor-Paginierung (§1.2) - opake Cursor, Limit-Grenzen, Ende-der-Liste-Marker und stabile Reihenfolge.

5 Min. Lesezeit
paginationcursorlimit

Jeder Listen-Endpunkt verwendet Forward-Cursor-Paginierung. Statt Seitenzahlen (die brechen, sobald Elemente eingefügt oder gelöscht werden), liefert der Server einen opaken nextCursor, der den exakten Fortsetzungspunkt kodiert.

Request-Parameter

  • limit - maximale Anzahl zurückgegebener Elemente. Ganzzahl, 1 bis 100 (Standard 25). Werte außerhalb des Bereichs liefern 400 VALIDATION (v2-Envelope - siehe Fehler).
  • cursor - opaker String, der in der vorherigen Antwort zurückgegeben wurde. Bei der ersten Anfrage weglassen.

Antwort-Schema

{
  "data": [ /* up to `limit` items */ ],
  "nextCursor": "eyJpZCI6ImtfN2Y4YSIsIl90cyI6MTcyMzAwMDAwMH0",
  "hasMore": true
}
  • nextCursor - unverändert an die nächste Anfrage übergeben. Nicht parsen, dekodieren oder verändern. Es handelt sich um base64url eines serverinternen Zustands, dessen Format sich ändern kann.
  • hasMore - false signalisiert das Ende der Liste; nextCursor ist dann null.

Reihenfolge

Alle paginierten Endpunkte sortieren nach Erstellungszeit (neueste zuerst) und brechen Gleichstände anhand der Dokument-ID. Während der Iteration eingefügte Elemente erscheinen am Anfang und werden vom laufenden Scan nicht erfasst. Löschungen werden stillschweigend übersprungen.

Vollständiges Beispiel

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

Ende-der-Liste-Marker

Der Scan endet, sobald hasMore === false ist. Ein leeres data-Array mit hasMore: false bedeutet, dass die Sammlung leer ist (oder alle verbleibenden Elemente herausgefiltert wurden). Schleifen Sie niemals nur bis nextCursor === null, ohne das Flag hasMore zu prüfen.

Anti-Patterns

  • Dekodieren Sie nicht die Cursor, um IDs zu extrahieren - verwenden Sie GET /:id, wenn Sie einen bestimmten Datensatz benötigen.
  • Cachen Sie nicht Cursor prozess- oder API-Key-widerrufungsübergreifend.
  • Verändern Sie nicht den Cursor-String; geben Sie ihn unverändert weiter.

Verwandt