import { PrismaClient as CentralPrismaClient } from '.prisma/central'; import knex, { Knex } from 'knex'; import { createDecipheriv } from 'crypto'; // Encryption configuration 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(':')) { 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', }, }); } /** * Get migration status for a specific tenant */ async function getTenantMigrationStatus(tenant: any): Promise<{ completed: string[]; pending: string[]; }> { const tenantKnex = createTenantKnexConnection(tenant); try { const [completed, pending] = await tenantKnex.migrate.list(); return { completed: completed[1] || [], pending: pending || [], }; } catch (error) { throw error; } finally { await tenantKnex.destroy(); } } /** * Check migration status across all tenants */ async function checkMigrationStatus() { console.log('šŸ” Checking migration status 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`); console.log('='.repeat(80)); let allUpToDate = true; const tenantsWithPending: { name: string; pending: string[] }[] = []; // Check each tenant for (const tenant of tenants) { try { const status = await getTenantMigrationStatus(tenant); console.log(`\nšŸ“¦ ${tenant.name} (${tenant.slug})`); console.log(` Database: ${tenant.dbName}`); console.log(` Completed: ${status.completed.length} migration(s)`); if (status.pending.length > 0) { allUpToDate = false; console.log(` āš ļø Pending: ${status.pending.length} migration(s)`); status.pending.forEach((migration) => { console.log(` - ${migration}`); }); tenantsWithPending.push({ name: tenant.name, pending: status.pending, }); } else { console.log(` āœ… Up to date`); } // Show last 3 completed migrations if (status.completed.length > 0) { const recent = status.completed.slice(-3); console.log(` Recent migrations:`); recent.forEach((migration) => { console.log(` - ${migration}`); }); } } catch (error) { console.log(`\nāŒ ${tenant.name}: Failed to check status`); console.log(` Error: ${error.message}`); allUpToDate = false; } } // Print summary console.log('\n' + '='.repeat(80)); console.log('šŸ“Š Summary'); console.log('='.repeat(80)); if (allUpToDate) { console.log('āœ… All tenants are up to date!'); } else { console.log(`āš ļø ${tenantsWithPending.length} tenant(s) have pending migrations:\n`); tenantsWithPending.forEach(({ name, pending }) => { console.log(` ${name}: ${pending.length} pending`); }); console.log('\nšŸ’” Run: npm run migrate:all-tenants'); } } catch (error) { console.error('āŒ Fatal error:', error); process.exit(1); } finally { await centralPrisma.$disconnect(); } } // Run the status check checkMigrationStatus() .then(() => { process.exit(0); }) .catch((error) => { console.error('Unhandled error:', error); process.exit(1); });