mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
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:
5 files changed
+34
-21
No files matched your search
@@ -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}.`,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
|
||||
@@ -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." },
|
||||
|
||||
@@ -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() {
|
||||
<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'il ne reste rien à lire :
|
||||
Tout un fournisseur en un seul appel — il suffit d'omettre <code className="font-mono">limit</code>,
|
||||
et <code className="font-mono">meta.complet</code> confirme qu'il ne reste rien à lire :
|
||||
</p>
|
||||
<CodeLine>{`curl -H "X-API-Key: VOTRE_CLE" \\\n "${base}/grid?fournisseur=FOU001&limit=5000"`}</CodeLine>
|
||||
<CodeLine>{`curl -H "X-API-Key: VOTRE_CLE" \\\n "${base}/grid?fournisseur=FOU001"`}</CodeLine>
|
||||
</div>
|
||||
|
||||
{/* Branchement d'une IA externe (ChatGPT) */}
|
||||
|
||||
+14
-8
@@ -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
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user