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().
105 lines
4.3 KiB
TypeScript
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`);
|
|
}
|
|
}
|
|
}
|