From c53bf60406a2a68018ebbd122bd3c2bedeb5f62e Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Tue, 12 May 2026 18:25:52 +0200 Subject: [PATCH] wip --- .../ee/Workspaces/Workspaces.constants.ts | 6 ++ .../ee/Workspaces/Workspaces.controller.ts | 3 +- .../commands/DeleteWorkspace.service.ts | 10 +- .../commands/DeleteWorkspaceJob.service.ts | 13 +-- .../commands/InactivateWorkspace.service.ts | 14 +-- .../commands/SetDefaultWorkspace.service.ts | 2 + .../queries/GetWorkspaces.service.ts | 35 ++---- .../transformers/WorkspaceTransformer.ts | 102 ++++++++++++------ 8 files changed, 106 insertions(+), 79 deletions(-) create mode 100644 packages/server/src/modules/ee/Workspaces/Workspaces.constants.ts diff --git a/packages/server/src/modules/ee/Workspaces/Workspaces.constants.ts b/packages/server/src/modules/ee/Workspaces/Workspaces.constants.ts new file mode 100644 index 000000000..a46bc82a7 --- /dev/null +++ b/packages/server/src/modules/ee/Workspaces/Workspaces.constants.ts @@ -0,0 +1,6 @@ +export enum WorkspacesError { + WORKSPACE_NOT_FOUND = 'WORKSPACE_NOT_FOUND', + NOT_WORKSPACE_OWNER = 'NOT_WORKSPACE_OWNER', + WORKSPACE_DELETING = 'WORKSPACE_DELETING', + CANNOT_DELETE_CURRENT_ORGANIZATION = 'CANNOT_DELETE_CURRENT_ORGANIZATION', +} diff --git a/packages/server/src/modules/ee/Workspaces/Workspaces.controller.ts b/packages/server/src/modules/ee/Workspaces/Workspaces.controller.ts index 1d56d6141..88787354d 100644 --- a/packages/server/src/modules/ee/Workspaces/Workspaces.controller.ts +++ b/packages/server/src/modules/ee/Workspaces/Workspaces.controller.ts @@ -31,6 +31,7 @@ import { } from './dtos/WorkspaceResponse.dto'; import { WorkspaceBuildJobResponseDto } from './dtos/WorkspaceBuildJobResponse.dto'; import { ServiceError } from '@/modules/Items/ServiceError'; +import { WorkspacesError } from './Workspaces.constants'; @ApiTags('Workspaces') @Controller('workspaces') @@ -128,7 +129,7 @@ export class WorkspacesController { if (organizationId === currentOrganizationId) { throw new ServiceError( - 'CANNOT_DELETE_CURRENT_ORGANIZATION', + WorkspacesError.CANNOT_DELETE_CURRENT_ORGANIZATION, 'Cannot delete the current organization', ); } diff --git a/packages/server/src/modules/ee/Workspaces/commands/DeleteWorkspace.service.ts b/packages/server/src/modules/ee/Workspaces/commands/DeleteWorkspace.service.ts index cb4427611..d4fa6d5ac 100644 --- a/packages/server/src/modules/ee/Workspaces/commands/DeleteWorkspace.service.ts +++ b/packages/server/src/modules/ee/Workspaces/commands/DeleteWorkspace.service.ts @@ -5,11 +5,7 @@ import { UserTenant } from '@/modules/System/models/UserTenant.model'; import { TenantModel } from '@/modules/System/models/TenantModel'; import { TenantDBManager } from '@/modules/TenantDBManager/TenantDBManager'; import { events } from '@/common/events/events'; - -const ERRORS = { - WORKSPACE_NOT_FOUND: 'WORKSPACE.NOT_FOUND', - NOT_WORKSPACE_OWNER: 'NOT.WORKSPACE.OWNER', -}; +import { WorkspacesError } from '../Workspaces.constants'; @Injectable() export class DeleteWorkspaceService { @@ -33,14 +29,14 @@ export class DeleteWorkspaceService { const tenant = await this.tenantModel.query().findOne({ organizationId }); if (!tenant) { - throw new ServiceError(ERRORS.WORKSPACE_NOT_FOUND); + throw new ServiceError(WorkspacesError.WORKSPACE_NOT_FOUND); } const membership = await this.userTenantModel .query() .findOne({ userId, tenantId: tenant.id }); if (!membership || membership.role !== 'owner') { - throw new ServiceError(ERRORS.NOT_WORKSPACE_OWNER); + throw new ServiceError(WorkspacesError.NOT_WORKSPACE_OWNER); } // Drop the physical tenant database if it exists. await this.tenantDBManager.dropDatabaseIfExists(); diff --git a/packages/server/src/modules/ee/Workspaces/commands/DeleteWorkspaceJob.service.ts b/packages/server/src/modules/ee/Workspaces/commands/DeleteWorkspaceJob.service.ts index 31f0def01..bf36d9908 100644 --- a/packages/server/src/modules/ee/Workspaces/commands/DeleteWorkspaceJob.service.ts +++ b/packages/server/src/modules/ee/Workspaces/commands/DeleteWorkspaceJob.service.ts @@ -10,12 +10,7 @@ import { DeleteWorkspaceQueueJobPayload, } from '../Workspaces.types'; import { events } from '@/common/events/events'; - -const ERRORS = { - WORKSPACE_NOT_FOUND: 'WORKSPACE.NOT_FOUND', - NOT_WORKSPACE_OWNER: 'NOT.WORKSPACE.OWNER', - WORKSPACE_DELETING: 'WORKSPACE.DELETING', -}; +import { WorkspacesError } from '../Workspaces.constants'; interface DeleteWorkspaceResult { jobId: string | number; @@ -51,7 +46,7 @@ export class DeleteWorkspaceJobService { const tenant = await this.tenantModel.query().findOne({ organizationId }); if (!tenant) { - throw new ServiceError(ERRORS.WORKSPACE_NOT_FOUND); + throw new ServiceError(WorkspacesError.WORKSPACE_NOT_FOUND); } const membership = await this.userTenantModel @@ -59,12 +54,12 @@ export class DeleteWorkspaceJobService { .findOne({ userId, tenantId: tenant.id }); if (!membership || membership.role !== 'owner') { - throw new ServiceError(ERRORS.NOT_WORKSPACE_OWNER); + throw new ServiceError(WorkspacesError.NOT_WORKSPACE_OWNER); } // Check if workspace is already being deleted. if (tenant.isDeleting) { - throw new ServiceError(ERRORS.WORKSPACE_DELETING); + throw new ServiceError(WorkspacesError.WORKSPACE_DELETING); } // Emit workspace deleting event. diff --git a/packages/server/src/modules/ee/Workspaces/commands/InactivateWorkspace.service.ts b/packages/server/src/modules/ee/Workspaces/commands/InactivateWorkspace.service.ts index 72178a441..4ab5be949 100644 --- a/packages/server/src/modules/ee/Workspaces/commands/InactivateWorkspace.service.ts +++ b/packages/server/src/modules/ee/Workspaces/commands/InactivateWorkspace.service.ts @@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { ServiceError } from '@/modules/Items/ServiceError'; import { UserTenant } from '@/modules/System/models/UserTenant.model'; import { TenantModel } from '@/modules/System/models/TenantModel'; +import { WorkspacesError } from '../Workspaces.constants'; @Injectable() export class InactivateWorkspaceService { @@ -23,20 +24,19 @@ export class InactivateWorkspaceService { const tenant = await this.tenantModel.query().findOne({ organizationId }); if (!tenant) { - throw new ServiceError('WORKSPACE_NOT_FOUND', 'Workspace not found'); + throw new ServiceError(WorkspacesError.WORKSPACE_NOT_FOUND, 'Workspace not found'); } - const membership = await this.userTenantModel .query() .findOne({ userId, tenantId: tenant.id }) .withGraphFetched('tenant'); if (!membership) { - throw new ServiceError('WORKSPACE_NOT_FOUND', 'Workspace not found'); + throw new ServiceError(WorkspacesError.WORKSPACE_NOT_FOUND, 'Workspace not found'); } if (membership.role !== 'owner') { throw new ServiceError( - 'NOT_OWNER', + WorkspacesError.NOT_WORKSPACE_OWNER, 'Only the workspace owner can inactivate the workspace', ); } @@ -58,7 +58,7 @@ export class InactivateWorkspaceService { const tenant = await this.tenantModel.query().findOne({ organizationId }); if (!tenant) { - throw new ServiceError('WORKSPACE_NOT_FOUND', 'Workspace not found'); + throw new ServiceError(WorkspacesError.WORKSPACE_NOT_FOUND, 'Workspace not found'); } const membership = await this.userTenantModel @@ -67,12 +67,12 @@ export class InactivateWorkspaceService { .withGraphFetched('tenant'); if (!membership) { - throw new ServiceError('WORKSPACE_NOT_FOUND', 'Workspace not found'); + throw new ServiceError(WorkspacesError.WORKSPACE_NOT_FOUND, 'Workspace not found'); } if (membership.role !== 'owner') { throw new ServiceError( - 'NOT_OWNER', + WorkspacesError.NOT_WORKSPACE_OWNER, 'Only the workspace owner can reactivate the workspace', ); } diff --git a/packages/server/src/modules/ee/Workspaces/commands/SetDefaultWorkspace.service.ts b/packages/server/src/modules/ee/Workspaces/commands/SetDefaultWorkspace.service.ts index b6d1d242b..87ed84631 100644 --- a/packages/server/src/modules/ee/Workspaces/commands/SetDefaultWorkspace.service.ts +++ b/packages/server/src/modules/ee/Workspaces/commands/SetDefaultWorkspace.service.ts @@ -12,8 +12,10 @@ export class SetDefaultWorkspaceService { constructor( @Inject(UserTenant.name) private readonly userTenantModel: typeof UserTenant, + @Inject(SystemUser.name) private readonly systemUserModel: typeof SystemUser, + @Inject(TenantModel.name) private readonly tenantModel: typeof TenantModel, private readonly eventEmitter: EventEmitter2, diff --git a/packages/server/src/modules/ee/Workspaces/queries/GetWorkspaces.service.ts b/packages/server/src/modules/ee/Workspaces/queries/GetWorkspaces.service.ts index 50d52650a..136489655 100644 --- a/packages/server/src/modules/ee/Workspaces/queries/GetWorkspaces.service.ts +++ b/packages/server/src/modules/ee/Workspaces/queries/GetWorkspaces.service.ts @@ -4,6 +4,7 @@ import { SystemUser } from '@/modules/System/models/SystemUser'; import { WorkspaceDto } from '../dtos/WorkspaceResponse.dto'; import { WorkspaceTransformer } from '../transformers/WorkspaceTransformer'; import { GetWorkspacesFinancialService } from './GetWorkspacesFinancial.service'; +import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; @Injectable() export class GetWorkspacesService { @@ -13,6 +14,7 @@ export class GetWorkspacesService { @Inject(SystemUser.name) private readonly systemUserModel: typeof SystemUser, private readonly financialService: GetWorkspacesFinancialService, + private readonly transformer: TransformerInjectable, ) {} /** @@ -50,30 +52,15 @@ export class GetWorkspacesService { const financialDataMap = await this.financialService.getWorkspacesFinancial(workspaceInfos); - const transformer = new WorkspaceTransformer(); - let workspaces = memberships.map((membership) => { - const financialData = financialDataMap.get(membership.tenantId); - return transformer.transform( - membership, + return this.transformer.transform( + memberships, + new WorkspaceTransformer(), + { defaultTenantId, - financialData, - ); - }); - - // Filter out inactive workspaces unless includeInactive is true - if (!includeInactive) { - workspaces = workspaces.filter((w) => w.isActive); - } - - // Sort: current organization first, then by name - return workspaces.sort((a, b) => { - if (currentOrganizationId) { - if (a.organizationId === currentOrganizationId) return -1; - if (b.organizationId === currentOrganizationId) return 1; - } - return (a.metadata?.name || a.organizationId).localeCompare( - b.metadata?.name || b.organizationId, - ); - }); + financialDataMap, + includeInactive, + currentOrganizationId, + }, + ); } } diff --git a/packages/server/src/modules/ee/Workspaces/transformers/WorkspaceTransformer.ts b/packages/server/src/modules/ee/Workspaces/transformers/WorkspaceTransformer.ts index d9bc67de0..4096a7ebb 100644 --- a/packages/server/src/modules/ee/Workspaces/transformers/WorkspaceTransformer.ts +++ b/packages/server/src/modules/ee/Workspaces/transformers/WorkspaceTransformer.ts @@ -15,9 +15,6 @@ interface FinancialData { * Transforms UserTenant (workspace membership) to WorkspaceDto. */ export class WorkspaceTransformer extends Transformer { - private defaultTenantId?: number; - private financialData?: FinancialData; - /** * Include these attributes in the transformed output. */ @@ -29,7 +26,6 @@ export class WorkspaceTransformer extends Transformer { 'isDeleting', 'isActive', 'buildJobId', - 'role', 'metadata', 'isDefault', 'totalIncome', @@ -104,44 +100,48 @@ export class WorkspaceTransformer extends Transformer { * Determine if this workspace is the user's default. */ protected isDefault = (membership: UserTenant): boolean => { - if (!this.defaultTenantId) return false; - return membership.tenantId === this.defaultTenantId; + const defaultTenantId = this.options?.defaultTenantId; + if (!defaultTenantId) return false; + return membership.tenantId === defaultTenantId; }; /** * Get total income from financial data. */ - protected totalIncome = (): number => { - return this.financialData?.totalIncome ?? 0; + protected totalIncome = (financialData?: FinancialData): number => { + return financialData?.totalIncome ?? 0; }; /** * Get total expenses from financial data. */ - protected totalExpenses = (): number => { - return this.financialData?.totalExpenses ?? 0; + protected totalExpenses = (financialData?: FinancialData): number => { + return financialData?.totalExpenses ?? 0; }; /** * Get total assets from financial data. */ - protected totalAssets = (): number => { - return this.financialData?.totalAssets ?? 0; + protected totalAssets = (financialData?: FinancialData): number => { + return financialData?.totalAssets ?? 0; }; /** * Get total liabilities from financial data. */ - protected totalLiabilities = (): number => { - return this.financialData?.totalLiabilities ?? 0; + protected totalLiabilities = (financialData?: FinancialData): number => { + return financialData?.totalLiabilities ?? 0; }; /** * Get formatted total assets. */ - protected formattedTotalAssets = (membership: UserTenant): string => { + protected formattedTotalAssets = ( + membership: UserTenant, + financialData?: FinancialData, + ): string => { const currencyCode = membership.tenant?.metadata?.baseCurrency; - return formatNumber(this.totalAssets(), { + return formatNumber(financialData?.totalAssets ?? 0, { currencyCode, money: true, }); @@ -150,9 +150,12 @@ export class WorkspaceTransformer extends Transformer { /** * Get formatted total liabilities. */ - protected formattedTotalLiabilities = (membership: UserTenant): string => { + protected formattedTotalLiabilities = ( + membership: UserTenant, + financialData?: FinancialData, + ): string => { const currencyCode = membership.tenant?.metadata?.baseCurrency; - return formatNumber(this.totalLiabilities(), { + return formatNumber(financialData?.totalLiabilities ?? 0, { currencyCode, money: true, }); @@ -161,13 +164,11 @@ export class WorkspaceTransformer extends Transformer { /** * Transform single membership to WorkspaceDto. */ - transform = ( - membership: UserTenant, - defaultTenantId?: number, - financialData?: FinancialData, - ): WorkspaceDto => { - this.defaultTenantId = defaultTenantId; - this.financialData = financialData; + transform = (membership: UserTenant): WorkspaceDto => { + const financialData = ( + this.options?.financialDataMap as Map + )?.get(membership.tenantId); + return { organizationId: this.organizationId(membership), isReady: this.isReady(membership), @@ -178,12 +179,51 @@ export class WorkspaceTransformer extends Transformer { role: membership.role, isDefault: this.isDefault(membership), metadata: this.metadata(membership), - totalIncome: this.totalIncome(), - totalExpenses: this.totalExpenses(), - totalAssets: this.totalAssets(), - totalLiabilities: this.totalLiabilities(), - formattedTotalAssets: this.formattedTotalAssets(membership), - formattedTotalLiabilities: this.formattedTotalLiabilities(membership), + totalIncome: this.totalIncome(financialData), + totalExpenses: this.totalExpenses(financialData), + totalAssets: this.totalAssets(financialData), + totalLiabilities: this.totalLiabilities(financialData), + formattedTotalAssets: this.formattedTotalAssets(membership, financialData), + formattedTotalLiabilities: this.formattedTotalLiabilities( + membership, + financialData, + ), }; }; + + /** + * Process collections directly through transform, then apply + * post-collection filtering and sorting. + */ + public work = (object: any) => { + if (Array.isArray(object)) { + const transformed = object.map((item) => this.transform(item)); + return this.postCollectionTransform(transformed); + } + return this.transform(object); + }; + + /** + * Filter and sort the transformed workspaces collection. + */ + protected postCollectionTransform = ( + workspaces: WorkspaceDto[], + ): WorkspaceDto[] => { + let result = workspaces; + + if (!this.options?.includeInactive) { + result = result.filter((w) => w.isActive); + } + + return result.sort((a, b) => { + const currentOrganizationId = this.options?.currentOrganizationId as string; + if (currentOrganizationId) { + if (a.organizationId === currentOrganizationId) return -1; + if (b.organizationId === currentOrganizationId) return 1; + } + return (a.metadata?.name || a.organizationId).localeCompare( + b.metadata?.name || b.organizationId, + ); + }); + }; }