Files
codegirl007 f420f888af Remove legacy question storage (#7)
Deletes obsolete question/answer/vote persistence and the compatibility answer endpoint. Existing databases drop the legacy tables through migration 009. Plumber replies now notify the root homeowner even when nested beneath another plumber reply. Post and reply forms prevent duplicate submissions and show progress while posting.

Reviewed-on: #7
Co-authored-by: codegirl-007 <s.raide@gmail.com>
2026-08-27 16:17:57 +00: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)
}