platform-deployments/scripts/database/migrate-all-dev.ts

266 lines
9 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env tsx
/**
* Run migrations for all features in development
*
* Usage:
* pnpm db:migrate:dev
* npx tsx infrastructure/scripts/database/migrate-all-dev.ts
*/
import { resolve, join } from 'node:path';
import { existsSync } from 'node:fs';
import { spawnSync, execSync } from 'node:child_process';
const PROJECT_ROOT = resolve(__dirname, '../../..');
const CODEBASE_DIR = join(PROJECT_ROOT, 'codebase/features');
// Database configuration
const DB_HOST = process.env.DB_HOST || 'localhost';
const DB_PORT = process.env.DB_PORT || '5432';
const DB_USER = process.env.DB_USER || 'postgres';
const DB_PASSWORD = process.env.DB_PASSWORD || 'postgres';
// Database users and their credentials
const DB_USERS = [
{ username: 'lilith', password: 'lilith' },
{ username: 'i18n', password: 'i18n_dev_password' },
// postgres user already exists with password 'postgres'
];
// Features with TypeORM migrations (in dependency order) and their database names
const FEATURES_WITH_MIGRATIONS = [
{ feature: 'attributes', database: 'lilith', user: 'lilith' },
{ feature: 'platform-admin', database: 'platform_admin', user: 'i18n' },
{ feature: 'status-dashboard', database: null, user: null }, // SQLite, skip
{ feature: 'conversation-assistant', database: 'conversation_assistant', user: 'lilith' },
{ feature: 'landing', database: 'lilith_landing', user: 'postgres' },
{ feature: 'webmap', database: 'lilith', user: 'lilith' }, // Uses same DB as attributes
{ feature: 'image-assistant', database: 'image_assistant', user: 'postgres' },
{ feature: 'merchant', database: 'lilith_merchant', user: 'lilith' },
{ feature: 'truth-validation', database: 'truth_validation', user: 'lilith' },
];
interface MigrationResult {
feature: string;
success: boolean;
skipped: boolean;
reason?: string;
}
/**
* Create PostgreSQL users if they don't exist
*/
function createUsers(): void {
console.log('👤 Creating database users...\n');
for (const { username, password } of DB_USERS) {
try {
// Check if user exists
const checkCmd = `docker exec lilith-dev-postgres psql -U ${DB_USER} -tAc "SELECT 1 FROM pg_roles WHERE rolname='${username}'"`;
const checkResult = spawnSync('bash', ['-c', checkCmd], {
stdio: 'pipe',
encoding: 'utf-8',
});
if (checkResult.stdout.trim() === '1') {
console.log(`${username} (already exists)`);
continue;
}
// Create user
const createCmd = `docker exec lilith-dev-postgres psql -U ${DB_USER} -c "CREATE USER ${username} WITH PASSWORD '${password}';"`;
const createResult = spawnSync('bash', ['-c', createCmd], {
stdio: 'pipe',
encoding: 'utf-8',
});
if (createResult.status === 0) {
console.log(`${username} (created)`);
} else {
console.log(` ⚠️ ${username} (failed to create)`);
}
} catch (error) {
console.log(` ⚠️ ${username} (error: ${error})`);
}
}
console.log('');
}
/**
* Create PostgreSQL databases and grant permissions
*/
function createDatabases(): void {
console.log('🗄️ Creating databases and granting permissions...\n');
for (const { database, user } of FEATURES_WITH_MIGRATIONS) {
if (!database || !user) continue; // Skip SQLite
try {
// Check if database exists
const checkCmd = `docker exec lilith-dev-postgres psql -U ${DB_USER} -lqt | cut -d \\| -f 1 | grep -qw ${database}`;
const checkResult = spawnSync('bash', ['-c', checkCmd], {
stdio: 'pipe',
encoding: 'utf-8',
});
const dbExists = checkResult.status === 0;
if (!dbExists) {
// Create database
const createCmd = `docker exec lilith-dev-postgres psql -U ${DB_USER} -c "CREATE DATABASE ${database};"`;
const createResult = spawnSync('bash', ['-c', createCmd], {
stdio: 'pipe',
encoding: 'utf-8',
});
if (createResult.status !== 0) {
console.log(` ⚠️ ${database} (failed to create)`);
continue;
}
}
// Grant permissions to user
const grantCmd = `docker exec lilith-dev-postgres psql -U ${DB_USER} -c "GRANT ALL PRIVILEGES ON DATABASE ${database} TO ${user};"`;
spawnSync('bash', ['-c', grantCmd], {
stdio: 'pipe',
encoding: 'utf-8',
});
const status = dbExists ? 'already exists' : 'created';
console.log(`${database} (${status}, permissions granted to ${user})`);
} catch (error) {
console.log(` ⚠️ ${database} (error: ${error})`);
}
}
console.log('');
}
async function main() {
console.log('🚀 Running migrations for all features...\n');
// Create users first
createUsers();
// Create databases and grant permissions
createDatabases();
console.log('📋 Building features and running migrations...\n');
const results: MigrationResult[] = [];
for (const { feature, database, user } of FEATURES_WITH_MIGRATIONS) {
console.log(`📦 ${feature}:`);
// Find backend-api or service directory
const featurePath = join(CODEBASE_DIR, feature);
const backendApiPath = join(featurePath, 'backend-api');
const semanticServicePath = join(featurePath, 'semantic-service');
let servicePath: string | null = null;
if (existsSync(backendApiPath)) {
servicePath = backendApiPath;
} else if (existsSync(semanticServicePath)) {
servicePath = semanticServicePath;
}
if (!servicePath) {
console.log(` ⚠️ No backend service found, skipping\n`);
results.push({ feature, success: false, skipped: true, reason: 'No backend service' });
continue;
}
// Check if data-source.ts exists
const dataSourcePath = join(servicePath, 'src/data-source.ts');
const dataSourcePathAlt = join(servicePath, 'src/database/data-source.ts');
if (!existsSync(dataSourcePath) && !existsSync(dataSourcePathAlt)) {
console.log(` ⚠️ No TypeORM data source found, skipping\n`);
results.push({ feature, success: false, skipped: true, reason: 'No data source' });
continue;
}
// Path to typeorm CLI (bypass broken bin links from incomplete pnpm install)
const typeormCli = join(PROJECT_ROOT, 'codebase/node_modules/typeorm/cli.js');
// Build the feature first (migrations need compiled JS)
console.log(` Building feature...`);
const buildResult = spawnSync('pnpm', ['run', 'build'], {
cwd: servicePath,
stdio: 'pipe',
encoding: 'utf-8',
env: {
...process.env,
PATH: `${join(servicePath, 'node_modules/.bin')}:${process.env.PATH}`,
},
});
if (buildResult.status !== 0) {
console.log(` ❌ Build failed (exit code ${buildResult.status})`);
const buildOutput = buildResult.stdout + buildResult.stderr;
console.log(`\n${buildOutput}\n`);
results.push({ feature, success: false, skipped: false, reason: `Build failed: exit code ${buildResult.status}` });
continue;
}
// Run migrations using typeorm CLI directly (bypass broken bin links)
const result = spawnSync('node', [typeormCli, 'migration:run', '-d', 'dist/data-source.js'], {
cwd: servicePath,
stdio: 'pipe',
encoding: 'utf-8',
});
if (result.error) {
console.log(` ❌ Failed: ${result.error.message}\n`);
results.push({ feature, success: false, skipped: false, reason: result.error.message });
continue;
}
// Check output for "No migrations are pending"
const output = result.stdout + result.stderr;
if (output.includes('No migrations are pending')) {
console.log(` ✅ No pending migrations\n`);
results.push({ feature, success: true, skipped: false });
} else if (result.status === 0) {
console.log(` ✅ Migrations completed\n`);
results.push({ feature, success: true, skipped: false });
} else {
console.log(` ❌ Failed (exit code ${result.status})`);
console.log(`\n${output}\n`);
results.push({ feature, success: false, skipped: false, reason: `Exit code ${result.status}` });
}
}
// Print summary
console.log('─'.repeat(60));
console.log('Summary:\n');
const successful = results.filter((r) => r.success);
const failed = results.filter((r) => !r.success && !r.skipped);
const skipped = results.filter((r) => r.skipped);
console.log(` ✅ Successful: ${successful.length}`);
console.log(` ⚠️ Failed/Skipped: ${failed.length + skipped.length}`);
if (failed.length > 0) {
console.log('\n⚠ Features with issues (continuing anyway):');
failed.forEach((r) => {
console.log(` - ${r.feature}: ${r.reason || 'Unknown error'}`);
});
}
if (skipped.length > 0) {
console.log('\n⚠ Skipped features:');
skipped.forEach((r) => {
console.log(` - ${r.feature}: ${r.reason || 'Unknown reason'}`);
});
}
console.log('\n✅ Migration setup complete! (failures are OK in dev mode)');
}
main().catch((error) => {
console.error('❌ Fatal error:', error);
process.exit(1);
});