feat(bausteine): Abschnitte, die mehrere Vorlagen teilen (#57)
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s

This commit was merged in pull request #57.
This commit is contained in:
2026-10-04 17:07:12 +00:00
parent b688e2ebc8
commit 80870133b8
24 changed files with 2862 additions and 46 deletions
@@ -0,0 +1,160 @@
-- ---------------------------------------------------------------------------
-- HANDGESCHRIEBEN. Prisma erzeugt hier zusaetzlich Loeschbefehle fuer die
-- zusammengesetzten Fremdschluessel (Mandantengrenze), die Trigramm-Indizes
-- (Archivsuche), die partiellen eindeutigen Indizes und die Trigger auf
-- veroeffentlichten Versionen: 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.
-- ---------------------------------------------------------------------------
-- Wiederverwendbare Bausteine (#030).
CREATE TABLE "Baustein" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"beschreibung" TEXT,
"archiviert" BOOLEAN NOT NULL DEFAULT false,
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"geaendertAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Baustein_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "Baustein_name_key" ON "Baustein"("name");
CREATE INDEX "Baustein_archiviert_idx" ON "Baustein"("archiviert");
CREATE TABLE "BausteinVersion" (
"id" TEXT NOT NULL,
"bausteinId" TEXT NOT NULL,
"version" INTEGER NOT NULL,
"status" "VersionStatus" NOT NULL DEFAULT 'ENTWURF',
"titel" TEXT NOT NULL,
"aenderungsnotiz" TEXT,
"veroeffentlichtAm" TIMESTAMP(3),
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"geaendertAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "BausteinVersion_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "BausteinVersion_bausteinId_version_key"
ON "BausteinVersion"("bausteinId", "version");
CREATE INDEX "BausteinVersion_bausteinId_status_idx"
ON "BausteinVersion"("bausteinId", "status");
CREATE TABLE "BausteinSchritt" (
"id" TEXT NOT NULL,
"versionId" TEXT NOT NULL,
"position" INTEGER NOT NULL DEFAULT 0,
"titel" TEXT NOT NULL,
"hilfetext" TEXT,
"typ" "SchrittTyp" NOT NULL DEFAULT 'HAKEN',
"pflicht" BOOLEAN NOT NULL DEFAULT false,
"konfig" JSONB NOT NULL DEFAULT '{}',
"dokuUrl" TEXT,
CONSTRAINT "BausteinSchritt_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "BausteinSchritt_versionId_position_idx"
ON "BausteinSchritt"("versionId", "position");
ALTER TABLE "BausteinVersion"
ADD CONSTRAINT "BausteinVersion_bausteinId_fkey"
FOREIGN KEY ("bausteinId") REFERENCES "Baustein"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "BausteinSchritt"
ADD CONSTRAINT "BausteinSchritt_versionId_fkey"
FOREIGN KEY ("versionId") REFERENCES "BausteinVersion"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- Der Verweis am Abschnitt. RESTRICT, nicht CASCADE: eine Baustein-Version, auf
-- die eine Vorlage zeigt, darf nicht unter ihr wegbrechen.
ALTER TABLE "VorlagenAbschnitt"
ADD COLUMN "bausteinVersionId" TEXT;
CREATE INDEX "VorlagenAbschnitt_bausteinVersionId_idx"
ON "VorlagenAbschnitt"("bausteinVersionId");
ALTER TABLE "VorlagenAbschnitt"
ADD CONSTRAINT "VorlagenAbschnitt_bausteinVersionId_fkey"
FOREIGN KEY ("bausteinVersionId") REFERENCES "BausteinVersion"("id")
ON DELETE RESTRICT ON UPDATE CASCADE;
-- ---------------------------------------------------------------------------
-- Dieselben Garantien wie bei Vorlagen, und zwar in der Datenbank.
--
-- Ein Baustein ist eine Quelle fuer Schritte, die in Laeufe wandern. Stuende
-- seine Unveraenderlichkeit nur im Dienst, liesse sich die Nachvollziehbarkeit
-- des Archivs ueber eine Baustein-Aenderung aushebeln.
-- ---------------------------------------------------------------------------
-- Je Baustein hoechstens ein Entwurf.
CREATE UNIQUE INDEX "BausteinVersion_ein_entwurf"
ON "BausteinVersion" ("bausteinId")
WHERE "status" = 'ENTWURF';
-- Eine veroeffentlichte Baustein-Version ist unveraenderlich.
CREATE OR REPLACE FUNCTION laufzettel_baustein_unveraenderlich() RETURNS trigger AS $$
DECLARE
betroffene_version TEXT;
aktueller_status TEXT;
BEGIN
betroffene_version := COALESCE(NEW."versionId", OLD."versionId");
SELECT status::TEXT INTO aktueller_status
FROM "BausteinVersion" WHERE id = betroffene_version;
IF aktueller_status = 'VEROEFFENTLICHT' THEN
RAISE EXCEPTION
'Baustein-Version % ist veroeffentlicht und unveraenderlich (%).',
betroffene_version, TG_TABLE_NAME
USING ERRCODE = 'integrity_constraint_violation';
END IF;
RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "BausteinSchritt_unveraenderlich"
BEFORE INSERT OR UPDATE OR DELETE ON "BausteinSchritt"
FOR EACH ROW EXECUTE FUNCTION laufzettel_baustein_unveraenderlich();
-- Der Weg zurueck waere die Hintertuer, durch die sich das Obige aushebeln
-- liesse. Dieselbe Regel wie bei VorlagenVersion.
CREATE OR REPLACE FUNCTION laufzettel_baustein_status_einbahn() RETURNS trigger AS $$
BEGIN
IF OLD.status = 'VEROEFFENTLICHT' AND NEW.status <> 'VEROEFFENTLICHT' THEN
RAISE EXCEPTION
'Eine veroeffentlichte Baustein-Version laesst sich nicht wieder zum Entwurf machen.'
USING ERRCODE = 'integrity_constraint_violation';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "BausteinVersion_status_einbahn"
BEFORE UPDATE ON "BausteinVersion"
FOR EACH ROW EXECUTE FUNCTION laufzettel_baustein_status_einbahn();
-- Ein Verweis zeigt nur auf eine **veroeffentlichte** Baustein-Version. Einen
-- Entwurf einzusetzen hiesse, beim Veroeffentlichen etwas zu kopieren, das sich
-- danach noch aendert.
CREATE OR REPLACE FUNCTION laufzettel_baustein_verweis_veroeffentlicht() RETURNS trigger AS $$
DECLARE
verwiesener_status TEXT;
BEGIN
IF NEW."bausteinVersionId" IS NULL THEN
RETURN NEW;
END IF;
SELECT status::TEXT INTO verwiesener_status
FROM "BausteinVersion" WHERE id = NEW."bausteinVersionId";
IF verwiesener_status <> 'VEROEFFENTLICHT' THEN
RAISE EXCEPTION
'Ein Abschnitt kann nur auf eine veroeffentlichte Baustein-Version verweisen.'
USING ERRCODE = 'integrity_constraint_violation';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "VorlagenAbschnitt_baustein_veroeffentlicht"
BEFORE INSERT OR UPDATE ON "VorlagenAbschnitt"
FOR EACH ROW EXECUTE FUNCTION laufzettel_baustein_verweis_veroeffentlicht();
+76 -6
View File
@@ -464,13 +464,83 @@ model VorlagenVariable {
}
model VorlagenAbschnitt {
id String @id @default(cuid())
versionId String
titel String
position Int @default(0)
id String @id @default(cuid())
versionId String
titel String
position Int @default(0)
/// Verweis auf einen wiederverwendbaren Baustein (#030).
///
/// In einem **Entwurf** heisst das: beim Veroeffentlichen hier die Schritte des
/// Bausteins einsetzen. In einer **veroeffentlichten** Version heisst dasselbe
/// Feld: diese Schritte stammen von dort — es ist dann nur noch Herkunft.
/// Welche der beiden Lesarten gilt, sagt der Status der Version; ein zweites
/// Feld dafuer waere eine zweite Wahrheit.
bausteinVersionId String?
version VorlagenVersion @relation(fields: [versionId], references: [id], onDelete: Cascade)
schritte VorlagenSchritt[]
version VorlagenVersion @relation(fields: [versionId], references: [id], onDelete: Cascade)
bausteinVersion BausteinVersion? @relation(fields: [bausteinVersionId], references: [id], onDelete: Restrict)
schritte VorlagenSchritt[]
@@index([versionId, position])
@@index([bausteinVersionId])
}
// ---- Bausteine -------------------------------------------------------------
/// Ein Abschnitt, den mehrere Vorlagen teilen (#030).
///
/// Versioniert nach denselben Regeln wie eine Vorlage: hoechstens ein Entwurf,
/// eine veroeffentlichte Version wird nie veraendert. Beim Veroeffentlichen
/// einer Vorlage wird der Baustein **tief kopiert** — ein Lauf sieht damit
/// weiterhin konkrete Schritte, und Schrittergebnis, Druckansicht und Archiv
/// muessen nichts lernen.
model Baustein {
id String @id @default(cuid())
/// Eindeutig: der Name ist, wonach ein Redakteur ihn sucht.
name String @unique
beschreibung String?
archiviert Boolean @default(false)
erstelltAm DateTime @default(now())
geaendertAm DateTime @default(now()) @updatedAt
versionen BausteinVersion[]
@@index([archiviert])
}
model BausteinVersion {
id String @id @default(cuid())
bausteinId String
version Int
status VersionStatus @default(ENTWURF)
/// Titel des Abschnitts, der daraus entsteht. Steht an der Version und nicht
/// am Baustein, damit er sich aendern laesst, ohne alte Vorlagen umzuschreiben.
titel String
aenderungsnotiz String?
veroeffentlichtAm DateTime?
erstelltAm DateTime @default(now())
geaendertAm DateTime @default(now()) @updatedAt
baustein Baustein @relation(fields: [bausteinId], references: [id], onDelete: Cascade)
schritte BausteinSchritt[]
verwendungen VorlagenAbschnitt[]
@@unique([bausteinId, version])
@@index([bausteinId, status])
}
model BausteinSchritt {
id String @id @default(cuid())
versionId String
position Int @default(0)
titel String
hilfetext String?
typ SchrittTyp @default(HAKEN)
pflicht Boolean @default(false)
konfig Json @default("{}")
dokuUrl String?
version BausteinVersion @relation(fields: [versionId], references: [id], onDelete: Cascade)
@@index([versionId, position])
}