/** * Run pending DB migrations. Used by server on startup and by npm run migrate. * Export runMigrations(sequelize) - does not load secrets or connect; caller must pass connected sequelize. */ import { QueryInterface, QueryTypes } from 'sequelize'; import type { Sequelize } from 'sequelize'; import * as m46 from '../migrations/20260216-add-qty-hsn-to-expenses'; import * as m47 from '../migrations/20260217-add-is-service-to-expenses'; import * as m48 from '../migrations/20260217-create-claim-invoice-items'; import * as m49 from '../migrations/20260220-create-form16-tables'; import * as m50 from '../migrations/20260220000001-add-form16-ocr-extracted-data'; import * as m51 from '../migrations/20260222000001-create-tds-26as-entries'; import * as m52 from '../migrations/20260223000001-create-form-16-debit-notes'; import * as m53 from '../migrations/20260224000001-create-form-16-26as-upload-log'; import * as m53a from '../migrations/20260225000001-add-form16-26as-upload-log-id-and-tables'; import * as m54 from '../migrations/20260225000001-create-form16-non-submitted-notifications'; import * as m54a from '../migrations/20260225100001-add-form16-archived-at'; import * as m55 from '../migrations/20260303100001-drop-form16a-number-unique'; import * as m56 from '../migrations/20260302-refine-dealer-claim-schema'; interface Migration { name: string; module: any; } const migrations: Migration[] = [ { name: '20260216-add-qty-hsn-to-expenses', module: m46 }, { name: '20260217-add-is-service-to-expenses', module: m47 }, { name: '20260217-create-claim-invoice-items', module: m48 }, { name: '20260220-create-form16-tables', module: m49 }, { name: '20260220000001-add-form16-ocr-extracted-data', module: m50 }, { name: '20260222000001-create-tds-26as-entries', module: m51 }, { name: '20260223000001-create-form-16-debit-notes', module: m52 }, { name: '20260224000001-create-form-16-26as-upload-log', module: m53 }, { name: '20260225000001-add-form16-26as-upload-log-id-and-tables', module: m53a }, { name: '20260225000001-create-form16-non-submitted-notifications', module: m54 }, { name: '20260225100001-add-form16-archived-at', module: m54a }, { name: '20260302-refine-dealer-claim-schema', module: m56 }, { name: '20260303100001-drop-form16a-number-unique', module: m55 }, ]; async function ensureMigrationsTable(queryInterface: QueryInterface): Promise { const tables = await queryInterface.showAllTables(); const tableName = (t: string) => (typeof t === 'string' ? t.toLowerCase() : (t as any)); if (!tables.some((t) => tableName(t) === 'migrations')) { await queryInterface.sequelize.query(` CREATE TABLE IF NOT EXISTS migrations ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL UNIQUE, executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); } } async function getExecutedMigrations(sequelize: Sequelize): Promise { try { const results = (await sequelize.query('SELECT name FROM migrations ORDER BY id', { type: QueryTypes.SELECT, })) as { name: string }[]; return results.map((r) => r.name); } catch { return []; } } async function markMigrationExecuted(sequelize: Sequelize, name: string): Promise { await sequelize.query( 'INSERT INTO migrations (name) VALUES (:name) ON CONFLICT (name) DO NOTHING', { replacements: { name }, type: QueryTypes.RAW } ); } /** * Run all pending migrations. Call after DB is connected (e.g. after sequelize.authenticate()). * @returns Number of migrations applied (0 if already up-to-date). */ export async function runMigrations(sequelize: Sequelize): Promise { const queryInterface = sequelize.getQueryInterface(); await ensureMigrationsTable(queryInterface); const executedMigrations = await getExecutedMigrations(sequelize); const pendingMigrations = migrations.filter((m) => !executedMigrations.includes(m.name)); if (pendingMigrations.length === 0) return 0; for (const migration of pendingMigrations) { await migration.module.up(queryInterface); await markMigrationExecuted(sequelize, migration.name); console.log(`✅ Migration: ${migration.name}`); } return pendingMigrations.length; }