Files
OwnCord/Server/db/role_queries.go
T
Claude 44323373e3 refactor(server/db): adopt sqlc as the query layer — phase 1 (D2)
Wire the sqlc-generated dbgen package into db.DB so it stops being dead
code (audit A-2026-07-05) and becomes the real, CI-verified query layer.

db.DB now holds a *dbgen.Queries (initialized in Open via dbgen.New).
Query method bodies delegate to it; sqlc owns the SQL text and parameter
binding (make sqlc-verify), while db keeps its stable public API and
domain model types so no caller in api/admin/ws/service changes. The
migration is incremental — a method either delegates to d.q.* or still
runs raw SQL — so both layers are correct during the transition.

Converted domains (now load-bearing through sqlc):
- blocks: BlockUser, UnblockUser, IsBlocked, IsEitherBlocked,
  ListBlockedUsers (added the query to blocks.sql + regenerated).
  Empty ListBlockedUsers now returns []int64{} instead of nil, matching
  the MemStore backend — a latent inconsistency fixed, not a regression.
- lockouts: UpsertLockout, LoadActiveLockouts, CleanupExpiredLockouts,
  DeleteLockout (RFC3339 time formatting/parsing kept in the wrappers).
- roles: GetRoleByID, ListRoles, GetRoleForUser via a shared roleFromGen
  mapper (int64 position/is_default -> int/bool). GetUserWithRole stays
  raw for now.

Remaining domains stay on raw SQL and are tracked in
docs/plans/sqlc-adoption.md; store/ event+plugin SQL is intentionally
excluded (that layer is removed in D3). Decisions doc + audit closure
updated (A-2026-07-05 -> in progress).

Verified: go build ./...; go test -race ./db ./service ./auth ./ws (api
green non-race, race run matches CI's -timeout 20m); make sqlc-verify and
protocol-verify pass with the regenerated output committed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 15:01:54 +00:00

98 lines
2.7 KiB
Go

package db
import (
"database/sql"
"errors"
"fmt"
"github.com/owncord/server/db/dbgen"
)
// roleFromGen maps the sqlc-generated Role row to the domain Role model,
// narrowing the int64 position and converting the int64 is_default flag to a
// bool. Shared by all role reads that delegate to dbgen.
func roleFromGen(r dbgen.Role) *Role {
return &Role{
ID: r.ID,
Name: r.Name,
Color: r.Color,
Permissions: r.Permissions,
Position: int(r.Position),
IsDefault: r.IsDefault != 0,
}
}
// GetRoleByID returns the role with the given ID, or nil if not found.
func (d *DB) GetRoleByID(id int64) (*Role, error) {
r, err := d.q.GetRoleByID(dbCtx(), id)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("GetRoleByID: %w", err)
}
return roleFromGen(r), nil
}
// ListRoles returns all roles ordered by position descending.
func (d *DB) ListRoles() ([]*Role, error) {
rows, err := d.q.ListRoles(dbCtx())
if err != nil {
return nil, fmt.Errorf("ListRoles: %w", err)
}
roles := make([]*Role, 0, len(rows))
for _, r := range rows {
roles = append(roles, roleFromGen(r))
}
return roles, nil
}
// GetRoleForUser returns only the role for a given user via a single JOIN.
// Unlike GetUserWithRole, this does not fetch sensitive user columns (password,
// TOTP secret). Use this on hot paths like permission checks.
// Returns (nil, nil) when the user is not found.
func (d *DB) GetRoleForUser(userID int64) (*Role, error) {
r, err := d.q.GetRoleForUser(dbCtx(), userID)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("GetRoleForUser: %w", err)
}
return roleFromGen(r), nil
}
// GetUserWithRole returns the user and their role in a single query.
// Returns (nil, nil, nil) when the user is not found.
func (d *DB) GetUserWithRole(userID int64) (*User, *Role, error) {
row := d.sqlDB.QueryRow(
`SELECT u.id, u.username, u.password, u.avatar, u.role_id,
u.totp_secret, u.status, u.created_at, u.last_seen,
u.banned, u.ban_reason, u.ban_expires,
r.id, r.name, r.color, r.permissions, r.position, r.is_default
FROM users u
JOIN roles r ON u.role_id = r.id
WHERE u.id = ?`,
userID,
)
u := &User{}
r := &Role{}
var banned, isDefault int
err := row.Scan(
&u.ID, &u.Username, &u.PasswordHash, &u.Avatar, &u.RoleID,
&u.TOTPSecret, &u.Status, &u.CreatedAt, &u.LastSeen,
&banned, &u.BanReason, &u.BanExpires,
&r.ID, &r.Name, &r.Color, &r.Permissions, &r.Position, &isDefault,
)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil, nil
}
if err != nil {
return nil, nil, fmt.Errorf("GetUserWithRole: %w", err)
}
u.Banned = banned != 0
r.IsDefault = isDefault != 0
return u, r, nil
}