Files
backspace/packages/server/src/db/migrate.ts
T
Jannis Braun 3acaea2e45 refactor: replace 2100-line migration system with ensureDefaults + baseline
Remove all manual migrations — Drizzle-kit now manages schema DDL.
Data-fix migrations have all completed on both instances.
Startup initialization (settings row, worker ID, first admin)
moves to idempotent ensureDefaults().
2026-04-09 00:30:00 +02:00

105 lines
4.3 KiB
TypeScript

import Database from 'better-sqlite3';
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
/**
* Baseline an existing install so Drizzle's migrate() skips the initial
* migration (tables already exist). Must be called BEFORE migrate().
*
* Detects existing installs by checking: users table exists but
* __drizzle_migrations table does not.
*
* Drizzle's __drizzle_migrations table schema (verified SQLite DDL):
* "id" integer PRIMARY KEY AUTOINCREMENT NOT NULL
* "hash" text NOT NULL -- SHA-256 hex of the migration SQL file (raw UTF-8)
* "created_at" numeric -- journalEntry.when (ms timestamp from journal)
*
* Hash must match Drizzle's exactly: read SQL file as raw UTF-8 string,
* hash it with SHA-256. Line endings matter — don't normalize \r\n vs \n.
*/
export function baselineExistingInstall(db: Database.Database): void {
const hasUsers = db.prepare(
"SELECT name FROM sqlite_master WHERE type='table' AND name='users'"
).get();
const hasJournal = db.prepare(
"SELECT name FROM sqlite_master WHERE type='table' AND name='__drizzle_migrations'"
).get();
if (!hasUsers || hasJournal) return; // Fresh install or already baselined
console.log('[migrate] Existing install detected — baselining Drizzle migrations...');
// Read the journal to get the initial migration metadata
const migrationsFolder = path.resolve(__dirname, '../../drizzle');
const journalPath = path.join(migrationsFolder, 'meta', '_journal.json');
const journal = JSON.parse(fs.readFileSync(journalPath, 'utf-8'));
const initialEntry = journal.entries[0];
if (!initialEntry) {
throw new Error('No entries found in drizzle migration journal');
}
// Compute the hash the same way Drizzle does: SHA-256 of the SQL file content
const sqlPath = path.join(migrationsFolder, `${initialEntry.tag}.sql`);
const sqlContent = fs.readFileSync(sqlPath, 'utf-8');
const hash = crypto.createHash('sha256').update(sqlContent).digest('hex');
// Create the journal table matching Drizzle's exact SQLite DDL
db.exec(`
CREATE TABLE IF NOT EXISTS "__drizzle_migrations" (
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
"hash" text NOT NULL,
"created_at" numeric
)
`);
db.prepare(
'INSERT INTO __drizzle_migrations (hash, created_at) VALUES (?, ?)'
).run(hash, initialEntry.when);
console.log(`[migrate] Baselined initial migration: ${initialEntry.tag} (hash: ${hash.slice(0, 12)}...)`);
}
/**
* 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}`);
}
// 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`);
}
}
}