import { Container } from 'typedi'; import { Knex, knex } from 'knex'; import { knexSnakeCaseMappers } from 'objection'; import { tenantKnexConfig, tenantSeedConfig } from '@/config/knexConfig'; import config from '@/config'; import { ITenant, ITenantDBManager } from '@/interfaces'; import SystemService from '@/services/Tenancy/SystemService'; import { TenantDBAlreadyExists } from '@/exceptions'; import { sanitizeDatabaseName } from '@/utils/sanitizers'; export default class TenantDBManager implements ITenantDBManager { static knexCache: { [key: string]: Knex } = {}; // System knex instance. sysKnex: Knex; /** * Constructor method. * @param {ITenant} tenant */ constructor() { const systemService = Container.get(SystemService); this.sysKnex = systemService.knex(); } /** * Retrieve the tenant database name. * @return {string} */ private getDatabaseName(tenant: ITenant) { return sanitizeDatabaseName( `${config.tenant.db_name_prefix}${tenant.organizationId}` ); } /** * Detarmines the tenant database weather exists. * @return {Promise} */ public async databaseExists(tenant: ITenant) { const databaseName = this.getDatabaseName(tenant); const results = await this.sysKnex.raw( 'SELECT * FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = "' + databaseName + '"' ); return results[0].length > 0; } /** * Creates a tenant database. * @throws {TenantAlreadyInitialized} * @return {Promise} */ public async createDatabase(tenant: ITenant): Promise { await this.throwErrorIfTenantDBExists(tenant); const databaseName = this.getDatabaseName(tenant); await this.sysKnex.raw( `CREATE DATABASE ${databaseName} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci` ); } /** * Dropdowns the tenant database if it was exist. * @param {ITenant} tenant - */ public async dropDatabaseIfExists(tenant: ITenant) { const isExists = await this.databaseExists(tenant); if (!isExists) { return; } await this.dropDatabase(tenant); } /** * dropdowns the tenant's database. * @param {ITenant} tenant */ public async dropDatabase(tenant: ITenant) { const databaseName = this.getDatabaseName(tenant); await this.sysKnex.raw(`DROP DATABASE IF EXISTS ${databaseName}`); } /** * Migrate tenant database schema to the latest version. * @return {Promise} */ public async migrate(tenant: ITenant): Promise { const knex = this.setupKnexInstance(tenant); await knex.migrate.latest(); } /** * Seeds initial data to the tenant database. * @return {Promise} */ public async seed(tenant: ITenant): Promise { const knex = this.setupKnexInstance(tenant); await knex.migrate.latest({ ...tenantSeedConfig(tenant), disableMigrationsListValidation: true, }); } /** * Retrieve the knex instance of tenant. * @return {Knex} */ public setupKnexInstance(tenant: ITenant) { const key: string = `${tenant.id}`; let knexInstance = TenantDBManager.knexCache[key]; if (!knexInstance) { knexInstance = knex({ ...tenantKnexConfig(tenant), ...knexSnakeCaseMappers({ upperCase: true }), }); TenantDBManager.knexCache[key] = knexInstance; } return knexInstance; } /** * Retrieve knex instance from the givne tenant. */ public getKnexInstance(tenantId: number) { const key: string = `${tenantId}`; let knexInstance = TenantDBManager.knexCache[key]; if (!knexInstance) { throw new Error('Knex instance is not initialized yut.'); } return knexInstance; } /** * Throws error if the tenant database already exists. * @return {Promise} */ async throwErrorIfTenantDBExists(tenant: ITenant) { const isExists = await this.databaseExists(tenant); if (isExists) { throw new TenantDBAlreadyExists(); } } }