Commit 38938fbe authored by Kourser's avatar Kourser
Browse files

« Ligne absente » ne voulait pas dire « jamais servie »

Les formes livrées hier n'atteignaient pas les organisations qui en avaient le
plus besoin : celles qui se servent déjà du tableau blanc.

L'amorçage se déclenchait sur l'absence de ligne en base. Or la toile
enregistre la bibliothèque au moindre changement, fût-il vide : toute
organisation ayant une fois ouvert un tableau portait une ligne « [] », et la
livraison la sautait. Le défaut se présentait exactement comme celui qu'on
corrigeait — une bibliothèque vide — ce qui est la pire forme qu'il pouvait
prendre.

« livree_at » dit désormais ce que l'absence prétendait dire. Nul sur les
lignes existantes, à dessein : elles n'ont jamais été servies et le seront à la
prochaine lecture. Les formes s'ajoutent à ce qui est là plutôt que de le
remplacer — une organisation qui avait constitué les siennes les garde.

La garantie ne pouvait pas voir ce cas : elle partait d'une organisation neuve,
où l'absence de ligne et la non-livraison coïncident. Deux contrôles de plus
partent d'une ligne vide et d'une bibliothèque déjà peuplée, et l'ancien
comportement les fait rougir.

Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
parent 1ae686e4
Loading
Loading
Loading
Loading
Loading
+19 −0
Changes for apps/api/src/db/migrations/0044_bibliotheque_livree.sql: 19 added lines, 0 removed lines.
Original line number Diff line number Diff line
-- ═══════════════════════════════════════════════════════════════════════════
-- Dire explicitement que les formes livrées ont été posées (EF-1002a).
--
-- La livraison se déclenchait sur l'absence de ligne. C'était un mauvais
-- substitut : la toile enregistre la bibliothèque au moindre changement, y
-- compris vide, si bien qu'une organisation ayant déjà ouvert un tableau
-- portait une ligne « [] » — et ne recevait donc jamais rien. Le défaut se
-- présentait exactement comme celui qu'on corrigeait : une bibliothèque vide.
--
-- « livree_at » dit ce que l'absence prétendait dire. Nul sur les lignes
-- existantes, à dessein : elles n'ont jamais été servies, et le seront à la
-- prochaine lecture.
-- ═══════════════════════════════════════════════════════════════════════════

alter table bibliotheque_tableau
  add column if not exists livree_at timestamptz;

comment on column bibliotheque_tableau.livree_at is
  'EF-1002a : quand les formes livrées ont été posées. Nul = jamais servie.';
+23 −12
Changes for apps/api/src/domaine/bibliotheque.ts: 23 added lines, 12 removed lines.
Original line number Diff line number Diff line
@@ -30,10 +30,12 @@ export const PLAFOND_OCTETS = 2 * 1024 * 1024;
 * si elle a été constituée avant, et rien n'est constitué avant si tout est à
 * dessiner. Les formes livrées sont donc posées à la première lecture.
 *
 * Posées, et non ajoutées à chaque lecture : l'existence de la ligne dit que
 * l'organisation a été servie. Ce qu'elle en retire ensuite reste retiré —
 * une bibliothèque volontairement vidée le demeure, là où une injection
 * permanente ferait revenir des formes que quelqu'un avait décidé d'ôter.
 * Posées une fois, et « livree_at » le dit. L'absence de ligne ne le disait
 * pas : la toile enregistre la bibliothèque au moindre changement, fût-il
 * vide, si bien qu'une organisation ayant déjà ouvert un tableau portait une
 * ligne « [] » et ne recevait jamais rien — le défaut qu'on corrigeait, à
 * l'identique. Ce qu'elle en retire ensuite reste retiré : une bibliothèque
 * volontairement vidée le demeure.
 *
 * L'écriture se fait à la lecture, ce qui n'est pas élégant. L'alternative
 * serait de semer à la création de l'organisation, et de rattraper les
@@ -42,16 +44,25 @@ export const PLAFOND_OCTETS = 2 * 1024 * 1024;
 * elles changent.
 */
export async function lire(sql: Sql, organisationId: string): Promise<unknown[]> {
  const [b] = await sql<{ items: unknown[] }[]>`
    select items from bibliotheque_tableau where organisation_id = ${organisationId}`;
  if (b) return b.items;
  const [b] = await sql<{ items: unknown[]; livree_at: Date | null }[]>`
    select items, livree_at from bibliotheque_tableau
     where organisation_id = ${organisationId}`;
  if (b?.livree_at) return b.items;

  const livrees = bibliothequeLivree();
  // Ajoutées à ce qui est là, jamais substituées : une organisation qui a
  // constitué ses propres formes avant cette livraison les garde.
  const deja = new Set((b?.items ?? []).map(
    (i) => (i as { id?: unknown }).id));
  const fusion = [
    ...(b?.items ?? []),
    ...bibliothequeLivree().filter((f) => !deja.has(f["id"])),
  ];
  await sql`
    insert into bibliotheque_tableau (organisation_id, items)
    values (${organisationId}, ${sql.json(livrees as never)})
    on conflict (organisation_id) do nothing`;
  return livrees;
    insert into bibliotheque_tableau (organisation_id, items, livree_at)
    values (${organisationId}, ${sql.json(fusion as never)}, now())
    on conflict (organisation_id) do update
      set items = excluded.items, livree_at = now()`;
  return fusion;
}

/** Lue depuis une crise : le canevas ne connaît que celle-ci. */
+31 −0
Changes for apps/api/src/verification/tableaux.ts: 31 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -117,6 +117,37 @@ export async function verifierTableaux(
  r.verifie("EF-1002a", "vidée volontairement, elle le reste",
    (await bibliotheque.lirePourCrise(sql, crise.criseId)).length === 0);

  /**
   * Une organisation qui a déjà ouvert un tableau porte une ligne vide.
   *
   * C'est le cas qui a fait manquer la livraison : la toile enregistre la
   * bibliothèque au moindre changement, fût-il vide, si bien que l'absence de
   * ligne ne disait pas « jamais servie ». Le contrôle qui précède ne pouvait
   * pas le voir — il partait d'une organisation neuve, où l'absence et la
   * non-livraison coïncident.
   */
  const [orgAvantLivraison] = await sql<{ id: string }[]>`
    insert into organisation (nom) values (${"Tableau déjà ouvert"}) returning id`;
  await sql`
    insert into bibliotheque_tableau (organisation_id, items)
    values (${orgAvantLivraison!.id}, '[]'::jsonb)`;
  r.verifie("EF-1002a", "une organisation qui avait déjà une ligne vide est servie aussi",
    (await bibliotheque.lire(sql, orgAvantLivraison!.id)).length === livree.length);

  // Et ce qu'elle avait constitué elle-même n'est pas remplacé.
  const [orgAvecFormes] = await sql<{ id: string }[]>`
    insert into organisation (nom) values (${"Formes maison"}) returning id`;
  await sql`
    insert into bibliotheque_tableau (organisation_id, items)
    values (${orgAvecFormes!.id},
            ${sql.json([{ id: "maison-1", status: "unpublished", name: "Baie",
              elements: [] }] as never)})`;
  const fusionnee = await bibliotheque.lire(sql, orgAvecFormes!.id) as { id: string }[];
  r.verifie("EF-1002a", "ses formes à elle sont gardées, les livrées s'y ajoutent",
    fusionnee.length === livree.length + 1
      && fusionnee.some((f) => f.id === "maison-1"),
    `${fusionnee.length} formes`);

  const formes = [
    { status: "published", id: "serveur", elements: [{ type: "rectangle", id: "a" }] },
    { status: "published", id: "site", elements: [{ type: "ellipse", id: "b" }] },