feat(api): un seul appel suffit pour tout un fournisseur

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675
This commit is contained in:
Claude committed 2026-08-06 11:30:25 +00:00
1 parent 3fa92e891b
commit 73accd6d60
5 files changed
+44 -4

No files matched your search

+17 -1
View File
@@ -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.`,
}
: {}),
},
},
);
+2 -1
View File
@@ -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: {
@@ -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() {
<p className="text-[12px] font-semibold" style={{ color: "var(--text-primary)" }}>Exemples</p>
<CodeLine>{`curl -H "X-API-Key: VOTRE_CLE" \\\n "${base}/products/search?q=tapis&limit=20"`}</CodeLine>
<CodeLine>{`curl -H "X-API-Key: VOTRE_CLE" \\\n "${base}/grid?fournisseur=FOU001&fields=codein,libelle1,totalCa,codeGammeServeur"`}</CodeLine>
<p className="text-[11px] pt-1" style={{ color: "var(--text-muted)" }}>
Tout un fournisseur en un seul appel — <code className="font-mono">meta.complet</code> confirme
qu&apos;il ne reste rien à lire :
</p>
<CodeLine>{`curl -H "X-API-Key: VOTRE_CLE" \\\n "${base}/grid?fournisseur=FOU001&limit=5000"`}</CodeLine>
</div>
{/* Branchement d'une IA externe (ChatGPT) */}
+6 -1
View File
@@ -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`). */
+13
View File
@@ -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. */