-- ============================================================================= -- Phase 1: Mandantentrennung, Lagerungsregel und Suchindizes. -- -- Die vorige Migration hat die Tabellen angelegt. Ohne Row Level Security wären -- sie für die Anwendungsrolle offen — jede neue Tabelle muss deshalb dieselbe -- Behandlung bekommen wie die aus Phase 0. -- -- Dazu kommen zwei Dinge, die sich nur in der Datenbank sauber durchsetzen -- lassen: die Regel, wo ein Gerät liegen darf, und schnelle Volltextsuche. -- ============================================================================= -- ----------------------------------------------------------------------------- -- Row Level Security für die neuen Tabellen -- ----------------------------------------------------------------------------- DO $$ DECLARE tabelle text; BEGIN FOREACH tabelle IN ARRAY ARRAY[ 'standort', 'geraeteraum', 'geraetetyp', 'qualifikationstyp', 'qualifikation' ] LOOP EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', tabelle); EXECUTE format( 'CREATE POLICY %I ON %I FOR ALL ' 'USING ("organisationId" = aktuelle_organisation()) ' 'WITH CHECK ("organisationId" = aktuelle_organisation())', tabelle || '_mandant', tabelle ); END LOOP; END; $$; -- ----------------------------------------------------------------------------- -- Bestandsdaten auf die neue Lagerungsregel bringen. -- -- Vor Phase 1 konnte ein Gerät nur einen Freitext-Lagerort haben. Damit die -- Prüfbedingung weiter unten greift, braucht jedes Gerät ohne Fahrzeug jetzt -- einen echten Standort. Je Wehr wird dafür ein Gerätehaus angelegt, sofern -- noch keines existiert. -- ----------------------------------------------------------------------------- INSERT INTO "standort" ("id", "organisationId", "name", "kuerzel", "art", "erstelltAm", "geaendertAm") SELECT gen_random_uuid(), o."id", 'Gerätehaus', 'GH', 'GERAETEHAUS', now(), now() FROM "organisation" o WHERE NOT EXISTS ( SELECT 1 FROM "standort" s WHERE s."organisationId" = o."id" ); UPDATE "geraet" g SET "standortId" = ( SELECT s."id" FROM "standort" s WHERE s."organisationId" = g."organisationId" AND s."art" = 'GERAETEHAUS' ORDER BY s."erstelltAm" LIMIT 1 ) WHERE g."fahrzeugId" IS NULL AND g."standortId" IS NULL; -- Fahrzeuge dem Gerätehaus zuordnen, sofern noch kein Standort gesetzt ist. UPDATE "fahrzeug" f SET "standortId" = ( SELECT s."id" FROM "standort" s WHERE s."organisationId" = f."organisationId" AND s."art" = 'GERAETEHAUS' ORDER BY s."erstelltAm" LIMIT 1 ) WHERE f."standortId" IS NULL; -- ----------------------------------------------------------------------------- -- Wo ein Gerät liegen darf. -- -- Entweder es steht an einem Standort — dann ist kein Fahrzeug gesetzt. Oder es -- ist auf einem Fahrzeug verlastet — dann ist kein Standort gesetzt, und der -- Geräteraum darf angegeben werden, muss aber nicht (manches liegt lose in der -- Kabine). Beides gleichzeitig oder gar nichts ist eine Falschangabe. -- -- Dass ein angegebener Geräteraum auch wirklich zum angegebenen Fahrzeug -- gehört, lässt sich als CHECK nicht ausdrücken — das prüft der Trigger -- darunter. -- ----------------------------------------------------------------------------- ALTER TABLE "geraet" ADD CONSTRAINT "geraet_lagerung_eindeutig" CHECK ( ("standortId" IS NOT NULL AND "fahrzeugId" IS NULL AND "geraeteraumId" IS NULL) OR ("standortId" IS NULL AND "fahrzeugId" IS NOT NULL) ); CREATE OR REPLACE FUNCTION geraeteraum_gehoert_zum_fahrzeug() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE raum_fahrzeug uuid; BEGIN IF NEW."geraeteraumId" IS NULL THEN RETURN NEW; END IF; SELECT "fahrzeugId" INTO raum_fahrzeug FROM "geraeteraum" WHERE "id" = NEW."geraeteraumId"; IF raum_fahrzeug IS DISTINCT FROM NEW."fahrzeugId" THEN RAISE EXCEPTION 'Der Geraeteraum gehoert nicht zu dem angegebenen Fahrzeug.' USING ERRCODE = 'check_violation'; END IF; RETURN NEW; END; $$; CREATE TRIGGER geraet_raum_pruefen BEFORE INSERT OR UPDATE OF "fahrzeugId", "geraeteraumId" ON "geraet" FOR EACH ROW EXECUTE FUNCTION geraeteraum_gehoert_zum_fahrzeug(); -- ----------------------------------------------------------------------------- -- Volltextsuche. -- -- Die Listen sollen auf Tippen reagieren, auch bei einigen tausend Geräten. -- pg_trgm macht ILIKE '%text%' indexfähig — ohne diese Indizes läuft jede -- Suche als vollständiger Tabellendurchlauf. -- ----------------------------------------------------------------------------- CREATE INDEX "geraet_suche_bezeichnung" ON "geraet" USING gin ("bezeichnung" gin_trgm_ops); CREATE INDEX "geraet_suche_inventarnummer" ON "geraet" USING gin ("inventarnummer" gin_trgm_ops); CREATE INDEX "geraet_suche_seriennummer" ON "geraet" USING gin ("seriennummer" gin_trgm_ops); CREATE INDEX "geraet_suche_hersteller" ON "geraet" USING gin ("hersteller" gin_trgm_ops); CREATE INDEX "fahrzeug_suche_funkrufname" ON "fahrzeug" USING gin ("funkrufname" gin_trgm_ops); CREATE INDEX "fahrzeug_suche_bezeichnung" ON "fahrzeug" USING gin ("bezeichnung" gin_trgm_ops); CREATE INDEX "fahrzeug_suche_kennzeichen" ON "fahrzeug" USING gin ("kennzeichen" gin_trgm_ops); CREATE INDEX "person_suche_nachname" ON "person" USING gin ("nachname" gin_trgm_ops); CREATE INDEX "person_suche_vorname" ON "person" USING gin ("vorname" gin_trgm_ops); CREATE INDEX "geraetetyp_suche_name" ON "geraetetyp" USING gin ("name" gin_trgm_ops); CREATE INDEX "standort_suche_name" ON "standort" USING gin ("name" gin_trgm_ops); -- Räumliche Abfragen auf Standorte, ab Phase 6 gebraucht. CREATE INDEX "standort_koordinaten" ON "standort" USING gist ("koordinaten"); -- ----------------------------------------------------------------------------- -- Rechte für die Anwendungsrolle auf die neuen Tabellen. -- ----------------------------------------------------------------------------- DO $$ BEGIN IF EXISTS (SELECT FROM pg_roles WHERE rolname = 'florian_app') THEN EXECUTE 'GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO florian_app'; EXECUTE 'GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO florian_app'; EXECUTE 'GRANT EXECUTE ON FUNCTION aktuelle_organisation() TO florian_app'; -- Die Einschraenkungen aus Phase 0 gelten unveraendert weiter. EXECUTE 'REVOKE INSERT, UPDATE, DELETE ON "berechtigung" FROM florian_app'; EXECUTE 'REVOKE UPDATE, DELETE ON "audit_log" FROM florian_app'; ELSE RAISE NOTICE 'Rolle florian_app nicht vorhanden — Rechtevergabe uebersprungen.'; END IF; END; $$;