diff --git a/src/app/api/v1/grid/route.ts b/src/app/api/v1/grid/route.ts index a60a0c7..93b96ac 100644 --- a/src/app/api/v1/grid/route.ts +++ b/src/app/api/v1/grid/route.ts @@ -93,7 +93,9 @@ export async function GET(req: NextRequest) { // Métriques Qlik et gamme serveur relues au moment de l'appel (voir api-enrich). const rows = q.enrich === "1" ? await enrichRows(result.rows) : result.rows; - const pagination = buildPagination(q.page, q.limit, result.total); + // Sans `limit`, tout a été renvoyé : la pagination doit le refléter (une seule + // page couvrant le total), sinon `hasMore` mentirait. + const pagination = buildPagination(q.page, q.limit ?? Math.max(1, result.total), result.total); return ok( rows.map((r) => pickFields(r, q.fields)), @@ -117,8 +119,8 @@ export async function GET(req: NextRequest) { ...(pagination.hasMore ? { avertissement: - `Réponse partielle : ${rows.length} lignes sur ${result.total}. ` - + `Relancez avec page=${q.page + 1}, ou augmentez limit (5000 au maximum) pour tout obtenir en un appel.`, + `Réponse partielle : ${rows.length} lignes sur ${result.total}, parce que « limit » a été précisé. ` + + `Retirez « limit » pour obtenir tout le fournisseur en un appel, ou passez à page=${q.page + 1}.`, } : {}), }, diff --git a/src/app/api/v1/openapi.json/route.ts b/src/app/api/v1/openapi.json/route.ts index 1286104..72467d1 100644 --- a/src/app/api/v1/openapi.json/route.ts +++ b/src/app/api/v1/openapi.json/route.ts @@ -22,7 +22,7 @@ export async function GET(req: NextRequest) { const paginationParams = [ { name: "page", in: "query", schema: { type: "integer", minimum: 1, default: 1 }, description: "Numéro de page." }, - { name: "limit", in: "query", schema: { type: "integer", minimum: 1, maximum: 5000, default: 100 }, description: "Lignes par page. Jusqu'à 5000 sur /grid : un seul appel suffit pour un fournisseur entier. 500 maximum sur les autres endpoints." }, + { name: "limit", in: "query", schema: { type: "integer", minimum: 1 }, description: "Lignes par page, sans plafond. Sur /grid, OMETTRE ce paramètre renvoie toutes les lignes du fournisseur en un seul appel — c'est le mode recommandé. Sur les autres endpoints, le défaut est 100." }, ]; const sortParams = [ { name: "sort", in: "query", schema: { type: "string", enum: GRID_SORT_KEYS }, description: "Colonne de tri." }, diff --git a/src/features/admin/components/api-connection-info.tsx b/src/features/admin/components/api-connection-info.tsx index c7617d3..a8e9db0 100644 --- a/src/features/admin/components/api-connection-info.tsx +++ b/src/features/admin/components/api-connection-info.tsx @@ -50,7 +50,7 @@ const ENDPOINTS: Array<{ method: string; path: string; desc: string }> = [ ]; const PARAMS: Array<{ name: string; desc: string }> = [ - { name: "page, limit", desc: "Pagination. limit ≤ 5000 sur /grid — un seul appel suffit donc pour un fournisseur entier ; ≤ 500 ailleurs" }, + { name: "page, limit", desc: "Pagination, sans plafond. Sur /grid, omettre limit renvoie TOUT le fournisseur en un appel ; ailleurs le défaut est 100" }, { name: "sort, order", desc: "Tri, ex. sort=totalCa&order=desc" }, { name: "search", desc: "Libellé, codein, GTIN, référence ou code centrale" }, { name: "gamme, code1..code3", desc: "Filtres sur la gamme et la nomenclature" }, @@ -123,10 +123,10 @@ export function ApiConnectionInfo() { {`curl -H "X-API-Key: VOTRE_CLE" \\\n "${base}/products/search?q=tapis&limit=20"`} {`curl -H "X-API-Key: VOTRE_CLE" \\\n "${base}/grid?fournisseur=FOU001&fields=codein,libelle1,totalCa,codeGammeServeur"`}

- Tout un fournisseur en un seul appel — meta.complet confirme - qu'il ne reste rien à lire : + Tout un fournisseur en un seul appel — il suffit d'omettre limit, + et meta.complet confirme qu'il ne reste rien à lire :

- {`curl -H "X-API-Key: VOTRE_CLE" \\\n "${base}/grid?fournisseur=FOU001&limit=5000"`} + {`curl -H "X-API-Key: VOTRE_CLE" \\\n "${base}/grid?fournisseur=FOU001"`} {/* Branchement d'une IA externe (ChatGPT) */} diff --git a/src/lib/api-schemas.ts b/src/lib/api-schemas.ts index 8d82134..21c067e 100644 --- a/src/lib/api-schemas.ts +++ b/src/lib/api-schemas.ts @@ -13,7 +13,13 @@ const SORT_KEYS = GRID_SORT_KEYS as readonly string[]; export const paginationShape = { page: z.coerce.number().int().min(1).default(1), - limit: z.coerce.number().int().min(1).max(500).default(100), + /** + * Aucun plafond : l'appelant décide de la taille qu'il veut recevoir. + * Le défaut reste modeste (100) pour que les endpoints de recherche, non + * bornés par un fournisseur, ne renvoient pas tout le catalogue par accident. + * `/grid` redéfinit ce paramètre : sans `limit`, il renvoie tout. + */ + limit: z.coerce.number().int().min(1).default(100), }; const sortShape = { @@ -53,18 +59,18 @@ export const gridQuerySchema = z.object({ ...sortShape, ...paginationShape, /** - * Plafond relevé à 5000 pour `/grid` : un lot fournisseur dépasse souvent 500 - * références, et l'objectif est qu'une app ou un agent récupère **tout** le - * fournisseur en un seul appel plutôt que de paginer — ce que les agents font - * mal, concluant à tort qu'ils ont tout lu. + * **Aucun plafond, et aucune valeur par défaut** : sans `limit`, `/grid` + * renvoie *toutes* les lignes du fournisseur. * - * La borne reste à 500 sur la recherche transversale, qui elle n'est pas - * bornée par un fournisseur. + * C'est l'objectif de cet endpoint — qu'une app ou un agent récupère un + * fournisseur entier en un appel. Paginer par défaut condamnait l'appelant à + * boucler, ce que les agents font mal : ils s'arrêtent à la première page et + * raisonnent sur un catalogue tronqué. * * Réponse volumineuse : une ligne complète porte les séries mensuelles et les * ventilations par magasin. Combiner avec `fields` est vivement conseillé. */ - limit: z.coerce.number().int().min(1).max(5000).default(100), + limit: z.coerce.number().int().min(1).optional(), }); /** `/api/v1/products/search` — `fournisseur` facultatif : c'est la recherche transversale. */ diff --git a/src/lib/grid-store.ts b/src/lib/grid-store.ts index fc33ddc..a2c5c70 100644 --- a/src/lib/grid-store.ts +++ b/src/lib/grid-store.ts @@ -174,7 +174,10 @@ function buildWhere(q: GridQuery): SQL | undefined { */ export async function queryGridRows(q: GridQuery): Promise { const page = Math.max(1, q.page ?? 1); - const limit = Math.min(500, Math.max(1, q.limit ?? 100)); + // `limit` absent = aucune limite : on renvoie toutes les lignes du filtre. + // Il y avait ici un Math.min(500, …) qui re-plafonnait en silence, quelle que + // soit la valeur demandée — la réponse était tronquée sans que rien ne le dise. + const limit = q.limit != null ? Math.max(1, q.limit) : null; const where = buildWhere(q); const sortCol = SORTABLE[q.sort ?? "totalCa"] ?? gridRows.totalCa; @@ -186,13 +189,15 @@ export async function queryGridRows(q: GridQuery): Promise { .from(gridRows) .where(where); - const found = await db + const selection = db .select({ payload: gridRows.payload, computedAt: gridRows.computedAt }) .from(gridRows) .where(where) - .orderBy(orderBy, asc(gridRows.codein)) - .limit(limit) - .offset((page - 1) * limit); + .orderBy(orderBy, asc(gridRows.codein)); + + const found = limit != null + ? await selection.limit(limit).offset((page - 1) * limit) + : await selection; let oldest: Date | null = null; for (const r of found) {