mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +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
68 lines
1.9 KiB
TypeScript
68 lines
1.9 KiB
TypeScript
/**
|
|
* CollectFlow — Enveloppe de réponse de l'API publique `/api/v1`.
|
|
*
|
|
* Une forme unique pour tout : `{ data, pagination?, meta }` en succès,
|
|
* `{ error: { code, message } }` en échec, avec de vrais statuts HTTP.
|
|
* Les consommateurs (scripts, IA, outils externes) peuvent ainsi traiter
|
|
* toutes les réponses de la même manière.
|
|
*/
|
|
|
|
import { NextResponse } from "next/server";
|
|
|
|
export type ApiErrorCode =
|
|
| "unauthorized"
|
|
| "forbidden"
|
|
| "bad_request"
|
|
| "not_found"
|
|
| "not_ready"
|
|
| "internal_error";
|
|
|
|
const STATUS_BY_CODE: Record<ApiErrorCode, number> = {
|
|
unauthorized: 401,
|
|
forbidden: 403,
|
|
bad_request: 400,
|
|
not_found: 404,
|
|
not_ready: 202,
|
|
internal_error: 500,
|
|
};
|
|
|
|
export interface Pagination {
|
|
page: number;
|
|
limit: number;
|
|
total: number;
|
|
totalPages: number;
|
|
}
|
|
|
|
export function buildPagination(page: number, limit: number, total: number): Pagination {
|
|
return { page, limit, total, totalPages: Math.max(1, Math.ceil(total / limit)) };
|
|
}
|
|
|
|
/** Réponse de succès. `meta` porte la fraîcheur de la donnée (`computedAt`). */
|
|
export function ok<T>(
|
|
data: T,
|
|
opts: { pagination?: Pagination; meta?: Record<string, unknown> } = {},
|
|
): NextResponse {
|
|
return NextResponse.json({
|
|
data,
|
|
...(opts.pagination ? { pagination: opts.pagination } : {}),
|
|
meta: { ...(opts.meta ?? {}) },
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Réponse d'erreur. `details` sert notamment aux erreurs de validation zod.
|
|
*
|
|
* Note : `not_ready` renvoie un 202 — ce n'est pas un échec mais une donnée pas
|
|
* encore calculée (voir /api/v1/grid sur un fournisseur jamais ouvert).
|
|
*/
|
|
export function fail(
|
|
code: ApiErrorCode,
|
|
message: string,
|
|
details?: unknown,
|
|
): NextResponse {
|
|
return NextResponse.json(
|
|
{ error: { code, message, ...(details !== undefined ? { details } : {}) } },
|
|
{ status: STATUS_BY_CODE[code] },
|
|
);
|
|
}
|