Adopt sqlc for typed Postgres queries behind store entities.
This commit is contained in:
+58
-48
@@ -9,6 +9,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"plumber/internal/store/sqlc"
|
||||
)
|
||||
|
||||
// ErrLastAdmin is returned when demoting the only remaining admin.
|
||||
@@ -22,7 +24,7 @@ const (
|
||||
RoleAdmin Role = "admin"
|
||||
)
|
||||
|
||||
// User is an account row. Methods run SQL against db.
|
||||
// User is an account row. Methods run SQL against db via sqlc.
|
||||
type User struct {
|
||||
ID string
|
||||
Username string
|
||||
@@ -48,6 +50,20 @@ func NormalizeUsername(s string) string {
|
||||
return strings.ToLower(strings.TrimSpace(s))
|
||||
}
|
||||
|
||||
func toUser(db *sql.DB, id, username, name, role, avatarURL, state, createdAt, passwordHash string) *User {
|
||||
return &User{
|
||||
ID: id,
|
||||
Username: username,
|
||||
Name: name,
|
||||
Role: Role(role),
|
||||
AvatarURL: avatarURL,
|
||||
State: state,
|
||||
CreatedAt: createdAt,
|
||||
PasswordHash: passwordHash,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// Create inserts the user. Sets ID, Name, and CreatedAt when empty.
|
||||
func (u *User) Create(ctx context.Context) error {
|
||||
if u == nil || u.db == nil {
|
||||
@@ -66,9 +82,14 @@ func (u *User) Create(ctx context.Context) error {
|
||||
if u.CreatedAt == "" {
|
||||
u.CreatedAt = time.Now().UTC().Format(time.RFC3339)
|
||||
}
|
||||
_, err := u.db.ExecContext(ctx, `INSERT INTO users (id, username, name, password_hash, role, avatar_url, state, created_at) VALUES ($1, $2, $3, $4, $5, '', '', $6)`,
|
||||
u.ID, u.Username, u.Name, u.PasswordHash, string(u.Role), u.CreatedAt)
|
||||
return err
|
||||
return sqlc.New(u.db).CreateUser(ctx, sqlc.CreateUserParams{
|
||||
ID: u.ID,
|
||||
Username: u.Username,
|
||||
Name: u.Name,
|
||||
PasswordHash: u.PasswordHash,
|
||||
Role: string(u.Role),
|
||||
CreatedAt: u.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
// SetRole updates this user's role (last-admin safe).
|
||||
@@ -85,21 +106,24 @@ func (u *User) SetRole(ctx context.Context, role Role) error {
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
var current string
|
||||
err = tx.QueryRowContext(ctx, `SELECT role FROM users WHERE id = $1`, u.ID).Scan(¤t)
|
||||
q := sqlc.New(tx)
|
||||
current, err := q.GetUserRole(ctx, u.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if Role(current) == RoleAdmin && role == RoleUser {
|
||||
var n int
|
||||
if err := tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM users WHERE role = $1`, string(RoleAdmin)).Scan(&n); err != nil {
|
||||
n, err := q.CountAdmins(ctx, string(RoleAdmin))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n <= 1 {
|
||||
return ErrLastAdmin
|
||||
}
|
||||
}
|
||||
res, err := tx.ExecContext(ctx, `UPDATE users SET role = $1 WHERE id = $2`, string(role), u.ID)
|
||||
res, err := q.UpdateUserRole(ctx, sqlc.UpdateUserRoleParams{
|
||||
Role: string(role),
|
||||
ID: u.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -123,61 +147,47 @@ func (u *User) SaveProfile(ctx context.Context) error {
|
||||
return fmt.Errorf("user: no database")
|
||||
}
|
||||
u.State = strings.TrimSpace(u.State)
|
||||
q := sqlc.New(u.db)
|
||||
if u.AvatarURL == "" {
|
||||
_, err := u.db.ExecContext(ctx, `UPDATE users SET state = $1 WHERE id = $2`, u.State, u.ID)
|
||||
return err
|
||||
return q.UpdateUserState(ctx, sqlc.UpdateUserStateParams{State: u.State, ID: u.ID})
|
||||
}
|
||||
_, err := u.db.ExecContext(ctx, `UPDATE users SET state = $1, avatar_url = $2 WHERE id = $3`, u.State, u.AvatarURL, u.ID)
|
||||
return err
|
||||
return q.UpdateUserStateAndAvatar(ctx, sqlc.UpdateUserStateAndAvatarParams{
|
||||
State: u.State,
|
||||
AvatarUrl: u.AvatarURL,
|
||||
ID: u.ID,
|
||||
})
|
||||
}
|
||||
|
||||
func CountAdmins(ctx context.Context, db *sql.DB) (int, error) {
|
||||
var n int
|
||||
err := db.QueryRowContext(ctx, `SELECT COUNT(*) FROM users WHERE role = $1`, string(RoleAdmin)).Scan(&n)
|
||||
return n, err
|
||||
n, err := sqlc.New(db).CountAdmins(ctx, string(RoleAdmin))
|
||||
return int(n), err
|
||||
}
|
||||
|
||||
func ListUsers(ctx context.Context, db *sql.DB) ([]User, error) {
|
||||
rows, err := db.QueryContext(ctx, `SELECT id, username, name, role, avatar_url, state, created_at FROM users ORDER BY created_at ASC`)
|
||||
rows, err := sqlc.New(db).ListUsers(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []User
|
||||
for rows.Next() {
|
||||
var u User
|
||||
var role string
|
||||
if err := rows.Scan(&u.ID, &u.Username, &u.Name, &role, &u.AvatarURL, &u.State, &u.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u.Role = Role(role)
|
||||
u.db = db
|
||||
out = append(out, u)
|
||||
out := make([]User, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
u := toUser(db, r.ID, r.Username, r.Name, r.Role, r.AvatarUrl, r.State, r.CreatedAt, "")
|
||||
out = append(out, *u)
|
||||
}
|
||||
return out, rows.Err()
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func UserByID(ctx context.Context, db *sql.DB, id string) (*User, error) {
|
||||
return scanUser(db, db.QueryRowContext(ctx, `SELECT id, username, name, role, avatar_url, state, created_at FROM users WHERE id = $1`, id), false)
|
||||
}
|
||||
|
||||
func UserByUsername(ctx context.Context, db *sql.DB, username string) (*User, error) {
|
||||
return scanUser(db, db.QueryRowContext(ctx, `SELECT id, username, name, role, avatar_url, state, created_at, password_hash FROM users WHERE username = $1`, NormalizeUsername(username)), true)
|
||||
}
|
||||
|
||||
func scanUser(db *sql.DB, row *sql.Row, withSecrets bool) (*User, error) {
|
||||
var u User
|
||||
var role string
|
||||
var err error
|
||||
if withSecrets {
|
||||
err = row.Scan(&u.ID, &u.Username, &u.Name, &role, &u.AvatarURL, &u.State, &u.CreatedAt, &u.PasswordHash)
|
||||
} else {
|
||||
err = row.Scan(&u.ID, &u.Username, &u.Name, &role, &u.AvatarURL, &u.State, &u.CreatedAt)
|
||||
}
|
||||
r, err := sqlc.New(db).GetUserByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u.Role = Role(role)
|
||||
u.db = db
|
||||
return &u, nil
|
||||
return toUser(db, r.ID, r.Username, r.Name, r.Role, r.AvatarUrl, r.State, r.CreatedAt, ""), nil
|
||||
}
|
||||
|
||||
func UserByUsername(ctx context.Context, db *sql.DB, username string) (*User, error) {
|
||||
r, err := sqlc.New(db).GetUserByUsername(ctx, NormalizeUsername(username))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return toUser(db, r.ID, r.Username, r.Name, r.Role, r.AvatarUrl, r.State, r.CreatedAt, r.PasswordHash), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user