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>
153 lines
3.8 KiB
Go
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)
|
|
}
|