52 lines
2.1 KiB
TypeScript
52 lines
2.1 KiB
TypeScript
/**
|
|
* 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();
|
|
}
|