WIP - work with miagrations and objects set up
This commit is contained in:
194
backend/scripts/README.md
Normal file
194
backend/scripts/README.md
Normal file
@@ -0,0 +1,194 @@
|
||||
# Tenant Migration Scripts
|
||||
|
||||
This directory contains scripts for managing database migrations across all tenants in the multi-tenant platform.
|
||||
|
||||
## Available Scripts
|
||||
|
||||
### 1. Create a New Migration
|
||||
|
||||
```bash
|
||||
npm run migrate:make <migration_name>
|
||||
```
|
||||
|
||||
Creates a new migration file in `migrations/tenant/` directory.
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
npm run migrate:make add_status_field_to_contacts
|
||||
```
|
||||
|
||||
### 2. Migrate a Single Tenant
|
||||
|
||||
```bash
|
||||
npm run migrate:tenant <tenant-slug-or-id>
|
||||
```
|
||||
|
||||
Runs all pending migrations for a specific tenant. You can identify the tenant by its slug or ID.
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
npm run migrate:tenant acme-corp
|
||||
npm run migrate:tenant cm5a1b2c3d4e5f6g7h8i9j0k
|
||||
```
|
||||
|
||||
### 3. Migrate All Tenants
|
||||
|
||||
```bash
|
||||
npm run migrate:all-tenants
|
||||
```
|
||||
|
||||
Runs all pending migrations for **all active tenants** in the system. This is useful when:
|
||||
- You've created a new migration that needs to be applied to all tenants
|
||||
- You're updating the schema across the entire platform
|
||||
- You need to ensure all tenants are up to date
|
||||
|
||||
**Output:**
|
||||
- Shows progress for each tenant
|
||||
- Lists which migrations were applied
|
||||
- Provides a summary at the end
|
||||
- Exits with error code if any tenant fails
|
||||
|
||||
### 4. Rollback Migration (Manual)
|
||||
|
||||
```bash
|
||||
npm run migrate:rollback
|
||||
```
|
||||
|
||||
⚠️ **Warning:** This runs a rollback on the **default database** configured in `knexfile.js`. For tenant-specific rollbacks, you'll need to manually configure the connection.
|
||||
|
||||
## Migration Flow
|
||||
|
||||
### During New Tenant Provisioning
|
||||
|
||||
When a new tenant is created via the API, migrations are automatically run as part of the provisioning process:
|
||||
|
||||
1. Tenant database is created
|
||||
2. `TenantProvisioningService.runTenantMigrations()` is called
|
||||
3. All migrations in `migrations/tenant/` are executed
|
||||
|
||||
### For Existing Tenants
|
||||
|
||||
When you add a new migration file and need to apply it to existing tenants:
|
||||
|
||||
1. Create the migration:
|
||||
```bash
|
||||
npm run migrate:make add_new_feature
|
||||
```
|
||||
|
||||
2. Edit the generated migration file in `migrations/tenant/`
|
||||
|
||||
3. Test on a single tenant first:
|
||||
```bash
|
||||
npm run migrate:tenant test-tenant
|
||||
```
|
||||
|
||||
4. If successful, apply to all tenants:
|
||||
```bash
|
||||
npm run migrate:all-tenants
|
||||
```
|
||||
|
||||
## Migration Directory Structure
|
||||
|
||||
```
|
||||
backend/
|
||||
├── migrations/
|
||||
│ └── tenant/ # Tenant-specific migrations
|
||||
│ ├── 20250126000001_create_users_and_rbac.js
|
||||
│ ├── 20250126000002_create_object_definitions.js
|
||||
│ └── ...
|
||||
├── scripts/
|
||||
│ ├── migrate-tenant.ts # Single tenant migration
|
||||
│ └── migrate-all-tenants.ts # All tenants migration
|
||||
└── knexfile.js # Knex configuration
|
||||
```
|
||||
|
||||
## Security Notes
|
||||
|
||||
### Database Password Encryption
|
||||
|
||||
Tenant database passwords are encrypted in the central database using AES-256-CBC encryption. The migration scripts automatically:
|
||||
|
||||
1. Fetch tenant connection details from the central database
|
||||
2. Decrypt the database password using the `DB_ENCRYPTION_KEY` environment variable
|
||||
3. Connect to the tenant database
|
||||
4. Run migrations
|
||||
5. Close the connection
|
||||
|
||||
**Required Environment Variable:**
|
||||
```bash
|
||||
DB_ENCRYPTION_KEY=your-32-character-secret-key!!
|
||||
```
|
||||
|
||||
This key must match the key used by `TenantService` for encryption.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Migration Fails for One Tenant
|
||||
|
||||
If `migrate:all-tenants` fails for a specific tenant:
|
||||
|
||||
1. Check the error message in the output
|
||||
2. Investigate the tenant's database directly
|
||||
3. Fix the issue (manual SQL, data cleanup, etc.)
|
||||
4. Re-run migrations for that tenant: `npm run migrate:tenant <slug>`
|
||||
5. Once fixed, run `migrate:all-tenants` again to ensure others are updated
|
||||
|
||||
### Migration Already Exists
|
||||
|
||||
Knex tracks which migrations have been run in the `knex_migrations` table in each tenant database. If a migration was already applied, it will be skipped automatically.
|
||||
|
||||
### Connection Issues
|
||||
|
||||
If you see connection errors:
|
||||
|
||||
1. Verify the central database is accessible
|
||||
2. Check that tenant database credentials are correct
|
||||
3. Ensure `DB_ENCRYPTION_KEY` matches the one used for encryption
|
||||
4. Verify the tenant's database server is running and accessible
|
||||
|
||||
## Example Migration File
|
||||
|
||||
```javascript
|
||||
// migrations/tenant/20250126000006_add_custom_fields.js
|
||||
|
||||
exports.up = async function(knex) {
|
||||
await knex.schema.table('field_definitions', (table) => {
|
||||
table.boolean('is_custom').defaultTo(false);
|
||||
table.string('custom_type', 50).nullable();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
await knex.schema.table('field_definitions', (table) => {
|
||||
table.dropColumn('is_custom');
|
||||
table.dropColumn('custom_type');
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always test on a single tenant first** before running migrations on all tenants
|
||||
2. **Include rollback logic** in your `down()` function
|
||||
3. **Use transactions** for complex multi-step migrations
|
||||
4. **Backup production databases** before running migrations
|
||||
5. **Monitor the output** when running `migrate:all-tenants` to catch any failures
|
||||
6. **Version control** your migration files
|
||||
7. **Document breaking changes** in migration comments
|
||||
8. **Consider data migrations** separately from schema migrations when dealing with large datasets
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
In your deployment pipeline, you can automatically migrate all tenants:
|
||||
|
||||
```bash
|
||||
# After deploying new code
|
||||
npm run migrate:all-tenants
|
||||
```
|
||||
|
||||
Or integrate it into your Docker deployment:
|
||||
|
||||
```dockerfile
|
||||
# In your Dockerfile or docker-compose.yml
|
||||
CMD npm run migrate:all-tenants && npm run start:prod
|
||||
```
|
||||
181
backend/scripts/check-migration-status.ts
Normal file
181
backend/scripts/check-migration-status.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
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);
|
||||
});
|
||||
165
backend/scripts/migrate-all-tenants.ts
Normal file
165
backend/scripts/migrate-all-tenants.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
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<void> {
|
||||
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);
|
||||
});
|
||||
134
backend/scripts/migrate-tenant.ts
Normal file
134
backend/scripts/migrate-tenant.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
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(':')) {
|
||||
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',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate a specific tenant by slug or ID
|
||||
*/
|
||||
async function migrateTenant() {
|
||||
const tenantIdentifier = process.argv[2];
|
||||
|
||||
if (!tenantIdentifier) {
|
||||
console.error('❌ Usage: npm run migrate:tenant <tenant-slug-or-id>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`🔍 Looking for tenant: ${tenantIdentifier}\n`);
|
||||
|
||||
const centralPrisma = new CentralPrismaClient();
|
||||
|
||||
try {
|
||||
// Find tenant by slug or ID
|
||||
const tenant = await centralPrisma.tenant.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ slug: tenantIdentifier },
|
||||
{ id: tenantIdentifier },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (!tenant) {
|
||||
console.error(`❌ Tenant not found: ${tenantIdentifier}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`📋 Tenant: ${tenant.name} (${tenant.slug})`);
|
||||
console.log(`📊 Database: ${tenant.dbName}`);
|
||||
console.log(`🔄 Running migrations...\n`);
|
||||
|
||||
const tenantKnex = createTenantKnexConnection(tenant);
|
||||
|
||||
try {
|
||||
const [batchNo, log] = await tenantKnex.migrate.latest();
|
||||
|
||||
if (log.length === 0) {
|
||||
console.log(`✅ Already up to date (batch ${batchNo})`);
|
||||
} else {
|
||||
console.log(`✅ Ran ${log.length} migration(s) (batch ${batchNo}):`);
|
||||
log.forEach((migration) => {
|
||||
console.log(` - ${migration}`);
|
||||
});
|
||||
}
|
||||
|
||||
console.log('\n🎉 Migration completed successfully!');
|
||||
} catch (error) {
|
||||
console.error('❌ Migration failed:', error.message);
|
||||
throw error;
|
||||
} finally {
|
||||
await tenantKnex.destroy();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Fatal error:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await centralPrisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// Run the migration
|
||||
migrateTenant()
|
||||
.then(() => {
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Unhandled error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user