Registry-DB (nur Tenant-Metadaten), Provisioning-Routine legt pro Mandant
eine physisch isolierte Postgres-DB an und registriert sie transaktional
(Rollback der DB bei fehlgeschlagener Registrierung). Schlanker HTTP-Handler
als Schnittstellen-Vorbereitung fuer API-01/TEN-02, kein eigenes REST-Grundgerüst.
Pruefungen:
1. Migration up/down geschrieben (0001_tenant_registry.{up,down}.sql) — nicht
gegen echte DB ausgefuehrt, da auf dieser Maschine kein Go/Postgres-Test-
Setup verfuegbar ist. Offen zur Ausfuehrung.
2. Integrationstest TestProvision_CreatesIsolatedDatabases geschrieben (zwei
Mandanten, prueft unterschiedliche db_name und current_database()) —
ebenfalls nicht ausgefuehrt, guarded per TEST_ADMIN_DSN env var. Offen.
3. Slug-Validierung (unit test TestValidateSlug) deckt SQL-Injection-Versuch
im Datenbanknamen ab — ebenfalls nicht lokal ausgefuehrt, da kein Go
Compiler auf dieser Maschine vorhanden ist. Offen.
Alle drei Pruefungen sind vorbereitet, aber NICHT durchgefuehrt worden —
zaehlen laut Vorgabe als offen bis auf einer Maschine mit Go+Postgres verifiziert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
70 lines
1.9 KiB
Go
70 lines
1.9 KiB
Go
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) {
|
|
var t Tenant
|
|
row := r.pool.QueryRow(ctx, `
|
|
SELECT id, slug, name, db_name, db_dsn, status, created_at
|
|
FROM tenants WHERE slug = $1
|
|
`, slug)
|
|
|
|
if err := row.Scan(&t.ID, &t.Slug, &t.Name, &t.DBName, &t.DBDSN, &t.Status, &t.CreatedAt); err != nil {
|
|
return Tenant{}, fmt.Errorf("tenant laden: %w", err)
|
|
}
|
|
return t, 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
|
|
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() {
|
|
var t Tenant
|
|
if err := rows.Scan(&t.ID, &t.Slug, &t.Name, &t.DBName, &t.DBDSN, &t.Status, &t.CreatedAt); err != nil {
|
|
return nil, fmt.Errorf("tenant lesen: %w", err)
|
|
}
|
|
out = append(out, t)
|
|
}
|
|
return out, rows.Err()
|
|
}
|