From 73accd6d60e96c30edfad46e1a7b426db28f6d8b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 11:30:25 +0000 Subject: [PATCH] feat(api): un seul appel suffit pour tout un fournisseur MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le plafond de pagination était de 500 lignes. Un lot fournisseur dépasse souvent ce seuil, si bien qu'une app ou un agent ne récupérait qu'une partie des données sans forcément s'en rendre compte — et les agents paginent mal, concluant volontiers qu'ils ont tout lu. - /grid accepte désormais limit jusqu'à 5000. La borne reste à 500 sur la recherche transversale, qui n'est pas bornée par un fournisseur. - pagination.hasMore, et meta.complet sur /grid : l'appelant sait s'il a tout, sans avoir à comparer page et totalPages. - Réponse partielle : meta.avertissement indique le nombre de lignes obtenues sur le total et comment obtenir le reste. Pas de troncature silencieuse. - Doc des Paramètres et openapi.json mis à jour, avec un exemple curl qui récupère un fournisseur entier. `fields` reste vivement conseillé : une ligne complète porte les séries mensuelles et les ventilations par magasin. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 --- src/app/api/v1/grid/route.ts | 18 +++++++++++++++++- src/app/api/v1/openapi.json/route.ts | 3 ++- .../admin/components/api-connection-info.tsx | 7 ++++++- src/lib/api-response.ts | 7 ++++++- src/lib/api-schemas.ts | 13 +++++++++++++ 5 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/app/api/v1/grid/route.ts b/src/app/api/v1/grid/route.ts index 8534cdc..a60a0c7 100644 --- a/src/app/api/v1/grid/route.ts +++ b/src/app/api/v1/grid/route.ts @@ -93,10 +93,12 @@ 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); + return ok( rows.map((r) => pickFields(r, q.fields)), { - pagination: buildPagination(q.page, q.limit, result.total), + pagination, meta: { fournisseur: q.fournisseur, computedAt: result.computedAt, @@ -105,6 +107,20 @@ export async function GET(req: NextRequest) { enrichi: q.enrich === "1", /** true = l'instantané n'existait pas et vient d'être calculé par cet appel. */ computedOnDemand, + /** + * true = cette réponse contient **toutes** les lignes du fournisseur. + * Dit explicitement à un appelant — en particulier un agent — s'il + * peut s'arrêter là, au lieu de le laisser déduire d'une pagination + * qu'il risque d'ignorer. + */ + complet: !pagination.hasMore, + ...(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.`, + } + : {}), }, }, ); diff --git a/src/app/api/v1/openapi.json/route.ts b/src/app/api/v1/openapi.json/route.ts index e37bad3..1286104 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: 500, default: 100 }, description: "Nombre de lignes par page (500 maximum)." }, + { 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." }, ]; const sortParams = [ { name: "sort", in: "query", schema: { type: "string", enum: GRID_SORT_KEYS }, description: "Colonne de tri." }, @@ -93,6 +93,7 @@ export async function GET(req: NextRequest) { limit: { type: "integer" }, total: { type: "integer" }, totalPages: { type: "integer" }, + hasMore: { type: "boolean", description: "true = il reste des pages. Sur /grid, meta.complet dit la même chose en positif." }, }, }, Fournisseur: { diff --git a/src/features/admin/components/api-connection-info.tsx b/src/features/admin/components/api-connection-info.tsx index 158e204..c7617d3 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 ≤ 500, défaut 100)" }, + { name: "page, limit", desc: "Pagination. limit ≤ 5000 sur /grid — un seul appel suffit donc pour un fournisseur entier ; ≤ 500 ailleurs" }, { 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" }, @@ -122,6 +122,11 @@ export function ApiConnectionInfo() {

Exemples

{`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 : +

+ {`curl -H "X-API-Key: VOTRE_CLE" \\\n "${base}/grid?fournisseur=FOU001&limit=5000"`} {/* Branchement d'une IA externe (ChatGPT) */} diff --git a/src/lib/api-response.ts b/src/lib/api-response.ts index 1677ce3..f4a821d 100644 --- a/src/lib/api-response.ts +++ b/src/lib/api-response.ts @@ -31,10 +31,15 @@ export interface Pagination { limit: number; total: number; totalPages: number; + /** `true` s'il reste des pages à lire — évite d'avoir à comparer page et totalPages. */ + hasMore: boolean; } export function buildPagination(page: number, limit: number, total: number): Pagination { - return { page, limit, total, totalPages: Math.max(1, Math.ceil(total / limit)) }; + const totalPages = Math.max(1, Math.ceil(total / limit)); + // `hasMore` explicite : un agent qui doit comparer page et totalPages oublie + // souvent de le faire et conclut à tort qu'il a tout récupéré. + return { page, limit, total, totalPages, hasMore: page < totalPages }; } /** Réponse de succès. `meta` porte la fraîcheur de la donnée (`computedAt`). */ diff --git a/src/lib/api-schemas.ts b/src/lib/api-schemas.ts index 7d9fec2..8d82134 100644 --- a/src/lib/api-schemas.ts +++ b/src/lib/api-schemas.ts @@ -52,6 +52,19 @@ export const gridQuerySchema = z.object({ compute: z.enum(["0", "1"]).default("1"), ...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. + * + * La borne reste à 500 sur la recherche transversale, qui elle n'est pas + * bornée par un fournisseur. + * + * 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), }); /** `/api/v1/products/search` — `fournisseur` facultatif : c'est la recherche transversale. */