wiki/b-reference/platform/database-architecture.md
2026-04-16 23:12:56 +02:00

13 KiB

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

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:

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:

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:

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

_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:

mandateId: str = Field(
    ...,
    json_schema_extra={
        ...,
        "fk_target": {"db": "poweron_app", "table": "Mandate"},
    },
)
  • Standard targetColumn ist "id", Sonderfall Feature.code nutzt "column": "code"
  • Felder ohne DB-FK (Stripe-IDs, Graph-Node-IDs, polymorphe referenceId) haben kein fk_target
  • fk_target ist rein fuer Backend-Orphan-Detection, fk_model/frontend_fk_* bleiben fuer das UI

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() — alle Orphans bereinigen
  • 5-Minuten-Cache fuer Orphan-Ergebnisse

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=...)
POST /api/admin/database-health/orphans/clean Einzeln-Cleanup {"db","table","column"}
POST /api/admin/database-health/orphans/clean-all Batch-Cleanup aller Orphans

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"

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