← back to Lifestyle Asset Intel

scripts/migrate.js

52 lines

#!/usr/bin/env node
// migrate.js — applies any unapplied SQL files from db/migrations/ in lexical order.
// Idempotent: tracks applied filenames in schema_migrations.

require('dotenv').config();
const fs = require('fs');
const path = require('path');
const { pool } = require('../lib/db');

const MIGRATIONS_DIR = path.join(__dirname, '..', 'db', 'migrations');

async function main() {
  await pool.query(`
    CREATE TABLE IF NOT EXISTS schema_migrations (
      filename text PRIMARY KEY,
      applied_at timestamptz NOT NULL DEFAULT now()
    );
  `);

  const files = fs.readdirSync(MIGRATIONS_DIR)
    .filter((f) => f.endsWith('.sql'))
    .sort();

  const { rows } = await pool.query('SELECT filename FROM schema_migrations');
  const applied = new Set(rows.map((r) => r.filename));

  let count = 0;
  for (const f of files) {
    if (applied.has(f)) {
      console.log(`[migrate] skip ${f} (already applied)`);
      continue;
    }
    const sql = fs.readFileSync(path.join(MIGRATIONS_DIR, f), 'utf8');
    console.log(`[migrate] apply ${f}`);
    await pool.query('BEGIN');
    try {
      await pool.query(sql);
      await pool.query('INSERT INTO schema_migrations(filename) VALUES ($1) ON CONFLICT DO NOTHING', [f]);
      await pool.query('COMMIT');
      count++;
    } catch (err) {
      await pool.query('ROLLBACK');
      console.error(`[migrate] FAILED ${f}:`, err.message);
      process.exit(1);
    }
  }
  console.log(`[migrate] done — applied ${count} migration(s)`);
  await pool.end();
}

main().catch((err) => { console.error(err); process.exit(1); });