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) CreateQuestion(ctx context.Context, q *RankedQuestion) error { q.db = p.db return q.Create(ctx) } func (p *Postgres) GetQuestion(ctx context.Context, id, viewerID string) (*RankedQuestion, error) { return GetQuestion(ctx, p.db, id, viewerID) } func (p *Postgres) ListHunt(ctx context.Context, huntDate, viewerID string) ([]RankedQuestion, error) { return ListHunt(ctx, p.db, huntDate, viewerID) } func (p *Postgres) ListQuestionsByAuthor(ctx context.Context, authorID string) ([]RankedQuestion, error) { return ListQuestionsByAuthor(ctx, p.db, authorID) } func (p *Postgres) ListQuestionsAnsweredBy(ctx context.Context, adminID string) ([]RankedQuestion, error) { return ListQuestionsAnsweredBy(ctx, p.db, adminID) } func (p *Postgres) HideQuestion(ctx context.Context, id string) error { q := &RankedQuestion{ID: id, db: p.db} return q.Hide(ctx) } func (p *Postgres) GetAnswer(ctx context.Context, questionID string) (*Answer, error) { return GetAnswer(ctx, p.db, questionID) } func (p *Postgres) UpsertAnswer(ctx context.Context, a *Answer) error { a.db = p.db return a.Upsert(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) 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) VotePost(ctx context.Context, userID, postID string, value int) error { return SetPostVote(ctx, p.db, userID, postID, value) } func (p *Postgres) Vote(ctx context.Context, userID, questionID string, value int) error { return Vote(ctx, p.db, userID, questionID, value) }