diff --git a/apps/api/src/app/bridge/e2e/sync.e2e.ts b/apps/api/src/app/bridge/e2e/sync.e2e.ts index 10fa9260e5b..3d49f9b8dd0 100644 --- a/apps/api/src/app/bridge/e2e/sync.e2e.ts +++ b/apps/api/src/app/bridge/e2e/sync.e2e.ts @@ -6,7 +6,7 @@ import { MessageTemplateRepository, ControlValuesRepository, } from '@novu/dal'; -import { WorkflowTypeEnum } from '@novu/shared'; +import { WorkflowOriginEnum, WorkflowTypeEnum } from '@novu/shared'; import { workflow } from '@novu/framework'; import { BridgeServer } from '../../../../e2e/bridge.server'; @@ -650,4 +650,82 @@ describe('Bridge Sync - /bridge/sync (POST)', async () => { const secondStepResponse = await session.testAgent.get(`/v1/bridge/controls/${workflowId}/send-email`); expect(secondStepResponse.body.data.controls.subject).to.equal('Hello World again'); }); + + it('should throw an error when trying to sync a workflow with an ID that exists in dashboard', async () => { + const workflowId = 'dashboard-created-workflow'; + + // First create a workflow directly (simulating dashboard creation) + const dashboardWorkflow = await workflowsRepository.create({ + _environmentId: session.environment._id, + name: workflowId, + triggers: [{ identifier: workflowId, type: 'event', variables: [] }], + steps: [], + active: true, + draft: false, + workflowId, + origin: WorkflowOriginEnum.NOVU_CLOUD, + }); + + // Now try to sync a workflow with the same ID through bridge + const newWorkflow = workflow(workflowId, async ({ step }) => { + await step.email('send-email', () => ({ + subject: 'Welcome!', + body: 'Hello there', + })); + }); + await bridgeServer.start({ workflows: [newWorkflow] }); + + const result = await session.testAgent.post(`/v1/bridge/sync`).send({ + bridgeUrl: bridgeServer.serverPath, + }); + + expect(result.status).to.equal(400); + expect(result.body.message).to.contain(`was already created in Dashboard. Please use another workflowId.`); + + // Verify the original workflow wasn't modified + const workflows = await workflowsRepository.findOne({ + _environmentId: session.environment._id, + _id: dashboardWorkflow._id, + }); + expect(workflows).to.deep.equal(dashboardWorkflow); + }); + + it('should allow syncing a workflow with same ID if original was created externally', async () => { + const workflowId = 'external-created-workflow'; + + // First create a workflow as external + const externalWorkflow = await workflowsRepository.create({ + _environmentId: session.environment._id, + name: workflowId, + triggers: [{ identifier: workflowId, type: 'event', variables: [] }], + steps: [], + active: true, + draft: false, + workflowId, + origin: WorkflowOriginEnum.EXTERNAL, + }); + + // Now try to sync a workflow with the same ID through bridge + const newWorkflow = workflow(workflowId, async ({ step }) => { + await step.email('send-email', () => ({ + subject: 'Updated Welcome!', + body: 'Updated Hello there', + })); + }); + await bridgeServer.start({ workflows: [newWorkflow] }); + + const result = await session.testAgent.post(`/v1/bridge/sync`).send({ + bridgeUrl: bridgeServer.serverPath, + }); + + expect(result.status).to.equal(201); + + // Verify the workflow was updated + const workflows = await workflowsRepository.findOne({ + _environmentId: session.environment._id, + _id: externalWorkflow._id, + }); + expect(workflows?.origin).to.equal(WorkflowOriginEnum.EXTERNAL); + expect(workflows?.steps[0]?.stepId).to.equal('send-email'); + }); }); diff --git a/apps/api/src/app/bridge/usecases/sync/sync.usecase.ts b/apps/api/src/app/bridge/usecases/sync/sync.usecase.ts index 41d87e35c98..19060305d89 100644 --- a/apps/api/src/app/bridge/usecases/sync/sync.usecase.ts +++ b/apps/api/src/app/bridge/usecases/sync/sync.usecase.ts @@ -162,9 +162,25 @@ export class Sync { command: SyncCommand, workflowsFromBridge: DiscoverWorkflowOutput[] ): Promise { + const existingFrameworkWorkflows = await Promise.all( + workflowsFromBridge.map((workflow) => + this.notificationTemplateRepository.findByTriggerIdentifier(command.environmentId, workflow.workflowId) + ) + ); + + existingFrameworkWorkflows.forEach((workflow, index) => { + if (workflow?.origin && workflow.origin !== WorkflowOriginEnum.EXTERNAL) { + const { workflowId } = workflowsFromBridge[index]; + throw new BadRequestException( + `Workflow ${workflowId} was already created in Dashboard. Please use another workflowId.` + ); + } + }); + return Promise.all( - workflowsFromBridge.map(async (workflow) => { - let savedWorkflow = await this.upsertWorkflow(command, workflow); + workflowsFromBridge.map(async (workflow, index) => { + const existingFrameworkWorkflow = existingFrameworkWorkflows[index]; + let savedWorkflow = await this.upsertWorkflow(command, workflow, existingFrameworkWorkflow); const validatedWorkflowWithIssues = await this.workflowUpdatePostProcess.execute({ user: { @@ -197,24 +213,18 @@ export class Sync { private async upsertWorkflow( command: SyncCommand, - workflow: DiscoverWorkflowOutput + workflow: DiscoverWorkflowOutput, + existingFrameworkWorkflow: NotificationTemplateEntity | null ): Promise { - const workflowExist = await this.notificationTemplateRepository.findByTriggerIdentifier( - command.environmentId, - workflow.workflowId - ); - - let savedWorkflow: NotificationTemplateEntity | undefined; - - if (workflowExist) { - savedWorkflow = await this.updateWorkflowUsecase.execute( - UpdateWorkflowCommand.create(this.mapDiscoverWorkflowToUpdateWorkflowCommand(workflowExist, command, workflow)) + if (existingFrameworkWorkflow) { + return await this.updateWorkflowUsecase.execute( + UpdateWorkflowCommand.create( + this.mapDiscoverWorkflowToUpdateWorkflowCommand(existingFrameworkWorkflow, command, workflow) + ) ); - } else { - savedWorkflow = await this.createWorkflow(command, workflow); } - return savedWorkflow; + return await this.createWorkflow(command, workflow); } private async createWorkflow(