Phase 1a - Database Setup: - Add PostgreSQL connection pooling with pg client - Create 8 SQL migrations for all database schemas - Implement migration execution system with tracking - Add environment configuration for database and JWT settings Phase 1b - Authentication & Authorization: - Implement password hashing with bcrypt - Create JWT token generation (access + refresh tokens) - Implement RBAC with 5 roles (Admin, Manager, Analyst, Auditor, Viewer) - Create auth middleware for authentication and authorization - Add auth routes (login, register, refresh, logout, profile) Phase 1c - API Endpoints (Full CRUD): - Transaction endpoints with evaluation and batch processing - Account management (treasury and subledger accounts) - User management (admin-only) - FX contract management - Compliance endpoints (rules, results, thresholds) - Reporting endpoints (summary, compliance, audit logs) - Health check endpoints with database status Phase 1d - Data Seeding: - Create database seeding system with roles, permissions, users - Add sample data (treasury accounts, FX contracts) - Implement admin user creation from environment variables All endpoints protected with authentication and role-based access control.
130 lines
3.4 KiB
TypeScript
130 lines
3.4 KiB
TypeScript
/**
|
|
* Database Migration System
|
|
* Handles reading and executing SQL migrations
|
|
*/
|
|
|
|
import { readdir, readFile } from 'fs/promises';
|
|
import { join } from 'path';
|
|
import { query } from './connection';
|
|
|
|
/**
|
|
* Run all pending migrations
|
|
*/
|
|
export async function runMigrations(): Promise<void> {
|
|
console.log('Starting database migrations...');
|
|
|
|
try {
|
|
// Ensure schema_migrations table exists
|
|
await query(`
|
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
version INTEGER PRIMARY KEY,
|
|
name VARCHAR(255) NOT NULL,
|
|
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
`);
|
|
|
|
// Get all migration files
|
|
const migrationsDir = join(__dirname, 'migrations');
|
|
const files = await readdir(migrationsDir);
|
|
const migrationFiles = files
|
|
.filter((f) => f.endsWith('.sql'))
|
|
.sort();
|
|
|
|
if (migrationFiles.length === 0) {
|
|
console.log('No migration files found');
|
|
return;
|
|
}
|
|
|
|
// Get executed migrations
|
|
const executedResult = await query<{ version: number }>(
|
|
'SELECT version FROM schema_migrations ORDER BY version'
|
|
);
|
|
const executedVersions = new Set(executedResult.rows.map((r) => r.version));
|
|
|
|
// Execute pending migrations
|
|
let executedCount = 0;
|
|
for (const file of migrationFiles) {
|
|
const match = file.match(/^(\d+)_/);
|
|
if (!match) continue;
|
|
|
|
const version = parseInt(match[1], 10);
|
|
if (executedVersions.has(version)) {
|
|
console.log(`✓ Migration ${version} already executed`);
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
const filePath = join(migrationsDir, file);
|
|
const sqlContent = await readFile(filePath, 'utf-8');
|
|
|
|
// Split into individual statements (simple approach)
|
|
const statements = sqlContent
|
|
.split(';')
|
|
.map((s) => s.trim())
|
|
.filter((s) => s && !s.startsWith('--'));
|
|
|
|
for (const statement of statements) {
|
|
await query(statement + ';');
|
|
}
|
|
|
|
console.log(`✓ Migration ${version} executed: ${file}`);
|
|
executedCount++;
|
|
} catch (error) {
|
|
console.error(`✗ Migration ${version} failed:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
console.log(
|
|
`✓ Database migrations completed (${executedCount} new migrations executed)`
|
|
);
|
|
} catch (error) {
|
|
console.error('Migration failed:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get migration status
|
|
*/
|
|
export async function getMigrationStatus(): Promise<{
|
|
executed: number;
|
|
total: number;
|
|
pending: string[];
|
|
}> {
|
|
try {
|
|
const migrationsDir = join(__dirname, 'migrations');
|
|
const files = await readdir(migrationsDir);
|
|
const migrationFiles = files.filter((f) => f.endsWith('.sql')).sort();
|
|
|
|
const executedResult = await query<{ version: number; name: string }>(
|
|
'SELECT version, name FROM schema_migrations ORDER BY version'
|
|
);
|
|
const executedVersions = new Set(executedResult.rows.map((r) => r.version));
|
|
|
|
const pending: string[] = [];
|
|
for (const file of migrationFiles) {
|
|
const match = file.match(/^(\d+)_/);
|
|
if (!match) continue;
|
|
|
|
const version = parseInt(match[1], 10);
|
|
if (!executedVersions.has(version)) {
|
|
pending.push(file);
|
|
}
|
|
}
|
|
|
|
return {
|
|
executed: executedResult.rows.length,
|
|
total: migrationFiles.length,
|
|
pending,
|
|
};
|
|
} catch (error) {
|
|
console.error('Failed to get migration status:', error);
|
|
return {
|
|
executed: 0,
|
|
total: 0,
|
|
pending: [],
|
|
};
|
|
}
|
|
}
|