import type Database from "better-sqlite3"; export function runMigrations(db: Database.Database): void { db.exec(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, display_name TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'user', email_verified INTEGER DEFAULT 0, verify_token TEXT, verify_expires INTEGER, reset_token TEXT, reset_expires INTEGER, created_at INTEGER DEFAULT (unixepoch()), updated_at INTEGER DEFAULT (unixepoch()) ); CREATE TABLE IF NOT EXISTS saved_exams ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, title TEXT NOT NULL, subtitle TEXT, exam_json TEXT NOT NULL, created_at INTEGER DEFAULT (unixepoch()), updated_at INTEGER DEFAULT (unixepoch()) ); CREATE TABLE IF NOT EXISTS exam_results ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, exam_id INTEGER REFERENCES saved_exams(id) ON DELETE SET NULL, title TEXT NOT NULL, score_points REAL NOT NULL, total_points REAL NOT NULL, correct_count INTEGER NOT NULL, graded_count INTEGER NOT NULL, passing_score REAL DEFAULT 60, elapsed_ms INTEGER NOT NULL, state_json TEXT NOT NULL, exam_json TEXT NOT NULL, completed_at INTEGER DEFAULT (unixepoch()) ); CREATE TABLE IF NOT EXISTS site_settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at INTEGER DEFAULT (unixepoch()) ); CREATE INDEX IF NOT EXISTS idx_saved_exams_user ON saved_exams(user_id); CREATE INDEX IF NOT EXISTS idx_exam_results_user ON exam_results(user_id); `); // ── Add share_token and is_public columns if missing ── const examCols = db.prepare(`PRAGMA table_info(saved_exams)`).all() as { name: string }[]; if (!examCols.some((c) => c.name === 'share_token')) { db.exec(`ALTER TABLE saved_exams ADD COLUMN share_token TEXT`); } if (!examCols.some((c) => c.name === 'is_public')) { db.exec(`ALTER TABLE saved_exams ADD COLUMN is_public INTEGER DEFAULT 0`); } db.exec(` CREATE UNIQUE INDEX IF NOT EXISTS idx_saved_exams_share ON saved_exams(share_token); `); // ── Add role column if missing (existing databases) ── const cols = db.prepare(`PRAGMA table_info(users)`).all() as { name: string; }[]; if (!cols.some((c) => c.name === "role")) { db.exec(`ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'user'`); } // ── Add storage_quota_bytes if missing ── if (!cols.some((c) => c.name === "storage_quota_bytes")) { db.exec(`ALTER TABLE users ADD COLUMN storage_quota_bytes INTEGER NOT NULL DEFAULT 1073741824`); } // ── Add plan if missing ── if (!cols.some((c) => c.name === "plan")) { db.exec(`ALTER TABLE users ADD COLUMN plan TEXT NOT NULL DEFAULT 'free'`); } // ── Add is_favorite to saved_exams if missing ── const examSavedCols = db.prepare(`PRAGMA table_info(saved_exams)`).all() as { name: string }[]; if (!examSavedCols.some((c) => c.name === 'is_favorite')) { db.exec(`ALTER TABLE saved_exams ADD COLUMN is_favorite INTEGER DEFAULT 0`); } // ── Add tags to saved_exams if missing ── if (!examSavedCols.some((c) => c.name === 'tags')) { db.exec(`ALTER TABLE saved_exams ADD COLUMN tags TEXT DEFAULT '[]'`); } // ── Add collection_id to saved_exams if missing ── if (!examSavedCols.some((c) => c.name === 'collection_id')) { db.exec(`ALTER TABLE saved_exams ADD COLUMN collection_id INTEGER REFERENCES collections(id) ON DELETE SET NULL`); } // ── Add share_mode to saved_exams if missing ── if (!examSavedCols.some((c) => c.name === 'share_mode')) { db.exec(`ALTER TABLE saved_exams ADD COLUMN share_mode TEXT DEFAULT 'clone'`); } // ── Add cloned_from_share_token to saved_exams if missing ── if (!examSavedCols.some((c) => c.name === 'cloned_from_share_token')) { db.exec(`ALTER TABLE saved_exams ADD COLUMN cloned_from_share_token TEXT`); } // ── Collections table ── db.exec(` CREATE TABLE IF NOT EXISTS collections ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, name TEXT NOT NULL, created_at INTEGER DEFAULT (unixepoch()), updated_at INTEGER DEFAULT (unixepoch()) ); CREATE INDEX IF NOT EXISTS idx_collections_user ON collections(user_id); `); // ── Promote first user to admin if no admins exist ── const adminCount = db .prepare(`SELECT COUNT(*) AS cnt FROM users WHERE role = 'admin'`) .get() as { cnt: number }; if (adminCount.cnt === 0) { db.prepare( `UPDATE users SET role = 'admin' WHERE id = (SELECT MIN(id) FROM users)`, ).run(); } // ── Seed default settings ── const defaults: Record = { site_name: "BrainWave", max_upload_mb: "5", allow_guest_mode: "true", registration_enabled: "true", default_theme: "light", maintenance_mode: "false", maintenance_message: "We are currently performing maintenance. Please check back soon.", smtp_host: "", smtp_port: "587", smtp_user: "", smtp_pass: "", smtp_from: "BrainWave ", smtp_secure: "auto", app_url: "http://localhost:5173", }; const upsert = db.prepare(` INSERT INTO site_settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO NOTHING `); for (const [key, value] of Object.entries(defaults)) { upsert.run(key, value); } }