From 0e54b806516114b6907bc29b405ca6f849bd2ece Mon Sep 17 00:00:00 2001 From: javorosas Date: Thu, 27 Aug 2026 18:48:13 +0200 Subject: [PATCH 1/4] Document cursor pagination and capped totals - Add pagination guide (ES/EN) covering pagination=page|cursor, after/before navigation, totals capped at 3,000 and totals_are_capped. - Add shared SearchPagination/SearchAfter/SearchBefore parameters to search endpoints in both OpenAPI specs. - Extend SearchResult schema with previous_cursor, next_cursor and totals_are_capped. - Add CHANGELOG entry for the opt-in cursor pagination. --- CHANGELOG.md | 16 ++ website/docs/guides/pagination.mdx | 142 ++++++++++++++++++ .../current/guides/pagination.mdx | 142 ++++++++++++++++++ website/openapi_v2.en.yaml | 117 +++++++++++++++ website/openapi_v2.yaml | 117 +++++++++++++++ 5 files changed, 534 insertions(+) create mode 100644 website/docs/guides/pagination.mdx create mode 100644 website/i18n/en/docusaurus-plugin-content-docs/current/guides/pagination.mdx diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb..708169d59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +## Unreleased + +### Paginación por cursores (`pagination=cursor`) + +- Nuevo modo de paginación opt-in `pagination=cursor`, **recomendado** para + listas grandes: no ejecuta conteos exactos sobre la colección, por lo que es + más rápido que la paginación por páginas. +- Nuevos parámetros `after` y `before` para navegar entre páginas de forma + secuencial. Cada búsqueda regresa `previous_cursor` y `next_cursor`. +- Nuevo campo `totals_are_capped` en las respuestas de búsqueda de ambos modos: + cuando hay más de 3,000 resultados, `total_results` regresa `3000` y + `totals_are_capped` es `true` ("más de 3,000"). +- La paginación por páginas (`pagination=page`, el modo por defecto) queda + limitada a 30 páginas. diff --git a/website/docs/guides/pagination.mdx b/website/docs/guides/pagination.mdx new file mode 100644 index 000000000..e69f5df6e --- /dev/null +++ b/website/docs/guides/pagination.mdx @@ -0,0 +1,142 @@ +--- +sidebar_position: 11 +--- + +# Paginación + +Todos los listados de la API devuelven resultados paginados. Facturapi ofrece dos +modos de paginación que puedes elegir con el parámetro `pagination`: + +- **`pagination=page`** — el modo clásico, por **páginas numeradas**. Es el + comportamiento por defecto cuando no se envía `pagination`. +- **`pagination=cursor`** — el modo recomendado, por **cursores**. Es más rápido + porque no ejecuta conteos exactos sobre toda la colección y está pensado para + recorrer listas grandes de forma secuencial. + +## ¿Qué modo usar? + +| | `pagination=page` | `pagination=cursor` | +| --- | --- | --- | +| Navegación | Saltar a una página específica (`page=3`) | Secuencial: siguiente / anterior | +| Total de resultados | Conteo exacto (hasta 3,000) | Informativo (conteo acotado a 3,000) | +| Rendimiento | Ejecuta un conteo por búsqueda | Más rápido, sin conteos exactos | +| Límites | Máximo 30 páginas | Sin límite de páginas | +| Uso recomendado | Compatibilidad, interfaces con saltos de página | Listas grandes, iteración, exportaciones | + +Para la mayoría de los casos, y en particular para listas grandes, te recomendamos +usar `pagination=cursor`. + +## Parámetros comunes + +| Parámetro | Tipo | Descripción | +| --- | --- | --- | +| `limit` | `integer` | Cantidad máxima de resultados a regresar, del 1 al 100. | +| `pagination` | `string` | `page` (por defecto) o `cursor`. | + +Los parámetros `page`, `after` y `before` dependen del modo elegido y son +**mutuamente excluyentes** entre modos: + +- `pagination=page` acepta `page`, pero no `after` ni `before`. +- `pagination=cursor` acepta `after` o `before`, pero no `page`. + +Enviar `after` y `before` al mismo tiempo devuelve un error. + +## Paginación por páginas (`pagination=page`) + +Es el modo por defecto. Además de `limit`, acepta: + +| Parámetro | Descripción | +| --- | --- | +| `page` | Número de página a regresar, empezando desde 1. La página máxima es 30. | + +Respuesta: + +```json +{ + "data": [ ... ], + "page": 1, + "total_pages": 3, + "total_results": 250, + "totals_are_capped": false +} +``` + +## Paginación por cursores (`pagination=cursor`) + +En lugar de un número de página, cada respuesta regresa dos cursores que te +permiten avanzar o retroceder sobre los resultados: + +| Parámetro | Descripción | +| --- | --- | +| `after` | Devuelve los resultados posteriores al cursor indicado. | +| `before` | Devuelve los resultados anteriores al cursor indicado. | + +Respuesta: + +```json +{ + "data": [ ... ], + "previous_cursor": null, + "next_cursor": "d176717f4000000.i65a8282775d4e9263da48115", + "total_results": 250, + "totals_are_capped": false +} +``` + +### Navegar entre páginas + +1. **Primera página:** se obtiene sin enviar `after` ni `before`. + `previous_cursor` regresa `null`, indicando que no hay páginas anteriores. +2. **Página siguiente:** envía `after` con el valor de `next_cursor` de la + respuesta anterior. +3. **Página anterior:** envía `before` con el valor de `previous_cursor` de la + respuesta anterior. + +La presencia de `previous_cursor` y `next_cursor` es la única fuente de verdad +para saber si hay más resultados: si `next_cursor` es `null`, llegaste al final +de la lista; si `previous_cursor` es `null`, estás en la primera página. + +```bash +# Primera página +curl "https://www.facturapi.io/v2/invoices?pagination=cursor&limit=10" \ + -G \ + -H "Authorization: Bearer sk_test_API_KEY" + +# Página siguiente (usa next_cursor de la respuesta anterior) +curl "https://www.facturapi.io/v2/invoices?pagination=cursor&limit=10&after=NEXT_CURSOR" \ + -G \ + -H "Authorization: Bearer sk_test_API_KEY" + +# Página anterior (usa previous_cursor de la respuesta anterior) +curl "https://www.facturapi.io/v2/invoices?pagination=cursor&limit=10&before=PREVIOUS_CURSOR" \ + -G \ + -H "Authorization: Bearer sk_test_API_KEY" +``` + +Los cursores son tokens opacos y específicos de la búsqueda que los generó: no +los combines con otra búsqueda ni los compartas entre distintas consultas. +Usarlos con una búsqueda diferente puede regresar resultados inesperados. + +## Totales y límite de 3,000 + +Ambos modos regresan `total_results` con el número de elementos que coinciden con +la búsqueda, **con un tope de 3,000**: + +- Si hay 3,000 o menos resultados, `total_results` es exacto y + `totals_are_capped` es `false`. +- Si hay más de 3,000, `total_results` regresa `3000` y `totals_are_capped` es + `true`, indicando que el total real es "más de 3,000". + +```json +{ + "data": [ ... ], + "previous_cursor": "d176717f4000000.i65a8282775d4e9263da48115", + "next_cursor": null, + "total_results": 3000, + "totals_are_capped": true +} +``` + +En el modo `pagination=cursor`, `total_results` es informativo: el total se +calcula con un conteo acotado a 3,000 en lugar de un conteo exacto sobre toda +la colección, por lo que es más rápida incluso con listas muy grandes. diff --git a/website/i18n/en/docusaurus-plugin-content-docs/current/guides/pagination.mdx b/website/i18n/en/docusaurus-plugin-content-docs/current/guides/pagination.mdx new file mode 100644 index 000000000..fd8ff7e09 --- /dev/null +++ b/website/i18n/en/docusaurus-plugin-content-docs/current/guides/pagination.mdx @@ -0,0 +1,142 @@ +--- +sidebar_position: 11 +--- + +# Pagination + +All API listing endpoints return paginated results. Facturapi offers two +pagination modes that you can choose with the `pagination` parameter: + +- **`pagination=page`** — the classic mode, based on **numbered pages**. This is + the default behavior when `pagination` is not sent. +- **`pagination=cursor`** — the recommended mode, based on **cursors**. It is + faster because it does not run exact counts over the whole collection, and it + is designed to traverse large lists sequentially. + +## Which mode should I use? + +| | `pagination=page` | `pagination=cursor` | +| --- | --- | --- | +| Navigation | Jump to a specific page (`page=3`) | Sequential: next / previous | +| Total results | Exact count (up to 3,000) | Informational (count capped at 3,000) | +| Performance | Runs a count per search | Faster, no exact counts | +| Limits | Up to 30 pages | No page limit | +| Recommended for | Compatibility, UIs with page jumps | Large lists, iteration, exports | + +For most cases, and especially for large lists, we recommend using +`pagination=cursor`. + +## Common parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| `limit` | `integer` | Maximum number of results to return, from 1 to 100. | +| `pagination` | `string` | `page` (default) or `cursor`. | + +The `page`, `after` and `before` parameters depend on the selected mode and are +**mutually exclusive** between modes: + +- `pagination=page` accepts `page`, but not `after` or `before`. +- `pagination=cursor` accepts `after` or `before`, but not `page`. + +Sending `after` and `before` at the same time returns an error. + +## Page pagination (`pagination=page`) + +This is the default mode. In addition to `limit`, it accepts: + +| Parameter | Description | +| --- | --- | +| `page` | Page number to return, starting from 1. The maximum page is 30. | + +Response: + +```json +{ + "data": [ ... ], + "page": 1, + "total_pages": 3, + "total_results": 250, + "totals_are_capped": false +} +``` + +## Cursor pagination (`pagination=cursor`) + +Instead of a page number, each response returns two cursors that let you move +forward or backward through the results: + +| Parameter | Description | +| --- | --- | +| `after` | Returns the results after the given cursor. | +| `before` | Returns the results before the given cursor. | + +Response: + +```json +{ + "data": [ ... ], + "previous_cursor": null, + "next_cursor": "d176717f4000000.i65a8282775d4e9263da48115", + "total_results": 250, + "totals_are_capped": false +} +``` + +### Navigating between pages + +1. **First page:** obtained by not sending `after` or `before`. + `previous_cursor` returns `null`, meaning there are no previous pages. +2. **Next page:** send `after` with the `next_cursor` value from the previous + response. +3. **Previous page:** send `before` with the `previous_cursor` value from the + previous response. + +The presence of `previous_cursor` and `next_cursor` is the only source of truth +for whether there are more results: if `next_cursor` is `null`, you reached the +end of the list; if `previous_cursor` is `null`, you are on the first page. + +```bash +# First page +curl "https://www.facturapi.io/v2/invoices?pagination=cursor&limit=10" \ + -G \ + -H "Authorization: Bearer sk_test_API_KEY" + +# Next page (use next_cursor from the previous response) +curl "https://www.facturapi.io/v2/invoices?pagination=cursor&limit=10&after=NEXT_CURSOR" \ + -G \ + -H "Authorization: Bearer sk_test_API_KEY" + +# Previous page (use previous_cursor from the previous response) +curl "https://www.facturapi.io/v2/invoices?pagination=cursor&limit=10&before=PREVIOUS_CURSOR" \ + -G \ + -H "Authorization: Bearer sk_test_API_KEY" +``` + +Cursors are opaque tokens tied to the search that generated them: do not combine +them with a different search or share them between queries. Using them with a +different search may return unexpected results. + +## Totals and the 3,000 limit + +Both modes return `total_results` with the number of elements matching the +search, **capped at 3,000**: + +- If there are 3,000 or fewer results, `total_results` is exact and + `totals_are_capped` is `false`. +- If there are more than 3,000, `total_results` returns `3000` and + `totals_are_capped` is `true`, indicating the real total is "more than 3,000". + +```json +{ + "data": [ ... ], + "previous_cursor": "d176717f4000000.i65a8282775d4e9263da48115", + "next_cursor": null, + "total_results": 3000, + "totals_are_capped": true +} +``` + +In `pagination=cursor` mode, `total_results` is informational: the total is +computed with a count capped at 3,000 instead of an exact count over the whole +collection, so it stays fast even with very large lists. diff --git a/website/openapi_v2.en.yaml b/website/openapi_v2.en.yaml index 4948857b8..661028f04 100644 --- a/website/openapi_v2.en.yaml +++ b/website/openapi_v2.en.yaml @@ -1038,6 +1038,10 @@ paths: ["limit"] = 10 }); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -1139,6 +1143,10 @@ paths: ["limit"] = 10 }); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -1222,6 +1230,10 @@ paths: ["limit"] = 10 }); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -1317,6 +1329,10 @@ paths: ["limit"] = 10 }); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -1400,6 +1416,10 @@ paths: ["limit"] = 10 }); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -1483,6 +1503,10 @@ paths: ["limit"] = 10 }); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -1566,6 +1590,10 @@ paths: ["limit"] = 10 }); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -1649,6 +1677,10 @@ paths: ["limit"] = 10 }); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -1732,6 +1764,10 @@ paths: ["limit"] = 10 }); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -1829,6 +1865,10 @@ paths: ["limit"] = 10 }); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -1912,6 +1952,10 @@ paths: ["limit"] = 10 }); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -2141,6 +2185,10 @@ paths: "page" => 1 ]); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -2771,6 +2819,10 @@ paths: "page" => 1 ]); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -3335,6 +3387,10 @@ paths: limit => 10, ]); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -4772,6 +4828,10 @@ paths: limit => 10, ]); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -6100,6 +6160,10 @@ paths: limit => 10, ]); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -6727,6 +6791,10 @@ paths: $organizations = $facturapi->Organizations->all() parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -10677,6 +10745,10 @@ paths: "q" => "ukelele" ]); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -10760,6 +10832,10 @@ paths: "q" => "pulgada" ]); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -11306,6 +11382,31 @@ components: maximum: 100 description: Number from 1 to 100 representing the maximum amount of results to return for pagination purposes. + SearchPagination: + in: query + name: pagination + schema: + type: string + enum: + - page + - cursor + description: | + Pagination mode for the search. `page` (default) or `cursor` (recommended for large lists). + SearchAfter: + in: query + name: after + schema: + type: string + description: | + Returns the results after the given cursor. Only with `pagination=cursor`; mutually exclusive with `before`. + SearchBefore: + in: query + name: before + schema: + type: string + description: | + Returns the results before the given cursor. Only with `pagination=cursor`; mutually exclusive with `after`. + schemas: RelatedResourceMessage: type: object @@ -11492,6 +11593,22 @@ components: example: 1 title: Total results description: The total number of results available in the search + previous_cursor: + type: string + example: null + title: Previous cursor + description: Cursor to fetch the previous page of results. It is `null` on the first page. Only available with `pagination=cursor`. + next_cursor: + type: string + example: null + title: Next cursor + description: Cursor to fetch the next page of results. It is `null` when there are no more results. Only available with `pagination=cursor`. + totals_are_capped: + type: boolean + example: false + title: Totals capped + description: Indicates whether `total_results` is capped at 3,000 because there are more results than reported. + ResourceAutoGeneratedProps: type: object required: diff --git a/website/openapi_v2.yaml b/website/openapi_v2.yaml index ed060b82a..43e8229c8 100644 --- a/website/openapi_v2.yaml +++ b/website/openapi_v2.yaml @@ -1009,6 +1009,10 @@ paths: ["limit"] = 10 }); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -1118,6 +1122,10 @@ paths: ["limit"] = 10 }); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -1227,6 +1235,10 @@ paths: ["limit"] = 10 }); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -1336,6 +1348,10 @@ paths: ["limit"] = 10 }); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -1445,6 +1461,10 @@ paths: ["limit"] = 10 }); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -1554,6 +1574,10 @@ paths: ["limit"] = 10 }); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -1663,6 +1687,10 @@ paths: ["limit"] = 10 }); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -1772,6 +1800,10 @@ paths: ["limit"] = 10 }); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -1881,6 +1913,10 @@ paths: ["limit"] = 10 }); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -2004,6 +2040,10 @@ paths: ["limit"] = 10 }); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -2113,6 +2153,10 @@ paths: ["limit"] = 10 }); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -2369,6 +2413,10 @@ paths: "page" => 1 ]); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -2999,6 +3047,10 @@ paths: "page" => 1 ]); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -3561,6 +3613,10 @@ paths: limit => 10, ]); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -4992,6 +5048,10 @@ paths: limit => 10, ]); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -6316,6 +6376,10 @@ paths: limit => 10, ]); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -6945,6 +7009,10 @@ paths: $organizations = $facturapi->Organizations->all() parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -10883,6 +10951,10 @@ paths: "q" => "ukelele" ]); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -10966,6 +11038,10 @@ paths: "q" => "pulgada" ]); parameters: + - $ref: "#/components/parameters/SearchPagination" + - $ref: "#/components/parameters/SearchAfter" + - $ref: "#/components/parameters/SearchBefore" + - in: query name: q schema: @@ -11512,6 +11588,31 @@ components: maximum: 100 description: Número del 1 al 100 que representa la cantidad máxima de resultados a regresar con motivos de paginación. + SearchPagination: + in: query + name: pagination + schema: + type: string + enum: + - page + - cursor + description: | + Modo de paginación de la búsqueda. `page` (por defecto) o `cursor` (recomendado para listas grandes). + SearchAfter: + in: query + name: after + schema: + type: string + description: | + Devuelve los resultados posteriores al cursor indicado. Solo con `pagination=cursor`; mutuamente excluyente con `before`. + SearchBefore: + in: query + name: before + schema: + type: string + description: | + Devuelve los resultados anteriores al cursor indicado. Solo con `pagination=cursor`; mutuamente excluyente con `after`. + schemas: SearchKeyDescriptionResult: type: object @@ -11727,6 +11828,22 @@ components: example: 1 title: Resultados totales description: Número de elementos individuales en todas las páginas de resultados + previous_cursor: + type: string + example: null + title: Cursor anterior + description: Cursor para obtener la página anterior de resultados. Es `null` en la primera página. Solo disponible con `pagination=cursor`. + next_cursor: + type: string + example: null + title: Cursor siguiente + description: Cursor para obtener la página siguiente de resultados. Es `null` cuando no hay más resultados. Solo disponible con `pagination=cursor`. + totals_are_capped: + type: boolean + example: false + title: Resultados con tope + description: Indica si `total_results` está limitado a 3,000 porque existen más resultados de los reportados. + ResourceAutoGeneratedProps: type: object required: From 0fbe05cb076e664f35952b8ca8f743dd558ee61d Mon Sep 17 00:00:00 2001 From: javorosas Date: Thu, 27 Aug 2026 19:07:45 +0200 Subject: [PATCH 2/4] Emphasize cursor reach over page's 3,000-result limit --- website/docs/guides/pagination.mdx | 7 +++++++ .../current/guides/pagination.mdx | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/website/docs/guides/pagination.mdx b/website/docs/guides/pagination.mdx index e69f5df6e..6bd53fc10 100644 --- a/website/docs/guides/pagination.mdx +++ b/website/docs/guides/pagination.mdx @@ -26,6 +26,13 @@ modos de paginación que puedes elegir con el parámetro `pagination`: Para la mayoría de los casos, y en particular para listas grandes, te recomendamos usar `pagination=cursor`. +La diferencia práctica más importante entre ambos modos es el **alcance**: +`pagination=page` solo permite acceder a los primeros 3,000 resultados +(30 páginas × 100 por página), mientras que `pagination=cursor` puede recorrer +**todos** los resultados de la búsqueda, incluso cuando `total_results` está +capeado a 3,000. Si necesitas exportar o procesar más de 3,000 resultados, usa +`pagination=cursor`. + ## Parámetros comunes | Parámetro | Tipo | Descripción | diff --git a/website/i18n/en/docusaurus-plugin-content-docs/current/guides/pagination.mdx b/website/i18n/en/docusaurus-plugin-content-docs/current/guides/pagination.mdx index fd8ff7e09..d8168434c 100644 --- a/website/i18n/en/docusaurus-plugin-content-docs/current/guides/pagination.mdx +++ b/website/i18n/en/docusaurus-plugin-content-docs/current/guides/pagination.mdx @@ -26,6 +26,12 @@ pagination modes that you can choose with the `pagination` parameter: For most cases, and especially for large lists, we recommend using `pagination=cursor`. +The most important practical difference between both modes is **reach**: +`pagination=page` only gives access to the first 3,000 results +(30 pages × 100 per page), while `pagination=cursor` can traverse **all** the +search results, even when `total_results` is capped at 3,000. If you need to +export or process more than 3,000 results, use `pagination=cursor`. + ## Common parameters | Parameter | Type | Description | From aa997ed59a184a5fa5fab76a324a7a2a57492323 Mon Sep 17 00:00:00 2001 From: javorosas Date: Thu, 27 Aug 2026 21:30:23 +0200 Subject: [PATCH 3/4] docs(pagination): drop speed claim; recommend cursor for reach and stability Both pagination modes cap the total count at 3,000, so cursor is not faster because it avoids exact counts. Reframe the recommendation around reach (page caps at 3,000 results / 30 pages) and stable position-based pages. --- CHANGELOG.md | 4 ++-- website/docs/guides/pagination.mdx | 18 +++++++++--------- .../current/guides/pagination.mdx | 18 +++++++++--------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 708169d59..13198b4a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,8 @@ ### Paginación por cursores (`pagination=cursor`) - Nuevo modo de paginación opt-in `pagination=cursor`, **recomendado** para - listas grandes: no ejecuta conteos exactos sobre la colección, por lo que es - más rápido que la paginación por páginas. + listas grandes: recorre todos los resultados sin el tope de 30 páginas del + modo `page`, con páginas estables aunque cambien los datos. - Nuevos parámetros `after` y `before` para navegar entre páginas de forma secuencial. Cada búsqueda regresa `previous_cursor` y `next_cursor`. - Nuevo campo `totals_are_capped` en las respuestas de búsqueda de ambos modos: diff --git a/website/docs/guides/pagination.mdx b/website/docs/guides/pagination.mdx index 6bd53fc10..bb78c6884 100644 --- a/website/docs/guides/pagination.mdx +++ b/website/docs/guides/pagination.mdx @@ -9,18 +9,17 @@ modos de paginación que puedes elegir con el parámetro `pagination`: - **`pagination=page`** — el modo clásico, por **páginas numeradas**. Es el comportamiento por defecto cuando no se envía `pagination`. -- **`pagination=cursor`** — el modo recomendado, por **cursores**. Es más rápido - porque no ejecuta conteos exactos sobre toda la colección y está pensado para - recorrer listas grandes de forma secuencial. +- **`pagination=cursor`** — el modo recomendado, por **cursores**. Recorre los + resultados de forma secuencial, sin el tope de 30 páginas del modo `page`, y + con páginas estables aunque cambien los datos. ## ¿Qué modo usar? | | `pagination=page` | `pagination=cursor` | | --- | --- | --- | | Navegación | Saltar a una página específica (`page=3`) | Secuencial: siguiente / anterior | -| Total de resultados | Conteo exacto (hasta 3,000) | Informativo (conteo acotado a 3,000) | -| Rendimiento | Ejecuta un conteo por búsqueda | Más rápido, sin conteos exactos | -| Límites | Máximo 30 páginas | Sin límite de páginas | +| Alcance | Hasta 3,000 resultados (30 páginas × 100) | Todos los resultados de la búsqueda | +| Estabilidad | Las páginas se corren si cambian los datos | Páginas estables (basadas en posición) | | Uso recomendado | Compatibilidad, interfaces con saltos de página | Listas grandes, iteración, exportaciones | Para la mayoría de los casos, y en particular para listas grandes, te recomendamos @@ -144,6 +143,7 @@ la búsqueda, **con un tope de 3,000**: } ``` -En el modo `pagination=cursor`, `total_results` es informativo: el total se -calcula con un conteo acotado a 3,000 en lugar de un conteo exacto sobre toda -la colección, por lo que es más rápida incluso con listas muy grandes. +En ambos modos `total_results` es informativo: el conteo se acota a 3,000 y no +recorre toda la colección. La ventaja práctica de `pagination=cursor` no es la +velocidad del conteo (ambos modos lo acotan), sino su **alcance** y la +**estabilidad** de las páginas. diff --git a/website/i18n/en/docusaurus-plugin-content-docs/current/guides/pagination.mdx b/website/i18n/en/docusaurus-plugin-content-docs/current/guides/pagination.mdx index d8168434c..92410288c 100644 --- a/website/i18n/en/docusaurus-plugin-content-docs/current/guides/pagination.mdx +++ b/website/i18n/en/docusaurus-plugin-content-docs/current/guides/pagination.mdx @@ -9,18 +9,17 @@ pagination modes that you can choose with the `pagination` parameter: - **`pagination=page`** — the classic mode, based on **numbered pages**. This is the default behavior when `pagination` is not sent. -- **`pagination=cursor`** — the recommended mode, based on **cursors**. It is - faster because it does not run exact counts over the whole collection, and it - is designed to traverse large lists sequentially. +- **`pagination=cursor`** — the recommended mode, based on **cursors**. It + traverses results sequentially, without the 30-page limit of `page` mode, and + with stable pages even when the data changes. ## Which mode should I use? | | `pagination=page` | `pagination=cursor` | | --- | --- | --- | | Navigation | Jump to a specific page (`page=3`) | Sequential: next / previous | -| Total results | Exact count (up to 3,000) | Informational (count capped at 3,000) | -| Performance | Runs a count per search | Faster, no exact counts | -| Limits | Up to 30 pages | No page limit | +| Reach | Up to 3,000 results (30 pages × 100) | All search results | +| Stability | Pages shift when the data changes | Stable pages (position-based) | | Recommended for | Compatibility, UIs with page jumps | Large lists, iteration, exports | For most cases, and especially for large lists, we recommend using @@ -143,6 +142,7 @@ search, **capped at 3,000**: } ``` -In `pagination=cursor` mode, `total_results` is informational: the total is -computed with a count capped at 3,000 instead of an exact count over the whole -collection, so it stays fast even with very large lists. +In both modes, `total_results` is informational and the count is capped at +3,000 without traversing the whole collection. The practical advantage of +`pagination=cursor` is not count speed (both modes cap it), but **reach** and +**stable pages**. From 3a1469a7c12256ae0a1028364ba25f5b7606eaa6 Mon Sep 17 00:00:00 2001 From: javorosas Date: Thu, 27 Aug 2026 22:38:52 +0200 Subject: [PATCH 4/4] docs(pagination): totals are only returned on the first cursor page Align the guide and OpenAPI SearchResult with perf #2206: a cursor search only includes total_results/totals_are_capped on the first page (no after/before); later pages return data and cursors only. --- website/docs/guides/pagination.mdx | 4 ++++ .../current/guides/pagination.mdx | 4 ++++ website/openapi_v2.en.yaml | 3 ++- website/openapi_v2.yaml | 3 ++- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/website/docs/guides/pagination.mdx b/website/docs/guides/pagination.mdx index bb78c6884..e27598c21 100644 --- a/website/docs/guides/pagination.mdx +++ b/website/docs/guides/pagination.mdx @@ -89,6 +89,10 @@ Respuesta: } ``` +`total_results` y `totals_are_capped` se incluyen únicamente en la **primera +página** de la búsqueda (cuando no se envían `after` ni `before`): el total no +cambia entre páginas, así que las siguientes solo regresan `data` y los cursores. + ### Navegar entre páginas 1. **Primera página:** se obtiene sin enviar `after` ni `before`. diff --git a/website/i18n/en/docusaurus-plugin-content-docs/current/guides/pagination.mdx b/website/i18n/en/docusaurus-plugin-content-docs/current/guides/pagination.mdx index 92410288c..48f320884 100644 --- a/website/i18n/en/docusaurus-plugin-content-docs/current/guides/pagination.mdx +++ b/website/i18n/en/docusaurus-plugin-content-docs/current/guides/pagination.mdx @@ -88,6 +88,10 @@ Response: } ``` +`total_results` and `totals_are_capped` are only included on the **first page** +of the search (when neither `after` nor `before` is sent): the total does not +change between pages, so subsequent pages only return `data` and the cursors. + ### Navigating between pages 1. **First page:** obtained by not sending `after` or `before`. diff --git a/website/openapi_v2.en.yaml b/website/openapi_v2.en.yaml index 661028f04..7bd6a063b 100644 --- a/website/openapi_v2.en.yaml +++ b/website/openapi_v2.en.yaml @@ -11592,7 +11592,8 @@ components: type: integer example: 1 title: Total results - description: The total number of results available in the search + description: | + The total number of results available in the search. In `pagination=cursor` mode it is only included on the first page of the search (no `after`/`before`); the total does not change between pages. previous_cursor: type: string example: null diff --git a/website/openapi_v2.yaml b/website/openapi_v2.yaml index 43e8229c8..e206bca99 100644 --- a/website/openapi_v2.yaml +++ b/website/openapi_v2.yaml @@ -11827,7 +11827,8 @@ components: type: integer example: 1 title: Resultados totales - description: Número de elementos individuales en todas las páginas de resultados + description: | + Número de elementos individuales en todas las páginas de resultados. En modo `pagination=cursor` solo se incluye en la primera página de la búsqueda (sin `after`/`before`); el total no cambia entre páginas. previous_cursor: type: string example: null