mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 20:30:33 +00:00
feat(nestjs): migrate to NestJS
This commit is contained in:
@@ -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 {}
|
||||
123
packages/server/src/modules/TenantDBManager/TenantDBManager.ts
Normal file
123
packages/server/src/modules/TenantDBManager/TenantDBManager.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
131
packages/server/src/modules/TenantDBManager/TenantsManager.ts
Normal file
131
packages/server/src/modules/TenantDBManager/TenantsManager.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
34
packages/server/src/modules/TenantDBManager/_utils.ts
Normal file
34
packages/server/src/modules/TenantDBManager/_utils.ts
Normal 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();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export class TenantAlreadyInitialized extends Error {
|
||||
constructor(description: string = 'Tenant is already initialized') {
|
||||
super(description);
|
||||
this.name = 'TenantAlreadyInitialized';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export class TenantAlreadySeeded extends Error {
|
||||
constructor(description: string = 'Tenant is already seeded') {
|
||||
super(description);
|
||||
this.name = 'TenantAlreadySeeded';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export class TenantDBAlreadyExists extends Error {
|
||||
constructor(description: string = 'Tenant DB is already exists.') {
|
||||
super(description);
|
||||
this.name = 'TenantDBAlreadyExists';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export class TenantDatabaseNotBuilt extends Error {
|
||||
constructor(description: string = 'Tenant database is not built yet.') {
|
||||
super(description);
|
||||
this.name = 'TenantDatabaseNotBuilt';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user