import { DatabaseSync, type SQLOutputValue } from "node:sqlite"; import { Dump, type RichContent, type User } from "./interfaces.ts"; export const db = new DatabaseSync("api/sql/gerbeur.db"); db.exec("PRAGMA journal_mode = WAL;"); db.exec("PRAGMA foreign_keys = ON;"); /** * Database Row Types */ export interface DumpRow { id: string; kind: string; title: string; comment: string | null; user_id: string; created_at: string; url: string | null; rich_content: string | null; file_name: string | null; file_mime: string | null; file_size: number | null; vote_count: number; [key: string]: SQLOutputValue; // Index signature } export interface UserRow { id: string; username: string; password_hash: string; is_admin: number; created_at: string; avatar_mime: string | null; [key: string]: SQLOutputValue; // Index signature } /** * Type Guards */ export function isDumpRow(obj: Record): obj is DumpRow { return !!obj && typeof obj === "object" && "id" in obj && typeof obj.id === "string" && "kind" in obj && typeof obj.kind === "string" && "title" in obj && typeof obj.title === "string" && "comment" in obj && (typeof obj.comment === "string" || obj.comment === null) && "user_id" in obj && typeof obj.user_id === "string" && "created_at" in obj && typeof obj.created_at === "string" && "url" in obj && (typeof obj.url === "string" || obj.url === null) && "rich_content" in obj && (typeof obj.rich_content === "string" || obj.rich_content === null) && "file_name" in obj && (typeof obj.file_name === "string" || obj.file_name === null) && "file_mime" in obj && (typeof obj.file_mime === "string" || obj.file_mime === null) && "file_size" in obj && (typeof obj.file_size === "number" || obj.file_size === null) && "vote_count" in obj && typeof obj.vote_count === "number"; } export function isUserRow(obj: Record): 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" && "is_admin" in obj && typeof obj.is_admin === "number" && "created_at" in obj && typeof obj.created_at === "string" && "avatar_mime" in obj && (typeof obj.avatar_mime === "string" || obj.avatar_mime === null); } /** * Conversion Helpers */ export function dumpRowToApi(row: DumpRow): Dump { return { id: row.id, kind: row.kind as "url" | "file", title: row.title, comment: row.comment ?? undefined, userId: row.user_id, createdAt: new Date(row.created_at), url: row.url ?? undefined, richContent: row.rich_content ? (JSON.parse(row.rich_content) as RichContent) : undefined, fileName: row.file_name ?? undefined, fileMime: row.file_mime ?? undefined, fileSize: row.file_size ?? undefined, voteCount: row.vote_count, }; } export function dumpApiToRow(dump: Dump): DumpRow { return { id: dump.id, kind: dump.kind, title: dump.title, comment: dump.comment ?? null, user_id: dump.userId, created_at: dump.createdAt.toISOString(), url: dump.url ?? null, rich_content: dump.richContent ? JSON.stringify(dump.richContent) : null, file_name: dump.fileName ?? null, file_mime: dump.fileMime ?? null, file_size: dump.fileSize ?? null, vote_count: dump.voteCount, }; } export function userRowToApi(row: UserRow): User { return { id: row.id, username: row.username, passwordHash: row.password_hash, isAdmin: Boolean(row.is_admin), createdAt: new Date(row.created_at), avatarMime: row.avatar_mime ?? undefined, }; } export function userApiToRow(user: User): UserRow { return { id: user.id, username: user.username, password_hash: user.passwordHash, is_admin: user.isAdmin ? 1 : 0, created_at: user.createdAt.toISOString(), avatar_mime: user.avatarMime ?? null, }; }