mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 12:50:38 +00:00
add server to monorepo.
This commit is contained in:
@@ -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);
|
||||
};
|
||||
}
|
||||
330
packages/server/src/services/Organization/OrganizationService.ts
Normal file
330
packages/server/src/services/Organization/OrganizationService.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
105
packages/server/src/services/Organization/OrganizationUpgrade.ts
Normal file
105
packages/server/src/services/Organization/OrganizationUpgrade.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
45
packages/server/src/services/Organization/constants.ts
Normal file
45
packages/server/src/services/Organization/constants.ts
Normal 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'
|
||||
};
|
||||
Reference in New Issue
Block a user