Zum Inhalt springen

Datenbank

Die Persistenz läuft über ORM++ (privates Go-Modul, GOPRIVATE=gitlab.techeve.de). Backend per Config:

  • SQLite (Default): database_path in der config.json — eingebettet, null Betriebsaufwand.
  • PostgreSQL: database_dsn: "postgres://..." — sonst nichts. ORM++ garantiert Verhaltensgleichheit; App-Code verzweigt nie nach Backend.

Verbindung, Model-Registrierung, Migration und die ORM++-Worker (Projektionen, Snapshots, Archivierung) kapselt internal/storage/database.go (storage.Open). Die vollständige ORM++-API-Referenz liegt im ORM++-Repo unter doc/API.md.

Zwei Ebenen, bewusst getrennt:

  1. Gateway-Konfiguration (User, APIKey, Tenant, SmtpRoute, BlocklistEntry, OutboundMail): orm.TenantFree() — globale Tabellen ohne Mandanten-Spalte.
  2. Formulardaten (Submission, Nonce): leben im ORM++-Tenant der jeweiligen Kundenseite (Tenant.OrmTenantID, bei Mandanten-Anlage über db.Tenants().Create erzeugt). Jeder Zugriff braucht orm.WithTenant(ctx, tenant.OrmTenantID) — fail-closed. Dadurch gibt es DSGVO-Export (Tenants().Export) und Recht-auf-Vergessen (Archive + Purge) pro Kundenseite geschenkt.

Submission ist event-sourced: Statusübergänge (received → scored → sent/quarantined → released/discarded/blocked/failed) sind Events; die Historie (sub.History(ctx)) ist das Audit-Log des Konzepts. Das Struct selbst ist nur das gefaltete Read-Model (Apply-Funktion in internal/core/domain/submission.go).

Achtung: Der Query-Builder kennt auf ES-Read-Models nur die eigenen Struct-Felder — die Aggregate-Zeitstempel sind Methoden. Deshalb hat Submission ein eigenes ReceivedAt-Feld (orm:"index").

Tenant.SigningSecret und SmtpRoute.Password tragen orm:"encrypted" (AES-256-GCM, Schlüssel = encryption_key aus der config.json). Die DB sieht nur Ciphertext; die Felder sind nicht filter-/sortierbar und werden nie serialisiert. Den Key sichern — ohne ihn sind diese Felder verloren.

  • Additiv (neue Spalte/Index/Model): Model anpassen bzw. in registerModels() registrieren — ORM++ Auto-Diff erledigt den Rest.
  • Nicht additiv (Feld umbauen/entfernen): storage.SchemaVersion erhöhen und orm.MigrationTo(db, n, ...)-Schritt ergänzen (Expand/Contract mit Dual-Write, siehe ORM++ doc/API.md §8). Entfallende Felder erst mit orm:"deprecated" markieren.
  • Drift-Schutz: Model geändert ohne Versions-Erhöhung ⇒ Startfehler.

CRUD-Zugriffe kapseln die Repositories (internal/storage/repositories); orm.ErrNotFound wird via translate() zu repositories.ErrNotFound. Für Submissions (ES) nutzen die Services orm.New/Load/Query direkt.

Tests öffnen eine Wegwerf-DB im Temp-Verzeichnis:

db, err := storage.Open(ctx, orm.SQLite(filepath.Join(t.TempDir(), "test.db")), make([]byte, 32))

Vorlage: internal/core/services/services_test.go.

  • UpdateSet nie mit Pointer-Werten aufrufen (orm.Set("X", time.Now()), nicht &now) — Pointer umgehen die ORM++-Wert-Kodierung und machen die Zeile beim nächsten Scan unlesbar.
  • Nonce-Einlösung nutzt den Unique-Constraint als atomare Sperre (repositories.NonceRepository.Consume): zweiter Insert derselben Nonce ⇒ ErrNonceUsed (Replay).