import Database from 'better-sqlite3'; import { drizzle } from 'drizzle-orm/better-sqlite3'; import * as schema from './schema.js'; import { config } from '../config.js'; const sqlite = new Database(config.DATABASE_PATH); sqlite.pragma('journal_mode = WAL'); sqlite.pragma('foreign_keys = ON'); export const db = drizzle(sqlite, { schema }); // Raw sqlite client — used by the SQL runner endpoint. // eslint-disable-next-line @typescript-eslint/no-explicit-any export const rawSqlite: any = sqlite; export function runMigrations() { sqlite.exec(` CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE, name TEXT, image TEXT, google_id TEXT UNIQUE, role TEXT NOT NULL DEFAULT 'user', consent_given INTEGER NOT NULL DEFAULT 0, consent_at TEXT, created_at TEXT NOT NULL, deleted_at TEXT ); CREATE TABLE IF NOT EXISTS integration_tokens ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id), provider TEXT NOT NULL, access_token TEXT NOT NULL, refresh_token TEXT, expires_at TEXT, connected_at TEXT NOT NULL, UNIQUE(user_id, provider) ); CREATE TABLE IF NOT EXISTS tip_feedback ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id), tip_id TEXT NOT NULL, action TEXT NOT NULL, source_id TEXT, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS tip_views ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id), tip_id TEXT NOT NULL, served_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS push_subscriptions ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id), endpoint TEXT NOT NULL UNIQUE, p256dh TEXT NOT NULL, auth TEXT NOT NULL, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS sessions ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id), expires_at TEXT NOT NULL, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS admin_actions ( id TEXT PRIMARY KEY, admin_id TEXT NOT NULL REFERENCES users(id), action TEXT NOT NULL, target_type TEXT, target_id TEXT, detail TEXT, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS tip_scores ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id), tip_id TEXT NOT NULL, policy TEXT NOT NULL, ml_score INTEGER, features_json TEXT, candidate_count INTEGER, latency_ms INTEGER, served_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS saved_queries ( id TEXT PRIMARY KEY, admin_id TEXT NOT NULL REFERENCES users(id), name TEXT NOT NULL, sql TEXT NOT NULL, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS sim_runs ( id TEXT PRIMARY KEY, policy_a TEXT NOT NULL, policy_b TEXT NOT NULL, n_users INTEGER NOT NULL, n_rounds INTEGER NOT NULL, tasks_per_round INTEGER NOT NULL DEFAULT 8, use_llm INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'pending', summary_json TEXT, winner TEXT, persona_breakdown_json TEXT, created_at TEXT NOT NULL, finished_at TEXT ); CREATE TABLE IF NOT EXISTS sim_events ( id TEXT PRIMARY KEY, run_id TEXT NOT NULL REFERENCES sim_runs(id), round INTEGER NOT NULL, user_id TEXT NOT NULL, persona TEXT NOT NULL, policy TEXT NOT NULL, tip_content TEXT NOT NULL, priority INTEGER NOT NULL, is_overdue INTEGER NOT NULL, action TEXT NOT NULL, dwell_ms INTEGER, reward_milli INTEGER NOT NULL, hour INTEGER NOT NULL, day_of_week INTEGER NOT NULL, created_at TEXT NOT NULL ); `); // Additive column migrations — safe to run on existing DBs. // SQLite doesn't support IF NOT EXISTS on ALTER TABLE; we ignore the error if already present. for (const stmt of [ `ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'user'`, `ALTER TABLE push_subscriptions ADD COLUMN created_at TEXT NOT NULL DEFAULT ''`, `ALTER TABLE tip_feedback ADD COLUMN dwell_ms INTEGER`, `ALTER TABLE tip_feedback ADD COLUMN reward_milli INTEGER`, ]) { try { sqlite.exec(stmt); } catch { /* column already exists */ } } // Seed first admin from env (ADMIN_SEED_EMAIL). const seedEmail = process.env.ADMIN_SEED_EMAIL; if (seedEmail) { sqlite.prepare(`UPDATE users SET role = 'admin' WHERE email = ? AND role = 'user'`).run(seedEmail); } }