mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 06:10:31 +00:00
feat: remove path alias.
feat: remove Webpack and depend on nodemon. feat: refactoring expenses. feat: optimize system users with caching. feat: architecture tenant optimize.
This commit is contained in:
26
server/src/services/Tenancy/SystemService.ts
Normal file
26
server/src/services/Tenancy/SystemService.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import Container from "typedi"
|
||||
import { Service, Inject } from 'typedi';
|
||||
|
||||
@Service()
|
||||
export default class HasSystemService implements SystemService{
|
||||
|
||||
private container(key: string) {
|
||||
return Container.get(key);
|
||||
}
|
||||
|
||||
knex() {
|
||||
return this.container('knex');
|
||||
}
|
||||
|
||||
repositories() {
|
||||
return this.container('repositories');
|
||||
}
|
||||
|
||||
cache() {
|
||||
return this.container('cache');
|
||||
}
|
||||
|
||||
dbManager() {
|
||||
return this.container('dbManager');
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,55 @@
|
||||
import { Container, Service } from 'typedi';
|
||||
import { Container, Service, Inject } from 'typedi';
|
||||
import TenantsManagerService from 'services/Tenancy/TenantsManager';
|
||||
import tenantModelsLoader from 'loaders/tenantModels';
|
||||
import tenantRepositoriesLoader from 'loaders/tenantRepositories';
|
||||
import tenantCacheLoader from 'loaders/tenantCache';
|
||||
|
||||
@Service()
|
||||
export default class HasTenancyService {
|
||||
@Inject()
|
||||
tenantsManager: TenantsManagerService;
|
||||
|
||||
/**
|
||||
* Retrieve the given tenant container.
|
||||
* @param {number} tenantId
|
||||
* @param {number} tenantId
|
||||
* @return {Container}
|
||||
*/
|
||||
tenantContainer(tenantId: number) {
|
||||
return Container.of(`tenant-${tenantId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton tenant service.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {string} key - Service key.
|
||||
* @param {Function} callback
|
||||
*/
|
||||
singletonService(tenantId: number, key: string, callback: Function) {
|
||||
const container = this.tenantContainer(tenantId);
|
||||
const Logger = Container.get('logger');
|
||||
|
||||
const hasServiceInstnace = container.has(key);
|
||||
|
||||
if (!hasServiceInstnace) {
|
||||
const serviceInstance = callback();
|
||||
|
||||
container.set(key, serviceInstance);
|
||||
Logger.info(`[tenant_DI] ${key} injected to tenant container.`, { tenantId, key });
|
||||
|
||||
return serviceInstance;
|
||||
} else {
|
||||
return container.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve knex instance of the given tenant id.
|
||||
* @param {number} tenantId
|
||||
*/
|
||||
knex(tenantId: number) {
|
||||
return this.tenantContainer(tenantId).get('knex');
|
||||
return this.singletonService(tenantId, 'tenantManager', () => {
|
||||
return this.tenantsManager.getKnexInstance(tenantId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,30 +57,39 @@ export default class HasTenancyService {
|
||||
* @param {number} tenantId - The tenant id.
|
||||
*/
|
||||
models(tenantId: number) {
|
||||
return this.tenantContainer(tenantId).get('models');
|
||||
const knexInstance = this.knex(tenantId);
|
||||
|
||||
return this.singletonService(tenantId, 'models', () => {
|
||||
return tenantModelsLoader(knexInstance);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve repositories of the given tenant id.
|
||||
* @param {number} tenantId
|
||||
* @param {number} tenantId - Tenant id.
|
||||
*/
|
||||
repositories(tenantId: number) {
|
||||
return this.tenantContainer(tenantId).get('repositories');
|
||||
return this.singletonService(tenantId, 'repositories', () => {
|
||||
return tenantRepositoriesLoader(tenantId);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve i18n locales methods.
|
||||
* @param {number} tenantId
|
||||
* @param {number} tenantId - Tenant id.
|
||||
*/
|
||||
i18n(tenantId: number) {
|
||||
return this.tenantContainer(tenantId).get('i18n');
|
||||
return this.singletonService(tenantId, 'i18n', () => {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve tenant cache instance.
|
||||
* @param {number} tenantId -
|
||||
* @param {number} tenantId - Tenant id.
|
||||
*/
|
||||
cache(tenantId: number) {
|
||||
return this.tenantContainer(tenantId).get('cache');
|
||||
return this.singletonService(tenantId, 'cache', () => {
|
||||
return tenantCacheLoader(tenantId);
|
||||
});
|
||||
}
|
||||
}
|
||||
123
server/src/services/Tenancy/TenantDBManager.ts
Normal file
123
server/src/services/Tenancy/TenantDBManager.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { Container } from 'typedi';
|
||||
import Knex from 'knex';
|
||||
import { knexSnakeCaseMappers } from 'objection';
|
||||
import config from 'config';
|
||||
import { ITenant, ITenantDBManager, ISystemService } from 'interfaces';
|
||||
import SystemService from 'services/Tenancy/SystemService';
|
||||
import { TenantDBAlreadyExists } from 'exceptions';
|
||||
import { tenantKnexConfig, tenantSeedConfig } from 'config/knexConfig';
|
||||
|
||||
export default class TenantDBManager implements ITenantDBManager{
|
||||
static knexCache: { [key: string]: Knex; } = {};
|
||||
|
||||
// System database manager.
|
||||
dbManager: any;
|
||||
|
||||
// System knex instance.
|
||||
sysKnex: Knex;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {ITenant} tenant
|
||||
*/
|
||||
constructor() {
|
||||
const systemService = Container.get(SystemService);
|
||||
|
||||
this.dbManager = systemService.dbManager();
|
||||
this.sysKnex = systemService.knex();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the tenant database name.
|
||||
* @return {string}
|
||||
*/
|
||||
private getDatabaseName(tenant: ITenant) {
|
||||
return `${config.tenant.db_name_prefix}${tenant.organizationId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines the tenant database weather exists.
|
||||
* @return {Promise<boolean>}
|
||||
*/
|
||||
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<void>}
|
||||
*/
|
||||
public async createDatabase(tenant: ITenant): Promise<void> {
|
||||
await this.throwErrorIfTenantDBExists(tenant);
|
||||
|
||||
const databaseName = this.getDatabaseName(tenant);
|
||||
await this.dbManager.createDb(databaseName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate tenant database schema to the latest version.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async migrate(tenant: ITenant): Promise<void> {
|
||||
const knex = this.setupKnexInstance(tenant);
|
||||
|
||||
await knex.migrate.latest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeds initial data to the tenant database.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async seed(tenant: ITenant): Promise<void> {
|
||||
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;
|
||||
}
|
||||
|
||||
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<void>}
|
||||
*/
|
||||
async throwErrorIfTenantDBExists(tenant: ITenant) {
|
||||
const isExists = await this.databaseExists(tenant);
|
||||
if (isExists) {
|
||||
throw new TenantDBAlreadyExists();
|
||||
}
|
||||
}
|
||||
}
|
||||
0
server/src/services/Tenancy/TenantService.ts
Normal file
0
server/src/services/Tenancy/TenantService.ts
Normal file
158
server/src/services/Tenancy/TenantsManager.ts
Normal file
158
server/src/services/Tenancy/TenantsManager.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { Container, Inject, Service } from 'typedi';
|
||||
import { ServiceError } from 'exceptions';
|
||||
import {
|
||||
ITenantManager,
|
||||
ITenant,
|
||||
ITenantDBManager,
|
||||
} from 'interfaces';
|
||||
import {
|
||||
EventDispatcherInterface,
|
||||
EventDispatcher,
|
||||
} from 'decorators/eventDispatcher';
|
||||
import { TenantAlreadyInitialized, TenantAlreadySeeded, TenantDatabaseNotBuilt } from 'exceptions';
|
||||
import TenantDBManager from 'services/Tenancy/TenantDBManager';
|
||||
import events from 'subscribers/events';
|
||||
|
||||
const ERRORS = {
|
||||
TENANT_ALREADY_CREATED: 'TENANT_ALREADY_CREATED',
|
||||
TENANT_NOT_EXISTS: 'TENANT_NOT_EXISTS'
|
||||
};
|
||||
|
||||
// Tenants manager service.
|
||||
@Service()
|
||||
export default class TenantsManagerService implements ITenantManager{
|
||||
static instances: { [key: number]: ITenantManager } = {};
|
||||
|
||||
@EventDispatcher()
|
||||
private eventDispatcher: EventDispatcherInterface;
|
||||
|
||||
@Inject('repositories')
|
||||
private sysRepositories: any;
|
||||
|
||||
private tenantDBManager: ITenantDBManager;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
constructor() {
|
||||
this.tenantDBManager = new TenantDBManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new teant with unique organization id.
|
||||
* @param {ITenant} tenant
|
||||
* @return {Promise<ITenant>}
|
||||
*/
|
||||
public async createTenant(): Promise<ITenant> {
|
||||
const { tenantRepository } = this.sysRepositories;
|
||||
const tenant = await tenantRepository.newTenantWithUniqueOrgId();
|
||||
|
||||
return tenant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new tenant database.
|
||||
* @param {ITenant} tenant -
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async createDatabase(tenant: ITenant): Promise<void> {
|
||||
this.throwErrorIfTenantAlreadyInitialized(tenant);
|
||||
|
||||
await this.tenantDBManager.createDatabase(tenant);
|
||||
|
||||
this.eventDispatcher.dispatch(events.tenantManager.databaseCreated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines the tenant has database.
|
||||
* @param {ITenant} tenant
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
public async hasDatabase(tenant: ITenant): Promise<boolean> {
|
||||
return this.tenantDBManager.databaseExists(tenant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates the tenant database.
|
||||
* @param {ITenant} tenant
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async migrateTenant(tenant: ITenant) {
|
||||
this.throwErrorIfTenantAlreadyInitialized(tenant);
|
||||
|
||||
const { tenantRepository } = this.sysRepositories;
|
||||
|
||||
await this.tenantDBManager.migrate(tenant);
|
||||
await tenantRepository.markAsInitialized(tenant.id);
|
||||
|
||||
this.eventDispatcher.dispatch(events.tenantManager.tenantMigrated, { tenant });
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeds the tenant database.
|
||||
* @param {ITenant} tenant
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async seedTenant(tenant: ITenant) {
|
||||
this.throwErrorIfTenantNotBuilt(tenant);
|
||||
this.throwErrorIfTenantAlreadySeeded(tenant);
|
||||
|
||||
const { tenantRepository } = this.sysRepositories;
|
||||
|
||||
// Seed the tenant database.
|
||||
await this.tenantDBManager.seed(tenant);
|
||||
|
||||
// Mark the tenant as seeded in specific date.
|
||||
await tenantRepository.markAsSeeded(tenant.id);
|
||||
|
||||
this.eventDispatcher.dispatch(events.tenantManager.tenantSeeded);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize knex instance or retrieve the instance of cache map.
|
||||
* @param {ITenant} tenant
|
||||
* @returns {Knex}
|
||||
*/
|
||||
public setupKnexInstance(tenant: ITenant) {
|
||||
return this.tenantDBManager.setupKnexInstance(tenant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve tenant knex instance or throw error in case was not initialized.
|
||||
* @param {number} tenantId
|
||||
* @returns {Knex}
|
||||
*/
|
||||
public getKnexInstance(tenantId: number) {
|
||||
return this.tenantDBManager.getKnexInstance(tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws error if the tenant already seeded.
|
||||
* @throws {TenantAlreadySeeded}
|
||||
*/
|
||||
private throwErrorIfTenantAlreadySeeded(tenant: ITenant) {
|
||||
if (tenant.seededAt) {
|
||||
throw new TenantAlreadySeeded();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws error if the tenant database is not built yut.
|
||||
* @param tenant
|
||||
*/
|
||||
private throwErrorIfTenantNotBuilt(tenant: ITenant) {
|
||||
if (!tenant.initializedAt) {
|
||||
throw new TenantDatabaseNotBuilt();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws error if the tenant already migrated.
|
||||
* @throws {TenantAlreadyInitialized}
|
||||
*/
|
||||
private throwErrorIfTenantAlreadyInitialized(tenant: ITenant) {
|
||||
if (tenant.initializedAt) {
|
||||
throw new TenantAlreadyInitialized();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user