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 { 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 { 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 { 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; } }