Files
plumber/internal/store/postgres_store.go
codegirl007 df17e0e6f3
CI / test (pull_request) Failing after 5m44s
Remove legacy question storage
2026-08-27 09:08:40 -07:00

153 lines
3.8 KiB
Go

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)
}