add server to monorepo.

This commit is contained in:
a.bouhuolia
2023-02-03 11:57:50 +02:00
parent 28e309981b
commit 80b97b5fdc
1303 changed files with 137049 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
import Container from 'typedi';
import { Service } 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');
}
}

View File

@@ -0,0 +1,132 @@
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';
import SmsClientLoader from '@/loaders/smsClient';
@Service()
export default class HasTenancyService {
@Inject()
tenantsManager: TenantsManagerService;
/**
* Retrieve the given tenant container.
* @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.singletonService(tenantId, 'tenantManager', () => {
return this.tenantsManager.getKnexInstance(tenantId);
});
}
/**
* Retrieve models of the givne tenant id.
* @param {number} tenantId - The tenant id.
*/
models(tenantId: number) {
const knexInstance = this.knex(tenantId);
return this.singletonService(tenantId, 'models', () => {
return tenantModelsLoader(knexInstance);
});
}
/**
* Retrieve repositories of the given tenant id.
* @param {number} tenantId - Tenant id.
*/
repositories(tenantId: number) {
return this.singletonService(tenantId, 'repositories', () => {
const cache = this.cache(tenantId);
const knex = this.knex(tenantId);
const i18n = this.i18n(tenantId);
return tenantRepositoriesLoader(knex, cache, i18n);
});
}
/**
* Sets i18n locals function.
* @param {number} tenantId
* @param locals
*/
setI18nLocals(tenantId: number, locals: any) {
return this.singletonService(tenantId, 'i18n', () => {
return locals;
});
}
/**
* Retrieve i18n locales methods.
* @param {number} tenantId - Tenant id.
*/
i18n(tenantId: number) {
return this.singletonService(tenantId, 'i18n', () => {
throw new Error('I18n locals is not set yet.');
});
}
/**
* Retrieve tenant cache instance.
* @param {number} tenantId - Tenant id.
*/
cache(tenantId: number) {
return this.singletonService(tenantId, 'cache', () => {
return tenantCacheLoader(tenantId);
});
}
settings(tenantId: number) {
return this.singletonService(tenantId, 'settings', () => {
throw new Error('Settings is not injected yet.');
});
}
smsClient(tenantId: number) {
return this.singletonService(tenantId, 'smsClient', () => {
const settings = this.settings(tenantId);
const token = settings.get({
group: 'sms_integration',
key: 'easysms_token',
});
return SmsClientLoader(token);
});
}
}

View File

@@ -0,0 +1,153 @@
import { Container } from 'typedi';
import 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';
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);
}
/**
* 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.dbManager.dropDb(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;
}
/**
* 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<void>}
*/
async throwErrorIfTenantDBExists(tenant: ITenant) {
const isExists = await this.databaseExists(tenant);
if (isExists) {
throw new TenantDBAlreadyExists();
}
}
}

View File

@@ -0,0 +1,197 @@
import { Container, Inject, Service } from 'typedi';
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';
import { Tenant } from '@/system/models';
import { SeedMigration } from '@/lib/Seeder/SeedMigration';
import i18n from '../../loaders/i18n';
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.createWithUniqueOrgId();
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);
}
/**
* Drops the database if the given tenant.
* @param {number} tenantId
*/
async dropDatabaseIfExists(tenant: ITenant) {
// Drop the database if exists.
await this.tenantDBManager.dropDatabaseIfExists(tenant);
}
/**
* 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): Promise<void> {
// Throw error if the tenant already initialized.
this.throwErrorIfTenantAlreadyInitialized(tenant);
// Migrate the database tenant.
await this.tenantDBManager.migrate(tenant);
// Mark the tenant as initialized.
await Tenant.markAsInitialized(tenant.id);
// Triggers `onTenantMigrated` event.
this.eventDispatcher.dispatch(events.tenantManager.tenantMigrated, {
tenantId: tenant.id,
});
}
/**
* Seeds the tenant database.
* @param {ITenant} tenant
* @return {Promise<void>}
*/
public async seedTenant(tenant: ITenant, tenancyContext): Promise<void> {
// Throw error if the tenant is not built yet.
this.throwErrorIfTenantNotBuilt(tenant);
// Throw error if the tenant is not seeded yet.
this.throwErrorIfTenantAlreadySeeded(tenant);
// Seeds the organization database data.
await new SeedMigration(tenancyContext.knex, tenancyContext).latest();
// Mark the tenant as seeded in specific date.
await Tenant.markAsSeeded(tenant.id);
// Triggers `onTenantSeeded` event.
this.eventDispatcher.dispatch(events.tenantManager.tenantSeeded, {
tenantId: tenant.id,
});
}
/**
* 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 {ITenant} 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();
}
}
/**
* Initialize seed migration contxt.
* @param {ITenant} tenant
* @returns
*/
public getSeedMigrationContext(tenant: ITenant) {
// Initialize the knex instance.
const knex = this.setupKnexInstance(tenant);
const i18nInstance = i18n();
i18nInstance.setLocale(tenant.metadata.language);
return {
knex,
i18n: i18nInstance,
tenant,
};
}
}