feat(nestjs): migrate to NestJS

This commit is contained in:
Ahmed Bouhuolia
2025-04-07 11:51:24 +02:00
parent f068218a16
commit 55fcc908ef
3779 changed files with 631 additions and 195332 deletions

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { TenantsManagerService } from './TenantsManager';
import { TenantDBManager } from './TenantDBManager';
import { TenancyContext } from '../Tenancy/TenancyContext.service';
@Module({
providers: [TenancyContext, TenantsManagerService, TenantDBManager],
exports: [TenantsManagerService, TenantDBManager],
})
export class TenantDBManagerModule {}

View File

@@ -0,0 +1,123 @@
// @ts-nocheck
import { Knex, knex } from 'knex';
import { knexSnakeCaseMappers } from 'objection';
import { TenantDBAlreadyExists } from './exceptions/TenantDBAlreadyExists';
import { sanitizeDatabaseName } from '@/utils/sanitize-database-name';
import { ConfigService } from '@nestjs/config';
import { SystemKnexConnection } from '../System/SystemDB/SystemDB.constants';
import { Inject, Injectable } from '@nestjs/common';
import { TenantModel } from '../System/models/TenantModel';
import { TenancyContext } from '../Tenancy/TenancyContext.service';
import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants';
@Injectable()
export class TenantDBManager {
static knexCache: { [key: string]: Knex } = {};
constructor(
private readonly configService: ConfigService,
private readonly tenancyContext: TenancyContext,
@Inject(TENANCY_DB_CONNECTION)
private readonly tenantKnex: () => Knex,
@Inject(SystemKnexConnection)
private readonly systemKnex: Knex,
) {}
/**
* Retrieves the tenant database name.
* @return {string}
*/
private getDatabaseName(tenant: TenantModel) {
return sanitizeDatabaseName(
`${this.configService.get('tenantDatabase.dbNamePrefix')}${tenant.organizationId}`,
);
}
/**
* Determines the tenant database weather exists.
* @return {Promise<boolean>}
*/
public async databaseExists() {
const tenant = await this.tenancyContext.getTenant();
const databaseName = this.getDatabaseName(tenant);
const results = await this.systemKnex.raw(
'SELECT * FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = "' +
databaseName +
'"',
);
return results[0].length > 0;
}
/**
* Creates a tenant database.
* @throws {TenantAlreadyInitialized}
* @return {Promise<void>}
*/
public async createDatabase(): Promise<void> {
const tenant = await this.tenancyContext.getTenant();
const databaseName = this.getDatabaseName(tenant);
await this.throwErrorIfTenantDBExists(tenant);
await this.systemKnex.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() {
const tenant = await this.tenancyContext.getTenant();
const isExists = await this.databaseExists(tenant);
if (!isExists) {
return;
}
await this.dropDatabase(tenant);
}
/**
* dropdowns the tenant's database.
*/
public async dropDatabase() {
const tenant = await this.tenancyContext.getTenant();
const databaseName = this.getDatabaseName(tenant);
await this.systemKnex.raw(`DROP DATABASE IF EXISTS ${databaseName}`);
}
/**
* Migrate tenant database schema to the latest version.
* @return {Promise<void>}
*/
public async migrate(): Promise<void> {
await this.tenantKnex().migrate.latest();
}
/**
* Seeds initial data to the tenant database.
* @return {Promise<void>}
*/
public async seed(): Promise<void> {
await this.tenantKnex().migrate.latest({
...tenantSeedConfig(tenant),
disableMigrationsListValidation: true,
});
}
/**
* Throws error if the tenant database already exists.
* @return {Promise<void>}
*/
async throwErrorIfTenantDBExists(tenant: TenantModel) {
const isExists = await this.databaseExists(tenant);
if (isExists) {
throw new TenantDBAlreadyExists();
}
}
}

View File

@@ -0,0 +1,131 @@
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import { I18nService } from 'nestjs-i18n';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { TenantDBManager } from './TenantDBManager';
import { events } from '@/common/events/events';
import { TenantModel } from '../System/models/TenantModel';
import {
throwErrorIfTenantAlreadyInitialized,
throwErrorIfTenantAlreadySeeded,
throwErrorIfTenantNotBuilt,
} from './_utils';
import { SeedMigration } from '@/libs/migration-seed/SeedMigration';
import { TenantRepository } from '../System/repositories/Tenant.repository';
import { TenancyContext } from '../Tenancy/TenancyContext.service';
import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants';
@Injectable()
export class TenantsManagerService {
constructor(
private readonly tenantDbManager: TenantDBManager,
private readonly tenancyContext: TenancyContext,
private readonly eventEmitter: EventEmitter2,
private readonly tenantRepository: TenantRepository,
private readonly i18nService: I18nService,
@Inject(TENANCY_DB_CONNECTION)
private readonly tenantKnex: () => Knex,
) {}
/**
* Creates a new teant with unique organization id.
* @return {Promise<TenantModel>}
*/
public async createTenant(): Promise<TenantModel> {
return this.tenantRepository.createWithUniqueOrgId();
}
/**
* Creates a new tenant database.
* @return {Promise<void>}
*/
public async createDatabase(): Promise<void> {
const tenant = await this.tenancyContext.getTenant();
throwErrorIfTenantAlreadyInitialized(tenant);
await this.tenantDbManager.createDatabase();
await this.eventEmitter.emitAsync(events.tenantManager.databaseCreated);
}
/**
* Drops the database if the given tenant.
* @param {number} tenantId
*/
async dropDatabaseIfExists() {
await this.tenantDbManager.dropDatabaseIfExists();
}
/**
* Determines the tenant has database.
* @param {ITenant} tenant
* @returns {Promise<boolean>}
*/
public async hasDatabase(): Promise<boolean> {
return this.tenantDbManager.databaseExists();
}
/**
* Migrates the tenant database.
* @return {Promise<void>}
*/
public async migrateTenant(): Promise<void> {
const tenant = await this.tenancyContext.getTenant();
// Throw error if the tenant already initialized.
throwErrorIfTenantAlreadyInitialized(tenant);
// Migrate the database tenant.
await this.tenantDbManager.migrate();
// Mark the tenant as initialized.
await this.tenantRepository.markAsInitialized().findById(tenant.id);
// Triggers `onTenantMigrated` event.
this.eventEmitter.emitAsync(events.tenantManager.tenantMigrated, {
tenantId: tenant.id,
});
}
/**
* Seeds the tenant database.
* @return {Promise<void>}
*/
public async seedTenant(): Promise<void> {
const tenant = await this.tenancyContext.getTenant();
// Throw error if the tenant is not built yet.
throwErrorIfTenantNotBuilt(tenant);
// Throw error if the tenant is not seeded yet.
throwErrorIfTenantAlreadySeeded(tenant);
const seedContext = await this.getSeedMigrationContext();
// Seeds the organization database data.
await new SeedMigration(this.tenantKnex(), seedContext).latest();
// Mark the tenant as seeded in specific date.
await this.tenantRepository.markAsSeeded().findById(tenant.id);
// Triggers `onTenantSeeded` event.
this.eventEmitter.emitAsync(events.tenantManager.tenantSeeded, {
tenantId: tenant.id,
});
}
/**
* Initialize seed migration contxt.
* @param {ITenant} tenant
* @returns
*/
public async getSeedMigrationContext() {
const tenant = await this.tenancyContext.getTenant(true);
return {
knex: this.tenantKnex(),
i18n: this.i18nService,
tenant,
};
}
}

View File

@@ -0,0 +1,34 @@
import { TenantModel } from '../System/models/TenantModel';
import { TenantAlreadyInitialized } from './exceptions/TenantAlreadyInitialized';
import { TenantAlreadySeeded } from './exceptions/TenantAlreadySeeded';
import { TenantDatabaseNotBuilt } from './exceptions/TenantDatabaseNotBuilt';
/**
* Throws error if the tenant already seeded.
* @throws {TenantAlreadySeeded}
*/
export const throwErrorIfTenantAlreadySeeded = (tenant: TenantModel) => {
if (tenant.seededAt) {
throw new TenantAlreadySeeded();
}
};
/**
* Throws error if the tenant database is not built yut.
* @param {ITenant} tenant
*/
export const throwErrorIfTenantNotBuilt = (tenant: TenantModel) => {
if (!tenant.initializedAt) {
throw new TenantDatabaseNotBuilt();
}
};
/**
* Throws error if the tenant already migrated.
* @throws {TenantAlreadyInitialized}
*/
export const throwErrorIfTenantAlreadyInitialized = (tenant: TenantModel) => {
if (tenant.initializedAt) {
throw new TenantAlreadyInitialized();
}
};

View File

@@ -0,0 +1,6 @@
export class TenantAlreadyInitialized extends Error {
constructor(description: string = 'Tenant is already initialized') {
super(description);
this.name = 'TenantAlreadyInitialized';
}
}

View File

@@ -0,0 +1,6 @@
export class TenantAlreadySeeded extends Error {
constructor(description: string = 'Tenant is already seeded') {
super(description);
this.name = 'TenantAlreadySeeded';
}
}

View File

@@ -0,0 +1,6 @@
export class TenantDBAlreadyExists extends Error {
constructor(description: string = 'Tenant DB is already exists.') {
super(description);
this.name = 'TenantDBAlreadyExists';
}
}

View File

@@ -0,0 +1,6 @@
export class TenantDatabaseNotBuilt extends Error {
constructor(description: string = 'Tenant database is not built yet.') {
super(description);
this.name = 'TenantDatabaseNotBuilt';
}
}