馃 distributed transcription service thistle.dunkirk.sh
1import { Database } from "bun:sqlite"; 2 3export const db = new Database("thistle.db"); 4 5// Schema version tracking 6db.run(` 7 CREATE TABLE IF NOT EXISTS schema_migrations ( 8 version INTEGER PRIMARY KEY, 9 applied_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) 10 ) 11`); 12 13const migrations = [ 14 { 15 version: 1, 16 name: "Complete user schema", 17 sql: ` 18 CREATE TABLE IF NOT EXISTS users ( 19 id INTEGER PRIMARY KEY AUTOINCREMENT, 20 email TEXT UNIQUE NOT NULL, 21 password_hash TEXT NOT NULL, 22 name TEXT, 23 avatar TEXT DEFAULT 'd', 24 created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) 25 ); 26 27 CREATE TABLE IF NOT EXISTS sessions ( 28 id TEXT PRIMARY KEY, 29 user_id INTEGER NOT NULL, 30 ip_address TEXT, 31 user_agent TEXT, 32 created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), 33 expires_at INTEGER NOT NULL, 34 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE 35 ); 36 37 CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id); 38 CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at); 39 `, 40 }, 41 { 42 version: 2, 43 name: "Add transcriptions table", 44 sql: ` 45 CREATE TABLE IF NOT EXISTS transcriptions ( 46 id TEXT PRIMARY KEY, 47 user_id INTEGER NOT NULL, 48 filename TEXT NOT NULL, 49 original_filename TEXT NOT NULL, 50 status TEXT NOT NULL DEFAULT 'uploading', 51 progress INTEGER NOT NULL DEFAULT 0, 52 transcript TEXT, 53 error_message TEXT, 54 created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), 55 updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), 56 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE 57 ); 58 59 CREATE INDEX IF NOT EXISTS idx_transcriptions_user_id ON transcriptions(user_id); 60 CREATE INDEX IF NOT EXISTS idx_transcriptions_status ON transcriptions(status); 61 `, 62 }, 63 { 64 version: 3, 65 name: "Add whisper_job_id to transcriptions", 66 sql: ` 67 ALTER TABLE transcriptions ADD COLUMN whisper_job_id TEXT; 68 CREATE INDEX IF NOT EXISTS idx_transcriptions_whisper_job_id ON transcriptions(whisper_job_id); 69 `, 70 }, 71 { 72 version: 4, 73 name: "Remove transcript column from transcriptions", 74 sql: ` 75 -- SQLite 3.35.0+ supports DROP COLUMN 76 ALTER TABLE transcriptions DROP COLUMN transcript; 77 `, 78 }, 79]; 80 81function getCurrentVersion(): number { 82 const result = db 83 .query<{ version: number }, []>( 84 "SELECT MAX(version) as version FROM schema_migrations", 85 ) 86 .get(); 87 return result?.version ?? 0; 88} 89 90function applyMigration( 91 version: number, 92 sql: string, 93 index: number, 94 total: number, 95) { 96 const current = getCurrentVersion(); 97 if (current >= version) return; 98 99 const isTTY = typeof process !== "undefined" && process.stdout?.isTTY; 100 const startMsg = `Applying migration ${index + 1} of ${total}`; 101 102 if (isTTY) { 103 process.stdout.write(`${startMsg}...`); 104 const start = performance.now(); 105 db.run(sql); 106 db.run("INSERT INTO schema_migrations (version) VALUES (?)", [version]); 107 const duration = Math.round(performance.now() - start); 108 process.stdout.write(`\r${startMsg} (${duration}ms)\n`); 109 } else { 110 console.log(startMsg); 111 db.run(sql); 112 db.run("INSERT INTO schema_migrations (version) VALUES (?)", [version]); 113 } 114} 115 116// Apply all migrations 117const current = getCurrentVersion(); 118const pending = migrations.filter((m) => m.version > current); 119 120for (const [index, migration] of pending.entries()) { 121 applyMigration(migration.version, migration.sql, index, pending.length); 122} 123 124export default db;