package user import ( "context" "errors" "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgxpool" ) // TenantUserStore verwaltet Benutzer innerhalb GENAU EINER Tenant-Datenbank. // Welcher Mandant gemeint ist, ergibt sich ausschliesslich aus dem // uebergebenen Pool — es gibt keine tenant_id-Spalte (siehe migrations/tenant/0001_users.up.sql). type TenantUserStore struct { pool *pgxpool.Pool } func NewTenantUserStore(pool *pgxpool.Pool) *TenantUserStore { return &TenantUserStore{pool: pool} } func (s *TenantUserStore) Create(ctx context.Context, email, name string) (User, error) { if err := ValidateEmail(email); err != nil { return User{}, err } var u User u.Email, u.Name, u.Status = email, name, StatusActive row := s.pool.QueryRow(ctx, ` INSERT INTO users (email, name, status) VALUES ($1, $2, $3) RETURNING id, created_at, updated_at `, u.Email, u.Name, u.Status) if err := row.Scan(&u.ID, &u.CreatedAt, &u.UpdatedAt); err != nil { return User{}, mapWriteErr(err) } return u, nil } func (s *TenantUserStore) Get(ctx context.Context, id string) (User, error) { return scanUser(s.pool.QueryRow(ctx, ` SELECT id, email, name, status, created_at, updated_at FROM users WHERE id = $1 `, id)) } func (s *TenantUserStore) List(ctx context.Context) ([]User, error) { rows, err := s.pool.Query(ctx, ` SELECT id, email, name, status, created_at, updated_at FROM users ORDER BY created_at `) if err != nil { return nil, fmt.Errorf("benutzer auflisten: %w", err) } defer rows.Close() var out []User for rows.Next() { var u User if err := rows.Scan(&u.ID, &u.Email, &u.Name, &u.Status, &u.CreatedAt, &u.UpdatedAt); err != nil { return nil, fmt.Errorf("benutzer lesen: %w", err) } out = append(out, u) } return out, rows.Err() } // Update aendert Name und E-Mail. Eine leere email/name laesst das jeweilige // Feld unveraendert. func (s *TenantUserStore) Update(ctx context.Context, id, email, name string) (User, error) { if email != "" { if err := ValidateEmail(email); err != nil { return User{}, err } } row := s.pool.QueryRow(ctx, ` UPDATE users SET email = COALESCE(NULLIF($2, ''), email), name = COALESCE(NULLIF($3, ''), name), updated_at = now() WHERE id = $1 RETURNING id, email, name, status, created_at, updated_at `, id, email, name) u, err := scanUser(row) if err != nil { return User{}, mapWriteErr(err) } return u, nil } // Deactivate setzt den Benutzer auf inaktiv statt ihn zu loeschen. func (s *TenantUserStore) Deactivate(ctx context.Context, id string) (User, error) { return scanUser(s.pool.QueryRow(ctx, ` UPDATE users SET status = $2, updated_at = now() WHERE id = $1 RETURNING id, email, name, status, created_at, updated_at `, id, StatusInactive)) } func scanUser(row pgx.Row) (User, error) { var u User if err := row.Scan(&u.ID, &u.Email, &u.Name, &u.Status, &u.CreatedAt, &u.UpdatedAt); err != nil { if errors.Is(err, pgx.ErrNoRows) { return User{}, ErrNotFound } return User{}, fmt.Errorf("benutzer lesen: %w", err) } return u, nil } // mapWriteErr uebersetzt den Unique-Constraint-Verstoss der E-Mail-Spalte in // einen sprechenden Fehler statt der rohen Postgres-Fehlermeldung. func mapWriteErr(err error) error { var pgErr *pgconn.PgError if errors.As(err, &pgErr) && pgErr.Code == "23505" { return ErrEmailTaken } if errors.Is(err, pgx.ErrNoRows) { return ErrNotFound } return fmt.Errorf("benutzer schreiben: %w", err) }