import { PrismaClient as CentralPrismaClient } from '.prisma/central'; import knex, { Knex } from 'knex'; import { createDecipheriv } from 'crypto'; // Encryption configuration - must match the one used in tenant service const ALGORITHM = 'aes-256-cbc'; /** * Decrypt a tenant's database password */ function decryptPassword(encryptedPassword: string): string { try { // Check if password is already plaintext (for legacy/development) if (!encryptedPassword.includes(':')) { console.warn('āš ļø Password appears to be unencrypted, using as-is'); return encryptedPassword; } const key = Buffer.from(process.env.ENCRYPTION_KEY, 'hex'); const parts = encryptedPassword.split(':'); if (parts.length !== 2) { throw new Error('Invalid encrypted password format'); } const iv = Buffer.from(parts[0], 'hex'); const encrypted = parts[1]; const decipher = createDecipheriv(ALGORITHM, key, iv); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } catch (error) { console.error('Error decrypting password:', error); throw error; } } /** * Create a Knex connection for a specific tenant */ function createTenantKnexConnection(tenant: any): Knex { const decryptedPassword = decryptPassword(tenant.dbPassword); return knex({ client: 'mysql2', connection: { host: tenant.dbHost, port: tenant.dbPort, user: tenant.dbUsername, password: decryptedPassword, database: tenant.dbName, }, migrations: { tableName: 'knex_migrations', directory: './migrations/tenant', }, }); } /** * Run migrations for a specific tenant */ async function migrateTenant(tenant: any): Promise { console.log(`\nšŸ”„ Migrating tenant: ${tenant.name} (${tenant.dbName})`); const tenantKnex = createTenantKnexConnection(tenant); try { const [batchNo, log] = await tenantKnex.migrate.latest(); if (log.length === 0) { console.log(`āœ… ${tenant.name}: Already up to date`); } else { console.log(`āœ… ${tenant.name}: Ran ${log.length} migrations:`); log.forEach((migration) => { console.log(` - ${migration}`); }); } } catch (error) { console.error(`āŒ ${tenant.name}: Migration failed:`, error.message); throw error; } finally { await tenantKnex.destroy(); } } /** * Main function to migrate all active tenants */ async function migrateAllTenants() { console.log('šŸš€ Starting migration for all tenants...\n'); const centralPrisma = new CentralPrismaClient(); try { // Fetch all active tenants const tenants = await centralPrisma.tenant.findMany({ where: { status: 'ACTIVE', }, orderBy: { name: 'asc', }, }); if (tenants.length === 0) { console.log('āš ļø No active tenants found.'); return; } console.log(`šŸ“‹ Found ${tenants.length} active tenant(s)\n`); let successCount = 0; let failureCount = 0; const failures: { tenant: string; error: string }[] = []; // Migrate each tenant sequentially for (const tenant of tenants) { try { await migrateTenant(tenant); successCount++; } catch (error) { failureCount++; failures.push({ tenant: tenant.name, error: error.message, }); } } // Print summary console.log('\n' + '='.repeat(60)); console.log('šŸ“Š Migration Summary'); console.log('='.repeat(60)); console.log(`āœ… Successful: ${successCount}`); console.log(`āŒ Failed: ${failureCount}`); if (failures.length > 0) { console.log('\nāŒ Failed Tenants:'); failures.forEach(({ tenant, error }) => { console.log(` - ${tenant}: ${error}`); }); process.exit(1); } else { console.log('\nšŸŽ‰ All tenant migrations completed successfully!'); } } catch (error) { console.error('āŒ Fatal error:', error); process.exit(1); } finally { await centralPrisma.$disconnect(); } } // Run the migration migrateAllTenants() .then(() => { process.exit(0); }) .catch((error) => { console.error('Unhandled error:', error); process.exit(1); });