Eine veroeffentlichte Vorlagen-Version ist fuer immer unveraenderlich — daran haengt, dass ein archivierter Lauf in zehn Jahren noch genau die Schritte zeigt, mit denen er gestartet wurde. Die Regel steht deshalb zweimal: im Service, der die verstaendliche Meldung gibt, und in der Datenbank, die die Garantie gibt. Vier Trigger fangen Schreibzugriffe ab, die nicht durch den Service kommen — Migrationen, psql, der Seed und ab Phase 4 die Connectoren — und versperren den Rueckweg nach ENTWURF, der alles andere ausgehebelt haette. - Service: anlegen, Kopfdaten bearbeiten, Entwurf als tiefe Kopie der letzten veroeffentlichten Version, speichern, veroeffentlichen, verwerfen, duplizieren, archivieren - Hoechstens ein Entwurf je Vorlage, durchgesetzt vom partiellen Index; zwei gleichzeitige Klicks auf „Bearbeiten“ bekommen denselben Entwurf statt eines Fehlers - Optimistisches Sperren ueber geaendertAm: wer auf einem ueberholten Stand speichert, wird abgewiesen statt die Arbeit des anderen spurlos zu ersetzen - JSON-Export und -Import samt Format-Dokumentation; der Export ist nachsichtig, der Import streng - src/lib/validierung.ts als einzige Zod-Quelle mit deutschen Meldungen, per ESLint-Regel erzwungen Der unabhaengige Review fand drei Blocker, alle an der Mandantengrenze: der Suchfilter brachte ein zweites OR mit und ueberschrieb die Kundenbedingung, Historie und Aufbau einer Version filterten gar nicht. Zu queries/vorlagen.ts gab es keinen Test — jetzt gibt es tests/vorlagen-sicht.test.ts. 428 Tests gruen (vorher 336). Schliesst #009 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
96 lines
3.7 KiB
TypeScript
96 lines
3.7 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { datenbankVerfuegbar, testPrisma } from "./helpers/db";
|
|
|
|
/**
|
|
* Wächter über die Datenbankobjekte, die Prisma nicht kennt (#009).
|
|
*
|
|
* Zusammengesetzte Fremdschlüssel, Trigramm-Indizes und Trigger lassen sich im
|
|
* Prisma-Schema nicht ausdrücken. Deshalb erzeugt `prisma migrate dev` bei
|
|
* **jeder** künftigen Migration Löschbefehle für sie — sie sind aus Prismas
|
|
* Sicht Abweichungen von der Vorgabe. Wer die Löschbefehle einmal übersieht,
|
|
* entfernt stillschweigend die Mandantengrenze, die Archivsuche oder den Schutz
|
|
* der Unveränderlichkeit.
|
|
*
|
|
* Dieser Test macht das laut. Er prüft nicht, dass die Regeln *wirken* — das tun
|
|
* `tests/datenmodell.test.ts` und `tests/vorlagen.test.ts` —, sondern nur, dass
|
|
* die Objekte überhaupt noch da sind.
|
|
*/
|
|
|
|
async function namen(abfrage: string): Promise<string[]> {
|
|
const zeilen = await testPrisma.$queryRawUnsafe<{ name: string }[]>(abfrage);
|
|
return zeilen.map((zeile) => zeile.name).sort();
|
|
}
|
|
|
|
describe.runIf(datenbankVerfuegbar)("Handgeschriebene Datenbankobjekte", () => {
|
|
it("hat die zusammengesetzten Fremdschlüssel der Mandantengrenze", async () => {
|
|
// Ohne sie kann ein Lauf ein Gerät eines fremden Kunden ziehen.
|
|
expect(
|
|
await namen(`SELECT conname AS name FROM pg_constraint WHERE conname LIKE '%gleicher_kunde'`),
|
|
).toEqual([
|
|
"Geraet_besitzer_gleicher_kunde",
|
|
"Lauf_abteilung_gleicher_kunde",
|
|
"Lauf_geraet_gleicher_kunde",
|
|
"Lauf_mitarbeiter_gleicher_kunde",
|
|
]);
|
|
});
|
|
|
|
it("hat die Trigramm-Indizes für die Archivsuche", async () => {
|
|
// Ohne sie wird jede Archivsuche zum vollständigen Durchlauf der Tabelle.
|
|
expect(
|
|
await namen(
|
|
`SELECT indexname AS name FROM pg_indexes WHERE schemaname='public' AND indexdef LIKE '%gin_trgm_ops%'`,
|
|
),
|
|
).toEqual([
|
|
"Kommentar_rumpf_trgm_idx",
|
|
"Lauf_titel_trgm_idx",
|
|
"Schrittergebnis_notiz_trgm_idx",
|
|
"Schrittergebnis_wert_trgm_idx",
|
|
]);
|
|
});
|
|
|
|
it("hat die Trigger, die eine veröffentlichte Version schützen", async () => {
|
|
expect(await namen(`SELECT tgname AS name FROM pg_trigger WHERE NOT tgisinternal`)).toEqual([
|
|
"VorlagenAbschnitt_unveraenderlich",
|
|
"VorlagenSchritt_unveraenderlich",
|
|
"VorlagenVariable_unveraenderlich",
|
|
"VorlagenVersion_status_einbahn",
|
|
]);
|
|
});
|
|
|
|
it("hat die partiellen Eindeutigkeitsindizes", async () => {
|
|
expect(
|
|
await namen(
|
|
`SELECT indexname AS name FROM pg_indexes WHERE schemaname='public' AND indexdef LIKE '%WHERE%'`,
|
|
),
|
|
).toEqual(["Kunde_eigene_organisation_einmalig", "VorlagenVersion_ein_entwurf"]);
|
|
});
|
|
|
|
it("hat alle Prüfbedingungen", async () => {
|
|
const vorhanden = await namen(
|
|
`SELECT conname AS name FROM pg_constraint WHERE contype='c' AND connamespace='public'::regnamespace`,
|
|
);
|
|
// Namentlich aufgezählt, damit auch das Verschwinden einer einzelnen auffällt.
|
|
expect(vorhanden).toEqual([
|
|
"Einstellung_einzelne_zeile",
|
|
"Geraet_registriert_braucht_namen",
|
|
"Geraet_tagcode_form",
|
|
"Kunde_kuerzel_form",
|
|
"Lauf_abgeschlossen_mit_zeitpunkt",
|
|
"Lauf_archiviert_mit_zeitpunkt",
|
|
"Lauf_begonnen_mit_zeitpunkt",
|
|
"Lauf_mitarbeiter_eindeutig",
|
|
"Schrittergebnis_begruendung_noetig",
|
|
"TagCodeVergabe_form",
|
|
"Verzeichnisquelle_bindpasswort_verschluesselt",
|
|
"VorlagenVersion_veroeffentlicht_mit_zeitpunkt",
|
|
]);
|
|
});
|
|
|
|
it("hat die Erweiterung für die Trigramm-Suche", async () => {
|
|
const zeilen = await testPrisma.$queryRaw<{ extname: string }[]>`
|
|
SELECT extname FROM pg_extension WHERE extname = 'pg_trgm'
|
|
`;
|
|
expect(zeilen).toHaveLength(1);
|
|
});
|
|
});
|