266 lines
9 KiB
TypeScript
266 lines
9 KiB
TypeScript
#!/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);
|
||
});
|