Commit cbc1b6fa authored by Kourser's avatar Kourser
Browse files

Le tri du dossier se déclare, et se contrôle

Le correctif précédent traitait le symptôme : modele_message triait par
« libelle » au lieu de « nom ». Le piège, lui, restait armé — un défaut
« nom » et deux exceptions écartées à la main, alors qu'il se trouvait
seulement que quatre tables sur sept décrivaient des gens ou des
organisations, qui ont des noms. Un défaut vrai quatre fois sur sept n'en est
pas un.

Le piège se referme des deux côtés, parce qu'il en avait deux.

Déclarer
  Record<TablePreparation, readonly string[]>, comme COLONNE_SUPPRESSION le
  fait déjà deux lignes plus haut. Ajouter une table à l'union sans lui donner
  son tri ne compile plus. Le défaut disparaît : il n'y a plus de « sinon,
  nom », il y a sept déclarations.

Vérifier
  Le type oblige à déclarer un tri ; il ne dit pas que la colonne existe.
  C'était l'autre moitié : « nom » était bel et bien déclaré pour
  modele_message, et la table portait un libelle. Un contrôle relit donc
  information_schema et confronte chaque tri déclaré aux colonnes réelles.

  Il tourne juste après les migrations, avant toute lecture du dossier, et ce
  placement est le contrôle lui-même : posé plus tard — il l'a d'abord été —
  la suite tombait avant de l'atteindre, sur « column "nom" does not exist »,
  une erreur qui ne nomme ni la table ni le tri fautif. Après la première
  lecture, il n'aurait plus rien à dire que la chute n'ait déjà dit, et plus
  mal.

Éprouvé en le cassant
  Le défaut d'origine a été réintroduit pour voir le contrôle parler : il
  échoue sur la bonne table et nomme la colonne absente — « le tri de
  modele_message désigne des colonnes qui existent — absente(s) : nom » —
  avant que quoi que ce soit ne tombe. Un contrôle qu'on n'a pas vu échouer
  ne prouve rien.

  898 garanties vérifiées une fois la valeur rétablie, aucune en échec.

Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
parent 5dca6446
Loading
Loading
Loading
Loading
Loading
+24 −8
Changes for apps/api/src/preparation/dossier.ts: 24 added lines, 8 removed lines.
Original line number Diff line number Diff line
@@ -22,6 +22,29 @@ export type TablePreparation =
/** Colonnes gérées par le versionnement : jamais fournies par l'appelant. */
const RESERVEES = new Set(["organisation_id", "id", "depuis_version", "jusqu_a_version"]);

/**
 * Le tri de chaque table, déclaré et non deviné.
 *
 * Il y avait ici un défaut — « nom » — et deux exceptions écartées à la main.
 * Il se trouvait juste que quatre tables sur sept décrivaient des gens ou des
 * organisations, qui ont des noms. Un défaut vrai quatre fois sur sept n'en
 * est pas un : c'est un piège tendu au prochain ajout, et modele_message est
 * tombé dedans — sa table porte un libelle, et toute lecture du dossier
 * échouait sur « column "nom" does not exist ».
 *
 * Record<TablePreparation, …> rend l'oubli impossible : ajouter une table à
 * l'union sans lui donner son tri ne compile pas.
 */
export const TRI: Record<TablePreparation, readonly string[]> = {
  personne: ["nom"],
  tiers: ["nom"],
  fiche_reflexe: ["ordre", "titre"],
  document_kit: ["categorie", "titre"],
  application_metier: ["nom"],
  partie_prenante: ["nom"],
  modele_message: ["libelle"],
};

const COLONNE_SUPPRESSION: Record<TablePreparation, string> = {
  personne: "supprimee",
  tiers: "supprime",
@@ -140,14 +163,7 @@ export async function lire<T extends object>(
         ? sql`and jusqu_a_version is null`
         : sql`and depuis_version <= ${version}
               and (jusqu_a_version is null or jusqu_a_version > ${version})`}
     order by ${table === "fiche_reflexe" ? sql`ordre, titre`
                : table === "document_kit" ? sql`categorie, titre`
                // Un modèle de message porte un libellé, pas un nom : ce sont
                // les gens et les organisations qui ont des noms. Toute table
                // ajoutée ici doit déclarer son tri — « nom » n'est pas un
                // défaut universel, c'est celui des quatre tables qui en ont un.
                : table === "modele_message" ? sql`libelle`
                : sql`nom`}`;
     order by ${sql(TRI[table] as string[])}`;
}

export async function lireUn<T extends object>(
+5 −0
Changes for apps/api/src/verif.ts: 5 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ import { verifierContinuite } from "./verification/continuite.js";
import { verifierVeille } from "./verification/veille.js";
import { interceptet } from "./verification/boite.js";
import { verifierPreparation } from "./verification/preparation.js";
import { verifierTriDossier } from "./verification/tri.js";
import { verifierConduite } from "./verification/conduite.js";
import { verifierChat } from "./verification/chat.js";
import { verifierDocuments } from "./verification/documents.js";
@@ -143,6 +144,10 @@ try {

console.log("\nMigrations :");
const faites = await migrer(sql);

// Avant toute lecture du dossier : un tri qui désigne une colonne absente
// fait tomber la suite là où elle lit, et non là où le défaut se trouve.
await verifierTriDossier(sql, rapport);
if (faites.length === 0) console.log("  (aucune, base à jour)");

console.log("\nJeu d'essai :");
+36 −0
Changes for apps/api/src/verification/tri.ts: 36 added lines, 0 removed lines.
Original line number Diff line number Diff line
import type { Sql } from "../db/client.js";
import { TRI } from "../preparation/dossier.js";
import type { Rapport } from "./rapport.js";

/**
 * Le tri déclaré de chaque table du dossier désigne-t-il une colonne réelle ?
 *
 * `Record<TablePreparation, …>` oblige à déclarer un tri ; il ne dit pas que
 * la colonne existe. C'est l'autre moitié du piège : « nom » avait été déclaré
 * pour modele_message, dont la table porte un libelle, et le défaut ne se
 * voyait qu'à la première lecture du dossier — sous une erreur de Postgres qui
 * ne nommait ni la table ni le tri fautif.
 *
 * Ce contrôle passe avant tous les autres, et c'est délibéré : après la
 * première lecture, il n'aurait plus rien à dire que la chute n'ait déjà dit,
 * et plus mal.
 */
export async function verifierTriDossier(sql: Sql, r: Rapport): Promise<void> {
  r.titre("Tri du dossier de préparation");

  const colonnes = await sql<{ table_name: string; column_name: string }[]>`
    select table_name, column_name from information_schema.columns
     where table_schema = 'public'
       and table_name = any(${Object.keys(TRI)})`;

  for (const [table, tri] of Object.entries(TRI)) {
    const connues = new Set(
      colonnes.filter((c) => c.table_name === table).map((c) => c.column_name),
    );
    const absentes = tri.filter((c) => !connues.has(c));
    r.verifie("EX-05", `le tri de ${table} désigne des colonnes qui existent`,
      connues.size > 0 && absentes.length === 0,
      connues.size === 0 ? "table introuvable"
        : absentes.length > 0 ? `absente(s) : ${absentes.join(", ")}` : "");
  }
}