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,84 @@
import { isEmpty } from 'lodash';
import { Inject, Service } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { TimeoutSettings } from 'puppeteer';
interface MutateBaseCurrencyLockMeta {
modelName: string;
pluralName?: string;
}
@Service()
export default class OrganizationBaseCurrencyLocking {
@Inject()
tenancy: HasTenancyService;
/**
* Retrieves the tenant models that have prevented mutation base currency.
*/
private getModelsPreventsMutate = (tenantId: number) => {
const Models = this.tenancy.models(tenantId);
const filteredEntries = Object.entries(Models).filter(
([key, Model]) => !!Model.preventMutateBaseCurrency
);
return Object.fromEntries(filteredEntries);
};
/**
* Detarmines the mutation base currency model is locked.
* @param {Model} Model
* @returns {Promise<MutateBaseCurrencyLockMeta | false>}
*/
private isModelMutateLocked = async (
Model
): Promise<MutateBaseCurrencyLockMeta | false> => {
const validateQuery = Model.query();
if (typeof Model?.modifiers?.preventMutateBaseCurrency !== 'undefined') {
validateQuery.modify('preventMutateBaseCurrency');
} else {
validateQuery.select(['id']).first();
}
const validateResult = await validateQuery;
const isValid = !isEmpty(validateResult);
return isValid
? {
modelName: Model.name,
pluralName: Model.pluralName,
}
: false;
};
/**
* Retrieves the base currency mutation locks of the tenant models.
* @param {number} tenantId
* @returns {Promise<MutateBaseCurrencyLockMeta[]>}
*/
public async baseCurrencyMutateLocks(
tenantId: number
): Promise<MutateBaseCurrencyLockMeta[]> {
const PreventedModels = this.getModelsPreventsMutate(tenantId);
const opers = Object.entries(PreventedModels).map(([ModelName, Model]) =>
this.isModelMutateLocked(Model)
);
const results = await Promise.all(opers);
return results.filter(
(result) => result !== false
) as MutateBaseCurrencyLockMeta[];
}
/**
* Detarmines the base currency mutation locked.
* @param {number} tenantId
* @returns {Promise<boolean>}
*/
public isBaseCurrencyMutateLocked = async (tenantId: number) => {
const locks = await this.baseCurrencyMutateLocks(tenantId);
return !isEmpty(locks);
};
}

View File

@@ -0,0 +1,330 @@
import { Service, Inject } from 'typedi';
import { ObjectId } from 'mongodb';
import { defaultTo, pick } from 'lodash';
import { ServiceError } from '@/exceptions';
import {
IOrganizationBuildDTO,
IOrganizationBuildEventPayload,
IOrganizationUpdateDTO,
ISystemUser,
ITenant,
} from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import config from '../../config';
import TenantsManager from '@/services/Tenancy/TenantsManager';
import { Tenant } from '@/system/models';
import OrganizationBaseCurrencyLocking from './OrganizationBaseCurrencyLocking';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ERRORS } from './constants';
@Service()
export default class OrganizationService {
@Inject()
eventPublisher: EventPublisher;
@Inject('logger')
logger: any;
@Inject('repositories')
sysRepositories: any;
@Inject()
tenantsManager: TenantsManager;
@Inject('agenda')
agenda: any;
@Inject()
baseCurrencyMutateLocking: OrganizationBaseCurrencyLocking;
@Inject()
tenancy: HasTenancyService;
/**
* Builds the database schema and seed data of the given organization id.
* @param {srting} organizationId
* @return {Promise<void>}
*/
public async build(
tenantId: number,
buildDTO: IOrganizationBuildDTO,
systemUser: ISystemUser
): Promise<void> {
const tenant = await this.getTenantOrThrowError(tenantId);
// Throw error if the tenant is already initialized.
this.throwIfTenantInitizalized(tenant);
// Drop the database if is already exists.
await this.tenantsManager.dropDatabaseIfExists(tenant);
// Creates a new database.
await this.tenantsManager.createDatabase(tenant);
// Migrate the tenant.
await this.tenantsManager.migrateTenant(tenant);
// Migrated tenant.
const migratedTenant = await tenant.$query().withGraphFetched('metadata');
// Creates a tenancy object from given tenant model.
const tenancyContext =
this.tenantsManager.getSeedMigrationContext(migratedTenant);
// Seed tenant.
await this.tenantsManager.seedTenant(migratedTenant, tenancyContext);
// Throws `onOrganizationBuild` event.
await this.eventPublisher.emitAsync(events.organization.build, {
tenantId: tenant.id,
buildDTO,
systemUser,
} as IOrganizationBuildEventPayload);
// Markes the tenant as completed builing.
await Tenant.markAsBuilt(tenantId);
await Tenant.markAsBuildCompleted(tenantId);
//
await this.flagTenantDBBatch(tenantId);
}
/**
*
* @param {number} tenantId
* @param {IOrganizationBuildDTO} buildDTO
* @returns
*/
async buildRunJob(
tenantId: number,
buildDTO: IOrganizationBuildDTO,
authorizedUser: ISystemUser
) {
const tenant = await this.getTenantOrThrowError(tenantId);
// Throw error if the tenant is already initialized.
this.throwIfTenantInitizalized(tenant);
// Throw error if tenant is currently building.
this.throwIfTenantIsBuilding(tenant);
// Transformes build DTO object.
const transformedBuildDTO = this.transformBuildDTO(buildDTO);
// Saves the tenant metadata.
await tenant.saveMetadata(transformedBuildDTO);
// Send welcome mail to the user.
const jobMeta = await this.agenda.now('organization-setup', {
tenantId,
buildDTO,
authorizedUser,
});
// Transformes the mangodb id to string.
const jobId = new ObjectId(jobMeta.attrs._id).toString();
// Markes the tenant as currently building.
await Tenant.markAsBuilding(tenantId, jobId);
return {
nextRunAt: jobMeta.attrs.nextRunAt,
jobId: jobMeta.attrs._id,
};
}
/**
* Unlocks tenant build run job.
* @param {number} tenantId
* @param {number} jobId
*/
public async revertBuildRunJob(tenantId: number, jobId: string) {
await Tenant.markAsBuildCompleted(tenantId, jobId);
}
/**
* Retrieve the current organization metadata.
* @param {number} tenantId
* @returns {Promise<ITenant[]>}
*/
public async currentOrganization(tenantId: number): Promise<ITenant> {
const tenant = await Tenant.query()
.findById(tenantId)
.withGraphFetched('subscriptions')
.withGraphFetched('metadata');
this.throwIfTenantNotExists(tenant);
return tenant;
}
/**
* Retrieve organization ability of mutate base currency
* @param {number} tenantId
* @returns
*/
public mutateBaseCurrencyAbility(tenantId: number) {
return this.baseCurrencyMutateLocking.baseCurrencyMutateLocks(tenantId);
}
/**
* Updates organization information.
* @param {ITenant} tenantId
* @param {IOrganizationUpdateDTO} organizationDTO
*/
public async updateOrganization(
tenantId: number,
organizationDTO: IOrganizationUpdateDTO
): Promise<void> {
const tenant = await Tenant.query()
.findById(tenantId)
.withGraphFetched('metadata');
// Throw error if the tenant not exists.
this.throwIfTenantNotExists(tenant);
// Validate organization transactions before mutate base currency.
await this.validateMutateBaseCurrency(
tenant,
organizationDTO.baseCurrency,
tenant.metadata?.baseCurrency
);
await tenant.saveMetadata(organizationDTO);
if (organizationDTO.baseCurrency !== tenant.metadata?.baseCurrency) {
// Triggers `onOrganizationBaseCurrencyUpdated` event.
await this.eventPublisher.emitAsync(
events.organization.baseCurrencyUpdated,
{
tenantId,
organizationDTO,
}
);
}
}
/**
* Transformes build DTO object.
* @param {IOrganizationBuildDTO} buildDTO
* @returns {IOrganizationBuildDTO}
*/
private transformBuildDTO(
buildDTO: IOrganizationBuildDTO
): IOrganizationBuildDTO {
return {
...buildDTO,
dateFormat: defaultTo(buildDTO.dateFormat, 'DD/MM/yyyy'),
};
}
/**
* Throw base currency mutate locked error.
*/
private throwBaseCurrencyMutateLocked() {
throw new ServiceError(ERRORS.BASE_CURRENCY_MUTATE_LOCKED);
}
/**
* Validate mutate base currency ability.
* @param {Tenant} tenant -
* @param {string} newBaseCurrency -
* @param {string} oldBaseCurrency -
*/
private async validateMutateBaseCurrency(
tenant: Tenant,
newBaseCurrency: string,
oldBaseCurrency: string
) {
if (tenant.isReady && newBaseCurrency !== oldBaseCurrency) {
const isLocked =
await this.baseCurrencyMutateLocking.isBaseCurrencyMutateLocked(
tenant.id
);
if (isLocked) {
this.throwBaseCurrencyMutateLocked();
}
}
}
/**
* Throws error in case the given tenant is undefined.
* @param {ITenant} tenant
*/
private throwIfTenantNotExists(tenant: ITenant) {
if (!tenant) {
throw new ServiceError(ERRORS.TENANT_NOT_FOUND);
}
}
/**
* Throws error in case the given tenant is already initialized.
* @param {ITenant} tenant
*/
private throwIfTenantInitizalized(tenant: ITenant) {
if (tenant.builtAt) {
throw new ServiceError(ERRORS.TENANT_ALREADY_BUILT);
}
}
/**
* Throw error if the tenant is building.
* @param {ITenant} tenant
*/
private throwIfTenantIsBuilding(tenant) {
if (tenant.buildJobId) {
throw new ServiceError(ERRORS.TENANT_IS_BUILDING);
}
}
/**
* Retrieve tenant of throw not found error.
* @param {number} tenantId -
*/
async getTenantOrThrowError(tenantId: number): Promise<ITenant> {
const tenant = await Tenant.query().findById(tenantId);
if (!tenant) {
throw new ServiceError(ERRORS.TENANT_NOT_FOUND);
}
return tenant;
}
/**
* Adds organization database latest batch number.
* @param {number} tenantId
* @param {number} version
*/
public async flagTenantDBBatch(tenantId: number) {
await Tenant.query()
.update({
databaseBatch: config.databaseBatch,
})
.where({ id: tenantId });
}
/**
* Syncs system user to tenant user.
*/
public async syncSystemUserToTenant(
tenantId: number,
systemUser: ISystemUser
) {
const { User, Role } = this.tenancy.models(tenantId);
const adminRole = await Role.query().findOne('slug', 'admin');
await User.query().insert({
...pick(systemUser, [
'firstName',
'lastName',
'phoneNumber',
'email',
'active',
'inviteAcceptedAt',
]),
systemUserId: systemUser.id,
roleId: adminRole.id,
});
}
}

View File

@@ -0,0 +1,105 @@
import { Inject, Service } from 'typedi';
import { ObjectId } from 'mongodb';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { SeedMigration } from '@/lib/Seeder/SeedMigration';
import { Tenant } from '@/system/models';
import { ServiceError } from '@/exceptions';
import TenantDBManager from '@/services/Tenancy/TenantDBManager';
import config from '../../config';
import { ERRORS } from './constants';
import OrganizationService from './OrganizationService';
import TenantsManagerService from '@/services/Tenancy/TenantsManager';
@Service()
export default class OrganizationUpgrade {
@Inject()
tenancy: HasTenancyService;
@Inject()
organizationService: OrganizationService;
@Inject()
tenantsManager: TenantsManagerService;
@Inject('agenda')
agenda: any;
/**
* Upgrades the given organization database.
* @param {number} tenantId - Tenant id.
* @returns {Promise<void>}
*/
public upgradeJob = async (tenantId: number): Promise<void> => {
const tenant = await Tenant.query()
.findById(tenantId)
.withGraphFetched('metadata');
// Validate tenant version.
this.validateTenantVersion(tenant);
// Initialize the tenant.
const seedContext = this.tenantsManager.getSeedMigrationContext(tenant);
// Database manager.
const dbManager = new TenantDBManager();
// Migrate the organization database schema.
await dbManager.migrate(tenant);
// Seeds the organization database data.
await new SeedMigration(seedContext.knex, seedContext).latest();
// Update the organization database version.
await this.organizationService.flagTenantDBBatch(tenantId);
// Remove the tenant job id.
await Tenant.markAsUpgraded(tenantId);
};
/**
* Running organization upgrade job.
* @param {number} tenantId - Tenant id.
* @return {Promise<void>}
*/
public upgrade = async (tenantId: number): Promise<{ jobId: string }> => {
const tenant = await Tenant.query().findById(tenantId);
// Validate tenant version.
this.validateTenantVersion(tenant);
// Validate tenant upgrade is not running.
this.validateTenantUpgradeNotRunning(tenant);
// Send welcome mail to the user.
const jobMeta = await this.agenda.now('organization-upgrade', {
tenantId,
});
// Transformes the mangodb id to string.
const jobId = new ObjectId(jobMeta.attrs._id).toString();
// Markes the tenant as currently building.
await Tenant.markAsUpgrading(tenantId, jobId);
return { jobId };
};
/**
* Validates the given tenant version.
* @param {ITenant} tenant
*/
private validateTenantVersion(tenant) {
if (tenant.databaseBatch >= config.databaseBatch) {
throw new ServiceError(ERRORS.TENANT_DATABASE_UPGRADED);
}
}
/**
* Validates the given tenant upgrade is not running.
* @param tenant
*/
private validateTenantUpgradeNotRunning(tenant) {
if (tenant.isUpgradeRunning) {
throw new ServiceError(ERRORS.TENANT_UPGRADE_IS_RUNNING);
}
}
}

View File

@@ -0,0 +1,45 @@
import currencies from 'js-money/lib/currency';
export const DATE_FORMATS = [
'MM.dd.yy',
'dd.MM.yy',
'yy.MM.dd',
'MM.dd.yyyy',
'dd.MM.yyyy',
'yyyy.MM.dd',
'MM/DD/YYYY',
'M/D/YYYY',
'dd MMM YYYY',
'dd MMMM YYYY',
'MMMM dd, YYYY',
'EEE, MMMM dd, YYYY',
];
export const ACCEPTED_CURRENCIES = Object.keys(currencies);
export const MONTHS = [
'january',
'february',
'march',
'april',
'may',
'june',
'july',
'august',
'september',
'october',
'november',
'december',
];
export const ACCEPTED_LOCALES = ['en', 'ar'];
export const ERRORS = {
TENANT_DATABASE_UPGRADED: 'TENANT_DATABASE_UPGRADED',
TENANT_NOT_FOUND: 'tenant_not_found',
TENANT_ALREADY_BUILT: 'TENANT_ALREADY_BUILT',
TENANT_ALREADY_SEEDED: 'tenant_already_seeded',
TENANT_DB_NOT_BUILT: 'tenant_db_not_built',
TENANT_IS_BUILDING: 'TENANT_IS_BUILDING',
BASE_CURRENCY_MUTATE_LOCKED: 'BASE_CURRENCY_MUTATE_LOCKED',
TENANT_UPGRADE_IS_RUNNING: 'TENANT_UPGRADE_IS_RUNNING'
};