dsec-notebook/src/lib/server/db.ts
2026-09-02 23:11:14 +08:00

285 lines
8.8 KiB
TypeScript

import { DatabaseSync } from "node:sqlite";
import { mkdirSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { randomUUID } from "node:crypto";
import { env } from "$env/dynamic/private";
const DB_PATH = resolve(env.DATABASE_PATH ?? "data/dsec.db");
let db: DatabaseSync | null = null;
function createSchema(database: DatabaseSync) {
database.exec(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
sessionToken TEXT,
passwordHash TEXT,
avatarUrl TEXT,
role TEXT NOT NULL DEFAULT 'user',
createdAt INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS topics (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
description TEXT
);
CREATE TABLE IF NOT EXISTS units (
id TEXT PRIMARY KEY,
code TEXT NOT NULL UNIQUE,
code2 TEXT,
name TEXT NOT NULL,
description TEXT
);
CREATE TABLE IF NOT EXISTS notes (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
topicId TEXT NOT NULL,
unitId TEXT NOT NULL,
authorId TEXT NOT NULL,
authorName TEXT NOT NULL,
createdAt INTEGER NOT NULL,
updatedAt INTEGER NOT NULL,
voteCount INTEGER NOT NULL DEFAULT 0,
commentCount INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS questions (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
topicId TEXT NOT NULL,
unitId TEXT NOT NULL,
authorId TEXT NOT NULL,
authorName TEXT NOT NULL,
createdAt INTEGER NOT NULL,
updatedAt INTEGER NOT NULL,
voteCount INTEGER NOT NULL DEFAULT 0,
answerCount INTEGER NOT NULL DEFAULT 0,
solved INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS comments (
id TEXT PRIMARY KEY,
content TEXT NOT NULL,
authorId TEXT NOT NULL,
authorName TEXT NOT NULL,
parentId TEXT,
questionId TEXT,
parentCommentId TEXT,
createdAt INTEGER NOT NULL,
updatedAt INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS votes (
id TEXT PRIMARY KEY,
userId TEXT NOT NULL,
targetType TEXT NOT NULL,
targetId TEXT NOT NULL,
value INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS pinned_units (
userId TEXT NOT NULL,
unitId TEXT NOT NULL,
createdAt INTEGER NOT NULL,
PRIMARY KEY (userId, unitId)
);
CREATE TABLE IF NOT EXISTS email_verifications (
email TEXT PRIMARY KEY,
name TEXT NOT NULL,
codeHash TEXT NOT NULL,
expiresAt INTEGER NOT NULL,
attempts INTEGER NOT NULL DEFAULT 0,
createdAt INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS verification_request_limits (
identifier TEXT PRIMARY KEY,
requestCount INTEGER NOT NULL DEFAULT 0,
windowStartedAt INTEGER NOT NULL,
blockedUntil INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_notes_topic ON notes(topicId);
CREATE INDEX IF NOT EXISTS idx_notes_unit ON notes(unitId);
CREATE INDEX IF NOT EXISTS idx_notes_created ON notes(createdAt);
CREATE INDEX IF NOT EXISTS idx_questions_topic ON questions(topicId);
CREATE INDEX IF NOT EXISTS idx_questions_unit ON questions(unitId);
CREATE INDEX IF NOT EXISTS idx_questions_created ON questions(createdAt);
CREATE INDEX IF NOT EXISTS idx_comments_note ON comments(parentId);
CREATE INDEX IF NOT EXISTS idx_comments_question ON comments(questionId);
CREATE UNIQUE INDEX IF NOT EXISTS idx_votes_user_target ON votes(userId, targetType, targetId);
CREATE INDEX IF NOT EXISTS idx_pinned_units_user ON pinned_units(userId);
`);
}
const SEED_TOPICS = [
{
name: "Algorithms",
slug: "algorithms",
description: "Algorithm design, analysis, and common patterns",
},
{
name: "Data Structures",
slug: "data-structures",
description: "Arrays, linked lists, trees, graphs, and more",
},
{
name: "Networking",
slug: "networking",
description: "Computer networks, protocols, and architectures",
},
{
name: "Cybersecurity",
slug: "cybersecurity",
description: "Security principles, threats, and defenses",
},
{
name: "Databases",
slug: "databases",
description: "Relational and NoSQL databases, SQL, and design",
},
{
name: "Web Development",
slug: "web-development",
description: "HTML, CSS, JavaScript, and frameworks",
},
{
name: "Operating Systems",
slug: "operating-systems",
description: "OS concepts, processes, memory, and file systems",
},
{
name: "Software Engineering",
slug: "software-engineering",
description: "Design patterns, testing, and methodologies",
},
{
name: "Programming Languages",
slug: "programming-languages",
description: "Language concepts, paradigms, and syntax",
},
{
name: "Mathematics",
slug: "mathematics",
description: "Discrete math, linear algebra, statistics for CS",
},
{
name: "Machine Learning",
slug: "machine-learning",
description: "ML concepts, models, and techniques",
},
{
name: "Cloud Computing",
slug: "cloud-computing",
description: "Cloud platforms, services, and architecture",
},
{
name: "Mobile Development",
slug: "mobile-development",
description: "iOS, Android, and cross-platform development",
},
{ name: "DevOps", slug: "devops", description: "CI/CD, containers, and infrastructure" },
{
name: "Computer Architecture",
slug: "computer-architecture",
description: "CPU, memory, and hardware design",
},
];
const SEED_UNITS = [
{ code: "SIT102", name: "Introduction to Programming" },
{ code: "SIT111", name: "Computer Systems" },
{ code: "SIT192", name: "Discrete Mathematics" },
{ code: "SIT202", name: "Computer Networks and Communication" },
{ code: "SIT210", name: "Embedded Systems Development" },
{ code: "SIT221", name: "Data Structures and Algorithms" },
{ code: "SIT232", name: "Object-Oriented Development" },
{ code: "SIT281", name: "Cryptography" },
{ code: "SIT282", name: "Computer Forensics" },
{ code: "SIT283", name: "Ethical Hacking" },
{ code: "SIT284", name: "Cyber Security Management" },
{ code: "SIT313", name: "Full Stack Web Development" },
{ code: "SIT315", name: "Programming Paradigms" },
{ code: "SIT323", name: "Cloud Native Application Development" },
{ code: "SIT331", name: "IT Security" },
{ code: "SIT374", name: "Team Project (A) - Project Management" },
{ code: "SIT378", name: "Team Project (B) - Execution" },
{ code: "SIT379", name: "Ethical Hacking" },
{ code: "SIT384", name: "Cyber Security Analytics" },
{ code: "SIT393", name: "Computing Internship" },
{ code: "MIS771", name: "Business Intelligence and Data Warehousing" },
{ code: "MIS772", name: "Predictive Analytics" },
{ code: "MIS782", name: "Information Security Governance" },
{ code: "MIS784", name: "Cyber Security Management and Practices" },
{ code: "MIS785", name: "IT Strategy and Governance" },
{ code: "MIS798", name: "Business Process Management" },
];
function migrate(database: DatabaseSync) {
const columns = database.prepare("PRAGMA table_info(users)").all() as { name: string }[];
if (!columns.some((c) => c.name === "passwordHash")) {
database.exec("ALTER TABLE users ADD COLUMN passwordHash TEXT");
}
if (!columns.some((c) => c.name === "role")) {
database.exec("ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'user'");
}
if (!columns.some((c) => c.name === "avatarUrl")) {
database.exec("ALTER TABLE users ADD COLUMN avatarUrl TEXT");
}
const unitColumns = database.prepare("PRAGMA table_info(units)").all() as { name: string }[];
if (!unitColumns.some((c) => c.name === "code2")) {
database.exec("ALTER TABLE units ADD COLUMN code2 TEXT");
}
const commentColumns = database.prepare("PRAGMA table_info(comments)").all() as {
name: string;
}[];
if (!commentColumns.some((c) => c.name === "updatedAt")) {
database.exec("ALTER TABLE comments ADD COLUMN updatedAt INTEGER NOT NULL DEFAULT 0");
}
}
function seed(database: DatabaseSync) {
const existing = database.prepare("SELECT COUNT(*) AS c FROM topics").get() as { c: number };
if (existing.c > 0) return;
const insertTopic = database.prepare(
"INSERT INTO topics (id, name, slug, description) VALUES (?, ?, ?, ?)",
);
const insertUnit = database.prepare("INSERT INTO units (id, code, name) VALUES (?, ?, ?)");
database.exec("BEGIN");
try {
for (const topic of SEED_TOPICS) {
insertTopic.run(randomUUID(), topic.name, topic.slug, topic.description ?? null);
}
for (const unit of SEED_UNITS) {
insertUnit.run(randomUUID(), unit.code, unit.name);
}
database.exec("COMMIT");
} catch (err) {
database.exec("ROLLBACK");
throw err;
}
}
export function getDb(): DatabaseSync {
if (!db) {
mkdirSync(dirname(DB_PATH), { recursive: true });
db = new DatabaseSync(DB_PATH);
createSchema(db);
migrate(db);
seed(db);
}
return db;
}