Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die README; an keiner Stelle Bedeutung, nur Zeichensetzung.
244 lines
11 KiB
PL/PgSQL
244 lines
11 KiB
PL/PgSQL
-- Phase 4: Personalakten, Dokumente, Beteiligte, Verknüpfungen.
|
|
--
|
|
-- Von Hand geschrieben, weil zwei Tabellen **umbenannt** werden. Prisma würde
|
|
-- daraus DROP und CREATE machen und dabei die Vorlagen und die laufenden
|
|
-- Nummernkreise verlieren. Siehe docs/13-entscheidungen.md.
|
|
|
|
-- ===========================================================================
|
|
-- 1. CaseTemplate -> DocumentTemplate
|
|
-- Eine Vorlage beschreibt jetzt auch Dokumente, nicht nur Fallarten.
|
|
-- ===========================================================================
|
|
|
|
ALTER TABLE "CaseTemplate" RENAME TO "DocumentTemplate";
|
|
ALTER TABLE "DocumentTemplate" RENAME CONSTRAINT "CaseTemplate_pkey" TO "DocumentTemplate_pkey";
|
|
ALTER INDEX "CaseTemplate_key_version_key" RENAME TO "DocumentTemplate_key_version_key";
|
|
ALTER INDEX "CaseTemplate_key_isActive_idx" RENAME TO "DocumentTemplate_key_isActive_idx";
|
|
|
|
CREATE TYPE "TemplateUse" AS ENUM ('CASE', 'CASE_DOCUMENT', 'PERSON_DOCUMENT', 'STANDALONE');
|
|
CREATE TYPE "DocumentLayout" AS ENUM ('FORM', 'PROSE', 'NOTE');
|
|
CREATE TYPE "DocumentStatus" AS ENUM ('DRAFT', 'FINAL', 'RETRACTED');
|
|
|
|
ALTER TABLE "DocumentTemplate"
|
|
ADD COLUMN "uses" "TemplateUse"[] NOT NULL DEFAULT ARRAY['CASE']::"TemplateUse"[],
|
|
ADD COLUMN "layout" "DocumentLayout" NOT NULL DEFAULT 'FORM';
|
|
|
|
-- ===========================================================================
|
|
-- 2. CaseCounter -> NumberSeries
|
|
-- Aktenzeichen und Dokumentnummern laufen über denselben Zähler, getrennt
|
|
-- durch das Präfix aus der Vorlage. Die bestehenden Stände bleiben stehen.
|
|
-- ===========================================================================
|
|
|
|
ALTER TABLE "CaseCounter" RENAME TO "NumberSeries";
|
|
ALTER TABLE "NumberSeries" RENAME CONSTRAINT "CaseCounter_pkey" TO "NumberSeries_pkey";
|
|
|
|
-- ===========================================================================
|
|
-- 3. Personalakte: Herkunft und Freitext im Kopf
|
|
-- ===========================================================================
|
|
|
|
ALTER TABLE "PersonnelFile"
|
|
ADD COLUMN "note" TEXT,
|
|
ADD COLUMN "createdById" TEXT;
|
|
|
|
-- ===========================================================================
|
|
-- 4. Verknüpfungen: wer sie gezogen hat
|
|
-- ===========================================================================
|
|
|
|
ALTER TABLE "CaseLink" ADD COLUMN "createdById" TEXT;
|
|
|
|
-- ===========================================================================
|
|
-- 5. Dokumente
|
|
-- ===========================================================================
|
|
|
|
CREATE TABLE "Document" (
|
|
"id" TEXT NOT NULL,
|
|
"documentNumber" TEXT NOT NULL,
|
|
"title" TEXT NOT NULL,
|
|
"templateId" TEXT NOT NULL,
|
|
"templateKey" TEXT NOT NULL,
|
|
"templateVersion" INTEGER NOT NULL,
|
|
"layout" "DocumentLayout" NOT NULL DEFAULT 'FORM',
|
|
"status" "DocumentStatus" NOT NULL DEFAULT 'DRAFT',
|
|
"classification" INTEGER NOT NULL DEFAULT 1,
|
|
"caseId" TEXT,
|
|
"personnelFileId" TEXT,
|
|
"createdById" TEXT NOT NULL,
|
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
"finalizedAt" TIMESTAMP(3),
|
|
"finalizedById" TEXT,
|
|
"retractedAt" TIMESTAMP(3),
|
|
"retractedById" TEXT,
|
|
"retractionReason" TEXT,
|
|
"searchVector" tsvector,
|
|
|
|
CONSTRAINT "Document_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- Ein Dokument gehört zu höchstens einem Besitzer. Ein Bußgeld steht für sich,
|
|
-- ein Sonderarbeitsvertrag an einer Person, ein Vorfallsbericht an einer Akte.
|
|
-- Beides zugleich wäre eine Aussage über die Sichtbarkeit, die niemand prüfen
|
|
-- könnte: die Prüfung müsste sich für einen der beiden Besitzer entscheiden.
|
|
ALTER TABLE "Document" ADD CONSTRAINT "Document_one_owner"
|
|
CHECK (("caseId" IS NULL) OR ("personnelFileId" IS NULL));
|
|
|
|
CREATE UNIQUE INDEX "Document_documentNumber_key" ON "Document"("documentNumber");
|
|
CREATE INDEX "Document_caseId_createdAt_idx" ON "Document"("caseId", "createdAt" DESC);
|
|
CREATE INDEX "Document_personnelFileId_createdAt_idx" ON "Document"("personnelFileId", "createdAt" DESC);
|
|
CREATE INDEX "Document_classification_idx" ON "Document"("classification");
|
|
CREATE INDEX "Document_templateKey_idx" ON "Document"("templateKey");
|
|
CREATE INDEX "Document_createdById_idx" ON "Document"("createdById");
|
|
|
|
ALTER TABLE "Document" ADD CONSTRAINT "Document_templateId_fkey"
|
|
FOREIGN KEY ("templateId") REFERENCES "DocumentTemplate"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
ALTER TABLE "Document" ADD CONSTRAINT "Document_caseId_fkey"
|
|
FOREIGN KEY ("caseId") REFERENCES "Case"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
ALTER TABLE "Document" ADD CONSTRAINT "Document_personnelFileId_fkey"
|
|
FOREIGN KEY ("personnelFileId") REFERENCES "PersonnelFile"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
|
|
CREATE TABLE "DocumentSection" (
|
|
"id" TEXT NOT NULL,
|
|
"documentId" TEXT NOT NULL,
|
|
"fieldKey" TEXT NOT NULL,
|
|
"minClearance" INTEGER NOT NULL DEFAULT 0,
|
|
"currentVersion" INTEGER NOT NULL DEFAULT 1,
|
|
"value" JSONB NOT NULL,
|
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
"updatedById" TEXT NOT NULL,
|
|
|
|
CONSTRAINT "DocumentSection_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
CREATE UNIQUE INDEX "DocumentSection_documentId_fieldKey_key"
|
|
ON "DocumentSection"("documentId", "fieldKey");
|
|
|
|
ALTER TABLE "DocumentSection" ADD CONSTRAINT "DocumentSection_documentId_fkey"
|
|
FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
CREATE TABLE "DocumentSectionVersion" (
|
|
"id" TEXT NOT NULL,
|
|
"sectionId" TEXT NOT NULL,
|
|
"version" INTEGER NOT NULL,
|
|
"value" JSONB NOT NULL,
|
|
"changeReason" TEXT,
|
|
"authorId" TEXT NOT NULL,
|
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
CONSTRAINT "DocumentSectionVersion_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
CREATE UNIQUE INDEX "DocumentSectionVersion_sectionId_version_key"
|
|
ON "DocumentSectionVersion"("sectionId", "version");
|
|
CREATE INDEX "DocumentSectionVersion_sectionId_version_idx"
|
|
ON "DocumentSectionVersion"("sectionId", "version" DESC);
|
|
|
|
ALTER TABLE "DocumentSectionVersion" ADD CONSTRAINT "DocumentSectionVersion_sectionId_fkey"
|
|
FOREIGN KEY ("sectionId") REFERENCES "DocumentSection"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
-- ===========================================================================
|
|
-- 6. Dokumente sind anhängend
|
|
--
|
|
-- Dieselbe Zusicherung wie beim Verlaufseintrag: kein DELETE, weder über
|
|
-- die Anwendung noch über rohes SQL. Wer ein Dokument loswerden will, zieht
|
|
-- es zurück; der Grabstein bleibt sichtbar. Siehe Zusicherung 2 der README.
|
|
-- ===========================================================================
|
|
|
|
CREATE RULE document_no_delete AS ON DELETE TO "Document" DO INSTEAD NOTHING;
|
|
CREATE RULE document_version_no_delete AS ON DELETE TO "DocumentSectionVersion" DO INSTEAD NOTHING;
|
|
|
|
-- Die Versionstabelle wird nicht umgeschrieben. Einzige Ausnahme wie beim
|
|
-- Verlaufseintrag: minClearance darf angehoben werden, damit eine
|
|
-- Höherstufung rückwirkend gilt und der alte Text nicht über die Historie
|
|
-- leckt.
|
|
CREATE OR REPLACE FUNCTION document_version_guard() RETURNS trigger AS $$
|
|
BEGIN
|
|
IF NEW."id" IS DISTINCT FROM OLD."id"
|
|
OR NEW."sectionId" IS DISTINCT FROM OLD."sectionId"
|
|
OR NEW."version" IS DISTINCT FROM OLD."version"
|
|
OR NEW."value" IS DISTINCT FROM OLD."value"
|
|
OR NEW."changeReason" IS DISTINCT FROM OLD."changeReason"
|
|
OR NEW."authorId" IS DISTINCT FROM OLD."authorId"
|
|
OR NEW."createdAt" IS DISTINCT FROM OLD."createdAt" THEN
|
|
RAISE EXCEPTION 'DocumentSectionVersion ist anhängend';
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER document_version_guard_trg
|
|
BEFORE UPDATE ON "DocumentSectionVersion"
|
|
FOR EACH ROW EXECUTE FUNCTION document_version_guard();
|
|
|
|
-- Herkunft, Vorlage und Besitzer eines Dokuments sind unveränderlich. Ein
|
|
-- Vertrag, der nachträglich an einer anderen Person hängt, wäre eine Fälschung.
|
|
CREATE OR REPLACE FUNCTION document_immutable_guard() RETURNS trigger AS $$
|
|
BEGIN
|
|
IF NEW."createdById" IS DISTINCT FROM OLD."createdById"
|
|
OR NEW."createdAt" IS DISTINCT FROM OLD."createdAt"
|
|
OR NEW."caseId" IS DISTINCT FROM OLD."caseId"
|
|
OR NEW."personnelFileId" IS DISTINCT FROM OLD."personnelFileId"
|
|
OR NEW."documentNumber" IS DISTINCT FROM OLD."documentNumber"
|
|
OR NEW."templateId" IS DISTINCT FROM OLD."templateId" THEN
|
|
RAISE EXCEPTION 'Herkunft, Besitzer und Vorlage eines Document sind unveränderlich';
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER document_immutable_guard_trg
|
|
BEFORE UPDATE ON "Document"
|
|
FOR EACH ROW EXECUTE FUNCTION document_immutable_guard();
|
|
|
|
-- ===========================================================================
|
|
-- 7. Volltextsuche für Dokumente
|
|
--
|
|
-- Wie beim Fall: aufgenommen wird nur, was jeder lesen darf, der das
|
|
-- Dokument überhaupt sehen darf. Felder mit minClearance > 0 bleiben
|
|
-- draußen, sonst verrät die Trefferliste sie an niedrigere Stufen.
|
|
-- ===========================================================================
|
|
|
|
CREATE OR REPLACE FUNCTION document_search_vector(target_id text) RETURNS tsvector AS $$
|
|
SELECT
|
|
setweight(to_tsvector('german',
|
|
coalesce((SELECT d."documentNumber" || ' ' || d."title" FROM "Document" d WHERE d."id" = target_id), '')
|
|
), 'A')
|
|
||
|
|
setweight(coalesce((
|
|
SELECT jsonb_to_tsvector('german', jsonb_agg(to_jsonb(strip_redactions(s."value" #>> '{}'))), '["string"]')
|
|
FROM "DocumentSection" s
|
|
WHERE s."documentId" = target_id AND s."minClearance" = 0
|
|
), ''::tsvector), 'B');
|
|
$$ LANGUAGE sql STABLE;
|
|
|
|
-- Wie bei case_refresh_search_vector: mit IF verzweigt, nicht mit einem
|
|
-- CASE-Ausdruck. Der würde auch im nicht genommenen Zweig verlangen, dass
|
|
-- NEW."documentId" existiert - auf "Document" gibt es das Feld nicht.
|
|
CREATE OR REPLACE FUNCTION document_refresh_search_vector() RETURNS trigger AS $$
|
|
DECLARE
|
|
target text;
|
|
BEGIN
|
|
IF TG_TABLE_NAME = 'Document' THEN
|
|
target := NEW."id";
|
|
ELSIF TG_OP = 'DELETE' THEN
|
|
target := OLD."documentId";
|
|
ELSE
|
|
target := NEW."documentId";
|
|
END IF;
|
|
|
|
IF target IS NOT NULL THEN
|
|
UPDATE "Document" SET "searchVector" = document_search_vector(target) WHERE "id" = target;
|
|
END IF;
|
|
RETURN NULL;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER document_search_on_document
|
|
AFTER INSERT OR UPDATE OF "title", "documentNumber" ON "Document"
|
|
FOR EACH ROW EXECUTE FUNCTION document_refresh_search_vector();
|
|
|
|
CREATE TRIGGER document_search_on_section
|
|
AFTER INSERT OR UPDATE OR DELETE ON "DocumentSection"
|
|
FOR EACH ROW EXECUTE FUNCTION document_refresh_search_vector();
|
|
|
|
CREATE INDEX document_search_idx ON "Document" USING GIN ("searchVector");
|
|
CREATE INDEX document_number_trgm_idx ON "Document" USING GIN ("documentNumber" gin_trgm_ops);
|