diff --git a/packages/twenty-server/src/engine/core-modules/workflow/services/__tests__/workflow-version-core-sync.service.spec.ts b/packages/twenty-server/src/engine/core-modules/workflow/services/__tests__/workflow-version-core-sync.service.spec.ts new file mode 100644 index 0000000000..a0cac3c708 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/workflow/services/__tests__/workflow-version-core-sync.service.spec.ts @@ -0,0 +1,97 @@ +import { STANDARD_OBJECTS } from 'twenty-shared/metadata'; +import { type Repository } from 'typeorm'; + +import { type GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { type WorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/workspace-scoped-repository'; +import { type WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; +import { WorkflowVersionCoreSyncService } from 'src/engine/core-modules/workflow/services/workflow-version-core-sync.service'; +import { type WorkflowVersionEntity } from 'src/engine/core-modules/workflow/entities/workflow-version.entity'; +import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { type WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; + +const CORE_VERSION_ID_FIELD = + STANDARD_OBJECTS.workflowVersion.fields.coreWorkflowVersionId + .universalIdentifier; + +describe('WorkflowVersionCoreSyncService', () => { + const workspaceId = '20202020-1c25-4d02-bf25-6aeccf7ea419'; + + let service: WorkflowVersionCoreSyncService; + let workflowVersionRepository: { upsert: jest.Mock; delete: jest.Mock }; + let workspaceRepository: { findOne: jest.Mock }; + let globalWorkspaceOrmManager: { executeInWorkspaceContext: jest.Mock }; + let workspaceCacheService: { + getOrRecompute: jest.Mock; + invalidateAndRecompute: jest.Mock; + }; + + const buildVersion = (): WorkflowVersionWorkspaceEntity => + ({ + id: '1dddc806-4144-5020-898f-b1ab287b89d5', + workflowId: 'c95c78b4-48d2-56f6-8e15-36ff8572f1d8', + status: 'DRAFT', + trigger: null, + steps: null, + coreWorkflowVersionId: null, + }) as unknown as WorkflowVersionWorkspaceEntity; + + const mockFieldPresence = (present: boolean) => + workspaceCacheService.getOrRecompute.mockResolvedValue({ + flatFieldMetadataMaps: { + byUniversalIdentifier: present ? { [CORE_VERSION_ID_FIELD]: {} } : {}, + }, + }); + + beforeEach(() => { + workflowVersionRepository = { upsert: jest.fn(), delete: jest.fn() }; + workspaceRepository = { + findOne: jest.fn().mockResolvedValue({ + id: workspaceId, + workspaceCustomApplicationId: 'application-1', + }), + }; + globalWorkspaceOrmManager = { + executeInWorkspaceContext: jest.fn().mockResolvedValue(undefined), + }; + workspaceCacheService = { + getOrRecompute: jest.fn(), + invalidateAndRecompute: jest.fn().mockResolvedValue(undefined), + }; + + service = new WorkflowVersionCoreSyncService( + workflowVersionRepository as unknown as WorkspaceScopedRepository, + workspaceRepository as unknown as Repository, + globalWorkspaceOrmManager as unknown as GlobalWorkspaceOrmManager, + workspaceCacheService as unknown as WorkspaceCacheService, + ); + + jest.spyOn(service['logger'], 'warn').mockImplementation(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('skips the core id write-back when the workspace lacks the coreWorkflowVersionId field', async () => { + mockFieldPresence(false); + + await expect( + service.upsertToCore(workspaceId, [buildVersion()]), + ).resolves.toBeUndefined(); + + expect(workflowVersionRepository.upsert).toHaveBeenCalledTimes(1); + expect( + globalWorkspaceOrmManager.executeInWorkspaceContext, + ).not.toHaveBeenCalled(); + }); + + it('writes the core id back when the field is present', async () => { + mockFieldPresence(true); + + await service.upsertToCore(workspaceId, [buildVersion()]); + + expect( + globalWorkspaceOrmManager.executeInWorkspaceContext, + ).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/workflow/services/workflow-version-core-sync.service.ts b/packages/twenty-server/src/engine/core-modules/workflow/services/workflow-version-core-sync.service.ts index a31dcc9bba..14286d8bf4 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/services/workflow-version-core-sync.service.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/services/workflow-version-core-sync.service.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { STANDARD_OBJECTS } from 'twenty-shared/metadata'; import { isDefined } from 'twenty-shared/utils'; import { In, Repository } from 'typeorm'; import { v4 as uuidv4, v5 as uuidv5 } from 'uuid'; @@ -25,6 +26,8 @@ const CORE_WORKFLOW_VERSION_ID_NAMESPACE = @Injectable() export class WorkflowVersionCoreSyncService { + private readonly logger = new Logger(WorkflowVersionCoreSyncService.name); + constructor( @InjectWorkspaceScopedRepository(WorkflowVersionEntity) private readonly workflowVersionRepository: WorkspaceScopedRepository, @@ -128,6 +131,14 @@ export class WorkflowVersionCoreSyncService { return; } + if (!(await this.workspaceHasCoreWorkflowVersionIdField(workspaceId))) { + this.logger.warn( + `workflowVersion.coreWorkflowVersionId field missing for workspace ${workspaceId}, skipping core id write-back`, + ); + + return; + } + await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { const workflowVersionRepository = await this.globalWorkspaceOrmManager.getRepository( @@ -147,6 +158,22 @@ export class WorkflowVersionCoreSyncService { }, buildSystemAuthContext(workspaceId)); } + private async workspaceHasCoreWorkflowVersionIdField( + workspaceId: string, + ): Promise { + const { flatFieldMetadataMaps } = + await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'flatFieldMetadataMaps', + ]); + + return isDefined( + flatFieldMetadataMaps.byUniversalIdentifier[ + STANDARD_OBJECTS.workflowVersion.fields.coreWorkflowVersionId + .universalIdentifier + ], + ); + } + private async getCustomApplicationIdOrThrow( workspaceId: string, ): Promise {