import { readdir, readFile } from 'node:fs/promises'; import { basename, dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { loadConfig } from './config.js'; import { getPool, closePool, quoteIdentifier, withTransaction } from './db.js'; const here = dirname(fileURLToPath(import.meta.url)); async function resolveMigrationsDir(): Promise { const candidates = [ resolve(here, '../migrations'), resolve(process.cwd(), 'control-plane/migrations') ]; for (const candidate of candidates) { try { await readdir(candidate); return candidate; } catch { // The compiled layout does not copy SQL files into dist; continue with // the project-root source migration directory. } } throw new Error('迁移目录不存在。'); } export async function migrateDatabase(): Promise { const config = loadConfig(); const migrationsDir = await resolveMigrationsDir(); await getPool(config).query(`CREATE SCHEMA IF NOT EXISTS ${quoteIdentifier(config.DATABASE_SCHEMA)}`); await getPool(config).query(` CREATE TABLE IF NOT EXISTS schema_migrations ( version text PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT now() ) `); const files = (await readdir(migrationsDir)) .filter((file) => /^\d+_.+\.sql$/.test(file)) .sort(); const applied: string[] = []; for (const file of files) { const version = basename(file, '.sql'); const result = await withTransaction(config, async (client) => { const existing = await client.query('SELECT 1 FROM schema_migrations WHERE version = $1', [version]); if (existing.rowCount) return false; const sql = await readFile(resolve(migrationsDir, file), 'utf8'); await client.query(sql); await client.query('INSERT INTO schema_migrations (version) VALUES ($1)', [version]); return true; }); if (result) applied.push(version); } return applied; } if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) { migrateDatabase() .then((applied) => { console.log(JSON.stringify({ ok: true, applied })); }) .catch((error) => { console.error(JSON.stringify({ ok: false, error: error.message || String(error) })); process.exitCode = 1; }) .finally(() => closePool()); }