import Database from 'better-sqlite3'; import crypto from 'crypto'; /** * Ensure data invariants after schema migration. Idempotent — safe to run * on every boot. Uses raw better-sqlite3 handle (not Drizzle ORM). */ export function ensureDefaults(db: Database.Database): void { // 1. Ensure the single-row instance_settings row exists const row = db.prepare('SELECT id FROM instance_settings WHERE id = 1').get(); if (!row) { db.prepare( `INSERT OR IGNORE INTO instance_settings (id, max_bitrate_kbps, min_bitrate_kbps, bitrate_step_kbps, allowed_resolutions, allowed_framerates, max_resolution, max_framerate, updated_at) VALUES (1, 20000, 500, 500, ?, ?, 1080, 60, ?)` ).run('540,720,1080', '30,45,60', Date.now()); console.log('[defaults] Inserted default instance_settings row'); } // 2. Ensure a unique Snowflake worker ID is persisted (0-1023) const settings = db.prepare('SELECT worker_id FROM instance_settings WHERE id = 1').get() as { worker_id: number | null } | undefined; if (!settings || settings.worker_id === null) { const workerId = crypto.randomInt(0, 1024); db.prepare('UPDATE instance_settings SET worker_id = ? WHERE id = 1').run(workerId); console.log(`[defaults] Generated Snowflake worker ID: ${workerId}`); } // 2b. Ensure a persistent instance epoch (incarnation UUID) exists. A fresh // DB mints a new one — this is the discriminator for detecting resets. // The id=1 row is guaranteed by step 1's INSERT OR IGNORE above. const epochRow = db.prepare('SELECT instance_id FROM instance_settings WHERE id = 1').get() as { instance_id: string | null } | undefined; if (!epochRow || epochRow.instance_id === null) { const instanceId = crypto.randomUUID(); const res = db.prepare('UPDATE instance_settings SET instance_id = ? WHERE id = 1').run(instanceId); if (res.changes !== 1) throw new Error('ensureDefaults: instance_settings id=1 row missing — cannot mint epoch'); console.log('[defaults] Generated instance epoch'); } // 3. Ensure at least one admin exists (promote earliest registered user) const anyAdmin = db.prepare('SELECT id FROM users WHERE is_admin = 1 LIMIT 1').get(); if (!anyAdmin) { const firstUser = db.prepare( 'SELECT id FROM users ORDER BY created_at ASC LIMIT 1' ).get() as { id: string } | undefined; if (firstUser) { db.prepare('UPDATE users SET is_admin = 1 WHERE id = ?').run(firstUser.id); console.log(`[defaults] Promoted first user ${firstUser.id} to admin`); } } } /** * One-time, idempotent recovery of 1-on-1 DM threads broken before the DM * tombstone fix: those had the deleted partner's dm_members row removed, making * the thread UI-unreachable. For each ownerId-NULL channel with exactly one * member, re-insert membership for any distinct dm_messages author that is * missing from dm_members and still exists in users. Safe to run every boot. */ export function backfillOneOnOneDmMembership(db: Database.Database): void { const broken = db.prepare(` SELECT dc.id AS channelId FROM dm_channels dc WHERE dc.owner_id IS NULL AND (SELECT COUNT(*) FROM dm_members dm WHERE dm.dm_channel_id = dc.id) = 1 `).all() as { channelId: string }[]; if (broken.length === 0) return; const missingAuthors = db.prepare(` SELECT DISTINCT msg.user_id AS userId FROM dm_messages msg JOIN users u ON u.id = msg.user_id WHERE msg.dm_channel_id = ? AND msg.user_id NOT IN (SELECT user_id FROM dm_members WHERE dm_channel_id = ?) `); const insertMember = db.prepare('INSERT INTO dm_members (dm_channel_id, user_id, closed) VALUES (?, ?, 0)'); let restored = 0; const run = db.transaction(() => { for (const { channelId } of broken) { const authors = missingAuthors.all(channelId, channelId) as { userId: string }[]; for (const { userId } of authors) { insertMember.run(channelId, userId); restored++; } } }); run(); if (restored > 0) console.log(`[backfill] restored ${restored} deleted-partner DM membership row(s)`); }