v2: global player, infinite scroll, image picker, threaded comments
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { DatabaseSync, type SQLOutputValue } from "node:sqlite";
|
||||
import {
|
||||
type Comment,
|
||||
Dump,
|
||||
type Playlist,
|
||||
type RichContent,
|
||||
@@ -9,6 +10,32 @@ import {
|
||||
export const db = new DatabaseSync("api/sql/gerbeur.db");
|
||||
db.exec("PRAGMA foreign_keys = ON;");
|
||||
|
||||
// Migration: add is_private column if it doesn't exist yet
|
||||
try {
|
||||
db.exec(`ALTER TABLE dumps ADD COLUMN is_private INTEGER NOT NULL DEFAULT 0;`);
|
||||
} catch { /* column already exists */ }
|
||||
|
||||
// Migration: create comments table if it doesn't exist yet
|
||||
try {
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS comments (
|
||||
id TEXT PRIMARY KEY,
|
||||
dump_id TEXT NOT NULL REFERENCES dumps(id) ON DELETE CASCADE,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
parent_id TEXT REFERENCES comments(id) ON DELETE CASCADE,
|
||||
body TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
deleted INTEGER NOT NULL DEFAULT 0
|
||||
);`);
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_comments_dump ON comments(dump_id, created_at);`);
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_votes_user ON votes(user_id);`);
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_playlist_dumps_dump ON playlist_dumps(dump_id);`);
|
||||
} catch { /* already exists */ }
|
||||
|
||||
// Migration: add deleted column to comments if it doesn't exist yet
|
||||
try {
|
||||
db.exec(`ALTER TABLE comments ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0;`);
|
||||
} catch { /* column already exists */ }
|
||||
|
||||
/**
|
||||
* Database Row Types
|
||||
*/
|
||||
@@ -26,6 +53,8 @@ export interface DumpRow {
|
||||
file_mime: string | null;
|
||||
file_size: number | null;
|
||||
vote_count: number;
|
||||
comment_count?: number;
|
||||
is_private: number;
|
||||
[key: string]: SQLOutputValue; // Index signature
|
||||
}
|
||||
|
||||
@@ -62,7 +91,8 @@ export function isDumpRow(obj: Record<string, SQLOutputValue>): obj is DumpRow {
|
||||
(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";
|
||||
"vote_count" in obj && typeof obj.vote_count === "number" &&
|
||||
"is_private" in obj && typeof obj.is_private === "number";
|
||||
}
|
||||
|
||||
export function isUserRow(obj: Record<string, SQLOutputValue>): obj is UserRow {
|
||||
@@ -97,6 +127,8 @@ export function dumpRowToApi(row: DumpRow): Dump {
|
||||
fileMime: row.file_mime ?? undefined,
|
||||
fileSize: row.file_size ?? undefined,
|
||||
voteCount: row.vote_count,
|
||||
commentCount: row.comment_count ?? 0,
|
||||
isPrivate: Boolean(row.is_private),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -114,6 +146,7 @@ export function dumpApiToRow(dump: Dump): DumpRow {
|
||||
file_mime: dump.fileMime ?? null,
|
||||
file_size: dump.fileSize ?? null,
|
||||
vote_count: dump.voteCount,
|
||||
is_private: dump.isPrivate ? 1 : 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -139,6 +172,46 @@ export function userApiToRow(user: User): UserRow {
|
||||
};
|
||||
}
|
||||
|
||||
export interface CommentRow {
|
||||
id: string;
|
||||
dump_id: string;
|
||||
user_id: string;
|
||||
parent_id: string | null;
|
||||
body: string;
|
||||
created_at: string;
|
||||
deleted: number;
|
||||
author_username: string;
|
||||
author_avatar_mime: string | null;
|
||||
[key: string]: SQLOutputValue;
|
||||
}
|
||||
|
||||
export function isCommentRow(obj: Record<string, SQLOutputValue>): obj is CommentRow {
|
||||
return !!obj && typeof obj === "object" &&
|
||||
typeof obj.id === "string" &&
|
||||
typeof obj.dump_id === "string" &&
|
||||
typeof obj.user_id === "string" &&
|
||||
(typeof obj.parent_id === "string" || obj.parent_id === null) &&
|
||||
typeof obj.body === "string" &&
|
||||
typeof obj.created_at === "string" &&
|
||||
typeof obj.deleted === "number" &&
|
||||
typeof obj.author_username === "string" &&
|
||||
(typeof obj.author_avatar_mime === "string" || obj.author_avatar_mime === null);
|
||||
}
|
||||
|
||||
export function commentRowToApi(row: CommentRow): Comment {
|
||||
return {
|
||||
id: row.id,
|
||||
dumpId: row.dump_id,
|
||||
userId: row.user_id,
|
||||
parentId: row.parent_id ?? undefined,
|
||||
body: row.body,
|
||||
createdAt: new Date(row.created_at),
|
||||
deleted: Boolean(row.deleted),
|
||||
authorUsername: row.author_username,
|
||||
authorAvatarMime: row.author_avatar_mime ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export interface PlaylistRow {
|
||||
id: string;
|
||||
user_id: string;
|
||||
|
||||
Reference in New Issue
Block a user