Kartenmodule ergaenzen
Phase 6: alles, was einen Ort hat — Loeschwasser, Objekte mit Feuerwehrplan, die Lagekarte eines Einsatzes und die geplante Foerderstrecke. Keine Kacheln aus dem Netz. Wo eine Wehr ausrueckt und wo ihre Hydranten liegen, ist niemandes Geschaeft; der Kartenserver laeuft selbst (tileserver-gl im Compose-Profil "karte"). Ohne ihn zeigt die Karte einen leeren Grund und sagt das auch — Punkte, Entfernungen und Umkreissuche funktionieren trotzdem. Dasselbe gilt fuer einen eingetragenen, aber nicht erreichbaren Server. Die Umkreissuche laeuft ueber PostGIS: ST_DWithin auf der Geografie mit GiST-Index rechnet in Metern und nutzt den Index. Gefragt wird waehrend der Fahrt, und bei tausend Hydranten waere alles andere ein vollstaendiger Tabellendurchlauf. Der Import aus GeoJSON und Stadtwerke-CSV ist nachsichtig, aber ehrlich: er erkennt "DN 100", deutsche Kommazahlen, Datumsangaben und vertauschte Achsen — raet aber nichts. Fehlt die Position, wird die Zeile uebersprungen; ohne Statusangabe steht "ungeprueft", nicht "einsatzbereit". 22 taktische Zeichen nach FwDV 102 selbst gezeichnet, die Lagekarte gleicht ueber denselben Ereignisstrom ab wie die Atemschutzueberwachung. Die Wasserfoerderung rechnet Reibung, Hoehe und Pumpenzahl; das Ergebnis wird nie gespeichert, sondern bei jeder Anzeige neu gerechnet. Geht eine Strecke nicht auf, sagt die Ausgabe das. Behoben: die Reibungsbeiwerte lagen im ersten Wurf um den Faktor zehn daneben. Aufgefallen ist es nur, weil die Tests gegen die Tabellenwerte der Ausbildung pruefen und nicht gegen die eigene Implementierung. Dazu ein Abruf fuer abgelegte Dateien (/api/dateien/[id]) — bis hierher gab es nur den Upload. 59 neue Tests (48 Unit, 11 E2E je Browser). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e1480c0c3e
commit
1c1e141666
@@ -0,0 +1,174 @@
|
||||
-- =============================================================================
|
||||
-- Phase 6: Mandantentrennung, Koordinaten und die Indizes für die Karte.
|
||||
--
|
||||
-- Vier Dinge:
|
||||
--
|
||||
-- 1. Row Level Security für die acht neuen Tabellen.
|
||||
--
|
||||
-- 2. Koordinaten doppelt halten. Prisma kann den PostGIS-Typ nicht lesen,
|
||||
-- also stehen Dezimalgrade daneben — und ein Trigger sorgt dafür, dass
|
||||
-- beide nie auseinanderlaufen. Dasselbe Muster wie beim Einsatz aus
|
||||
-- Phase 3, jetzt auch für Standort, Hydrant, Objekt und Foerderpunkt.
|
||||
--
|
||||
-- 3. GiST-Indizes auf den Punkten. Ohne sie ist die Umkreissuche ein
|
||||
-- vollstaendiger Durchlauf der Tabelle — bei tausend Hydranten im
|
||||
-- Ausrueckegebiet auf der Anfahrt zu langsam.
|
||||
--
|
||||
-- 4. Volltextsuche ueber Objekte: der Name allein genuegt nicht, gesucht
|
||||
-- wird auch nach Strasse, Gefahren und Ansprechpartner.
|
||||
-- =============================================================================
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Row Level Security
|
||||
-- -----------------------------------------------------------------------------
|
||||
DO $$
|
||||
DECLARE
|
||||
tabelle text;
|
||||
BEGIN
|
||||
FOREACH tabelle IN ARRAY ARRAY[
|
||||
'hydrant',
|
||||
'objekt',
|
||||
'objektkontakt',
|
||||
'objektplan',
|
||||
'lagekarte',
|
||||
'lageelement',
|
||||
'wasserfoerderung',
|
||||
'foerderpunkt'
|
||||
]
|
||||
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;
|
||||
$$;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Koordinaten: Dezimalgrad und PostGIS-Punkt zusammenhalten
|
||||
--
|
||||
-- Eine Funktion fuer alle Tabellen: sie erwartet die Spalten "breitengrad",
|
||||
-- "laengengrad" und "koordinaten" und laesst sich damit an jede Tabelle
|
||||
-- haengen, die einen Ort hat.
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE OR REPLACE FUNCTION koordinaten_setzen()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
breite double precision;
|
||||
laenge double precision;
|
||||
BEGIN
|
||||
breite := (to_jsonb(NEW) ->> 'breitengrad')::double precision;
|
||||
laenge := (to_jsonb(NEW) ->> 'laengengrad')::double precision;
|
||||
|
||||
IF breite IS NULL OR laenge IS NULL THEN
|
||||
NEW := jsonb_populate_record(NEW, jsonb_build_object('koordinaten', NULL));
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
-- Reihenfolge beachten: ST_MakePoint nimmt erst die Laenge, dann die Breite.
|
||||
NEW := jsonb_populate_record(
|
||||
NEW,
|
||||
jsonb_build_object(
|
||||
'koordinaten',
|
||||
ST_AsEWKT(ST_SetSRID(ST_MakePoint(laenge, breite), 4326)::geography)
|
||||
)
|
||||
);
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION koordinaten_setzen() IS
|
||||
'Haelt den PostGIS-Punkt einer Tabelle mit ihren Dezimalgraden im Gleichlauf.';
|
||||
|
||||
CREATE TRIGGER standort_koordinaten
|
||||
BEFORE INSERT OR UPDATE OF "breitengrad", "laengengrad" ON "standort"
|
||||
FOR EACH ROW EXECUTE FUNCTION koordinaten_setzen();
|
||||
|
||||
CREATE TRIGGER hydrant_koordinaten
|
||||
BEFORE INSERT OR UPDATE OF "breitengrad", "laengengrad" ON "hydrant"
|
||||
FOR EACH ROW EXECUTE FUNCTION koordinaten_setzen();
|
||||
|
||||
CREATE TRIGGER objekt_koordinaten
|
||||
BEFORE INSERT OR UPDATE OF "breitengrad", "laengengrad" ON "objekt"
|
||||
FOR EACH ROW EXECUTE FUNCTION koordinaten_setzen();
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Koordinaten, die es auf der Erde nicht gibt, sind ein Tippfehler
|
||||
-- -----------------------------------------------------------------------------
|
||||
ALTER TABLE "hydrant" ADD CONSTRAINT "hydrant_koordinaten_gueltig" CHECK (
|
||||
"breitengrad" >= -90 AND "breitengrad" <= 90
|
||||
AND "laengengrad" >= -180 AND "laengengrad" <= 180
|
||||
);
|
||||
|
||||
ALTER TABLE "objekt" ADD CONSTRAINT "objekt_koordinaten_gueltig" CHECK (
|
||||
("breitengrad" IS NULL OR ("breitengrad" >= -90 AND "breitengrad" <= 90))
|
||||
AND ("laengengrad" IS NULL OR ("laengengrad" >= -180 AND "laengengrad" <= 180))
|
||||
);
|
||||
|
||||
ALTER TABLE "standort" ADD CONSTRAINT "standort_koordinaten_gueltig" CHECK (
|
||||
("breitengrad" IS NULL OR ("breitengrad" >= -90 AND "breitengrad" <= 90))
|
||||
AND ("laengengrad" IS NULL OR ("laengengrad" >= -180 AND "laengengrad" <= 180))
|
||||
);
|
||||
|
||||
ALTER TABLE "foerderpunkt" ADD CONSTRAINT "foerderpunkt_koordinaten_gueltig" CHECK (
|
||||
"breitengrad" >= -90 AND "breitengrad" <= 90
|
||||
AND "laengengrad" >= -180 AND "laengengrad" <= 180
|
||||
);
|
||||
|
||||
-- Eine Foerderstrecke ohne Wasser oder mit einem Schlauch, den es nicht gibt.
|
||||
ALTER TABLE "wasserfoerderung" ADD CONSTRAINT "wasserfoerderung_sinnvoll" CHECK (
|
||||
"durchmesser" IN (52, 75, 110)
|
||||
AND "foerdermenge" > 0 AND "foerdermenge" <= 10000
|
||||
AND "ausgangsdruck" > 0 AND "ausgangsdruck" <= 16
|
||||
AND "eingangsdruck" >= 0 AND "eingangsdruck" < "ausgangsdruck"
|
||||
);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Indizes fuer die Umkreissuche
|
||||
--
|
||||
-- GiST auf der Geografie: damit beantwortet Postgres "was liegt im Umkreis von
|
||||
-- 300 Metern" ueber den Index statt ueber die ganze Tabelle.
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE INDEX "hydrant_umkreis" ON "hydrant" USING GIST ("koordinaten")
|
||||
WHERE "geloeschtAm" IS NULL;
|
||||
|
||||
CREATE INDEX "objekt_umkreis" ON "objekt" USING GIST ("koordinaten")
|
||||
WHERE "geloeschtAm" IS NULL;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Volltextsuche ueber Objekte
|
||||
--
|
||||
-- Trigram wie bei den Geraeten aus Phase 1: gesucht wird mit Tippfehlern und
|
||||
-- Wortfragmenten, nicht mit vollstaendigen Woertern.
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE INDEX "objekt_suche_name" ON "objekt" USING GIN ("name" gin_trgm_ops);
|
||||
CREATE INDEX "objekt_suche_strasse" ON "objekt" USING GIN ("strasse" gin_trgm_ops);
|
||||
CREATE INDEX "objekt_suche_gefahren" ON "objekt" USING GIN ("gefahren" gin_trgm_ops);
|
||||
CREATE INDEX "hydrant_suche_bezeichnung" ON "hydrant" USING GIN ("bezeichnung" gin_trgm_ops);
|
||||
CREATE INDEX "hydrant_suche_strasse" ON "hydrant" USING GIN ("strasse" gin_trgm_ops);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Rechte fuer die Anwendungsrolle
|
||||
-- -----------------------------------------------------------------------------
|
||||
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 'REVOKE INSERT, UPDATE, DELETE ON "berechtigung" FROM florian_app';
|
||||
EXECUTE 'REVOKE UPDATE, DELETE ON "audit_log" FROM florian_app';
|
||||
EXECUTE 'REVOKE UPDATE, DELETE ON "kiosk_vorgang" FROM florian_app';
|
||||
EXECUTE 'REVOKE UPDATE, DELETE ON "druckmessung" FROM florian_app';
|
||||
EXECUTE 'REVOKE UPDATE, DELETE ON "atemschutz_ereignis" FROM florian_app';
|
||||
ELSE
|
||||
RAISE NOTICE 'Rolle florian_app nicht vorhanden — Rechtevergabe uebersprungen.';
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
Reference in New Issue
Block a user