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. */