# 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_chatbot` | Chatbot Feature-Daten | 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`. ### 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) --- ## Schluessel-Dateien | Thema | Pfad | |-------|------| | PostgreSQL Connector | `gateway/modules/connectors/connectorDbPostgre.py` | | DB-Registry | `gateway/modules/shared/dbRegistry.py` | | FK-Registry | `gateway/modules/shared/fkRegistry.py` | | Database Health | `gateway/modules/system/databaseHealth.py` | | Health API Route | `gateway/modules/routes/routeAdminDatabaseHealth.py` | | App-DB Interface | `gateway/modules/interfaces/interfaceDbApp.py` | | Chat-DB Interface | `gateway/modules/interfaces/interfaceDbChat.py` | | Management-DB Interface | `gateway/modules/interfaces/interfaceDbManagement.py` | | Knowledge-DB Interface | `gateway/modules/interfaces/interfaceDbKnowledge.py` | | Billing-DB Interface | `gateway/modules/interfaces/interfaceDbBilling.py` | | Subscription Interface | `gateway/modules/interfaces/interfaceDbSubscription.py` | | RBAC in DB-Schicht | `gateway/modules/interfaces/interfaceRbac.py` | | Feature-Interface (Template) | `gateway/modules/interfaces/interfaceFeatures.py` | | Bootstrap / DB-Seed | `gateway/modules/interfaces/interfaceBootstrap.py` | | DB-Migration Script | `gateway/scripts/script_db_export_migration.py` | | Datenmodelle (Pydantic) | `gateway/modules/datamodels/` | | Frontend Health Page | `frontend_nyla/src/pages/admin/AdminDatabaseHealthPage.tsx` |