fix(api): supprime le plafond de 500 lignes, qui tronquait en silence

Le passage à limit=5000 du commit précédent était sans effet : queryGridRows
re-plafonnait à 500 en dur (Math.min(500, …)). Pire, la pagination était
calculée sur le limit *demandé*, donc hasMore et meta.complet annonçaient une
réponse complète alors qu'elle était tronquée — exactement la troncature
silencieuse que ces champs devaient éliminer.

- Plus aucun plafond. Sur /grid, omettre `limit` renvoie toutes les lignes du
  fournisseur : plus de pagination à dérouler, ce que les agents font mal.
- La pagination reflète le mode sans limite (une page couvrant le total), donc
  hasMore et meta.complet disent la vérité.
- Le défaut reste 100 sur les endpoints de recherche, non bornés par un
  fournisseur — mais sans maximum imposé.
- meta.avertissement explique désormais que la troncature vient du `limit`
  fourni, et qu'il suffit de l'omettre.

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:46:32 +00:00
1 parent 73accd6d60
commit 33b30dc8bd
5 files changed
+34 -21

No files matched your search

+14 -8
View File
@@ -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. */
+10 -5
View File
@@ -174,7 +174,10 @@ function buildWhere(q: GridQuery): SQL | undefined {
*/
export async function queryGridRows(q: GridQuery): Promise<GridQueryResult> {
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<GridQueryResult> {
.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) {