Commit e46b1a95 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add auto-migration on startup with idempotent schema

- Migrator creates _migrations table, tracks executed files by name
- Skips already-run migrations on subsequent deploys
- Schema SQL uses IF NOT EXISTS, DO/EXCEPTION blocks, DROP POLICY IF EXISTS
- Runs before server starts, exits on failure
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 5a159882
Pipeline #37 canceled with stages
This diff is collapsed.
import { buildApp } from './app.js'; import { buildApp } from './app.js';
import { closeDatabase } from './config/database.js'; import { closeDatabase } from './config/database.js';
import { runMigrations } from './migrator.js';
async function main() { async function main() {
const databaseUrl = process.env.DATABASE_URL;
if (databaseUrl) {
try {
await runMigrations(databaseUrl);
} catch (err) {
console.error('[migrate] FAILED:', err);
process.exit(1);
}
}
const app = await buildApp(); const app = await buildApp();
const shutdown = async (signal: string) => { const shutdown = async (signal: string) => {
......
import postgres from 'postgres';
import { readFileSync, readdirSync } from 'fs';
import { join } from 'path';
import { createHash } from 'crypto';
export async function runMigrations(databaseUrl: string): Promise<void> {
const sql = postgres(databaseUrl, { max: 1 });
try {
await sql`
CREATE TABLE IF NOT EXISTS public._migrations (
id serial PRIMARY KEY,
name text NOT NULL UNIQUE,
hash text NOT NULL,
executed_at timestamptz DEFAULT now() NOT NULL
)
`;
const executed = await sql<{ name: string }[]>`SELECT name FROM public._migrations ORDER BY id`;
const executedNames = new Set(executed.map(r => r.name));
const migrationsDir = join(import.meta.dirname, '..', 'migrations');
const files = readdirSync(migrationsDir)
.filter(f => f.endsWith('.sql'))
.sort();
for (const file of files) {
if (executedNames.has(file)) {
console.log(`[migrate] skip: ${file} (already executed)`);
continue;
}
const filePath = join(migrationsDir, file);
const content = readFileSync(filePath, 'utf-8');
const hash = createHash('sha256').update(content).digest('hex').slice(0, 16);
console.log(`[migrate] running: ${file}...`);
await sql.unsafe(content);
await sql`INSERT INTO public._migrations (name, hash) VALUES (${file}, ${hash})`;
console.log(`[migrate] done: ${file}`);
}
console.log('[migrate] all migrations up to date');
} finally {
await sql.end();
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment