Split workspace service (#13642)

Splits workflow-version-step workspace-service into multiple once
This commit is contained in:
martmull
2025-08-05 14:10:11 +00:00
committed by GitHub
parent d62a1e0b5a
commit 44f5600300
19 changed files with 643 additions and 471 deletions
@@ -0,0 +1,8 @@
import { CustomException } from 'src/utils/custom-exception';
export class WorkflowVersionEdgeException extends CustomException<WorkflowVersionEdgeExceptionCode> {}
export enum WorkflowVersionEdgeExceptionCode {
UNKNOWN = 'UNKNOWN',
NOT_FOUND = 'NOT_FOUND',
}
@@ -1,4 +1,4 @@
import { computeWorkflowVersionStepChanges } from 'src/modules/workflow/workflow-builder/workflow-step/utils/compute-workflow-version-step-updates.util';
import { computeWorkflowVersionStepChanges } from 'src/modules/workflow/workflow-builder/utils/compute-workflow-version-step-updates.util';
import { WorkflowTrigger } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
@@ -4,20 +4,23 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkflowSchemaModule } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.module';
import { WorkflowVersionStepModule } from 'src/modules/workflow/workflow-builder/workflow-step/workflow-version-step.module';
import { WorkflowVersionStepModule } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.module';
import { WorkflowVersionModule } from 'src/modules/workflow/workflow-builder/workflow-version/workflow-version.module';
import { WorkflowVersionEdgeModule } from 'src/modules/workflow/workflow-builder/workflow-version-edge/workflow-version-edge.module';
@Module({
imports: [
WorkflowSchemaModule,
WorkflowVersionModule,
WorkflowVersionStepModule,
WorkflowVersionEdgeModule,
NestjsQueryTypeOrmModule.forFeature([ObjectMetadataEntity], 'core'),
],
exports: [
WorkflowSchemaModule,
WorkflowVersionModule,
WorkflowVersionStepModule,
WorkflowVersionEdgeModule,
],
})
export class WorkflowBuilderModule {}
@@ -1,16 +1,6 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-step/workflow-version-step.workspace-service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service';
import { WorkflowRunnerWorkspaceService } from 'src/modules/workflow/workflow-runner/workspace-services/workflow-runner.workspace-service';
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import {
@@ -18,6 +8,7 @@ import {
WorkflowActionType,
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
import { WorkflowTriggerType } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
import { WorkflowVersionEdgeWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-edge/workflow-version-edge.workspace-service';
type MockWorkspaceRepository = Partial<
WorkspaceRepository<WorkflowVersionWorkspaceEntity>
@@ -78,9 +69,9 @@ const mockWorkflowVersion = {
status: 'DRAFT',
} as WorkflowVersionWorkspaceEntity;
describe('WorkflowVersionStepWorkspaceService', () => {
describe('WorkflowVersionEdgeWorkspaceService', () => {
let twentyORMGlobalManager: jest.Mocked<TwentyORMGlobalManager>;
let service: WorkflowVersionStepWorkspaceService;
let service: WorkflowVersionEdgeWorkspaceService;
let mockWorkflowVersionWorkspaceRepository: MockWorkspaceRepository;
beforeEach(async () => {
@@ -101,135 +92,15 @@ describe('WorkflowVersionStepWorkspaceService', () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
WorkflowVersionStepWorkspaceService,
WorkflowVersionEdgeWorkspaceService,
{
provide: TwentyORMGlobalManager,
useValue: twentyORMGlobalManager,
},
{
provide: WorkflowSchemaWorkspaceService,
useValue: {
computeStepOutputSchema: jest.fn(),
},
},
{ provide: ServerlessFunctionService, useValue: {} },
{ provide: AgentService, useValue: {} },
{
provide: getRepositoryToken(ObjectMetadataEntity, 'core'),
useValue: {
findOne: jest.fn(),
},
},
{ provide: WorkflowRunWorkspaceService, useValue: {} },
{ provide: WorkflowRunnerWorkspaceService, useValue: {} },
{ provide: WorkflowCommonWorkspaceService, useValue: {} },
{ provide: ScopedWorkspaceContextFactory, useValue: {} },
],
}).compile();
service = module.get(WorkflowVersionStepWorkspaceService);
});
describe('createWorkflowVersionStep', () => {
it('should create a step linked to trigger', async () => {
const result = await service.createWorkflowVersionStep({
input: {
stepType: WorkflowActionType.FORM,
parentStepId: 'trigger',
nextStepId: undefined,
workflowVersionId: mockWorkflowVersionId,
},
workspaceId: mockWorkspaceId,
});
expect(mockWorkflowVersionWorkspaceRepository.update).toHaveBeenCalled();
expect(result.createdStep).toBeDefined();
const createdStepId = result.createdStep?.id;
expect(result.triggerNextStepIds).toEqual(['step-1', createdStepId]);
expect(result.stepsNextStepIds).toEqual({
'step-1': ['step-2'],
'step-2': [],
'step-3': [],
});
});
it('should create a step between a trigger and a step', async () => {
const result = await service.createWorkflowVersionStep({
input: {
stepType: WorkflowActionType.FORM,
parentStepId: 'trigger',
nextStepId: 'step-1',
workflowVersionId: mockWorkflowVersionId,
},
workspaceId: mockWorkspaceId,
});
expect(mockWorkflowVersionWorkspaceRepository.update).toHaveBeenCalled();
expect(result.createdStep).toBeDefined();
const createdStepId = result.createdStep?.id as string;
expect(result.triggerNextStepIds).toEqual([createdStepId]);
expect(result.stepsNextStepIds).toEqual({
[createdStepId]: ['step-1'],
'step-1': ['step-2'],
'step-2': [],
'step-3': [],
});
});
it('should create a step between two steps', async () => {
const result = await service.createWorkflowVersionStep({
input: {
stepType: WorkflowActionType.FORM,
parentStepId: 'step-1',
nextStepId: 'step-2',
workflowVersionId: mockWorkflowVersionId,
},
workspaceId: mockWorkspaceId,
});
expect(mockWorkflowVersionWorkspaceRepository.update).toHaveBeenCalled();
expect(result.createdStep).toBeDefined();
const createdStepId = result.createdStep?.id as string;
expect(result.triggerNextStepIds).toEqual(['step-1']);
expect(result.stepsNextStepIds).toEqual({
'step-1': [createdStepId],
[createdStepId]: ['step-2'],
'step-2': [],
'step-3': [],
});
});
it('should create a step without parent or children', async () => {
const result = await service.createWorkflowVersionStep({
input: {
stepType: WorkflowActionType.FORM,
parentStepId: undefined,
nextStepId: undefined,
workflowVersionId: mockWorkflowVersionId,
},
workspaceId: mockWorkspaceId,
});
expect(mockWorkflowVersionWorkspaceRepository.update).toHaveBeenCalled();
expect(result.createdStep).toBeDefined();
expect(result.triggerNextStepIds).toEqual(['step-1']);
expect(result.stepsNextStepIds).toEqual({
'step-1': ['step-2'],
'step-2': [],
'step-3': [],
});
});
service = module.get(WorkflowVersionEdgeWorkspaceService);
});
describe('createWorkflowVersionEdge', () => {
@@ -507,53 +378,4 @@ describe('WorkflowVersionStepWorkspaceService', () => {
});
});
});
describe('deleteWorkflowVersionStep', () => {
it('should delete step linked to trigger', async () => {
const result = await service.deleteWorkflowVersionStep({
stepIdToDelete: 'step-1',
workflowVersionId: mockWorkflowVersionId,
workspaceId: mockWorkspaceId,
});
expect(
mockWorkflowVersionWorkspaceRepository.update,
).toHaveBeenCalledWith(mockWorkflowVersionId, {
trigger: { ...mockTrigger, nextStepIds: ['step-2'] },
steps: mockSteps.filter((step) => step.id !== 'step-1'),
});
expect(result).toEqual({
triggerNextStepIds: ['step-2'],
stepsNextStepIds: {
'step-2': [],
'step-3': [],
},
deletedStepId: 'step-1',
});
});
it('should delete trigger', async () => {
const result = await service.deleteWorkflowVersionStep({
stepIdToDelete: 'trigger',
workflowVersionId: mockWorkflowVersionId,
workspaceId: mockWorkspaceId,
});
expect(
mockWorkflowVersionWorkspaceRepository.update,
).toHaveBeenCalledWith(mockWorkflowVersionId, {
trigger: null,
});
expect(result).toEqual({
stepsNextStepIds: {
'step-1': ['step-2'],
'step-2': [],
'step-3': [],
},
deletedStepId: 'trigger',
});
});
});
});
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { WorkflowVersionEdgeWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-edge/workflow-version-edge.workspace-service';
@Module({
providers: [WorkflowVersionEdgeWorkspaceService],
exports: [WorkflowVersionEdgeWorkspaceService],
})
export class WorkflowVersionEdgeModule {}
@@ -0,0 +1,258 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { WorkflowVersionStepChangesDTO } from 'src/engine/core-modules/workflow/dtos/workflow-version-step-changes.dto';
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import {
WorkflowVersionEdgeException,
WorkflowVersionEdgeExceptionCode,
} from 'src/modules/workflow/common/exceptions/workflow-version-edge.exception';
import { assertWorkflowVersionIsDraft } from 'src/modules/workflow/common/utils/assert-workflow-version-is-draft.util';
import { computeWorkflowVersionStepChanges } from 'src/modules/workflow/workflow-builder/utils/compute-workflow-version-step-updates.util';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@Injectable()
export class WorkflowVersionEdgeWorkspaceService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
async createWorkflowVersionEdge({
source,
target,
workflowVersionId,
workspaceId,
}: {
source: string;
target: string;
workflowVersionId: string;
workspaceId: string;
}): Promise<WorkflowVersionStepChangesDTO> {
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
const workflowVersion = await workflowVersionRepository.findOne({
where: {
id: workflowVersionId,
},
});
if (!isDefined(workflowVersion)) {
throw new WorkflowVersionEdgeException(
'WorkflowVersion not found',
WorkflowVersionEdgeExceptionCode.NOT_FOUND,
);
}
assertWorkflowVersionIsDraft(workflowVersion);
const steps = workflowVersion.steps || [];
const trigger = workflowVersion.trigger;
const isSourceTrigger = source === 'trigger';
const targetStep = steps.find((step) => step.id === target);
if (!isDefined(targetStep)) {
throw new WorkflowVersionEdgeException(
`Target step '${target}' not found in workflowVersion '${workflowVersionId}'`,
WorkflowVersionEdgeExceptionCode.NOT_FOUND,
);
}
if (isSourceTrigger) {
if (!isDefined(trigger)) {
throw new WorkflowVersionEdgeException(
`Trigger not found in workflowVersion '${workflowVersionId}'`,
WorkflowVersionEdgeExceptionCode.NOT_FOUND,
);
}
if (trigger.nextStepIds?.includes(target)) {
return computeWorkflowVersionStepChanges({
trigger,
steps,
});
}
const updatedTrigger = {
...trigger,
nextStepIds: [...(trigger.nextStepIds ?? []), target],
};
await workflowVersionRepository.update(workflowVersion.id, {
trigger: updatedTrigger,
});
return computeWorkflowVersionStepChanges({
trigger: updatedTrigger,
steps,
});
}
const sourceStep = steps.find((step) => step.id === source);
if (!isDefined(sourceStep)) {
throw new WorkflowVersionEdgeException(
`Source step '${source}' not found in workflowVersion '${workflowVersionId}'`,
WorkflowVersionEdgeExceptionCode.NOT_FOUND,
);
}
if (sourceStep.nextStepIds?.includes(target)) {
return computeWorkflowVersionStepChanges({
trigger,
steps,
});
}
const updatedSourceStep = {
...sourceStep,
nextStepIds: [...(sourceStep.nextStepIds ?? []), target],
};
const updatedSteps = steps.map((step) => {
if (step.id === source) {
return updatedSourceStep;
}
return step;
});
await workflowVersionRepository.update(workflowVersion.id, {
steps: updatedSteps,
});
return computeWorkflowVersionStepChanges({
trigger,
steps: updatedSteps,
});
}
async deleteWorkflowVersionEdge({
source,
target,
workflowVersionId,
workspaceId,
}: {
source: string;
target: string;
workflowVersionId: string;
workspaceId: string;
}): Promise<WorkflowVersionStepChangesDTO> {
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
const workflowVersion = await workflowVersionRepository.findOne({
where: {
id: workflowVersionId,
},
});
if (!isDefined(workflowVersion)) {
throw new WorkflowVersionEdgeException(
'WorkflowVersion not found',
WorkflowVersionEdgeExceptionCode.NOT_FOUND,
);
}
assertWorkflowVersionIsDraft(workflowVersion);
const steps = workflowVersion.steps || [];
const trigger = workflowVersion.trigger;
const isSourceTrigger = source === 'trigger';
const targetStep = steps.find((step) => step.id === target);
if (!isDefined(targetStep)) {
throw new WorkflowVersionEdgeException(
`Target step '${target}' not found in workflowVersion '${workflowVersionId}'`,
WorkflowVersionEdgeExceptionCode.NOT_FOUND,
);
}
if (isSourceTrigger) {
if (!isDefined(trigger)) {
throw new WorkflowVersionEdgeException(
`Trigger not found in workflowVersion '${workflowVersionId}'`,
WorkflowVersionEdgeExceptionCode.NOT_FOUND,
);
}
if (!trigger.nextStepIds?.includes(target)) {
return computeWorkflowVersionStepChanges({
trigger,
steps,
});
}
const updatedTrigger = {
...trigger,
nextStepIds: trigger.nextStepIds?.filter(
(nextStepId) => nextStepId !== target,
),
};
await workflowVersionRepository.update(workflowVersion.id, {
trigger: updatedTrigger,
});
return computeWorkflowVersionStepChanges({
trigger: updatedTrigger,
steps,
});
}
const sourceStep = steps.find((step) => step.id === source);
if (!isDefined(sourceStep)) {
throw new WorkflowVersionEdgeException(
`Source step '${source}' not found in workflowVersion '${workflowVersionId}'`,
WorkflowVersionEdgeExceptionCode.NOT_FOUND,
);
}
if (!sourceStep.nextStepIds?.includes(target)) {
return computeWorkflowVersionStepChanges({
trigger,
steps,
});
}
const updatedSourceStep = {
...sourceStep,
nextStepIds: sourceStep.nextStepIds?.filter(
(nextStepId) => nextStepId !== target,
),
};
const updatedSteps = steps.map((step) => {
if (step.id === source) {
return updatedSourceStep;
}
return step;
});
await workflowVersionRepository.update(workflowVersion.id, {
steps: updatedSteps,
});
return computeWorkflowVersionStepChanges({
trigger,
steps: updatedSteps,
});
}
}
@@ -0,0 +1,283 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.workspace-service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service';
import { WorkflowRunnerWorkspaceService } from 'src/modules/workflow/workflow-runner/workspace-services/workflow-runner.workspace-service';
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import {
WorkflowAction,
WorkflowActionType,
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
import { WorkflowTriggerType } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
type MockWorkspaceRepository = Partial<
WorkspaceRepository<WorkflowVersionWorkspaceEntity>
> & {
findOne: jest.Mock;
update: jest.Mock;
};
const mockWorkflowVersionId = 'workflow-version-id';
const mockWorkspaceId = 'workspace-id';
const mockSteps = [
{
id: 'step-1',
type: WorkflowActionType.FORM,
settings: {
errorHandlingOptions: {
continueOnFailure: { value: false },
retryOnFailure: { value: false },
},
},
nextStepIds: ['step-2'],
},
{
id: 'step-2',
type: WorkflowActionType.SEND_EMAIL,
settings: {
errorHandlingOptions: {
continueOnFailure: { value: false },
retryOnFailure: { value: false },
},
},
nextStepIds: [],
},
{
id: 'step-3',
type: WorkflowActionType.SEND_EMAIL,
settings: {
errorHandlingOptions: {
continueOnFailure: { value: false },
retryOnFailure: { value: false },
},
},
nextStepIds: [],
},
] as WorkflowAction[];
const mockTrigger = {
type: WorkflowTriggerType.MANUAL,
settings: {},
nextStepIds: ['step-1'],
};
const mockWorkflowVersion = {
id: mockWorkflowVersionId,
trigger: mockTrigger,
steps: mockSteps,
status: 'DRAFT',
} as WorkflowVersionWorkspaceEntity;
describe('WorkflowVersionStepWorkspaceService', () => {
let twentyORMGlobalManager: jest.Mocked<TwentyORMGlobalManager>;
let service: WorkflowVersionStepWorkspaceService;
let mockWorkflowVersionWorkspaceRepository: MockWorkspaceRepository;
beforeEach(async () => {
mockWorkflowVersionWorkspaceRepository = {
findOne: jest.fn(),
update: jest.fn(),
};
mockWorkflowVersionWorkspaceRepository.findOne.mockResolvedValue(
mockWorkflowVersion,
);
twentyORMGlobalManager = {
getRepositoryForWorkspace: jest
.fn()
.mockResolvedValue(mockWorkflowVersionWorkspaceRepository),
} as unknown as jest.Mocked<TwentyORMGlobalManager>;
const module: TestingModule = await Test.createTestingModule({
providers: [
WorkflowVersionStepWorkspaceService,
{
provide: TwentyORMGlobalManager,
useValue: twentyORMGlobalManager,
},
{
provide: WorkflowSchemaWorkspaceService,
useValue: {
computeStepOutputSchema: jest.fn(),
},
},
{ provide: ServerlessFunctionService, useValue: {} },
{ provide: AgentService, useValue: {} },
{
provide: getRepositoryToken(ObjectMetadataEntity, 'core'),
useValue: {
findOne: jest.fn(),
},
},
{ provide: WorkflowRunWorkspaceService, useValue: {} },
{ provide: WorkflowRunnerWorkspaceService, useValue: {} },
{ provide: WorkflowCommonWorkspaceService, useValue: {} },
{ provide: ScopedWorkspaceContextFactory, useValue: {} },
],
}).compile();
service = module.get(WorkflowVersionStepWorkspaceService);
});
describe('createWorkflowVersionStep', () => {
it('should create a step linked to trigger', async () => {
const result = await service.createWorkflowVersionStep({
input: {
stepType: WorkflowActionType.FORM,
parentStepId: 'trigger',
nextStepId: undefined,
workflowVersionId: mockWorkflowVersionId,
},
workspaceId: mockWorkspaceId,
});
expect(mockWorkflowVersionWorkspaceRepository.update).toHaveBeenCalled();
expect(result.createdStep).toBeDefined();
const createdStepId = result.createdStep?.id;
expect(result.triggerNextStepIds).toEqual(['step-1', createdStepId]);
expect(result.stepsNextStepIds).toEqual({
'step-1': ['step-2'],
'step-2': [],
'step-3': [],
});
});
it('should create a step between a trigger and a step', async () => {
const result = await service.createWorkflowVersionStep({
input: {
stepType: WorkflowActionType.FORM,
parentStepId: 'trigger',
nextStepId: 'step-1',
workflowVersionId: mockWorkflowVersionId,
},
workspaceId: mockWorkspaceId,
});
expect(mockWorkflowVersionWorkspaceRepository.update).toHaveBeenCalled();
expect(result.createdStep).toBeDefined();
const createdStepId = result.createdStep?.id as string;
expect(result.triggerNextStepIds).toEqual([createdStepId]);
expect(result.stepsNextStepIds).toEqual({
[createdStepId]: ['step-1'],
'step-1': ['step-2'],
'step-2': [],
'step-3': [],
});
});
it('should create a step between two steps', async () => {
const result = await service.createWorkflowVersionStep({
input: {
stepType: WorkflowActionType.FORM,
parentStepId: 'step-1',
nextStepId: 'step-2',
workflowVersionId: mockWorkflowVersionId,
},
workspaceId: mockWorkspaceId,
});
expect(mockWorkflowVersionWorkspaceRepository.update).toHaveBeenCalled();
expect(result.createdStep).toBeDefined();
const createdStepId = result.createdStep?.id as string;
expect(result.triggerNextStepIds).toEqual(['step-1']);
expect(result.stepsNextStepIds).toEqual({
'step-1': [createdStepId],
[createdStepId]: ['step-2'],
'step-2': [],
'step-3': [],
});
});
it('should create a step without parent or children', async () => {
const result = await service.createWorkflowVersionStep({
input: {
stepType: WorkflowActionType.FORM,
parentStepId: undefined,
nextStepId: undefined,
workflowVersionId: mockWorkflowVersionId,
},
workspaceId: mockWorkspaceId,
});
expect(mockWorkflowVersionWorkspaceRepository.update).toHaveBeenCalled();
expect(result.createdStep).toBeDefined();
expect(result.triggerNextStepIds).toEqual(['step-1']);
expect(result.stepsNextStepIds).toEqual({
'step-1': ['step-2'],
'step-2': [],
'step-3': [],
});
});
});
describe('deleteWorkflowVersionStep', () => {
it('should delete step linked to trigger', async () => {
const result = await service.deleteWorkflowVersionStep({
stepIdToDelete: 'step-1',
workflowVersionId: mockWorkflowVersionId,
workspaceId: mockWorkspaceId,
});
expect(
mockWorkflowVersionWorkspaceRepository.update,
).toHaveBeenCalledWith(mockWorkflowVersionId, {
trigger: { ...mockTrigger, nextStepIds: ['step-2'] },
steps: mockSteps.filter((step) => step.id !== 'step-1'),
});
expect(result).toEqual({
triggerNextStepIds: ['step-2'],
stepsNextStepIds: {
'step-2': [],
'step-3': [],
},
deletedStepId: 'step-1',
});
});
it('should delete trigger', async () => {
const result = await service.deleteWorkflowVersionStep({
stepIdToDelete: 'trigger',
workflowVersionId: mockWorkflowVersionId,
workspaceId: mockWorkspaceId,
});
expect(
mockWorkflowVersionWorkspaceRepository.update,
).toHaveBeenCalledWith(mockWorkflowVersionId, {
trigger: null,
});
expect(result).toEqual({
stepsNextStepIds: {
'step-1': ['step-2'],
'step-2': [],
'step-3': [],
},
deletedStepId: 'trigger',
});
});
});
});
@@ -1,4 +1,4 @@
import { insertStep } from 'src/modules/workflow/workflow-builder/workflow-step/utils/insert-step';
import { insertStep } from 'src/modules/workflow/workflow-builder/workflow-version-step/utils/insert-step';
import {
WorkflowAction,
WorkflowActionType,
@@ -1,4 +1,4 @@
import { removeStep } from 'src/modules/workflow/workflow-builder/workflow-step/utils/remove-step';
import { removeStep } from 'src/modules/workflow/workflow-builder/workflow-version-step/utils/remove-step';
import {
WorkflowAction,
WorkflowActionType,
@@ -5,10 +5,9 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { AgentModule } from 'src/engine/metadata-modules/agent/agent.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
import { WorkflowSchemaModule } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.module';
import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-step/workflow-version-step.workspace-service';
import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.workspace-service';
import { WorkflowRunModule } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.module';
import { WorkflowRunnerModule } from 'src/modules/workflow/workflow-runner/workflow-runner.module';
@@ -22,10 +21,7 @@ import { WorkflowRunnerModule } from 'src/modules/workflow/workflow-runner/workf
WorkflowCommonModule,
NestjsQueryTypeOrmModule.forFeature([ObjectMetadataEntity], 'core'),
],
providers: [
WorkflowVersionStepWorkspaceService,
ScopedWorkspaceContextFactory,
],
providers: [WorkflowVersionStepWorkspaceService],
exports: [WorkflowVersionStepWorkspaceService],
})
export class WorkflowVersionStepModule {}
@@ -13,7 +13,6 @@ import { CreateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import {
WorkflowVersionStepException,
@@ -23,8 +22,8 @@ import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/stan
import { assertWorkflowVersionIsDraft } from 'src/modules/workflow/common/utils/assert-workflow-version-is-draft.util';
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
import { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service';
import { insertStep } from 'src/modules/workflow/workflow-builder/workflow-step/utils/insert-step';
import { removeStep } from 'src/modules/workflow/workflow-builder/workflow-step/utils/remove-step';
import { insertStep } from 'src/modules/workflow/workflow-builder/workflow-version-step/utils/insert-step';
import { removeStep } from 'src/modules/workflow/workflow-builder/workflow-version-step/utils/remove-step';
import { BaseWorkflowActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type';
import {
WorkflowAction,
@@ -35,7 +34,7 @@ import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runne
import { WorkflowRunnerWorkspaceService } from 'src/modules/workflow/workflow-runner/workspace-services/workflow-runner.workspace-service';
import { WorkflowStepPositionInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-step-position-input.dto';
import { WorkflowVersionStepChangesDTO } from 'src/engine/core-modules/workflow/dtos/workflow-version-step-changes.dto';
import { computeWorkflowVersionStepChanges } from 'src/modules/workflow/workflow-builder/workflow-step/utils/compute-workflow-version-step-updates.util';
import { computeWorkflowVersionStepChanges } from 'src/modules/workflow/workflow-builder/utils/compute-workflow-version-step-updates.util';
const BASE_STEP_DEFINITION: BaseWorkflowActionSettings = {
outputSchema: {},
@@ -61,7 +60,6 @@ export class WorkflowVersionStepWorkspaceService {
private readonly workflowRunWorkspaceService: WorkflowRunWorkspaceService,
private readonly workflowRunnerWorkspaceService: WorkflowRunnerWorkspaceService,
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
) {}
async createWorkflowVersionStep({
@@ -368,244 +366,6 @@ export class WorkflowVersionStepWorkspaceService {
});
}
async createWorkflowVersionEdge({
source,
target,
workflowVersionId,
workspaceId,
}: {
source: string;
target: string;
workflowVersionId: string;
workspaceId: string;
}): Promise<WorkflowVersionStepChangesDTO> {
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
const workflowVersion = await workflowVersionRepository.findOne({
where: {
id: workflowVersionId,
},
});
if (!isDefined(workflowVersion)) {
throw new WorkflowVersionStepException(
'WorkflowVersion not found',
WorkflowVersionStepExceptionCode.NOT_FOUND,
);
}
assertWorkflowVersionIsDraft(workflowVersion);
const steps = workflowVersion.steps || [];
const trigger = workflowVersion.trigger;
const isSourceTrigger = source === 'trigger';
const targetStep = steps.find((step) => step.id === target);
if (!isDefined(targetStep)) {
throw new WorkflowVersionStepException(
`Target step '${target}' not found in workflowVersion '${workflowVersionId}'`,
WorkflowVersionStepExceptionCode.NOT_FOUND,
);
}
if (isSourceTrigger) {
if (!isDefined(trigger)) {
throw new WorkflowVersionStepException(
`Trigger not found in workflowVersion '${workflowVersionId}'`,
WorkflowVersionStepExceptionCode.NOT_FOUND,
);
}
if (trigger.nextStepIds?.includes(target)) {
return computeWorkflowVersionStepChanges({
trigger,
steps,
});
}
const updatedTrigger = {
...trigger,
nextStepIds: [...(trigger.nextStepIds ?? []), target],
};
await workflowVersionRepository.update(workflowVersion.id, {
trigger: updatedTrigger,
});
return computeWorkflowVersionStepChanges({
trigger: updatedTrigger,
steps,
});
}
const sourceStep = steps.find((step) => step.id === source);
if (!isDefined(sourceStep)) {
throw new WorkflowVersionStepException(
`Source step '${source}' not found in workflowVersion '${workflowVersionId}'`,
WorkflowVersionStepExceptionCode.NOT_FOUND,
);
}
if (sourceStep.nextStepIds?.includes(target)) {
return computeWorkflowVersionStepChanges({
trigger,
steps,
});
}
const updatedSourceStep = {
...sourceStep,
nextStepIds: [...(sourceStep.nextStepIds ?? []), target],
};
const updatedSteps = steps.map((step) => {
if (step.id === source) {
return updatedSourceStep;
}
return step;
});
await workflowVersionRepository.update(workflowVersion.id, {
steps: updatedSteps,
});
return computeWorkflowVersionStepChanges({
trigger,
steps: updatedSteps,
});
}
async deleteWorkflowVersionEdge({
source,
target,
workflowVersionId,
workspaceId,
}: {
source: string;
target: string;
workflowVersionId: string;
workspaceId: string;
}): Promise<WorkflowVersionStepChangesDTO> {
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
const workflowVersion = await workflowVersionRepository.findOne({
where: {
id: workflowVersionId,
},
});
if (!isDefined(workflowVersion)) {
throw new WorkflowVersionStepException(
'WorkflowVersion not found',
WorkflowVersionStepExceptionCode.NOT_FOUND,
);
}
assertWorkflowVersionIsDraft(workflowVersion);
const steps = workflowVersion.steps || [];
const trigger = workflowVersion.trigger;
const isSourceTrigger = source === 'trigger';
const targetStep = steps.find((step) => step.id === target);
if (!isDefined(targetStep)) {
throw new WorkflowVersionStepException(
`Target step '${target}' not found in workflowVersion '${workflowVersionId}'`,
WorkflowVersionStepExceptionCode.NOT_FOUND,
);
}
if (isSourceTrigger) {
if (!isDefined(trigger)) {
throw new WorkflowVersionStepException(
`Trigger not found in workflowVersion '${workflowVersionId}'`,
WorkflowVersionStepExceptionCode.NOT_FOUND,
);
}
if (!trigger.nextStepIds?.includes(target)) {
return computeWorkflowVersionStepChanges({
trigger,
steps,
});
}
const updatedTrigger = {
...trigger,
nextStepIds: trigger.nextStepIds?.filter(
(nextStepId) => nextStepId !== target,
),
};
await workflowVersionRepository.update(workflowVersion.id, {
trigger: updatedTrigger,
});
return computeWorkflowVersionStepChanges({
trigger: updatedTrigger,
steps,
});
}
const sourceStep = steps.find((step) => step.id === source);
if (!isDefined(sourceStep)) {
throw new WorkflowVersionStepException(
`Source step '${source}' not found in workflowVersion '${workflowVersionId}'`,
WorkflowVersionStepExceptionCode.NOT_FOUND,
);
}
if (!sourceStep.nextStepIds?.includes(target)) {
return computeWorkflowVersionStepChanges({
trigger,
steps,
});
}
const updatedSourceStep = {
...sourceStep,
nextStepIds: sourceStep.nextStepIds?.filter(
(nextStepId) => nextStepId !== target,
),
};
const updatedSteps = steps.map((step) => {
if (step.id === source) {
return updatedSourceStep;
}
return step;
});
await workflowVersionRepository.update(workflowVersion.id, {
steps: updatedSteps,
});
return computeWorkflowVersionStepChanges({
trigger,
steps: updatedSteps,
});
}
private async enrichOutputSchema({
step,
workspaceId,
@@ -6,7 +6,7 @@ import { RecordPositionModule } from 'src/engine/core-modules/record-position/re
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
import { WorkflowSchemaModule } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.module';
import { WorkflowVersionStepModule } from 'src/modules/workflow/workflow-builder/workflow-step/workflow-version-step.module';
import { WorkflowVersionStepModule } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.module';
import { WorkflowVersionWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version/workflow-version.workspace-service';
@Module({
@@ -15,7 +15,7 @@ import {
import { assertWorkflowVersionHasSteps } from 'src/modules/workflow/common/utils/assert-workflow-version-has-steps';
import { assertWorkflowVersionIsDraft } from 'src/modules/workflow/common/utils/assert-workflow-version-is-draft.util';
import { assertWorkflowVersionTriggerIsDefined } from 'src/modules/workflow/common/utils/assert-workflow-version-trigger-is-defined.util';
import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-step/workflow-version-step.workspace-service';
import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.workspace-service';
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
import { WorkflowStepPositionUpdateInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-step-position-update-input.dto';