Initial Commit

This commit is contained in:
Eric Gullickson
2025-09-17 16:09:15 -05:00
parent 0cdb9803de
commit a052040e3a
373 changed files with 437090 additions and 6773 deletions

View File

@@ -3,7 +3,7 @@
*/
import { Pool } from 'pg';
import { readFileSync, readdirSync } from 'fs';
import { join } from 'path';
import { join, resolve } from 'path';
import { env } from '../../core/config/environment';
const pool = new Pool({
@@ -14,26 +14,79 @@ const pool = new Pool({
password: env.DB_PASSWORD,
});
// Define migration order based on dependencies
// Define migration order based on dependencies and packaging layout
// We package migrations under /app/migrations with two roots: features/ and core/
// The update_updated_at_column() function is defined in features/vehicles first,
// and user-preferences trigger depends on it; so run vehicles before core/user-preferences.
const MIGRATION_ORDER = [
'vehicles', // Primary entity, no dependencies
'fuel-logs', // Depends on vehicles
'maintenance', // Depends on vehicles
'stations', // Independent
'features/vehicles', // Primary entity, defines update_updated_at_column()
'core/user-preferences', // Depends on update_updated_at_column()
'features/fuel-logs', // Depends on vehicles
'features/maintenance', // Depends on vehicles
'features/stations', // Independent
];
// Base directory where migrations are copied inside the image (set by Dockerfile)
const MIGRATIONS_DIR = resolve(process.env.MIGRATIONS_DIR || join(__dirname, '../../../migrations'));
async function getExecutedMigrations(): Promise<Record<string, Set<string>>> {
const executed: Record<string, Set<string>> = {};
// Ensure tracking table exists (retry across transient DB restarts)
const retry = async <T>(op: () => Promise<T>, timeoutMs = 60000): Promise<T> => {
const start = Date.now();
while (true) {
try { return await op(); } catch (e) {
if (Date.now() - start > timeoutMs) throw e;
await new Promise(res => setTimeout(res, 2000));
}
}
};
await retry(() => pool.query(`
CREATE TABLE IF NOT EXISTS _migrations (
id SERIAL PRIMARY KEY,
feature VARCHAR(100) NOT NULL,
file VARCHAR(255) NOT NULL,
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(feature, file)
);
`));
const res = await retry(() => pool.query('SELECT feature, file FROM _migrations'));
for (const row of res.rows) {
if (!executed[row.feature]) executed[row.feature] = new Set();
executed[row.feature].add(row.file);
}
return executed;
}
async function runFeatureMigrations(featureName: string) {
const migrationDir = join(__dirname, '../../features', featureName, 'migrations');
const migrationDir = join(MIGRATIONS_DIR, featureName, 'migrations');
try {
// Guard per-feature in case DB becomes available slightly later on cold start
const ping = async (timeoutMs = 60000) => {
const start = Date.now();
while (true) {
try { await pool.query('SELECT 1'); return; } catch (e) {
if (Date.now() - start > timeoutMs) throw e; await new Promise(r => setTimeout(r, 2000));
}
}
};
await ping();
const files = readdirSync(migrationDir)
.filter(f => f.endsWith('.sql'))
.sort();
const executed = await getExecutedMigrations();
const already = executed[featureName] || new Set<string>();
for (const file of files) {
if (already.has(file)) {
console.log(`↷ Skipping already executed migration: ${featureName}/${file}`);
continue;
}
const sql = readFileSync(join(migrationDir, file), 'utf-8');
console.log(`Running migration: ${featureName}/${file}`);
await pool.query(sql);
await pool.query('INSERT INTO _migrations(feature, file) VALUES ($1, $2) ON CONFLICT DO NOTHING', [featureName, file]);
console.log(`✅ Completed: ${featureName}/${file}`);
}
} catch (error) {
@@ -45,17 +98,22 @@ async function runFeatureMigrations(featureName: string) {
async function main() {
try {
console.log('Starting migration orchestration...');
// Create migrations tracking table
await pool.query(`
CREATE TABLE IF NOT EXISTS _migrations (
id SERIAL PRIMARY KEY,
feature VARCHAR(100) NOT NULL,
file VARCHAR(255) NOT NULL,
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(feature, file)
);
`);
console.log(`Using migrations directory: ${MIGRATIONS_DIR}`);
// Wait for database to be reachable (handles cold starts)
const waitForDb = async (timeoutMs = 60000) => {
const start = Date.now();
/* eslint-disable no-constant-condition */
while (true) {
try {
await pool.query('SELECT 1');
return;
} catch (e) {
if (Date.now() - start > timeoutMs) throw e;
await new Promise(res => setTimeout(res, 2000));
}
}
};
await waitForDb();
// Run migrations in order
for (const feature of MIGRATION_ORDER) {
@@ -74,4 +132,4 @@ async function main() {
if (require.main === module) {
main();
}
}