Ein Abschnitt oder ein einzelner Schritt erscheint nur, wenn seine Bedingung zutrifft — abhaengig von einer Variable des Laufs oder vom Zustand eines anderen Schritts. Der echte Fall steht im Seed: "Mitarbeiter-Onboarding" traegt beide Konto-Abschnitte, fuenf AD-Schritte und vier fuer Entra ID, und `konto_typ` sagt beim Start, welcher gilt. Bisher klickte ein Techniker die anderen neun bei jedem Onboarding weg. Entscheidungen, alle im Issue begruendet: - Laufend ausgewertet, nicht beim Start festgeschrieben. Fuer die Abteilungsabschnitte (#031) ist Festschreiben richtig; fuer Bedingungen nicht — die Antwort auf "ist Pruefen fehlgeschlagen?" entsteht erst waehrend der Arbeit. Ein struktureller Test prueft, dass alle drei Schichten (Abschlussregel, Abarbeitung, Druck) die Auswertung rufen. - Nicht entscheidbar heisst: gilt. Ein Lauf ohne konto_typ zeigt beide Abschnitte — genau wie vorher. Ein Abschnitt zu viel ist sichtbar und korrigierbar, ein fehlender faellt niemandem auf. - OFFEN ist eine Antwort, keine Luecke: waere es unentscheidbar, waere ein bedingter Abschnitt von Anfang an sichtbar. - Eine Bedingung, kein Ausdrucksbaum. Kein und, kein oder. - Verwiesen wird ueber Titel-Pfade, nicht ueber Kennungen: vier Wege schreiben eine Struktur neu und geben neue Kennungen. Zwei stille Datenverluste gefunden, der zweite aelter als dieser Meilenstein: - Der Import verwarf `nurFuerAbteilungen`. Der Export schreibt es seit #031, das Schema nimmt es an, der Import setzte es nicht — eine exportierte Vorlage verlor ihre Abteilungsabschnitte und galt dort fuer alle. - Die tiefe Kopie verwarf es ebenfalls, womit jedes "Bearbeiten" einer Vorlage alle Einschraenkungen verlor. Ursache war ein von Hand getippter Zwischentyp, der weniger Felder nannte als die Abfrage liefert; jetzt aus Prisma abgeleitet, beide Kopierwege teilen eine Stelle, und ein Test vergleicht Kopie und Original Feld fuer Feld. Gefunden hat beides die Klickprobe am gebauten Stand, nicht die Suite: im Editor stand "Bedingung gesetzt", nach "Bearbeiten" war sie weg. Dreizehn Gegenproben liefen, alle wurden rot — und der teuerste Befund lag ausserhalb von allen. 1499 Vitest-Tests (vorher 1457), 115 E2E-Ablaeufe gruen. Closes #39 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
162 lines
6.4 KiB
PL/PgSQL
162 lines
6.4 KiB
PL/PgSQL
-- ---------------------------------------------------------------------------
|
||
-- HANDGESCHRIEBEN. Prisma erzeugt hier zusaetzlich Loeschbefehle fuer die
|
||
-- zusammengesetzten Fremdschluessel (Mandantengrenze), die Trigramm-Indizes
|
||
-- (Archivsuche), die partiellen eindeutigen Indizes und alle Trigger: es kennt
|
||
-- sie nicht, weil sie sich im Schema nicht ausdruecken lassen. Diese
|
||
-- Loeschbefehle sind bewusst gestrichen.
|
||
--
|
||
-- tests/datenbankregeln.test.ts prueft, dass alle diese Objekte vorhanden sind –
|
||
-- ein versehentlich stehengelassener Loeschbefehl faellt dort sofort auf.
|
||
-- ---------------------------------------------------------------------------
|
||
|
||
-- Bedingte Abschnitte und Schritte (#039).
|
||
|
||
ALTER TABLE "VorlagenAbschnitt" ADD COLUMN "bedingung" JSONB;
|
||
ALTER TABLE "VorlagenSchritt" ADD COLUMN "bedingung" JSONB;
|
||
|
||
/*
|
||
* Die **Form** der Bedingung, in der Datenbank.
|
||
*
|
||
* Das vollstaendige Schema steht in `src/lib/bedingungen.ts` und wird beim
|
||
* Schreiben angewandt. Hier steht, was auch dann gelten muss, wenn jemand an
|
||
* der Service-Schicht vorbei schreibt (CLAUDE.md) — und das ist mehr als
|
||
* Formsache: eine Bedingung mit unbekannter `art` wuerde beim Lesen als „keine
|
||
* Bedingung“ gelten, der Abschnitt waere also sichtbar. Das ist die harmlose
|
||
* Richtung, aber es waere **stillschweigend** falsch, und niemand koennte sagen,
|
||
* warum ein Abschnitt, der eine Bedingung hat, immer erscheint.
|
||
*
|
||
* Geprueft wird deshalb: `art` ist einer der beiden Werte, und die Felder, die
|
||
* diese Art braucht, sind da. Nicht geprueft wird der Inhalt von `zustaende` —
|
||
* ein Array-Element einzeln zu pruefen ginge nur mit einer Unterabfrage, und
|
||
* die gehoert nicht in einen CHECK.
|
||
*/
|
||
ALTER TABLE "VorlagenAbschnitt"
|
||
ADD CONSTRAINT "VorlagenAbschnitt_bedingung_form"
|
||
CHECK (
|
||
"bedingung" IS NULL
|
||
OR (
|
||
"bedingung" ->> 'art' = 'variable'
|
||
AND "bedingung" ->> 'schluessel' IS NOT NULL
|
||
AND "bedingung" ->> 'operator' IN ('ist', 'istNicht', 'gesetzt', 'leer')
|
||
AND (
|
||
"bedingung" ->> 'operator' IN ('gesetzt', 'leer')
|
||
OR btrim(COALESCE("bedingung" ->> 'wert', '')) <> ''
|
||
)
|
||
)
|
||
OR (
|
||
"bedingung" ->> 'art' = 'schritt'
|
||
AND "bedingung" ->> 'schrittId' IS NOT NULL
|
||
AND jsonb_typeof("bedingung" -> 'zustaende') = 'array'
|
||
AND jsonb_array_length("bedingung" -> 'zustaende') > 0
|
||
)
|
||
);
|
||
|
||
ALTER TABLE "VorlagenSchritt"
|
||
ADD CONSTRAINT "VorlagenSchritt_bedingung_form"
|
||
CHECK (
|
||
"bedingung" IS NULL
|
||
OR (
|
||
"bedingung" ->> 'art' = 'variable'
|
||
AND "bedingung" ->> 'schluessel' IS NOT NULL
|
||
AND "bedingung" ->> 'operator' IN ('ist', 'istNicht', 'gesetzt', 'leer')
|
||
AND (
|
||
"bedingung" ->> 'operator' IN ('gesetzt', 'leer')
|
||
OR btrim(COALESCE("bedingung" ->> 'wert', '')) <> ''
|
||
)
|
||
)
|
||
OR (
|
||
"bedingung" ->> 'art' = 'schritt'
|
||
AND "bedingung" ->> 'schrittId' IS NOT NULL
|
||
AND jsonb_typeof("bedingung" -> 'zustaende') = 'array'
|
||
AND jsonb_array_length("bedingung" -> 'zustaende') > 0
|
||
)
|
||
);
|
||
|
||
/*
|
||
* **Eine Schritt-Bedingung zeigt auf einen Schritt derselben Version.**
|
||
*
|
||
* Das ist die eigentliche Regel, und ein CHECK kann sie nicht halten: sie
|
||
* verlangt einen Blick in zwei andere Tabellen. Ein Fremdschluessel auf
|
||
* `VorlagenSchritt` ginge auch nicht — der Wert steckt in einem Json, und
|
||
* „derselbe Version“ steht dort nicht.
|
||
*
|
||
* Ohne diese Pruefung zeigt eine Bedingung nach dem Veroeffentlichen ins Leere:
|
||
* „Bearbeiten“ erzeugt eine **tiefe Kopie** mit neuen Kennungen (CLAUDE.md), und
|
||
* ein Verweis auf die alte Kennung waere dann nicht mehr entscheidbar. Die
|
||
* Auswertung zeigt in diesem Fall an, der Abschnitt erscheint also immer — und
|
||
* niemand koennte sagen, warum die Bedingung nicht wirkt. Genau die Art Fehler,
|
||
* die eine gruene Suite nicht sieht.
|
||
*
|
||
* Geprueft wird beim Schreiben der **Bedingung**, nicht beim Loeschen des
|
||
* Ziels: einen Schritt zu loeschen, auf den eine Bedingung zeigt, ist nur im
|
||
* Entwurf moeglich, und dort faengt es der Dienst mit einer besseren Meldung.
|
||
*/
|
||
CREATE OR REPLACE FUNCTION laufzettel_bedingung_gleiche_version() RETURNS TRIGGER AS $$
|
||
DECLARE
|
||
eigene_version TEXT;
|
||
ziel_version TEXT;
|
||
ziel_id TEXT;
|
||
BEGIN
|
||
IF NEW."bedingung" IS NULL OR NEW."bedingung" ->> 'art' <> 'schritt' THEN
|
||
RETURN NEW;
|
||
END IF;
|
||
|
||
ziel_id := NEW."bedingung" ->> 'schrittId';
|
||
|
||
-- Die eigene Version: am Abschnitt steht sie direkt, am Schritt eine Ebene hoeher.
|
||
IF TG_TABLE_NAME = 'VorlagenAbschnitt' THEN
|
||
eigene_version := NEW."versionId";
|
||
ELSE
|
||
SELECT "versionId" INTO eigene_version
|
||
FROM "VorlagenAbschnitt" WHERE id = NEW."abschnittId";
|
||
END IF;
|
||
|
||
SELECT a."versionId" INTO ziel_version
|
||
FROM "VorlagenSchritt" s
|
||
JOIN "VorlagenAbschnitt" a ON a.id = s."abschnittId"
|
||
WHERE s.id = ziel_id;
|
||
|
||
IF ziel_version IS NULL THEN
|
||
RAISE EXCEPTION
|
||
'Die Bedingung zeigt auf den Schritt %, den es nicht gibt.', ziel_id
|
||
USING ERRCODE = 'integrity_constraint_violation';
|
||
END IF;
|
||
|
||
IF ziel_version <> eigene_version THEN
|
||
RAISE EXCEPTION
|
||
'Die Bedingung zeigt auf einen Schritt einer anderen Version. Nach dem Veroeffentlichen waere sie nicht mehr entscheidbar.'
|
||
USING ERRCODE = 'integrity_constraint_violation';
|
||
END IF;
|
||
|
||
RETURN NEW;
|
||
END;
|
||
$$ LANGUAGE plpgsql;
|
||
|
||
CREATE TRIGGER "VorlagenAbschnitt_bedingung_gleiche_version"
|
||
BEFORE INSERT OR UPDATE ON "VorlagenAbschnitt"
|
||
FOR EACH ROW EXECUTE FUNCTION laufzettel_bedingung_gleiche_version();
|
||
|
||
CREATE TRIGGER "VorlagenSchritt_bedingung_gleiche_version"
|
||
BEFORE INSERT OR UPDATE ON "VorlagenSchritt"
|
||
FOR EACH ROW EXECUTE FUNCTION laufzettel_bedingung_gleiche_version();
|
||
|
||
/*
|
||
* Gegenprobe: die neuen Spalten sind ueberall leer, und die CHECKs lassen die
|
||
* bestehenden Zeilen zu. `ALTER TABLE ... ADD CONSTRAINT` prueft das von sich
|
||
* aus — aber eine Migration, die ihre eigene Annahme nicht nachrechnet, ist
|
||
* genau die Art Zusage, die dieses Projekt teuer bezahlt hat.
|
||
*/
|
||
DO $$
|
||
DECLARE
|
||
mit_bedingung INTEGER;
|
||
BEGIN
|
||
SELECT count(*) INTO mit_bedingung FROM "VorlagenAbschnitt" WHERE "bedingung" IS NOT NULL;
|
||
IF mit_bedingung > 0 THEN
|
||
RAISE EXCEPTION 'Unerwartet % Abschnitte mit Bedingung vor der Migration.', mit_bedingung;
|
||
END IF;
|
||
SELECT count(*) INTO mit_bedingung FROM "VorlagenSchritt" WHERE "bedingung" IS NOT NULL;
|
||
IF mit_bedingung > 0 THEN
|
||
RAISE EXCEPTION 'Unerwartet % Schritte mit Bedingung vor der Migration.', mit_bedingung;
|
||
END IF;
|
||
END $$;
|