Files
brainwave/backend/src/db/migrations.ts
T
LaurensBartelds 54a0ede92a
Test / backend (push) Failing after 1m6s
Test / frontend (push) Successful in 34s
feat: implement AI usage tracking and quota management with daily usage records
2026-05-05 15:37:17 +02:00

181 lines
6.6 KiB
TypeScript

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 TABLE IF NOT EXISTS ai_usage_daily (
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
period_date TEXT NOT NULL,
tokens_used INTEGER NOT NULL DEFAULT 0,
requests_used INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER DEFAULT (unixepoch()),
PRIMARY KEY (user_id, period_date)
);
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);
CREATE INDEX IF NOT EXISTS idx_ai_usage_daily_user ON ai_usage_daily(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<string, string> = {
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 <noreply@brainwave.local>",
smtp_secure: "auto",
app_url: "http://localhost:5173",
ai_provider: "",
ai_model: "gpt-4o-mini",
ai_api_key: "",
ai_base_url: "",
ai_model_hint: "",
ai_model_generate: "",
ai_model_grading: "",
ai_model_tutor: "",
ai_model_study_coach: "",
ai_daily_token_limit: "0",
};
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);
}
}