Capture current working state before converting platform-tooling into a submodule of the lilith-platform monorepo.
691 lines
24 KiB
TypeScript
691 lines
24 KiB
TypeScript
#!/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
|
||
const portToContainer: Record<number, string> = {
|
||
5432: 'lilith-dev-postgres', // Main infrastructure postgres
|
||
5435: 'lilith-i18n-postgres', // I18N/platform-admin
|
||
5438: 'lilith-landing-postgres', // Landing
|
||
5444: 'lilith-marketplace-postgres', // Marketplace
|
||
5445: 'lilith-merchant-postgres', // Merchant
|
||
5448: 'lilith-image-assistant-postgres', // Image Assistant
|
||
};
|
||
|
||
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-i18n-postgres': { user: 'i18n', password: 'i18n_dev_password', defaultDb: 'platform_admin' },
|
||
'lilith-landing-postgres': { user: 'lilith', password: 'lilith', defaultDb: 'lilith_landing' },
|
||
'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' },
|
||
};
|
||
|
||
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('');
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
// 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);
|
||
});
|