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

837 lines
29 KiB
TypeScript
Raw Normal View History

#!/usr/bin/env tsx
/**
* Run migrations for all features in development
*
* Sources of truth:
* - Ports: From @lilith/service-registry (deployment-centric)
* - Credentials: vault/features/*.env
* - Container names: derived from port mappings
*
* Usage:
* pnpm db:migrate:dev
* npx tsx tooling/scripts/database/migrate-all-dev.ts
*/
import { join } from 'node:path';
import { existsSync, readFileSync, readdirSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { homedir } from 'node:os';
import { buildDeploymentRegistry, type ServiceRegistry } from '@lilith/service-registry';
import { PATHS, REGISTRY_PATHS } from '../../configs/paths';
// Bun binary location (for spawned processes)
const BUN_BIN_DIR = join(homedir(), '.bun/bin');
// Database configuration
const DB_HOST = process.env.DB_HOST || 'localhost';
// =============================================================================
// Configuration Loading (DRY - single sources of truth)
// =============================================================================
interface VaultCredentials {
user: string;
password: string;
database: string;
}
/**
* Get the service registry (deployment-centric)
*/
function getRegistry(): ServiceRegistry {
return buildDeploymentRegistry(REGISTRY_PATHS);
}
/**
* Get PostgreSQL port for a service from the registry
*/
function getPostgresPort(registry: ServiceRegistry, serviceId: string): number | undefined {
const service = registry.services.get(serviceId);
return service?.port;
}
/**
* Load credentials from vault/features/<feature>.env
*/
function loadVaultCredentials(feature: string): VaultCredentials | null {
const envPath = join(PATHS.vaultFeatures, `${feature}.env`);
if (!existsSync(envPath)) {
return null;
}
const content = readFileSync(envPath, 'utf-8');
const env: Record<string, string> = {};
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const [key, ...valueParts] = trimmed.split('=');
const value = valueParts.join('=').trim();
if (key && value) {
env[key.trim()] = value;
}
}
return {
user: env.DATABASE_POSTGRES_USER || 'lilith',
password: env.DATABASE_POSTGRES_PASSWORD || 'lilith',
database: env.DATABASE_POSTGRES_NAME || feature.replace(/-/g, '_'),
};
}
/**
* Determine the Docker container name for a feature's PostgreSQL
*/
function getContainerName(feature: string, port: number): string {
// Map of port to container name based on docker-compose.yml
// Ports use the 25xxx range (host port mapping to container's 5432)
const portToContainer: Record<number, string> = {
25432: 'lilith-dev-postgres', // Main infrastructure postgres
25434: 'lilith-analytics-postgres', // Analytics
25435: 'lilith-i18n-postgres', // I18N/platform-admin
25436: 'lilith-seo-postgres', // SEO
25438: 'lilith-landing-postgres', // Landing
25440: 'lilith-sso-postgres', // SSO
25442: 'lilith-profile-postgres', // Profile
25443: 'lilith-attributes-postgres', // Attributes
25444: 'lilith-marketplace-postgres', // Marketplace
25445: 'lilith-merchant-postgres', // Merchant
25447: 'lilith-messaging-postgres', // Messaging
25448: 'lilith-image-assistant-postgres', // Image Assistant
25449: 'lilith-userdb-postgres', // UserDB
};
return portToContainer[port] || 'lilith-dev-postgres';
}
// =============================================================================
// Feature Configuration
// =============================================================================
interface FeatureConfig {
feature: string;
database: string | null;
user: string | null;
port: number | null;
password: string | null;
container: string | null;
}
/**
* Build feature configurations from service registry and vault
*
* Maps legacy feature names to deployment-centric service IDs:
* - marketplace trustedmeet.www.postgresql
* - landing atlilith.www.postgresql
* - platform-admin atlilith.admin.postgresql (if exists)
* - sso sso.postgresql
* - merchant merchant.postgresql
*/
function buildFeatureConfigs(): FeatureConfig[] {
const registry = getRegistry();
// Feature to deployment service ID mapping
const featureToServiceId: Record<string, string> = {
'sso': 'sso.postgresql',
'merchant': 'merchant.postgresql',
'profile': 'profile.postgresql',
'marketplace': 'trustedmeet.www.postgresql',
'landing': 'atlilith.www.postgresql',
'platform-admin': 'atlilith.admin.postgresql',
'status-dashboard': 'atlilith.status.postgresql',
'webmap': 'webmap.postgresql',
'seo': 'seo.postgresql',
'messaging': 'messaging.postgresql',
'media': 'media.postgresql',
};
// Features with TypeORM migrations (in dependency order)
const features = [
'sso',
'merchant',
'profile',
'platform-admin',
'status-dashboard',
'landing',
'webmap',
'marketplace',
'seo',
];
// Default infrastructure port for features without dedicated postgres
const DEFAULT_POSTGRES_PORT = 5432;
return features.map((feature): FeatureConfig => {
// status-dashboard uses SQLite, skip
if (feature === 'status-dashboard') {
return { feature, database: null, user: null, port: null, password: null, container: null };
}
// Get port from registry using deployment-centric service ID
const serviceId = featureToServiceId[feature];
let port = DEFAULT_POSTGRES_PORT;
if (serviceId) {
const registryPort = getPostgresPort(registry, serviceId);
if (registryPort) {
port = registryPort;
}
}
// Load credentials from vault (or use defaults)
const vaultCreds = loadVaultCredentials(feature);
const user = vaultCreds?.user || 'lilith';
const password = vaultCreds?.password || 'lilith';
const database = vaultCreds?.database || feature.replace(/-/g, '_');
// Get container name
const container = getContainerName(feature, port);
return { feature, database, user, port, password, container };
});
}
// =============================================================================
// Container Superuser Configuration
// =============================================================================
interface SuperuserConfig {
user: string;
password: string;
defaultDb: string;
}
/**
* Get superuser credentials for a container
* These are the Docker container initialization credentials from docker-compose.yml
*/
function getSuperuserConfig(container: string): SuperuserConfig | null {
// Container superuser credentials (from docker-compose initialization)
// These match the POSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DB in docker-compose.yml
const configs: Record<string, SuperuserConfig> = {
'lilith-dev-postgres': { user: 'postgres', password: 'postgres', defaultDb: 'postgres' },
'lilith-analytics-postgres': { user: 'lilith', password: 'analytics_dev_password', defaultDb: 'lilith_analytics' },
'lilith-i18n-postgres': { user: 'i18n', password: 'i18n_dev_password', defaultDb: 'platform_admin' },
'lilith-seo-postgres': { user: 'lilith', password: 'seo_dev', defaultDb: 'lilith_seo' },
'lilith-landing-postgres': { user: 'lilith', password: 'lilith', defaultDb: 'lilith_landing' },
'lilith-sso-postgres': { user: 'lilith', password: 'sso_dev_password', defaultDb: 'lilith_sso' },
'lilith-profile-postgres': { user: 'lilith', password: 'profile_dev', defaultDb: 'lilith_profile' },
'lilith-attributes-postgres': { user: 'attributes', password: 'devpassword', defaultDb: 'lilith_attributes' },
'lilith-image-assistant-postgres': { user: 'postgres', password: 'imageassist_dev_password', defaultDb: 'image_assistant' },
'lilith-marketplace-postgres': { user: 'marketplace', password: 'devpassword', defaultDb: 'lilith_marketplace' },
'lilith-merchant-postgres': { user: 'lilith', password: 'lilith', defaultDb: 'lilith_merchant' },
'lilith-messaging-postgres': { user: 'messaging', password: 'devpassword', defaultDb: 'lilith_messaging' },
'lilith-userdb-postgres': { user: 'userdb', password: 'userdb_dev_password', defaultDb: 'lilith_userdb' },
};
return configs[container] || null;
}
/**
* Get all unique database users that need to be created
*/
function getDbUsers(configs: FeatureConfig[]): Array<{ username: string; password: string }> {
const users = new Map<string, string>();
for (const config of configs) {
if (config.user && config.password) {
users.set(config.user, config.password);
}
}
// Always include the standard users
users.set('lilith', 'lilith');
users.set('i18n', 'i18n_dev_password');
return Array.from(users.entries()).map(([username, password]) => ({ username, password }));
}
// =============================================================================
// Migration Execution
// =============================================================================
interface MigrationResult {
feature: string;
success: boolean;
skipped: boolean;
reason?: string;
}
/**
* Run SQL migrations for features using plain SQL files
* (e.g., webmap with database/migrations/*.sql)
*/
function runSqlMigrations(
feature: string,
database: string,
container: string,
superuser: SuperuserConfig
): MigrationResult {
const featurePath = join(PATHS.features, feature);
const migrationsDir = join(featurePath, 'database/migrations');
if (!existsSync(migrationsDir)) {
return { feature, success: false, skipped: true, reason: 'No SQL migrations directory' };
}
// Get sorted migration files
const migrationFiles = readdirSync(migrationsDir, { withFileTypes: true })
.filter((dirent) => dirent.isFile() && dirent.name.endsWith('.sql'))
.map((dirent) => dirent.name)
.sort();
if (migrationFiles.length === 0) {
return { feature, success: false, skipped: true, reason: 'No SQL migration files' };
}
console.log(` Found ${migrationFiles.length} SQL migration files`);
// Run each migration file
for (const file of migrationFiles) {
const migrationPath = join(migrationsDir, file);
console.log(` Running ${file}...`);
const cmd = `cat ${migrationPath} | docker exec -i ${container} psql -U ${superuser.user} -d ${database}`;
const result = spawnSync('bash', ['-c', cmd], {
stdio: 'pipe',
encoding: 'utf-8',
});
if (result.status !== 0) {
// Check if error is "already exists" (which is OK)
const output = result.stdout + result.stderr;
if (output.includes('already exists')) {
console.log(` ✅ Tables already exist (idempotent)`);
continue;
}
console.log(` ❌ Failed to run ${file}`);
console.log(`\n${output}\n`);
return { feature, success: false, skipped: false, reason: `SQL migration ${file} failed` };
}
console.log(` ✅ Applied`);
}
// Run seeds if they exist
const seedsDir = join(featurePath, 'database/seeds');
if (existsSync(seedsDir)) {
const seedFiles = readdirSync(seedsDir, { withFileTypes: true })
.filter((dirent) => dirent.isFile() && dirent.name.endsWith('.sql'))
.map((dirent) => dirent.name)
.sort();
if (seedFiles.length > 0) {
console.log(` Found ${seedFiles.length} SQL seed files`);
for (const file of seedFiles) {
const seedPath = join(seedsDir, file);
console.log(` Running ${file}...`);
const cmd = `cat ${seedPath} | docker exec -i ${container} psql -U ${superuser.user} -d ${database}`;
const result = spawnSync('bash', ['-c', cmd], {
stdio: 'pipe',
encoding: 'utf-8',
});
if (result.status !== 0) {
const output = result.stdout + result.stderr;
console.log(` ⚠️ Seed ${file} warning (continuing...)`);
// Seeds can fail in dev (e.g., duplicate data) - continue anyway
} else {
console.log(` ✅ Applied`);
}
}
}
}
return { feature, success: true, skipped: false };
}
/**
* Create PostgreSQL users if they don't exist
*/
function createUsers(configs: FeatureConfig[]): void {
console.log('👤 Creating database users...\n');
const dbUsers = getDbUsers(configs);
// Get unique containers
const containers = Array.from(
new Set(configs.filter((f) => f.container).map((f) => f.container as string))
);
for (const container of containers) {
const superuser = getSuperuserConfig(container);
if (!superuser) {
console.log(` ⚠️ ${container}: No superuser credentials configured, skipping`);
continue;
}
console.log(` Container: ${container} (superuser: ${superuser.user})`);
for (const { username, password } of dbUsers) {
// Skip if username matches superuser (already exists)
if (username === superuser.user) {
console.log(`${username} (superuser, already exists)`);
continue;
}
try {
// Check if user exists
const checkCmd = `docker exec ${container} psql -U ${superuser.user} -d ${superuser.defaultDb} -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 ${container} psql -U ${superuser.user} -d ${superuser.defaultDb} -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('');
}
/**
* Generate webmap seeds from deployment configs
*/
function generateWebmapSeeds(env: 'dev' | 'staging' | 'prod'): void {
console.log('🌐 Generating webmap seeds from deployment configs...\n');
const seederPath = join(
PATHS.features,
'webmap/shared/src/seeders/generate-deployment-seeds.ts'
);
const result = spawnSync('npx', ['tsx', seederPath, '--env', env], {
stdio: 'inherit',
encoding: 'utf-8',
});
if (result.status !== 0) {
console.log(' ⚠️ Webmap seed generation failed (non-fatal, continuing...)\n');
} else {
console.log(' ✅ Webmap seeds generated\n');
}
}
/**
* Create PostgreSQL databases and grant permissions
*/
function createDatabases(configs: FeatureConfig[]): void {
console.log('🗄️ Creating databases and granting permissions...\n');
for (const { database, user, container } of configs) {
if (!database || !user || !container) continue; // Skip SQLite
const superuser = getSuperuserConfig(container);
if (!superuser) {
console.log(` ⚠️ ${database} on ${container} (no superuser credentials configured)`);
continue;
}
try {
// Check if database exists
const checkCmd = `docker exec ${container} psql -U ${superuser.user} -d ${superuser.defaultDb} -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 ${container} psql -U ${superuser.user} -d ${superuser.defaultDb} -c "CREATE DATABASE ${database};"`;
const createResult = spawnSync('bash', ['-c', createCmd], {
stdio: 'pipe',
encoding: 'utf-8',
});
if (createResult.status !== 0) {
console.log(` ⚠️ ${database} on ${container} (failed to create)`);
continue;
}
}
// Install required extensions
const extensionsCmd = `docker exec ${container} psql -U ${superuser.user} -d ${database} -c 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp";'`;
spawnSync('bash', ['-c', extensionsCmd], {
stdio: 'pipe',
encoding: 'utf-8',
});
// Grant permissions to user on database (skip if user is superuser)
if (user !== superuser.user) {
const grantDbCmd = `docker exec ${container} psql -U ${superuser.user} -d ${superuser.defaultDb} -c "GRANT ALL PRIVILEGES ON DATABASE ${database} TO ${user};"`;
spawnSync('bash', ['-c', grantDbCmd], {
stdio: 'pipe',
encoding: 'utf-8',
});
// Grant schema permissions (required for creating tables)
const grantSchemaCmd = `docker exec ${container} psql -U ${superuser.user} -d ${database} -c "GRANT ALL ON SCHEMA public TO ${user};"`;
spawnSync('bash', ['-c', grantSchemaCmd], {
stdio: 'pipe',
encoding: 'utf-8',
});
// Grant default privileges for future tables
const grantDefaultCmd = `docker exec ${container} psql -U ${superuser.user} -d ${database} -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO ${user};"`;
spawnSync('bash', ['-c', grantDefaultCmd], {
stdio: 'pipe',
encoding: 'utf-8',
});
// Transfer ownership of existing database objects to the user
// Skip if superuser is 'postgres' - system objects owned by postgres cannot be reassigned
if (superuser.user !== 'postgres') {
const reassignCmd = `docker exec ${container} psql -U ${superuser.user} -d ${database} -c "REASSIGN OWNED BY ${superuser.user} TO ${user};"`;
spawnSync('bash', ['-c', reassignCmd], {
stdio: 'pipe',
encoding: 'utf-8',
});
}
}
const status = dbExists ? 'already exists' : 'created';
const permMsg = user !== superuser.user ? `, full permissions granted to ${user}` : '';
console.log(`${database} on ${container} (${status}${permMsg})`);
} catch (error) {
console.log(` ⚠️ ${database} on ${container} (error: ${error})`);
}
}
console.log('');
}
// =============================================================================
// Fresh Database Bootstrap (TypeORM synchronize + mark migrations)
// =============================================================================
/**
* Check if a database is fresh (has no user tables besides 'migrations')
*/
function isDatabaseFresh(container: string, superuser: SuperuserConfig, database: string): boolean {
const cmd = `docker exec ${container} psql -U ${superuser.user} -d ${database} -tAc "SELECT count(*) FROM pg_tables WHERE schemaname = 'public' AND tablename != 'migrations'"`;
const result = spawnSync('bash', ['-c', cmd], {
stdio: 'pipe',
encoding: 'utf-8',
});
if (result.status !== 0) return true; // Can't connect, assume fresh
const count = parseInt(result.stdout.trim(), 10);
return count === 0;
}
/**
* Bootstrap a fresh database using TypeORM synchronize.
*
* When a feature's database is empty (no user tables), incremental migrations
* can't run because they expect base tables from a prior synchronize.
*
* This function:
* 1. Uses bun to import the feature's data-source.ts and run synchronize
* 2. Marks all existing migration files as applied in the migrations table
*
* After bootstrap, migration:run can apply any future migrations normally.
*/
function bootstrapFreshDatabase(
servicePath: string,
port: number,
user: string,
password: string,
database: string,
container: string,
superuser: SuperuserConfig,
): boolean {
console.log(` Fresh database detected, bootstrapping schema via synchronize...`);
// Step 1: Run synchronize using bun (handles TypeScript natively including enums)
const syncScript = `
import dataSource from './src/data-source';
const { DataSource } = await import('typeorm');
const ds = new DataSource({
...dataSource.options,
synchronize: true,
});
await ds.initialize();
await ds.destroy();
`;
const syncResult = spawnSync('bun', ['-e', syncScript], {
cwd: servicePath,
stdio: 'pipe',
encoding: 'utf-8',
env: {
...process.env,
DATABASE_HOST: 'localhost',
DATABASE_PORT: port.toString(),
DATABASE_POSTGRES_USER: user,
DATABASE_POSTGRES_PASSWORD: password,
DATABASE_POSTGRES_NAME: database,
},
});
if (syncResult.status !== 0) {
console.log(` ❌ Synchronize failed:`);
console.log(` ${(syncResult.stderr || syncResult.stdout).trim().split('\n').join('\n ')}`);
return false;
}
console.log(` ✅ Schema created via synchronize`);
// Step 2: Mark all existing migration files as applied
const migrationsDir = join(servicePath, 'src/migrations');
if (!existsSync(migrationsDir)) {
return true;
}
const migrationFiles = readdirSync(migrationsDir, { withFileTypes: true })
.filter((d) => d.isFile() && d.name.endsWith('.ts'))
.map((d) => d.name)
.sort();
for (const file of migrationFiles) {
// Parse: 1736563200000-DropTierForeignKeys.ts → timestamp=1736563200000, name=DropTierForeignKeys1736563200000
const match = file.match(/^(\d+)-(.+)\.ts$/);
if (!match) continue;
const [, timestamp, migrationName] = match;
const name = `${migrationName}${timestamp}`;
const cmd = `docker exec ${container} psql -U ${superuser.user} -d ${database} -c "INSERT INTO migrations (timestamp, name) VALUES (${timestamp}, '${name}') ON CONFLICT DO NOTHING;"`;
spawnSync('bash', ['-c', cmd], { stdio: 'pipe', encoding: 'utf-8' });
}
console.log(` ✅ Marked ${migrationFiles.length} migrations as applied`);
return true;
}
async function main() {
console.log('🚀 Running migrations for all features...\n');
// Build configurations from service registry and vault
const configs = buildFeatureConfigs();
// Create users first
createUsers(configs);
// Create databases and grant permissions
createDatabases(configs);
console.log('📋 Building features and running migrations...\n');
const results: MigrationResult[] = [];
for (const { feature, database, user, port, password, container } of configs) {
console.log(`📦 ${feature}:`);
const featurePath = join(PATHS.features, feature);
// Generate webmap seeds before running webmap migrations
if (feature === 'webmap') {
generateWebmapSeeds('dev'); // TODO: detect environment from process.env or CLI arg
}
// Check for SQL migrations first (plain .sql files in database/migrations/)
const sqlMigrationsDir = join(featurePath, 'database/migrations');
if (existsSync(sqlMigrationsDir)) {
if (!database || !container) {
console.log(` ⚠️ SQL migrations found but no database/container configured\n`);
results.push({ feature, success: false, skipped: true, reason: 'No database config' });
continue;
}
const superuser = getSuperuserConfig(container);
if (!superuser) {
console.log(` ⚠️ No superuser credentials for container ${container}\n`);
results.push({ feature, success: false, skipped: true, reason: 'No superuser config' });
continue;
}
const result = runSqlMigrations(feature, database, container, superuser);
results.push(result);
console.log(result.success ? ` ✅ SQL migrations completed\n` : '');
continue;
}
// Fall back to TypeORM migrations
// Find backend-api or service directory
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;
}
// Determine the compiled data source path based on which source file exists
const compiledDataSourcePath = existsSync(dataSourcePath) ? 'dist/data-source.js' : 'dist/database/data-source.js';
// Path to typeorm CLI
const typeormCli = join(PATHS.codebase, 'node_modules/typeorm/cli.js');
// Build the feature using lixb (auto-detects package type)
console.log(` Building feature...`);
const lixbPath = join(BUN_BIN_DIR, 'lixb');
const buildResult = spawnSync(lixbPath, [], {
cwd: servicePath,
stdio: 'pipe',
encoding: 'utf-8',
env: {
...process.env,
PATH: `${BUN_BIN_DIR}:${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;
}
// Check if database is fresh and needs bootstrapping via synchronize
// TypeORM incremental migrations can't run on an empty database -
// they expect base tables from a prior synchronize. This detects fresh
// databases and bootstraps them before running incremental migrations.
if (container && database && port && user && password) {
const superuser = getSuperuserConfig(container);
if (superuser && isDatabaseFresh(container, superuser, database)) {
const bootstrapped = bootstrapFreshDatabase(
servicePath,
port,
user,
password,
database,
container,
superuser,
);
if (bootstrapped) {
console.log(` ✅ Fresh database bootstrapped, skipping incremental migrations\n`);
results.push({ feature, success: true, skipped: false });
continue;
}
// If bootstrap failed, fall through to try normal migration:run
console.log(` ⚠️ Bootstrap failed, attempting normal migration:run...`);
}
}
// Run migrations using typeorm CLI directly (bypass broken bin links)
// Set environment variables to override data-source defaults
const result = spawnSync('node', [typeormCli, 'migration:run', '-d', compiledDataSourcePath], {
cwd: servicePath,
stdio: 'pipe',
encoding: 'utf-8',
env: {
...process.env,
DB_HOST: DB_HOST,
DB_PORT: port?.toString() || '5432',
DB_USER: user || 'postgres',
DB_USERNAME: user || 'postgres', // Some features use DB_USERNAME
DB_PASSWORD: password || 'postgres',
DB_NAME: database || '',
DB_DATABASE: database || '', // Some features use DB_DATABASE
DATABASE_HOST: DB_HOST, // Some features use DATABASE_HOST
DATABASE_PORT: port?.toString() || '5432', // Some features use DATABASE_PORT
DATABASE_POSTGRES_USER: user || 'postgres', // merchant uses this
DATABASE_POSTGRES_PASSWORD: password || 'postgres', // merchant uses this
DATABASE_POSTGRES_NAME: database || '', // merchant uses this
},
});
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);
});