/** * The `users` table: its row type, its guard, its mappers to and from the * stored {@link User}, and the views of a user the API sends. * * @module */ import type { SQLOutputValue } from "node:sqlite"; import { isRole, type PublicUser, type Role, type SelfUser, } from "../../shared/user.ts"; /** A user as stored: what they see of themselves, plus the password hash. */ export interface User extends SelfUser { /** The scrypt hash of the password, as `hash.salt`. */ passwordHash: string; } /** Returns what anyone may see of `user`: no hash, no email. */ export function toPublicUser( { passwordHash: _, email: _email, ...user }: User, ): PublicUser { return user; } /** Returns what `user` sees of themselves: everything but the hash. */ export function toSelfUser({ passwordHash: _, ...user }: User): SelfUser { return user; } /** A row of the `users` table. */ export interface UserRow { /** The user's id. */ id: string; /** The user's name, unique. */ username: string; /** The scrypt hash of the password, as `hash.salt`. */ password_hash: string; /** The user's role. */ role: Role; /** When the user registered. */ created_at: string; /** When the profile was last changed, if ever. */ updated_at: string | null; /** The avatar's type, null without one. */ avatar_mime: string | null; /** The user's profile text, in Markdown. */ description: string | null; /** The id of the user whose invite they registered with. */ invited_by: string | null; /** * The inviter's name; present only when the query joins it (`LEFT JOIN users * i ON i.id = u.invited_by`). */ invited_by_username: string | null; /** The user's email address. */ email: string; /** Any other column the query selects. */ [key: string]: SQLOutputValue; } /** Checks whether `obj` is a {@link UserRow}. */ export function isUserRow(obj: unknown): obj is UserRow { return !!obj && typeof obj === "object" && "id" in obj && typeof obj.id === "string" && "username" in obj && typeof obj.username === "string" && "password_hash" in obj && typeof obj.password_hash === "string" && "role" in obj && isRole(obj.role) && "created_at" in obj && typeof obj.created_at === "string" && "updated_at" in obj && (typeof obj.updated_at === "string" || obj.updated_at === null) && "avatar_mime" in obj && (typeof obj.avatar_mime === "string" || obj.avatar_mime === null) && "description" in obj && (typeof obj.description === "string" || obj.description === null) && "invited_by" in obj && (typeof obj.invited_by === "string" || obj.invited_by === null) && "email" in obj && typeof obj.email === "string"; } /** Converts a row to the stored {@link User}. */ export function userRowToApi(row: UserRow): User { return { id: row.id, username: row.username, passwordHash: row.password_hash, role: row.role, createdAt: new Date(row.created_at), updatedAt: row.updated_at ? new Date(row.updated_at) : undefined, avatarMime: row.avatar_mime ?? undefined, description: row.description ?? undefined, invitedByUsername: typeof row.invited_by_username === "string" ? row.invited_by_username : undefined, email: row.email, }; } /** Converts a {@link User} back to a row, without the join-only columns. */ export function userApiToRow(user: User): UserRow { return { id: user.id, username: user.username, password_hash: user.passwordHash, role: user.role, created_at: user.createdAt.toISOString(), updated_at: user.updatedAt?.toISOString() ?? null, avatar_mime: user.avatarMime ?? null, description: user.description ?? null, invited_by: null, invited_by_username: null, email: user.email, }; }