package tenant import ( "context" "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) // Registry kapselt den Zugriff auf die Control-Plane-Registry-Datenbank. // Sie enthaelt ausschliesslich Tenant-Metadaten (Akzeptanzkriterium 1) — // niemals Geschaeftsdaten eines Mandanten. type Registry struct { pool *pgxpool.Pool } func NewRegistry(pool *pgxpool.Pool) *Registry { return &Registry{pool: pool} } // insertTx schreibt den Tenant-Datensatz innerhalb einer laufenden Transaktion, // damit Provisioner.Provision DB-Anlage und Registrierung atomar behandeln kann. func (r *Registry) insertTx(ctx context.Context, tx pgx.Tx, t Tenant) (Tenant, error) { row := tx.QueryRow(ctx, ` INSERT INTO tenants (slug, name, db_name, db_dsn, status) VALUES ($1, $2, $3, $4, $5) RETURNING id, created_at `, t.Slug, t.Name, t.DBName, t.DBDSN, t.Status) if err := row.Scan(&t.ID, &t.CreatedAt); err != nil { return Tenant{}, fmt.Errorf("tenant registrieren: %w", err) } return t, nil } func (r *Registry) GetBySlug(ctx context.Context, slug string) (Tenant, error) { // previous_status/deletion_scheduled_at werden mitgelesen, damit TEN-04 // (internal/tenant/lifecycle.go) den vollstaendigen Lebenszyklus-Zustand // ueber GetBySlug ansehen kann, statt eine eigene Abfrage zu duplizieren. // retention_block_reason/retention_checked_at (TEN-08) aus demselben Grund // fuer die Admin-Einsehbarkeit des Sperrgrunds (Akzeptanzkriterium 2). row := r.pool.QueryRow(ctx, ` SELECT id, slug, name, db_name, db_dsn, status, created_at, previous_status, deletion_scheduled_at, retention_block_reason, retention_checked_at FROM tenants WHERE slug = $1 `, slug) t, err := scanTenantWithLifecycle(row) if err != nil { return Tenant{}, fmt.Errorf("tenant laden: %w", err) } return t, nil } // Delete entfernt einen Registry-Eintrag. Wird von Provisioner.Deprovision // als Kompensation verwendet, wenn nach erfolgreicher Registrierung ein // nachgelagerter Schritt (z.B. Admin-Account-Anlage in TEN-02) fehlschlaegt. func (r *Registry) Delete(ctx context.Context, id string) error { if _, err := r.pool.Exec(ctx, `DELETE FROM tenants WHERE id = $1`, id); err != nil { return fmt.Errorf("tenant loeschen: %w", err) } return nil } func (r *Registry) List(ctx context.Context) ([]Tenant, error) { rows, err := r.pool.Query(ctx, ` SELECT id, slug, name, db_name, db_dsn, status, created_at, previous_status, deletion_scheduled_at, retention_block_reason, retention_checked_at FROM tenants ORDER BY created_at `) if err != nil { return nil, fmt.Errorf("tenants auflisten: %w", err) } defer rows.Close() var out []Tenant for rows.Next() { t, err := scanTenantWithLifecycle(rows) if err != nil { return nil, fmt.Errorf("tenant lesen: %w", err) } out = append(out, t) } return out, rows.Err() }