/** * Applies scripts/migrations/*.sql in name order, once each, inside one * transaction. Applied names are recorded in schema_migration. * * bun scripts/migrate.ts apply pending migrations * bun scripts/migrate.ts --status list applied and pending migrations * * Every migration is additive and idempotent, so a database that was set up * with drizzle-kit push or the old migrate-platform script is safe to run. */ import { SQL } from 'bun'; import { readdir, readFile } from 'node:fs/promises'; const databaseUrl = process.env.DB_URL || process.env.DATABASE_URL; if (!databaseUrl) throw new Error('DB_URL or DATABASE_URL is required'); const directory = new URL('./migrations/', import.meta.url); const files = (await readdir(directory)).filter((name) => name.endsWith('.sql')).sort(); const sql = new SQL(databaseUrl, { max: 1 }); try { await sql`CREATE TABLE IF NOT EXISTS schema_migration (name text PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT now())`; const applied = new Set( ((await sql`SELECT name FROM schema_migration`) as { name: string }[]).map((row) => row.name) ); const pending = files.filter((name) => !applied.has(name)); if (process.argv.includes('--status')) { for (const name of files) console.log(`${applied.has(name) ? 'applied' : 'pending'} ${name}`); } else if (pending.length === 0) { console.log('Database is up to date'); } else { await sql.begin(async (tx) => { for (const name of pending) { await tx.unsafe(await readFile(new URL(name, directory), 'utf8')); await tx`INSERT INTO schema_migration (name) VALUES (${name})`; console.log(`applied ${name}`); } // The root administrator is configured per environment, never guessed. const rootId = process.env.ROOT_ADMIN_USER_ID; const legacyRoot = process.env.ROOT_ADMIN_EMAIL; if (rootId) await tx`UPDATE "user" SET root_admin = true, role = 'admin' WHERE id = ${rootId}`; else if (legacyRoot) await tx`UPDATE "user" SET root_admin = true, role = 'admin' WHERE lower(email) = ${legacyRoot.toLowerCase()}`; }); console.log(`${pending.length} migration(s) applied atomically`); } } finally { await sql.end(); }