mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-19 05:06:56 +02:00
Les données de la grille (ventes 12 mois, stock, marges, gammes, métriques réseau Qlik) n'étaient accessibles par aucun moyen programmatique, et getProductRows() exige un fournisseur : chercher un produit sans le connaître était impossible. Contrainte de conception : l'API ne recalcule jamais rien. La grille était reconstruite en direct et gardée seulement 10 min en mémoire ; une API qui appellerait getProductRows() serait lente et imprévisible. On persiste donc le résultat d'un calcul qui a déjà lieu, et on le sert. - Table grid_rows : colonnes scalaires (filtre/tri/recherche en SQL) + payload jsonb du ProductRow complet. Remplie en effet de bord NON bloquant par getProductRows(), purge des articles disparus via computed_at. Survit aux redémarrages, contrairement au cache mémoire. - Endpoints /api/v1 : fournisseurs, grid, products/search (transversale, tous fournisseurs), products/:codein, network/:codeCentrale, openapi.json. Pagination, tri sur liste blanche, projection de champs, validation zod. 202 not_ready si un fournisseur n'a pas encore d'instantané. - Authentification double : clé d'API (X-API-Key ou Bearer, SHA-256 en base, révocable) ou session existante. Le middleware exempte /api/v1 — sans quoi un script recevait une redirection 302 vers /login au lieu d'un 401 JSON. - Gestion des clés dans /settings (server actions, clé affichée une seule fois). Vérifié contre une base PostgreSQL locale : 401 JSON sans clé, 401 sur clé révoquée, recherche renvoyant plusieurs fournisseurs, upsert + purge, et 24 appels /api/v1 sans déclencher un seul recalcul (l'ancienne route /api/grid/rows en déclenche un à chaque appel). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675
63 lines
2.3 KiB
TypeScript
63 lines
2.3 KiB
TypeScript
import { NextRequest } from "next/server";
|
|
import { requireApiAuth } from "@/lib/api-auth";
|
|
import { ok, fail } from "@/lib/api-response";
|
|
import { fournisseursQuerySchema } from "@/lib/api-schemas";
|
|
import { listGridSuppliers } from "@/lib/grid-store";
|
|
import { pgGetFournisseurs } from "@/lib/pg-ff-client";
|
|
|
|
export const runtime = "nodejs";
|
|
export const dynamic = "force-dynamic";
|
|
|
|
/**
|
|
* GET /api/v1/fournisseurs?search=…&withData=1
|
|
*
|
|
* Liste des fournisseurs. Par défaut, tout le référentiel (`fouident`), enrichi de la
|
|
* fraîcheur de l'instantané de grille quand il existe — ce qui permet à l'appelant de
|
|
* savoir d'avance quels fournisseurs `/api/v1/grid` peut servir immédiatement.
|
|
*
|
|
* `withData=1` restreint aux seuls fournisseurs déjà présents dans l'instantané.
|
|
*/
|
|
export async function GET(req: NextRequest) {
|
|
const authCtx = await requireApiAuth(req);
|
|
if (authCtx instanceof Response) return authCtx;
|
|
|
|
const parsed = fournisseursQuerySchema.safeParse(Object.fromEntries(req.nextUrl.searchParams));
|
|
if (!parsed.success) {
|
|
return fail("bad_request", "Paramètres invalides.", parsed.error.issues);
|
|
}
|
|
const { search, withData } = parsed.data;
|
|
|
|
const snapshot = await listGridSuppliers();
|
|
const byCode = new Map(snapshot.map((s) => [s.codeFournisseur, s]));
|
|
|
|
if (withData === "1") {
|
|
const term = search?.toLowerCase();
|
|
const data = snapshot
|
|
.filter((s) => !term
|
|
|| s.codeFournisseur.toLowerCase().includes(term)
|
|
|| (s.nomFournisseur ?? "").toLowerCase().includes(term))
|
|
.map((s) => ({
|
|
code: s.codeFournisseur,
|
|
nom: s.nomFournisseur,
|
|
hasSnapshot: true,
|
|
rowCount: s.rowCount,
|
|
computedAt: s.computedAt,
|
|
}));
|
|
return ok(data, { meta: { total: data.length, scope: "instantané uniquement" } });
|
|
}
|
|
|
|
const fournisseurs = await pgGetFournisseurs(search);
|
|
const data = fournisseurs.map((f) => {
|
|
const snap = byCode.get(f.code);
|
|
return {
|
|
code: f.code,
|
|
nom: f.nom,
|
|
hasSnapshot: Boolean(snap),
|
|
rowCount: snap?.rowCount ?? 0,
|
|
computedAt: snap?.computedAt ?? null,
|
|
};
|
|
});
|
|
|
|
return ok(data, { meta: { total: data.length, withSnapshot: byCode.size } });
|
|
}
|