package store import ( "context" "database/sql" "fmt" "time" "github.com/google/uuid" "plumber/internal/store/sqlc" ) // Postgres implements Store against a sqlc-backed database. type Postgres struct { db *sql.DB } // NewPostgres wraps db as a Store. func NewPostgres(db *sql.DB) *Postgres { return &Postgres{db: db} } func (p *Postgres) CreateUser(ctx context.Context, u *User) error { if u == nil { return fmt.Errorf("user: nil") } if u.Role != RoleUser && u.Role != RoleAdmin { return fmt.Errorf("invalid role") } u.Username = NormalizeUsername(u.Username) u.Email = NormalizeEmail(u.Email) if u.ID == "" { u.ID = uuid.NewString() } if u.Name == "" { u.Name = u.Username } if u.CreatedAt == "" { u.CreatedAt = time.Now().UTC().Format(time.RFC3339) } if u.Role != RoleAdmin { u.db = p.db return u.Create(ctx) } // Bootstrap admin: serialize count+insert so two setup-secret registers // cannot both observe zero admins. tx, err := p.db.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() if _, err := tx.ExecContext(ctx, `SELECT pg_advisory_xact_lock($1)`, adminRoleLockKey); err != nil { return err } q := sqlc.New(tx) n, err := q.CountAdmins(ctx, string(RoleAdmin)) if err != nil { return err } role := RoleAdmin if n > 0 { role = RoleUser } if err := q.CreateUser(ctx, sqlc.CreateUserParams{ ID: u.ID, Username: u.Username, Name: u.Name, PasswordHash: u.PasswordHash, Role: string(role), Email: u.Email, CreatedAt: u.CreatedAt, }); err != nil { return mapUniqueViolation(err) } if err := tx.Commit(); err != nil { return err } u.Role = role u.db = p.db return nil } func (p *Postgres) UserByID(ctx context.Context, id string) (*User, error) { return UserByID(ctx, p.db, id) } func (p *Postgres) UserByUsername(ctx context.Context, username string) (*User, error) { return UserByUsername(ctx, p.db, username) } func (p *Postgres) ListUsers(ctx context.Context, q ListUsersQuery) ([]User, string, string, error) { return ListUsers(ctx, p.db, q) } func (p *Postgres) CountAdmins(ctx context.Context) (int, error) { return CountAdmins(ctx, p.db) } func (p *Postgres) SetUserRole(ctx context.Context, id string, role Role) error { u := &User{ID: id, db: p.db} return u.SetRole(ctx, role) } func (p *Postgres) SaveUserProfile(ctx context.Context, u *User) error { u.db = p.db return u.SaveProfile(ctx) } func (p *Postgres) CreatePost(ctx context.Context, post *Post) error { post.db = p.db return post.Create(ctx) } func (p *Postgres) GetPost(ctx context.Context, id string) (*Post, error) { return GetPost(ctx, p.db, id) } func (p *Postgres) GetPostThread(ctx context.Context, rootID string) (*Post, error) { return GetPostThread(ctx, p.db, rootID) } func (p *Postgres) GetPostThreadForViewer(ctx context.Context, rootID, viewerID string) (*Post, error) { return GetPostThreadForViewer(ctx, p.db, rootID, viewerID) } func (p *Postgres) UpdatePost(ctx context.Context, post *Post) error { post.db = p.db return post.Update(ctx) } func (p *Postgres) ListRootPosts(ctx context.Context, postDate, viewerID string) ([]Post, error) { return ListRootPosts(ctx, p.db, postDate, viewerID) } func (p *Postgres) ListRootPostsByAuthor(ctx context.Context, authorID string) ([]Post, error) { return ListRootPostsByAuthor(ctx, p.db, authorID) } func (p *Postgres) ListRootPostsAnsweredBy(ctx context.Context, adminID string) ([]Post, error) { return ListRootPostsAnsweredBy(ctx, p.db, adminID) } func (p *Postgres) SetRootPostState(ctx context.Context, id string, state PostState) error { return SetRootPostState(ctx, p.db, id, state) } func (p *Postgres) VotePost(ctx context.Context, userID, postID string, value int) error { return SetPostVote(ctx, p.db, userID, postID, value) }