feat(nestjs): migrate to NestJS

This commit is contained in:
Ahmed Bouhuolia
2025-04-07 11:51:24 +02:00
parent f068218a16
commit 55fcc908ef
3779 changed files with 631 additions and 195332 deletions

View File

@@ -0,0 +1,43 @@
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 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'
};

View File

@@ -0,0 +1,91 @@
import { ApiTags, ApiOperation, ApiResponse, ApiBody } from '@nestjs/swagger';
import {
Controller,
Post,
Put,
Get,
Body,
Req,
Res,
Next,
} from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { BuildOrganizationService } from './commands/BuildOrganization.service';
import {
BuildOrganizationDto,
UpdateOrganizationDto,
} from './dtos/Organization.dto';
import { GetCurrentOrganizationService } from './queries/GetCurrentOrganization.service';
import { UpdateOrganizationService } from './commands/UpdateOrganization.service';
import { IgnoreTenantInitializedRoute } from '../Tenancy/EnsureTenantIsInitialized.guard';
import { IgnoreTenantSeededRoute } from '../Tenancy/EnsureTenantIsSeeded.guards';
@ApiTags('Organization')
@Controller('organization')
@IgnoreTenantInitializedRoute()
@IgnoreTenantSeededRoute()
export class OrganizationController {
constructor(
private readonly buildOrganizationService: BuildOrganizationService,
private readonly getCurrentOrgService: GetCurrentOrganizationService,
private readonly updateOrganizationService: UpdateOrganizationService,
) {}
@Post('build')
@ApiOperation({ summary: 'Build organization database' })
@ApiBody({ type: BuildOrganizationDto })
@ApiResponse({
status: 200,
description: 'The organization database has been initialized',
})
async build(
@Body() buildDTO: BuildOrganizationDto,
@Req() req: Request,
@Res() res: Response,
) {
const result = await this.buildOrganizationService.buildRunJob(buildDTO);
return res.status(200).send({
type: 'success',
code: 'ORGANIZATION.DATABASE.INITIALIZED',
message: 'The organization database has been initialized.',
data: result,
});
}
@Get('current')
@ApiOperation({ summary: 'Get current organization' })
@ApiResponse({
status: 200,
description: 'Returns the current organization',
})
async currentOrganization(
@Req() req: Request,
@Res() res: Response,
@Next() next: NextFunction,
) {
const organization =
await this.getCurrentOrgService.getCurrentOrganization();
return res.status(200).send({ organization });
}
@Put()
@ApiOperation({ summary: 'Update organization information' })
@ApiBody({ type: UpdateOrganizationDto })
@ApiResponse({
status: 200,
description: 'Organization information has been updated successfully',
})
async updateOrganization(
@Body() updateDTO: UpdateOrganizationDto,
@Res() res: Response,
) {
await this.updateOrganizationService.execute(updateDTO);
return res.status(200).send({
code: 200,
message: 'Organization information has been updated successfully.',
});
}
}

View File

@@ -0,0 +1,30 @@
import { Module } from '@nestjs/common';
import { GetCurrentOrganizationService } from './queries/GetCurrentOrganization.service';
import { BuildOrganizationService } from './commands/BuildOrganization.service';
import { UpdateOrganizationService } from './commands/UpdateOrganization.service';
import { OrganizationController } from './Organization.controller';
import { BullModule } from '@nestjs/bullmq';
import { OrganizationBuildQueue } from './Organization.types';
import { OrganizationBuildProcessor } from './processors/OrganizationBuild.processor';
import { CommandOrganizationValidators } from './commands/CommandOrganizationValidators.service';
import { TenancyContext } from '../Tenancy/TenancyContext.service';
import { TenantDBManagerModule } from '../TenantDBManager/TenantDBManager.module';
import { OrganizationBaseCurrencyLocking } from './Organization/OrganizationBaseCurrencyLocking.service';
@Module({
providers: [
TenancyContext,
GetCurrentOrganizationService,
BuildOrganizationService,
UpdateOrganizationService,
OrganizationBuildProcessor,
CommandOrganizationValidators,
OrganizationBaseCurrencyLocking,
],
imports: [
BullModule.registerQueue({ name: OrganizationBuildQueue }),
TenantDBManagerModule,
],
controllers: [OrganizationController],
})
export class OrganizationModule {}

View File

@@ -0,0 +1,60 @@
import { TenantJobPayload } from '@/interfaces/Tenant';
import { SystemUser } from '../System/models/SystemUser';
import { BuildOrganizationDto } from './dtos/Organization.dto';
export interface IOrganizationSetupDTO {
organizationName: string;
baseCurrency: string;
fiscalYear: string;
industry: string;
timeZone: string;
}
export interface IOrganizationBuildDTO {
name: string;
industry: string;
location: string;
baseCurrency: string;
timezone: string;
fiscalYear: string;
dateFormat?: string;
}
interface OrganizationAddressDTO {
address1: string;
address2: string;
postalCode: string;
city: string;
stateProvince: string;
phone: string;
}
export interface IOrganizationUpdateDTO {
name: string;
location?: string;
baseCurrency?: string;
timezone?: string;
fiscalYear?: string;
industry?: string;
taxNumber?: string;
primaryColor?: string;
logoKey?: string;
address?: OrganizationAddressDTO;
}
export interface IOrganizationBuildEventPayload {
tenantId: number;
buildDTO: IOrganizationBuildDTO;
systemUser: SystemUser;
}
export interface IOrganizationBuiltEventPayload {
tenantId: number;
}
export const OrganizationBuildQueue = 'OrganizationBuildQueue';
export const OrganizationBuildQueueJob = 'OrganizationBuildQueueJob';
export interface OrganizationBuildQueueJobPayload extends TenantJobPayload {
buildDto: BuildOrganizationDto;
}

View File

@@ -0,0 +1,17 @@
import { defaultTo } from 'lodash';
import { IOrganizationBuildDTO } from './Organization.types';
import { BuildOrganizationDto } from './dtos/Organization.dto';
/**
* Transformes build DTO object.
* @param {IOrganizationBuildDTO} buildDTO
* @returns {IOrganizationBuildDTO}
*/
export const transformBuildDto = (
buildDTO: BuildOrganizationDto,
): BuildOrganizationDto => {
return {
...buildDTO,
dateFormat: defaultTo(buildDTO.dateFormat, 'DD MMM yyyy'),
};
};

View File

@@ -0,0 +1,80 @@
// @ts-nocheck
import { Injectable } from '@nestjs/common';
import { isEmpty } from 'lodash';
interface MutateBaseCurrencyLockMeta {
modelName: string;
pluralName?: string;
}
@Injectable()
export class OrganizationBaseCurrencyLocking {
/**
* 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,102 @@
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()
private organizationService: OrganizationService;
@Inject()
private tenantsManager: TenantsManagerService;
@Inject('agenda')
private 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,40 @@
import { ServiceError } from '@/modules/Items/ServiceError';
import { ERRORS } from './constants';
import { TenantModel } from '@/modules/System/models/TenantModel';
/**
* Throw base currency mutate locked error.
*/
export function throwBaseCurrencyMutateLocked() {
throw new ServiceError(ERRORS.BASE_CURRENCY_MUTATE_LOCKED);
}
/**
* Throws error in case the given tenant is undefined.
* @param {TenantModel} tenant
*/
export function throwIfTenantNotExists(tenant: TenantModel) {
if (!tenant) {
throw new ServiceError(ERRORS.TENANT_NOT_FOUND);
}
}
/**
* Throws error in case the given tenant is already initialized.
* @param {TenantModel} tenant
*/
export function throwIfTenantInitizalized(tenant: TenantModel) {
if (tenant.builtAt) {
throw new ServiceError(ERRORS.TENANT_ALREADY_BUILT);
}
}
/**
* Throw error if the tenant is building.
* @param {TenantModel} tenant
*/
export function throwIfTenantIsBuilding(tenant: TenantModel) {
if (tenant.buildJobId) {
throw new ServiceError(ERRORS.TENANT_IS_BUILDING);
}
}

View File

@@ -0,0 +1,43 @@
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 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'
};

View File

@@ -0,0 +1,122 @@
import { Queue } from 'bullmq';
import { InjectQueue } from '@nestjs/bullmq';
import {
IOrganizationBuildEventPayload,
IOrganizationBuiltEventPayload,
OrganizationBuildQueue,
OrganizationBuildQueueJob,
OrganizationBuildQueueJobPayload,
} from '../Organization.types';
import { Injectable } from '@nestjs/common';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import {
throwIfTenantInitizalized,
throwIfTenantIsBuilding,
} from '../Organization/_utils';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { TenantsManagerService } from '@/modules/TenantDBManager/TenantsManager';
import { events } from '@/common/events/events';
import { transformBuildDto } from '../Organization.utils';
import { BuildOrganizationDto } from '../dtos/Organization.dto';
import { TenantRepository } from '@/modules/System/repositories/Tenant.repository';
@Injectable()
export class BuildOrganizationService {
constructor(
private readonly eventPublisher: EventEmitter2,
private readonly tenantsManager: TenantsManagerService,
private readonly tenancyContext: TenancyContext,
private readonly tenantRepository: TenantRepository,
@InjectQueue(OrganizationBuildQueue)
private readonly organizationBuildQueue: Queue,
) {}
/**
* Builds the database schema and seed data of the given organization id.
* @param {string} organizationId
* @return {Promise<void>}
*/
public async build(buildDTO: BuildOrganizationDto): Promise<void> {
const tenant = await this.tenancyContext.getTenant();
const systemUser = await this.tenancyContext.getSystemUser();
// Throw error if the tenant is already initialized.
throwIfTenantInitizalized(tenant);
await this.tenantsManager.dropDatabaseIfExists();
await this.tenantsManager.createDatabase();
await this.tenantsManager.migrateTenant();
await this.tenantsManager.seedTenant()
// Throws `onOrganizationBuild` event.
await this.eventPublisher.emitAsync(events.organization.build, {
tenantId: tenant.id,
buildDTO,
systemUser,
} as IOrganizationBuildEventPayload);
// Marks the tenant as completed builing.
await this.tenantRepository.markAsBuilt().findById(tenant.id);
await this.tenantRepository.markAsBuildCompleted().findById(tenant.id);
// Flags the tenant database batch.
await this.tenantRepository.flagTenantDBBatch().findById(tenant.id);
// Triggers the organization built event.
await this.eventPublisher.emitAsync(events.organization.built, {
tenantId: tenant.id,
} as IOrganizationBuiltEventPayload);
}
/**
* Execute the tenant database build process.
* @param {BuildOrganizationDto} buildDTO - Organization build dto.
* @returns {Promise<{ nextRunAt: Date; jobId: string }>} - Returns the next run date and job id.
*/
async buildRunJob(
buildDTO: BuildOrganizationDto,
): Promise<{ nextRunAt: Date; jobId: string }> {
const tenant = await this.tenancyContext.getTenant();
const systemUser = await this.tenancyContext.getSystemUser();
// Throw error if the tenant is already initialized.
throwIfTenantInitizalized(tenant);
// Throw error if tenant is currently building.
throwIfTenantIsBuilding(tenant);
// Transforms build DTO object.
const transformedBuildDTO = transformBuildDto(buildDTO);
// Saves the tenant metadata.
await this.tenantRepository.saveMetadata(tenant.id, transformedBuildDTO);
// Run the organization database build job.
const jobMeta = await this.organizationBuildQueue.add(
OrganizationBuildQueueJob,
{
organizationId: tenant.organizationId,
userId: systemUser.id,
buildDto: transformedBuildDTO,
} as OrganizationBuildQueueJobPayload,
);
// Marks the tenant as currently building.
await this.tenantRepository.markAsBuilding(jobMeta.id).findById(tenant.id);
return {
nextRunAt: jobMeta.data.nextRunAt,
jobId: jobMeta.data.id,
};
}
/**
* Unlocks tenant build run job.
*/
public async revertBuildRunJob() {
const tenant = await this.tenancyContext.getTenant();
await this.tenantRepository.markAsBuildCompleted().findById(tenant.id);
}
}

View File

@@ -0,0 +1,35 @@
import { ServiceError } from '@/modules/Items/ServiceError';
import { OrganizationBaseCurrencyLocking } from '../Organization/OrganizationBaseCurrencyLocking.service';
import { TenantModel } from '@/modules/System/models/TenantModel';
import { Injectable } from '@nestjs/common';
import { ERRORS } from '../Organization.constants';
@Injectable()
export class CommandOrganizationValidators {
constructor(
private readonly baseCurrencyMutateLocking: OrganizationBaseCurrencyLocking,
) {}
/**
* Validate mutate base currency ability.
* @param {Tenant} tenant -
* @param {string} newBaseCurrency -
* @param {string} oldBaseCurrency -
*/
async validateMutateBaseCurrency(
tenant: TenantModel,
newBaseCurrency: string,
oldBaseCurrency: string,
) {
if (tenant.isReady && newBaseCurrency !== oldBaseCurrency) {
const isLocked =
await this.baseCurrencyMutateLocking.isBaseCurrencyMutateLocked(
tenant.id,
);
if (isLocked) {
throw new ServiceError(ERRORS.BASE_CURRENCY_MUTATE_LOCKED);
}
}
}
}

View File

@@ -0,0 +1,50 @@
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { UpdateOrganizationDto } from '../dtos/Organization.dto';
import { throwIfTenantNotExists } from '../Organization/_utils';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
import { CommandOrganizationValidators } from './CommandOrganizationValidators.service';
import { TenantRepository } from '@/modules/System/repositories/Tenant.repository';
import { Injectable } from '@nestjs/common';
@Injectable()
export class UpdateOrganizationService {
constructor(
private readonly tenancyContext: TenancyContext,
private readonly eventEmitter: EventEmitter2,
private readonly commandOrganizationValidators: CommandOrganizationValidators,
private readonly tenantRepository: TenantRepository,
) {}
/**
* Updates organization information.
* @param {UpdateOrganizationDto} organizationDTO
*/
public async execute(organizationDTO: UpdateOrganizationDto): Promise<void> {
const tenant = await this.tenancyContext.getTenant(true);
// Throw error if the tenant not exists.
throwIfTenantNotExists(tenant);
// Validate organization transactions before mutate base currency.
if (organizationDTO.baseCurrency) {
await this.commandOrganizationValidators.validateMutateBaseCurrency(
tenant,
organizationDTO.baseCurrency,
tenant.metadata?.baseCurrency,
);
}
await this.tenantRepository.saveMetadata(tenant.id, organizationDTO);
if (organizationDTO.baseCurrency !== tenant.metadata?.baseCurrency) {
// Triggers `onOrganizationBaseCurrencyUpdated` event.
await this.eventEmitter.emitAsync(
events.organization.baseCurrencyUpdated,
{
organizationDTO,
},
);
}
}
}

View File

@@ -0,0 +1,183 @@
import * as momentTz from 'moment-timezone';
import {
IsHexColor,
IsIn,
IsISO31661Alpha2,
IsISO4217CurrencyCode,
IsOptional,
IsString,
} from 'class-validator';
import { MONTHS } from '../Organization/constants';
import { ACCEPTED_LOCALES, DATE_FORMATS } from '../Organization.constants';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class BuildOrganizationDto {
@IsString()
@ApiProperty({
description: 'Organization name',
example: 'Acme Inc.',
})
name: string;
@IsOptional()
@IsString()
@ApiPropertyOptional({
description: 'Industry of the organization',
example: 'Technology',
})
industry?: string;
@IsISO31661Alpha2()
@ApiProperty({
description: 'Country location in ISO 3166-1 alpha-2 format',
example: 'US',
})
location: string;
@IsISO4217CurrencyCode()
@ApiProperty({
description: 'Base currency in ISO 4217 format',
example: 'USD',
})
baseCurrency: string;
@IsIn(momentTz.tz.names())
@ApiProperty({
description: 'Timezone of the organization',
example: 'America/New_York',
})
timezone: string;
@IsIn(MONTHS)
@ApiProperty({
description: 'Starting month of fiscal year',
example: 'January',
})
fiscalYear: string;
@IsIn(ACCEPTED_LOCALES)
@ApiProperty({
description: 'Language/locale of the organization',
example: 'en-US',
})
language: string;
@IsOptional()
@IsIn(DATE_FORMATS)
@ApiPropertyOptional({
description: 'Date format used by the organization',
example: 'MM/DD/YYYY',
})
dateFormat?: string;
}
export class UpdateOrganizationDto {
@IsOptional()
@IsString()
@ApiPropertyOptional({
description: 'Organization name',
example: 'Acme Inc.',
})
name?: string;
@IsOptional()
@IsString()
@ApiPropertyOptional({
description: 'Industry of the organization',
example: 'Technology',
})
industry?: string;
@IsOptional()
@IsISO31661Alpha2()
@ApiPropertyOptional({
description: 'Country location in ISO 3166-1 alpha-2 format',
example: 'US',
})
location?: string;
@IsOptional()
@IsISO4217CurrencyCode()
@ApiPropertyOptional({
description: 'Base currency in ISO 4217 format',
example: 'USD',
})
baseCurrency?: string;
@IsOptional()
@IsIn(momentTz.tz.names())
@ApiPropertyOptional({
description: 'Timezone of the organization',
example: 'America/New_York',
})
timezone?: string;
@IsOptional()
@IsIn(MONTHS)
@ApiPropertyOptional({
description: 'Starting month of fiscal year',
example: 'January',
})
fiscalYear?: string;
@IsOptional()
@IsIn(ACCEPTED_LOCALES)
@ApiPropertyOptional({
description: 'Language/locale of the organization',
example: 'en-US',
})
language?: string;
@IsOptional()
@IsIn(DATE_FORMATS)
@ApiPropertyOptional({
description: 'Date format used by the organization',
example: 'MM/DD/YYYY',
})
dateFormat?: string;
@IsOptional()
@ApiPropertyOptional({
description: 'Organization address details',
example: {
address_1: '123 Main St',
address_2: 'Suite 100',
postal_code: '10001',
city: 'New York',
stateProvince: 'NY',
phone: '+1-555-123-4567',
},
})
address?: {
address_1?: string;
address_2?: string;
postal_code?: string;
city?: string;
stateProvince?: string;
phone?: string;
};
@IsOptional()
@IsHexColor()
@ApiPropertyOptional({
description: 'Primary brand color in hex format',
example: '#4285F4',
})
primaryColor?: string;
@IsOptional()
@IsString()
@ApiPropertyOptional({
description: 'Logo file key reference',
example: 'organizations/acme-logo-123456.png',
})
logoKey?: string;
@IsOptional()
@IsString()
@ApiPropertyOptional({
description: 'Organization tax identification number',
example: '12-3456789',
})
taxNumber?: string;
}

View File

@@ -0,0 +1,42 @@
import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Scope } from '@nestjs/common';
import { Job } from 'bullmq';
import { ClsService, UseCls } from 'nestjs-cls';
import { Process } from '@nestjs/bull';
import {
OrganizationBuildQueue,
OrganizationBuildQueueJob,
OrganizationBuildQueueJobPayload,
} from '../Organization.types';
import { BuildOrganizationService } from '../commands/BuildOrganization.service';
@Processor({
name: OrganizationBuildQueue,
scope: Scope.REQUEST,
})
export class OrganizationBuildProcessor extends WorkerHost {
constructor(
private readonly organizationBuildService: BuildOrganizationService,
private readonly clsService: ClsService,
) {
super();
}
@Process(OrganizationBuildQueueJob)
@UseCls()
async process(job: Job<OrganizationBuildQueueJobPayload>) {
console.log('Processing organization build job:', job.id);
this.clsService.set('organizationId', job.data.organizationId);
this.clsService.set('userId', job.data.userId);
try {
await this.organizationBuildService.build(job.data.buildDto);
} catch (e) {
// Unlock build status of the tenant.
await this.organizationBuildService.revertBuildRunJob();
console.error('Error processing organization build job:', e);
throw e; // Re-throw to mark job as failed
}
}
}

View File

@@ -0,0 +1,25 @@
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { throwIfTenantNotExists } from '../Organization/_utils';
import { TenantModel } from '@/modules/System/models/TenantModel';
import { Injectable } from '@nestjs/common';
@Injectable()
export class GetCurrentOrganizationService {
constructor(private readonly tenancyContext: TenancyContext) {}
/**
* Retrieve the current organization metadata.
* @param {number} tenantId
* @returns {Promise<ITenant[]>}
*/
async getCurrentOrganization(): Promise<TenantModel> {
const tenant = await this.tenancyContext
.getTenant()
.withGraphFetched('subscriptions')
.withGraphFetched('metadata');
throwIfTenantNotExists(tenant);
return tenant;
}
}