it0/packages/shared/database/src/tenant-provisioning.service.ts

87 lines
2.7 KiB
TypeScript

import { Injectable, Logger } from '@nestjs/common';
import { DataSource } from 'typeorm';
import * as fs from 'fs';
import * as path from 'path';
@Injectable()
export class TenantProvisioningService {
private readonly logger = new Logger(TenantProvisioningService.name);
constructor(private readonly dataSource: DataSource) {}
/**
* Create a new tenant schema with all required tables.
* Executes the tenant schema template SQL with the given tenant ID.
*/
async provisionTenant(tenantId: string): Promise<void> {
const schemaName = `it0_t_${tenantId}`;
this.logger.log(`Provisioning tenant schema: ${schemaName}`);
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
try {
await queryRunner.startTransaction();
// Read and execute the tenant schema template
const templatePath = path.resolve(__dirname, './migrations/002-create-tenant-schema-template.sql');
let sql = fs.readFileSync(templatePath, 'utf-8');
sql = sql.replace(/{TENANT_ID}/g, tenantId);
// Remove comment lines, then split by semicolon
const cleanedSql = sql
.split('\n')
.filter((line) => !line.trimStart().startsWith('--'))
.join('\n');
const statements = cleanedSql
.split(';')
.map((s) => s.trim())
.filter((s) => s.length > 0);
for (const stmt of statements) {
await queryRunner.query(stmt);
}
await queryRunner.commitTransaction();
this.logger.log(`Tenant schema ${schemaName} provisioned successfully.`);
} catch (err) {
await queryRunner.rollbackTransaction();
this.logger.error(`Failed to provision tenant ${tenantId}:`, err);
throw err;
} finally {
await queryRunner.release();
}
}
/**
* Drop a tenant schema (use with extreme caution).
*/
async deprovisionTenant(tenantId: string): Promise<void> {
const schemaName = `it0_t_${tenantId}`;
this.logger.warn(`Deprovisioning tenant schema: ${schemaName}`);
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
try {
await queryRunner.query(`DROP SCHEMA IF EXISTS "${schemaName}" CASCADE`);
this.logger.log(`Tenant schema ${schemaName} dropped.`);
} finally {
await queryRunner.release();
}
}
/**
* Check if a tenant schema exists.
*/
async schemaExists(tenantId: string): Promise<boolean> {
const schemaName = `it0_t_${tenantId}`;
const result = await this.dataSource.query(
`SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = $1)`,
[schemaName],
);
return result[0]?.exists ?? false;
}
}