6 · Laufzeitsicht¶
Die Laufzeitsicht zeigt das Verhalten des Systems zu konkreten Zeitpunkten: welche Dienste in welcher Reihenfolge kommunizieren und wo Entscheidungen fallen. Das Kapitel umfasst die zentralen Hauptprozesse der ODI – von der erstmaligen Datensatz-Anlage über die Aktualisierung und Fortführung von Datenreihen bis hin zur Verwaltung von Schemas.
6.1 · Laufzeitszenario: CSV-Datensatz hochladen¶
POST /upload/csv ist der zentrale Endpunkt des Staging-Backends. Er nimmt in einem Multipart-Request die CSV-Datei, die Schema-Referenz, Lese-Parameter (Trennzeichen, Zeichensatz) und die Metadaten entgegen. Das folgende Sequenzdiagramm zeigt den vollständigen Ablauf.
Das Diagramm zeigt sieben Beteiligte: den Client (Browser oder API-Client), das Staging-Backend, Keycloak (zur Token-Prüfung), das Schema-Repository, das Frictionless-Backend, den CKAN-Service und den Datenkatalog (CKAN oder Piveau). Der kritische Entscheidungspunkt liegt nach der Validierung: Schlägt sie fehl, bricht der Prozess ab, ohne einen Datensatz anzulegen.
Ablauf im Detail¶
Schritt 1 – Anfrage prüfen
Das Staging-Backend empfängt den Multipart-Request und extrahiert das JWT aus dem Authorization-Header. Es übermittelt das Token an Keycloak und prüft die enthaltenen Berechtigungs-Claims. Anhand von schemaId und schemaVersion ruft es parallel das passende Frictionless-Schema aus dem Schema-Repository ab.
Schritt 2 – CSV validieren (Entscheidungspunkt)
Das Frictionless-Backend prüft die CSV-Datei inhaltlich gegen das geladene Schema: Stimmen Spalten, Datentypen und Werteregeln? Ist die Datei nicht gültig, antwortet das Staging-Backend sofort mit 422 Unprocessable Entity und einer strukturierten Fehlerliste. Es wird kein Datensatz angelegt, und der Ablauf endet hier.
Kein Datensatz bei Validierungsfehler
Die Validierung ist der einzige Punkt, an dem der Prozess ohne Seiteneffekte abgebrochen werden kann. Alle nachfolgenden Schritte verändern den Systemzustand (Datenkatalog, Objektspeicher). Deshalb gilt: Erst validieren, dann schreiben.
Schritt 3 – Datensatz anlegen
Das Staging-Backend übergibt die Metadaten an den CKAN-Service. Dieser legt im Datenkatalog einen neuen Datensatz an und gibt die datasetId zurück. Ab diesem Moment existiert der Datensatz im Katalog.
Schritt 4 – Repräsentationen erzeugen und ablegen
Das Frictionless-Backend konvertiert die CSV-Datei in Parquet und JSON. Das Staging-Backend übergibt alle Repräsentationen (CSV-Original, Tabular Data Resource / TDR, Parquet, JSON) zusammen mit den Metadaten an den CKAN-Service, der sie als Distributionen am Datensatz ablegt.
Schritt 5 – Veröffentlichen
Der CKAN-Service setzt den Datensatz auf „veröffentlicht". Das Staging-Backend gibt 201 Created zurück, zusammen mit datasetId und datasetUrl.
Enthält das Schema Geo-Felder, durchläuft der Datensatz nach Schritt 4 zusätzlich den Geodaten-Zweig (Konvertierung nach GeoJSON/GeoParquet, Übergabe an GeoServer/UDP-Manager, Verknüpfung mit Masterportal und WFS/WMS-Diensten). Dieser Zweig ist nicht fatal – tritt dort ein Fehler auf, bleibt der Datensatz erhalten. Details beschreibt Prozess: Datensatz hochladen (CSV).
Zwei der Repräsentations-Schritte laufen als eigenständige, parallele Teilprozesse ab und sind im Diagramm oben nur als ref-Verweis dargestellt. Beide sind best-effort: Sie werden im Staging-Backend jeweils in einem try/catch ausgeführt, dessen Fehler nur per LOGGER.warn protokolliert werden. Schlägt einer der Zweige fehl, verhindert das weder das Ablegen der übrigen Distributionen noch die Veröffentlichung des Datensatzes.
RDF / Linked Data erzeugen¶
Nach dem Ablegen der Datei-Repräsentationen ruft das Staging-Backend uploadRdfFile(...) auf. Diese Methode delegiert an RdfServiceClientService.generateRDFFile(token, catalogue, datasetId, licenseId, dataResourceJsonUrl, publish), das den Request mit resource_url (Data-Resource-JSON-Distribution), triplestore_url (Fuseki) und datacatalog_url (Piveau-Distribution) baut und über rdfClient.generateRdfFile(rdfRequest) an den RDF-Service (triple-converter-worker) übergibt. Der RDF-Service liest die Data-Resource-JSON aus dem Katalog, erzeugt die RDF-Repräsentation, lädt sie in den Triple Store (Apache Jena Fuseki) und legt die Datei {datasetId}.rdf als Distribution im Datenkatalog (Piveau) ab.
| Auslöser | Schritte | Ergebnis |
|---|---|---|
uploadRdfFile(...) nach dem Anlegen des Datensatzes |
generateRDFFile → rdfClient.generateRdfFile: Request mit resource_url (Data-Resource-JSON), triplestore_url (Fuseki) und datacatalog_url (Piveau) bauen und an den RDF-Service übergeben |
Auftrag an den RDF-Service (triple-converter-worker) |
| RDF-Service verarbeitet den Auftrag | Data-Resource-JSON aus dem Katalog lesen · RDF-Repräsentation erzeugen · in den Triple Store (Apache Jena Fuseki) laden · {datasetId}.rdf als Distribution ablegen |
Tripel im Fuseki-Store, {datasetId}.rdf als Distribution im Datenkatalog |
Der Zweig ist best-effort: Ein Fehler wird als RDF file generation failed (non-critical) protokolliert, der Upload läuft ohne Abbruch weiter.
Geodaten erzeugen¶
Der Geodaten-Zweig (uploadGeoData(...)) wird nur ausgeführt, wenn das Schema Geo-Felder enthält (schema.schemaJSON.fields.some(f => f.geoType)); andernfalls wird er übersprungen. Das Staging-Backend konvertiert die CSV mit csvToGeoJson.toGeoJson(csvJson, schema) nach GeoJSON und legt GeoJSON sowie GeoParquet über uploadGeojsonToDatacatalogService.uploadGeoJsonFile(...) und uploadGeoParquetToDatacatalogService.uploadGeoParquetToPiveau(...) im Datenkatalog (CKAN / Piveau) ab. Anschließend übergibt es die Geodaten mit uploadGeojsonToMasterportalService.uploadGeoJsonFile(...) an die GIS-Datenbank und den UDP-Manager. Dieser Schritt gliedert sich in mehrere Teilschritte: ogr2ogr lädt die GeoJSON-Datei als Tabelle (<schema>.<tabelle>, Tabellenname = datasetId mit -→_) in die GIS-Datenbank (PostGIS); createUdpManagerObjects(...) legt über eine REST-Kette gegen den UDP-Manager Metadaten, Datenbankverbindung, Dataset, Attribute, Collection (inkl. Bounding-Box), Service und Layer an; abschließend erzeugt, publiziert und lädt der UDP-Manager die GeoServer-Konfiguration neu (generateGeoserverConfig, publishGeoserverConfig, reloadGeoserver), sodass WFS- und WMS-Layer verfügbar werden. Zuletzt verknüpft das Backend den Datensatz über publishMasterportalLinkService.linkToMasterPortal(...) mit dem Masterportal sowie über publishWebFeatureServiceLinkService.linkToWebFeatureService(...) und publishWebMapServiceLinkService.linkToWebMapService(...) mit WFS- und WMS-Diensten.
| Auslöser | Schritte | Ergebnis |
|---|---|---|
Schema enthält Geo-Felder (field.geoType) |
csvToGeoJson.toGeoJson: Feldtyp erkennen (geopointX/geopointY → Punkt-Features · well-known-text → WKT via wellknown.parse), cleanGeoJson, reprojectIfNeeded (EPSG-Umprojektion) |
bereinigtes, ggf. umprojiziertes GeoJSON |
| GeoJSON liegt vor | uploadGeojsonToDatacatalogService.uploadGeoJsonFile(...) · uploadGeoParquetToDatacatalogService.uploadGeoParquetToPiveau(...) |
GeoJSON- und GeoParquet-Distribution im Datenkatalog |
Geodaten an GIS/UDP-Manager übergeben (uploadGeojsonToMasterportalService) |
ogr2ogr.loadFile → Tabelle in PostGIS (<schema>.<tabelle>, Tabelle = datasetId mit -→_); createUdpManagerObjects: UDP-Manager-REST-Kette (Metadaten · DB-Verbindung · Dataset · Attribute · Collection + Bounding-Box · Service · Layer); generateGeoserverConfig · publishGeoserverConfig · reloadGeoserver |
Layer in PostGIS, WFS-/WMS-Layer im GeoServer; Rückgabe { bbox, udpDataset.md_id } |
Datensatz mit Diensten verknüpfen (addUrlToDataset) |
linkToMasterPortal: MASTERPORTAL_URL/?mdid=<md_id>&ZOOMTOEXTENT=<bbox> · linkToWebFeatureService/linkToWebMapService: GEOSERVER_URL/<tabelle>/ows?service=WFS\|WMS&…GetCapabilities |
Kartenansicht- (Masterportal), WFS- und WMS-Distribution (GeoServer) am Datensatz im Katalog |
Auch dieser Zweig ist best-effort (try/catch mit LOGGER.warn); die temporäre GeoJSON-Datei wird am Ende in jedem Fall gelöscht.
6.2 · Authentifizierungsablauf (Client Credentials)¶
Für den programmatischen Zugriff – Fachverfahren, Skripte, CI-Pipelines – verwendet die ODI den OAuth-2.0-Client-Credentials-Flow:
- Der Client sendet seine
client_idund seinclient_secretan den Keycloak-Endpunkt
https://keycloak.odi.schleswig-holstein.de/realms/open-data-infrastruktur/protocol/openid-connect/token. - Keycloak prüft die Credentials und stellt ein JWT aus, das die feingranularen Berechtigungs-Claims des Service-Accounts enthält.
- Der Client übergibt dieses JWT bei jedem schreibenden API-Aufruf als
Authorization: Bearer <token>. - Das jeweilige Backend (Staging, Schema-Repository …) prüft die Signatur des JWT lokal und liest die Claims, ohne Keycloak nochmals zu kontaktieren.
Dieses Muster gilt für alle Dienste in der ODI: Token einmalig holen, bei jedem Request mitsenden. Verwaltungsmitarbeitende, die sich über das Frontend einloggen, durchlaufen stattdessen den OIDC-Authorization-Code-Flow mit Umleitung zu Keycloak (und, bei Federierten Accounts, weiter zum Dataport Identity Provider).
6.3 · Laufzeitszenario: Datensatz aktualisieren¶
PUT /upload/csv/overwrite/:datasetId ersetzt die Daten eines bestehenden Datensatzes vollständig. Der Aufrufer übergibt eine neue CSV-Datei; das Staging-Backend aktualisiert alle zugehörigen Distributionen im Datenkatalog (CSV-Original, Tabular Data Resource, Parquet). Im Gegensatz zum erstmaligen Hochladen wird kein neuer Datensatz angelegt – der Datensatz behält seine ID und seine Metadaten.
Das Staging-Backend lädt zunächst den bestehenden Datensatz aus dem Piveau-Katalog, um schemaId, schemaVersion und den Katalognamen zu ermitteln. Fehlt die schemaId, bricht der Prozess sofort mit 400 Bad Request ab. Anschließend holt das Backend das zugehörige Frictionless-Schema aus dem odi-schema-backend und validiert die neue CSV-Datei gegen dieses Schema; schlägt die Validierung fehl, antwortet es mit 422 Unprocessable Entity und einer strukturierten Fehlerliste, ohne den Datensatz zu verändern. Erst nach erfolgreicher Validierung werden die vorhandenen Distributionen im Katalog über piveauClient.replaceDistributionFile ersetzt (CSV, TDR, Parquet). Die RDF-Distribution wird asynchron und ohne Wartezeit auf das Ergebnis über den RDF-Service erneuert. Abschließend aktualisiert das Backend die Datensatz-Metadaten (z. B. schemaVersion, Zeitraum, Beschreibung) im Katalog.
| Eingabe | Entscheidungspunkte | Ergebnis |
|---|---|---|
datasetId (Pfadparameter), CSV-Datei (Multipart), optionale schemaVersion |
schemaId am Datensatz vorhanden? · CSV valide? | 200 OK mit { datasetId, datasetUrl } |
Weiterführende Informationen: Datensatz aktualisieren (API) · API-Referenz
6.4 · Laufzeitszenario: Datensatzserie fortführen (Append)¶
POST /upload/csv/append hängt einen neuen Zeitabschnitt an eine bestehende Datenreihe an. Der Endpunkt erzeugt einen vollständig neuen Datensatz im Katalog und verknüpft ihn mit dem Vorgänger-Datensatz sowie mit der übergeordneten DataSeries.
Der CsvAppendService lädt zunächst den Vorgänger-Datensatz anhand der previousDatasetId aus dem Piveau-Katalog. Existiert dieser nicht, wird der Prozess mit 400 Bad Request abgebrochen. Sofern der Vorgänger noch kein Enddatum trägt, setzt das Backend es automatisch auf das startDate des neuen Eintrags (oder auf das heutige Datum, falls das neue Startdatum vor dem Startdatum des Vorgängers liegt). Danach läuft der vollständige Upload-Prozess für den neuen Datensatz ab – identisch mit POST /upload/csv (Schema laden, CSV validieren, Datensatz anlegen, Distributionen hochladen). Anschließend prüft der Service, ob bereits eine DataSeries mit der angegebenen dataSeriesId im Katalog existiert: Ist sie noch nicht vorhanden, legt das Backend sie neu an und trägt den Vorgänger-Datensatz als ersten Eintrag ein. Danach wird der neue Datensatz in die Datenserie eingetragen und der Datensatz im Katalog mit der Datenserie-Referenz aktualisiert.
| Eingabe | Entscheidungspunkte | Ergebnis |
|---|---|---|
CSV-Datei, previousDatasetId, dataSeriesId, startDate, Titel, Beschreibung |
Vorgänger vorhanden? · endDate bereits gesetzt? · Datenserie bereits vorhanden? | 200 OK mit Ergebnis-Objekt |
Weiterführende Informationen: Datensatzserie fortführen (API) · API-Referenz
6.5 · Laufzeitszenario: Schema anlegen¶
POST /schemas legt ein neues Datenschema im odi-schema-backend an. Das Schema wird versioniert in einem eigenen GitLab-Projekt gespeichert; jede Version entspricht einem Git-Tag.
Nach der JWT-Prüfung über Keycloak (Decorator @Protected()) ermittelt das Schema-Backend die konfigurierte Nutzergruppe (USER_GROUP_ID) in GitLab. Dort sucht es nach einer Untergruppe, die dem aufrufenden Nutzernamen entspricht, und legt sie bei Bedarf an. Bevor ein neues Projekt erstellt wird, prüft das Backend, ob bereits ein Projekt mit demselben Namen oder Pfad in der Nutzergruppe existiert; ist das der Fall, antwortet es mit 409 Conflict. Andernfalls erstellt die SchemaAPIService ein neues öffentliches GitLab-Projekt und lädt per initialem Commit alle Pflichtdateien hoch: schema.json (Frictionless Table Schema), README.md, CHANGELOG.md und LICENSE. Anschließend setzt sie einen Git-Tag, der der Schema-Version entspricht. Abschließend lädt das Backend das neue Schema und seine Dateien in den internen Cache (Redis/Datenbank), damit nachfolgende Lesezugriffe nicht immer GitLab befragen müssen.
| Eingabe | Entscheidungspunkte | Ergebnis |
|---|---|---|
| Schema-Definition (JSON), Version, Readme, optionale Beispiel-CSVs | Name bereits vergeben? · Tag = schema.version? | 201 Created mit { id, tag } |
Weiterführende Informationen: Schema anlegen (UI) · API-Referenz
6.6 · Laufzeitszenario: Schema aktualisieren¶
PUT /schemas/:id veröffentlicht eine neue Version eines bestehenden Schemas. Das Versionierungsmodell schreibt vor, dass jede neue Version semver-kompatibel größer als alle vorherigen sein muss.
Wie beim Anlegen prüft das Schema-Backend zuerst das JWT und stellt dann sicher, dass die Nutzergruppe des Aufrufers in GitLab existiert. Dann lädt es das vorhandene GitLab-Projekt anhand der Schema-ID sowie alle bereits gesetzten Versions-Tags. Ist die neue Version nicht größer als alle bisherigen, antwortet das Backend mit 400 Bad Request. Andernfalls prüft es, ob die CHANGELOG.md im Projekt bereits existiert, und erstellt einen neuen Commit mit den geänderten Dateien (schema.json, CHANGELOG.md, optional README.md und Beispiel-CSVs). Der Git-Tag für die neue Version wird danach gesetzt. Abschließend aktualisiert das Backend den internen Cache parallel für alle betroffenen Dateien und Versionen, damit Leseanfragen sofort die neue Version liefern.
| Eingabe | Entscheidungspunkte | Ergebnis |
|---|---|---|
| Schema-ID (Pfadparameter), neue Schema-Definition, neue Version | Version größer als letzte? · Changelog vorhanden? | 200 OK mit { id, tag } |
Weiterführende Informationen: Schema aktualisieren (UI) · API-Referenz
6.7 · Laufzeitszenario: Datensatz vervollständigen (Complete)¶
PUT /upload/csv/complete/:datasetId vervollständigt einen bestehenden – häufig als Entwurf angelegten – Datensatz: Der CsvCompleteService zieht fehlende Repräsentationen nach und veröffentlicht den Datensatz anschließend. Der Endpunkt gehört – wie Upload, Append und Update – zu den schreibenden CSV-Operationen des Staging-Backends.
Nach der JWT-Prüfung über Keycloak lädt das Backend Datensatz und Distributionen aus dem Datenkatalog (CKAN / Piveau) und sucht die Distributionen „Tabular Data Resource" und „CSV". Fehlen beide, bricht der Prozess mit dem Fehler INVALID_DATASET ab. Fehlt nur eine der beiden, wird sie aus der jeweils anderen erzeugt (completeCsvFromTdr bzw. completeTdrFromCsv). Danach liest das Backend die Tabular Data Resource, extrahiert daraus Schema-Id und -Version (extractSchemaInfoFromTDR), lädt das Schema aus dem Schema-Repository, holt die CSV-Daten und validiert sie über Frictionless. Schlägt die Validierung fehl, endet der Prozess. Anschließend werden fehlende Repräsentationen best-effort nachgezogen (completeParquetFile, completeGeoCsv – Details siehe Geodaten erzeugen, completeRdfFile – Details siehe RDF / Linked Data erzeugen); jeder dieser Schritte ist mit try/catch gekapselt. Zum Schluss veröffentlicht updateDatasetIssuedBy den Datensatz und der Client erhält 200 OK.
| Vorbedingung | Schritte | Ergebnis |
|---|---|---|
| Bestehender Datensatz mit mindestens einer der Distributionen TDR oder CSV | Distributionen prüfen · fehlende TDR/CSV nachziehen · Schema laden · validieren · Repräsentationen (Parquet, Geo, RDF) nachziehen · veröffentlichen | 200 OK – Datensatz vervollständigt und veröffentlicht |
Weiterführende Informationen: API-Referenz
6.8 · Laufzeitszenario: Masterportal-Konfiguration (services & config.json)¶
Das Masterportal ist ein konfigurationsgetriebener Web-Kartenviewer: Beim Laden liest es JSON-Konfigurationsdateien, die ihm sagen, welche Kartenebenen und Dienste es gibt (services.json – die Liste der Layer inklusive WMS/WFS, GetFeatureInfo-Attribute und Style-Verweis) und wie das Portal aufgebaut ist (config.json – Themenbaum, Ordnerstruktur und Portaleinstellungen). Normalerweise werden diese Dateien statisch von Hand gepflegt. In der ODI werden Geodaten aber laufend und automatisch veröffentlicht (siehe Geodaten erzeugen: Jeder Geo-Datensatz wird im GeoServer und beim UDP-Manager registriert). Für jeden neuen Datensatz die Konfiguration von Hand zu pflegen, skaliert nicht. Deshalb erzeugt der odi-masterportal-service diese Konfigurationen dynamisch aus dem aktuellen Systemzustand – die Layer kommen vom UDP-Manager, die Datensätze aus dem Datenkatalog, die Struktur der Attribute aus dem Schema-Repository. Das Ergebnis: Ein neu veröffentlichter Geo-Datensatz erscheint automatisch als auswählbare Kartenebene im Masterportal – ohne manuelles Editieren von Konfigurationsdateien. Das schließt den Kreis zum Geo-Upload: Dort werden die Layer angelegt (Geodaten erzeugen), hier werden sie dem Endnutzer im Kartenportal sichtbar gemacht.
Der odi-masterportal-service ist dabei Middleware: Er hält keine Geodaten vor, sondern setzt die Konfiguration aus den Metadaten anderer Dienste zusammen. Die eigentlichen Geodaten liegen im Geo-Stack (GeoServer/PostGIS), die Layer-Metadaten liefert der UDP-Manager.
service.json (Layer/Services) erzeugen¶
GET /masterportal/services liefert die services.json – den Katalog der verfügbaren Kartenebenen. Sie beschreibt für jeden Layer, welche Ebene es gibt, welche Attribute im GetFeatureInfo-Popup erscheinen und welcher Style verwendet wird. Der GetLayersService holt dazu die Layer vom UDP-Manager, bildet sie mit dem UdpLayerToLayerMapper auf Masterportal-Layer ab und reichert jeden Layer parallel (Promise.all) an: Über den Datenkatalog ermittelt er zum Datensatz schemaId und schemaVersion, lädt das zugehörige Schema aus dem Schema-Repository und leitet daraus die GetFeatureInfo-Attribute (getGfiAttributes) und den Style (getStyleId, aus dem Schema-Feld geoLayerId, sonst der Standard-Style) ab. Datensatz- und Schema-Zugriffe laufen über einen Cache; nur bei einem Cache-Miss wird der jeweilige Dienst kontaktiert. Zum Schluss werden zwei Basis-Layer (webmap-layer-color, webmap-layer-grey) angehängt, die die Hintergrundkarte liefern.
| Auslöser | Schritte | Ergebnis |
|---|---|---|
GET /masterportal/services (JWT) → GetLayersService.getLayers(token) |
getMasterportalLayers → Layer vom UDP-Manager holen und mappen; je Layer (parallel): getDataset (Cache → Datenkatalog) liefert schemaId/schemaVersion, getSchema (Cache → Schema-Repository), daraus getGfiAttributes und getStyleId; zwei Basis-Layer anhängen |
services.json – Layer-JSON mit Diensten, GetFeatureInfo-Attributen, Style und Hintergrundkarte |
config.json erzeugen¶
GET /masterportal/config.json liefert die config.json – den Aufbau und Themenbaum des Portals. Sie legt fest, wie die Layer in der Ebenenauswahl gruppiert und die Portaleinstellungen gesetzt sind, damit sich Nutzer zurechtfinden. Der GetConfigService holt die Layer vom UDP-Manager, lädt ein config.json-Template und ruft buildSubjectLayerStructure auf: Diese Methode gruppiert die Layer nach dem Feld kategorie_organisation (dem Herausgeber) zu einer hierarchischen subjectlayer-Struktur – ein Ordner „Herausgeber", darunter je Herausgeber ein Ordner mit den zugehörigen Layern. Diese Struktur wird in die Template-Konfiguration eingesetzt (config.layerConfig.subjectlayer.elements) und als config.json zurückgegeben.
| Auslöser | Schritte | Ergebnis |
|---|---|---|
GET /masterportal/config.json (JWT) → GetConfigService.getConfig(token) |
udpManagerClient.getLayers → Layer vom UDP-Manager holen; config.json-Template laden; buildSubjectLayerStructure: nach kategorie_organisation gruppieren → subjectlayer-Struktur (Ordner „Herausgeber" → Publisher-Ordner → Layer); config.layerConfig.subjectlayer.elements setzen |
config.json – Portal-Konfiguration mit Themenbaum/Ordnerstruktur |