361 lines
16 KiB
Markdown
361 lines
16 KiB
Markdown
<!-- status: canonical -->
|
|
<!-- lastReviewed: 2026-05-28 -->
|
|
<!-- verifiedAgainst: platform-core+ui-nyla (streaming migration update 2026-05-28) -->
|
|
|
|
# Datenbank-Architektur
|
|
|
|
## Ueberblick
|
|
|
|
PowerOn verwendet **PostgreSQL** mit einer klaren Trennung: jedes Modul (App, Chat, Billing, Knowledge, Features) hat eine eigene physische Datenbank (`poweron_*`). Datenbank-Zugriffe laufen ueber **Interfaces** (`interfaceDb*.py`), die auf dem zentralen **DatabaseConnector** (`connectorDbPostgre.py`) aufbauen. Datenbanken und Tabellen werden **automatisch** beim ersten Zugriff erzeugt -- kein manuelles Schema-Management noetig.
|
|
|
|
---
|
|
|
|
## Architektur-Stack
|
|
|
|
```
|
|
Route / Service
|
|
|
|
|
v
|
|
Interface (interfaceDb*.py)
|
|
|-- XxxObjects Klasse
|
|
|-- getInterface() Factory (cached)
|
|
|-- setUserContext() -> RBAC-Anbindung
|
|
|
|
|
v
|
|
DatabaseConnector (connectorDbPostgre.py)
|
|
|-- Auto-Create Database
|
|
|-- Auto-Create/Extend Tables (aus Pydantic-Modellen)
|
|
|-- CRUD-Operationen (getRecordset, recordCreate, recordUpdate, recordDelete)
|
|
|
|
|
v
|
|
PostgreSQL (+ pgvector Extension)
|
|
```
|
|
|
|
---
|
|
|
|
## Datenbanken
|
|
|
|
### Inventar
|
|
|
|
| Datenbank | Modul / Zweck | Interface |
|
|
|-----------|---------------|-----------|
|
|
| `poweron_app` | Kernapplikation: UAM, Mandanten, Rollen, RBAC-Regeln, Navigation, Config | `interfaceDbApp.py` |
|
|
| `poweron_chat` | Chat-Verlaeufe, Messages, Attachments | `interfaceDbChat.py` |
|
|
| `poweron_management` | System-Management, Logs | `interfaceDbManagement.py` |
|
|
| `poweron_knowledge` | RAG Knowledge-Store (mit pgvector) | `interfaceDbKnowledge.py` |
|
|
| `poweron_billing` | Billing Accounts, Transactions, Usage | `interfaceDbBilling.py` |
|
|
| `poweron_billing` | Subscriptions (gleiche DB wie Billing) | `interfaceDbSubscription.py` |
|
|
| `poweron_workspace` | AI Workspace Feature-Daten | Feature-Interface |
|
|
| `poweron_automation` | Automation v1 Workflows, Runs | Feature-Interface |
|
|
| `poweron_automation2` | Automation v2 (Graph-Editor) Workflows, Runs | Feature-Interface |
|
|
| `poweron_trustee` | Trustee Feature-Daten | Feature-Interface |
|
|
| `poweron_commcoach` | CommCoach Feature-Daten | Feature-Interface |
|
|
| `poweron_neutralization` | Neutralisierungs-Daten | Feature-Interface |
|
|
| `poweron_realestate` | RealEstate Feature-Daten | Feature-Interface |
|
|
| `poweron_teamsbot` | Teams-Bot Feature-Daten | Feature-Interface |
|
|
| `poweron_test` | Test-Datenbank | Nur in Tests |
|
|
|
|
### Namenskonvention
|
|
|
|
Feature-Datenbanken folgen dem Pattern `poweron_{featureCode.lower()}`. Der Datenbankname wird hart-codiert in der `_initializeDatabase()` Methode des jeweiligen Interfaces.
|
|
|
|
### Konfiguration
|
|
|
|
Verbindungsparameter kommen aus `APP_CONFIG` (config.ini / Environment):
|
|
|
|
| Key | Beschreibung |
|
|
|-----|-------------|
|
|
| `DB_HOST` | PostgreSQL Host |
|
|
| `DB_PORT` | PostgreSQL Port |
|
|
| `DB_USER` | Datenbank-Benutzer |
|
|
| `DB_PASSWORD_SECRET` | Passwort (ggf. verschluesselt) |
|
|
| `DB_DATABASE` | Default-DB-Name (Standard: `poweron_app`) |
|
|
|
|
---
|
|
|
|
## Interface-Pattern
|
|
|
|
Jedes Interface folgt einem einheitlichen Aufbau:
|
|
|
|
### XxxObjects Klasse
|
|
|
|
```python
|
|
class AppObjects:
|
|
db: DatabaseConnector # Connector zur eigenen DB
|
|
rbac: RbacClass # RBAC-Integration
|
|
currentUser: dict # Aktueller Benutzer
|
|
mandateId: str # Aktueller Mandant
|
|
featureInstanceId: str # (nur bei Feature-Interfaces)
|
|
```
|
|
|
|
### _initializeDatabase()
|
|
|
|
Setzt den Datenbanknamen und erstellt den Connector:
|
|
|
|
```python
|
|
def _initializeDatabase(self):
|
|
dbDatabase = "poweron_app" # hart-codiert pro Interface
|
|
self.db = DatabaseConnector(
|
|
host=APP_CONFIG.get("DB_HOST"),
|
|
database=dbDatabase,
|
|
user=APP_CONFIG.get("DB_USER"),
|
|
password=APP_CONFIG.get("DB_PASSWORD_SECRET"),
|
|
port=APP_CONFIG.get("DB_PORT")
|
|
)
|
|
```
|
|
|
|
### setUserContext()
|
|
|
|
Bindet Benutzer und RBAC an die Instanz:
|
|
|
|
```python
|
|
def setUserContext(self, currentUser, mandateId):
|
|
self.currentUser = currentUser
|
|
self.userId = currentUser["id"]
|
|
self.mandateId = mandateId
|
|
self.rbac = RbacClass(self.db, dbApp=dbApp)
|
|
self.db.updateContext(self.userId)
|
|
```
|
|
|
|
**RBAC-Besonderheit:** `poweron_app` ist Sonderfall (`dbApp=self.db`), weil RBAC-Regeln selbst in `poweron_app` liegen. Alle anderen Interfaces nutzen `getRootDbAppConnector()` um Rules aus der App-DB zu lesen:
|
|
|
|
```python
|
|
dbApp = getRootDbAppConnector()
|
|
self.rbac = RbacClass(self.db, dbApp=dbApp)
|
|
```
|
|
|
|
### getInterface() Factory
|
|
|
|
Cached pro Kontext:
|
|
|
|
| Interface | Cache-Key | Ergebnis |
|
|
|-----------|-----------|----------|
|
|
| App | `{mandateId}_{userId}` | `AppObjects` |
|
|
| Chat | `{mandateId}_{featureInstanceId}_{userId}` | `ChatObjects` |
|
|
| Billing | `{userId}_{mandateId}` | `BillingObjects` |
|
|
| Knowledge | `"default"` (Singleton) | `KnowledgeObjects` |
|
|
|
|
---
|
|
|
|
## DatabaseConnector
|
|
|
|
### Auto-Initialisierung
|
|
|
|
Beim Erstellen eines `DatabaseConnector` (`initDbSystem()`):
|
|
|
|
```
|
|
1. _create_database_if_not_exists()
|
|
-> Verbindet sich zu DB "postgres"
|
|
-> Prueft pg_database auf Existenz
|
|
-> CREATE DATABASE "{name}" falls fehlend
|
|
|
|
2. _create_tables()
|
|
-> Erstellt nur die _system Registry-Tabelle
|
|
-> Applikations-Tabellen werden lazy durch Interfaces erzeugt
|
|
|
|
3. _connect()
|
|
-> psycopg2-Verbindung zur Ziel-DB
|
|
|
|
4. _initializeSystemTable()
|
|
-> _ensureTableExists(SystemTable)
|
|
```
|
|
|
|
### Lazy Table Creation
|
|
|
|
Tabellen werden **bei erstem Zugriff** automatisch aus Pydantic-Modellen erzeugt (`_ensureTableExists(model_class)`):
|
|
|
|
- **Tabellenname** = Python-Klassenname des Pydantic-Modells (z.B. `Role`, `AccessRule`, `UserMandate`)
|
|
- **Spalten** werden aus Modellfeldern abgeleitet
|
|
- **JSONB** fuer komplexe Typen (dict, list, Pydantic-Submodelle)
|
|
- **vector** fuer pgvector-Felder (Knowledge-Store)
|
|
- **Additive Migration**: Wenn die Tabelle existiert, werden fehlende Spalten automatisch hinzugefuegt. Bestehende Spalten werden **nicht** geaendert oder entfernt.
|
|
|
|
### Connector-Caching
|
|
|
|
```python
|
|
_get_cached_connector(host, database, port)
|
|
```
|
|
|
|
Wiederverwendet einen `DatabaseConnector` pro Datenbank-Identitaet (`host:database:port`). Genutzt von App, Management, Knowledge. Chat und Billing instanziieren direkt (kein Cache).
|
|
|
|
### userId Context
|
|
|
|
`userId` wird ueber eine `contextvar` am Connector gesetzt (`updateContext(userId)`). Damit werden System-Felder (`_createdBy`, `_updatedBy`) automatisch befuellt.
|
|
|
|
---
|
|
|
|
## RBAC-Integration in DB-Abfragen
|
|
|
|
Die RBAC-Schicht greift direkt in Datenbank-Abfragen ein:
|
|
|
|
### Kern-Funktionen in interfaceRbac.py
|
|
|
|
| Funktion | Zweck |
|
|
|----------|-------|
|
|
| `getRecordsetWithRBAC()` | Listet Datensaetze mit RBAC-Filter in SQL WHERE |
|
|
| `buildRbacWhereClause()` | Uebersetzt Access Levels in SQL-Bedingungen |
|
|
| `buildDataObjectKey()` | Baut Keys wie `data.uam.UserInDB` fuer Rule-Lookup |
|
|
| `TABLE_NAMESPACE` | Mapping von Modellnamen zu logischen Namespaces |
|
|
|
|
### Namespace-Mapping
|
|
|
|
`TABLE_NAMESPACE` ordnet Pydantic-Modellnamen logischen Bereichen zu:
|
|
|
|
| Namespace | Modelle (Beispiele) |
|
|
|-----------|-------------------|
|
|
| `uam` | UserInDB, UserMandate, Role, AccessRule |
|
|
| `chat` | ChatHistory, ChatMessage |
|
|
| `files` | FileRecord, FileFolder |
|
|
| `feature.trustee` | TrusteePosition, TrusteeDocument |
|
|
| `feature.workspace` | Workspace-spezifische Modelle |
|
|
|
|
### SQL-Filter nach Access Level
|
|
|
|
| Level | SQL WHERE |
|
|
|-------|-----------|
|
|
| `a` (ALL) | Kein Filter |
|
|
| `m` (MANDATE) | `mandateId = :mandateId` |
|
|
| `o` (OWN) | `_createdBy = :userId` |
|
|
| `n` (NONE) | `1=0` (kein Ergebnis) |
|
|
|
|
---
|
|
|
|
## System-Felder
|
|
|
|
Jedes `PowerOnModel` erbt automatisch System-Felder:
|
|
|
|
| Feld | Beschreibung | Schutz |
|
|
|------|-------------|--------|
|
|
| `id` | UUID, auto-generiert | Immutable |
|
|
| `_createdBy` | userId des Erstellers | Immutable |
|
|
| `_createdAt` | Zeitstempel Erstellung | Immutable |
|
|
| `_updatedBy` | userId der letzten Aenderung | Auto-gesetzt |
|
|
| `_updatedAt` | Zeitstempel letzte Aenderung | Auto-gesetzt |
|
|
|
|
Felder mit fuehrendem `_` sind fuer Anwendungs-CUD geschuetzt -- der Connector erzwingt das unabhaengig von Access Rules.
|
|
|
|
---
|
|
|
|
## Feature-DB-Registrierung
|
|
|
|
Jedes Interface registriert seine Datenbank ueber `registerDatabase()` aus `dbRegistry.py`:
|
|
|
|
1. Jedes `interfaceDb*.py` / `interfaceFeature*.py` definiert eine Modul-Konstante (z.B. `appDatabase = "poweron_app"`)
|
|
2. Auf Modul-Ebene wird `registerDatabase(appDatabase)` aufgerufen — damit ist die DB im zentralen Registry
|
|
3. Der `DatabaseConnector` erzeugt die DB automatisch beim ersten Zugriff
|
|
4. Neue DBs werden automatisch erkannt, entfernte DBs verschwinden
|
|
|
|
---
|
|
|
|
## Database Health und Orphan-Scanner
|
|
|
|
### Ueberblick
|
|
|
|
SysAdmin-Seite unter **Admin > System > Datenbank-Gesundheit** mit zwei Funktionen:
|
|
|
|
1. **Tabellenstatistiken** — Row Count, Size, Index Size, Last Vacuum/Analyze fuer alle Tabellen
|
|
2. **Orphan Cleanup** — Generische Erkennung verwaister Datensaetze mit Clean-Buttons
|
|
|
|
### Dynamische DB-Registry (`modules/shared/dbRegistry.py`)
|
|
|
|
- `registerDatabase(dbName, configPrefix)` — oeffentliche API, aufgerufen in jedem Interface
|
|
- `_getRegisteredDatabases()` — alle registrierten DBs
|
|
- `_getConnectorForDb(dbName)` — Factory fuer read-only Connector
|
|
|
|
### Model-Registry (`modules/datamodels/datamodelBase.py`)
|
|
|
|
- `_MODEL_REGISTRY: Dict[str, Type[PowerOnModel]]` — automatisch via `__init_subclass__`
|
|
- Jede `PowerOnModel`-Subklasse registriert sich beim Import (Tabellenname = Klassenname)
|
|
|
|
### FK-Discovery (`modules/shared/fkRegistry.py`)
|
|
|
|
- Scannt alle PowerOnModel-Subklassen nach `fk_target` in `json_schema_extra`
|
|
- Baut automatisch `{tableName → dbName}` Mapping aus den Annotationen
|
|
- Fallback: Catalog-Query (`information_schema.tables`) fuer unmapped Tables
|
|
- Cached nach erstem Scan
|
|
- `FkRelationship` Dataclass: `sourceDb, sourceTable, sourceColumn, targetDb, targetTable, targetColumn`
|
|
|
|
### FK-Annotationen (`fk_target`)
|
|
|
|
Jedes `*Id`-Feld das eine echte FK-Beziehung darstellt, hat `fk_target` in `json_schema_extra` mit drei Pflichtschluesseln (`db`, `table`, `labelField`):
|
|
|
|
```python
|
|
mandateId: str = Field(
|
|
...,
|
|
json_schema_extra={
|
|
...,
|
|
"fk_target": {"db": "poweron_app", "table": "Mandate", "labelField": "label"},
|
|
},
|
|
)
|
|
```
|
|
|
|
- Standard `targetColumn` ist `"id"`, Sonderfall `Feature.code` nutzt `"column": "code"`
|
|
- `labelField` gibt das Feld fuer das menschenlesbare Label an; `None` = kein Label (Junction-IDs)
|
|
- `fk_target` ist die einzige FK-Annotation — sowohl fuer Orphan-Detection als auch Label-Resolution und UI-Attribute
|
|
- Felder ohne DB-FK (Stripe-IDs, Graph-Node-IDs, polymorphe referenceId) haben kein `fk_target`
|
|
- Startup-Validierung (`validateFkTargets`) prueft Vollstaendigkeit; fehlende Keys brechen den Start ab
|
|
- **Soft FK** (`"softFk": True`): Optionaler Marker fuer "weiche" Referenzen, die Sentinel-/Lineage-Werte halten, fuer die absichtlich keine DB-Zeile existiert (z. B. `AutoWorkflow.templateSourceId = "trustee-receipt-import"` aus `featureModule.getTemplateWorkflows()`). Label-Resolution funktioniert weiterhin; der Orphan-Scanner ueberspringt soft FKs vollstaendig (kein Display, kein Cleanup), damit korrekte Datensaetze nicht geloescht werden.
|
|
|
|
### Orphan-Scanner (`modules/system/databaseHealth.py`)
|
|
|
|
- `_getTableStats(dbFilter)` — `pg_stat_user_tables` + `pg_total_relation_size`
|
|
- `_scanOrphans(dbFilter)` — Same-DB: `NOT EXISTS`, Cross-DB: Parent-IDs laden + `NOT IN`
|
|
- `_cleanOrphans(db, table, column)` — loescht Orphans, gibt Count zurueck
|
|
- `_cleanAllOrphans(force, excludeUserFks)` — alle Orphans bereinigen; `excludeUserFks=True` ueberspringt UserInDB.id-Referenzen
|
|
- `_isUserIdFk(targetTable, targetColumn)` — Helper: matcht `UserInDB.id` (case-insensitive). Eine Stelle, die scan-route + clean-all + frontend gemeinsam nutzen
|
|
- 5-Minuten-Cache fuer Orphan-Ergebnisse
|
|
- **Pre-Filter:** Source-/Target-Tabelle muss existieren, Source-/Target-Spalte muss als physische Spalte vorhanden sein, FK darf nicht `softFk: True` sein. Erst dann wird ein Orphan-Eintrag erzeugt — sonst werden produktive Datensaetze geloescht (z. B. Trustee-Workflows mit Sentinel-`templateSourceId`).
|
|
- **User-FK-Filter:** Orphans, die auf `UserInDB.id` zeigen (Audit-/Billing-/Membership-Reste geloeschter User), gehoeren in den separaten User-Purge-Workflow. Frontend-Checkbox `Ohne FK-Referenzen zu UserInDB.id` (default ON) blendet sie aus dem Scan-Resultat aus, und `clean-all` ueberspringt sie identisch — die UI zeigt also nie Orphans an, die der naechste Klick nicht auch loeschen wuerde.
|
|
|
|
### API (`modules/routes/routeAdminDatabaseHealth.py`)
|
|
|
|
| Methode | Pfad | Beschreibung |
|
|
|---------|------|-------------|
|
|
| GET | `/api/admin/database-health/stats` | Tabellenstatistiken (optional `?db=...`) |
|
|
| GET | `/api/admin/database-health/orphans` | Orphan-Scan (optional `?db=...`, `?excludeUserFks=true`) |
|
|
| POST | `/api/admin/database-health/orphans/clean` | Einzeln-Cleanup `{"db","table","column"}` |
|
|
| POST | `/api/admin/database-health/orphans/clean-all` | Batch-Cleanup `{"force","excludeUserFks"}` |
|
|
|
|
Alle Endpunkte: SysAdmin-only via `requireSysAdminRole`.
|
|
|
|
### Migration API (Streaming, seit 2026-05-28)
|
|
|
|
| Methode | Pfad | Beschreibung |
|
|
|---------|------|-------------|
|
|
| GET | `/api/admin/database-health/migration/export-stream` | Streaming-Export aller/ausgewaehlter DBs als JSON via `StreamingResponse`. Query-Params: `databases` (kommagetrennt, optional), `filename`. Frontend nutzt `ReadableStream` + File System Access API fuer direktes Streaming-to-Disk. |
|
|
| GET | `/api/admin/database-health/migration/export-single?database=...` | Export einer einzelnen DB als JSON. |
|
|
| POST | `/api/admin/database-health/migration/upload-import` | Multipart-Upload der Import-JSON auf Disk. Gibt `{token, fileSizeMb}` zurueck. |
|
|
| GET | `/api/admin/database-health/migration/process-import-stream?token=...` | Streaming-Validierung + Split der hochgeladenen Datei via `StreamingResponse` (ndjson). Two-Pass mit `ijson`: Pass 1 validiert + extrahiert Metadaten, Pass 2 splittet in per-Table JSONL. Liefert Progress-Events `{phase, db, table, rows}`. |
|
|
| POST | `/api/admin/database-health/migration/import-single` | Import einer einzelnen DB (aus per-Table JSONL oder Legacy-Format). |
|
|
| POST | `/api/admin/database-health/migration/import-done` | Cleanup temporaerer Dateien nach abgeschlossenem Import. |
|
|
|
|
### Frontend (`AdminDatabaseHealthPage.tsx`)
|
|
|
|
- Tab "Statistiken": Sortierbare Tabelle mit DB-Filter und Summary-Leiste
|
|
- Tab "Orphan Cleanup": Tabelle mit Clean-Button pro Zeile + "Alle bereinigen", Checkboxen `Nur Probleme` und `Ohne FK-Referenzen zu UserInDB.id` (letzteres default ON)
|
|
- Tab "Migration": Streaming-Export (DB-Auswahl, Fortschrittslog mit DB x/n + MB) und Streaming-Import (Upload, Validierungs-Progress, per-DB-Import mit Modus Neu/Zusammenfuehren)
|
|
|
|
---
|
|
|
|
## Schluessel-Dateien
|
|
|
|
| Thema | Pfad |
|
|
|-------|------|
|
|
| PostgreSQL Connector | `platform-core/modules/connectors/connectorDbPostgre.py` |
|
|
| DB-Registry | `platform-core/modules/shared/dbRegistry.py` |
|
|
| FK-Registry | `platform-core/modules/shared/fkRegistry.py` |
|
|
| Database Health | `platform-core/modules/system/databaseHealth.py` |
|
|
| Health API Route | `platform-core/modules/routes/routeAdminDatabaseHealth.py` |
|
|
| App-DB Interface | `platform-core/modules/interfaces/interfaceDbApp.py` |
|
|
| Chat-DB Interface | `platform-core/modules/interfaces/interfaceDbChat.py` |
|
|
| Management-DB Interface | `platform-core/modules/interfaces/interfaceDbManagement.py` |
|
|
| Knowledge-DB Interface | `platform-core/modules/interfaces/interfaceDbKnowledge.py` |
|
|
| Billing-DB Interface | `platform-core/modules/interfaces/interfaceDbBilling.py` |
|
|
| Subscription Interface | `platform-core/modules/interfaces/interfaceDbSubscription.py` |
|
|
| RBAC in DB-Schicht | `platform-core/modules/interfaces/interfaceRbac.py` |
|
|
| Feature-Interface (Template) | `platform-core/modules/interfaces/interfaceFeatures.py` |
|
|
| Bootstrap / DB-Seed | `platform-core/modules/interfaces/interfaceBootstrap.py` |
|
|
| DB-Migration (Streaming) | `platform-core/modules/system/databaseMigration.py` |
|
|
| DB-Migration Script | `platform-core/scripts/script_db_export_migration.py` |
|
|
| Datenmodelle (Pydantic) | `platform-core/modules/datamodels/` |
|
|
| Frontend Health Page | `ui-nyla/src/pages/admin/AdminDatabaseHealthPage.tsx` |
|