66 lines
2.3 KiB
TypeScript
66 lines
2.3 KiB
TypeScript
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<string> {
|
|
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<string[]> {
|
|
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());
|
|
}
|