Update workflow version struct (#6716)

We want to avoid the nested structure of active pieces. Steps to execute
will now be separated from the trigger. It will be an array executed
sequentially.

For now a step can only be an action. But at some point it will also be
a branch or a loop
This commit is contained in:
Thomas Trompette
2024-08-22 17:59:16 +02:00
committed by GitHub
parent 579c2ebcea
commit 0a7700351f
18 changed files with 131 additions and 114 deletions

View File

@ -425,6 +425,7 @@ export const WORKFLOW_VERSION_STANDARD_FIELD_IDS = {
workflow: '20202020-afa3-46c3-91b0-0631ca6aa1c8', workflow: '20202020-afa3-46c3-91b0-0631ca6aa1c8',
trigger: '20202020-4eae-43e7-86e0-212b41a30b48', trigger: '20202020-4eae-43e7-86e0-212b41a30b48',
runs: '20202020-1d08-46df-901a-85045f18099a', runs: '20202020-1d08-46df-901a-85045f18099a',
steps: '20202020-5988-4a64-b94a-1f9b7b989039',
}; };
export const WORKSPACE_MEMBER_STANDARD_FIELD_IDS = { export const WORKSPACE_MEMBER_STANDARD_FIELD_IDS = {

View File

@ -21,6 +21,7 @@ import {
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
import { WorkflowRunWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity'; import { WorkflowRunWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity'; import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
import { WorkflowStep } from 'src/modules/workflow/common/types/workflow-step.type';
import { WorkflowTrigger } from 'src/modules/workflow/common/types/workflow-trigger.type'; import { WorkflowTrigger } from 'src/modules/workflow/common/types/workflow-trigger.type';
@WorkspaceEntity({ @WorkspaceEntity({
@ -51,11 +52,19 @@ export class WorkflowVersionWorkspaceEntity extends BaseWorkspaceEntity {
type: FieldMetadataType.RAW_JSON, type: FieldMetadataType.RAW_JSON,
label: 'Version trigger', label: 'Version trigger',
description: 'Json object to provide trigger', description: 'Json object to provide trigger',
icon: 'IconPlayerPlay',
}) })
@WorkspaceIsNullable() @WorkspaceIsNullable()
trigger: WorkflowTrigger | null; trigger: WorkflowTrigger | null;
@WorkspaceField({
standardId: WORKFLOW_VERSION_STANDARD_FIELD_IDS.steps,
type: FieldMetadataType.RAW_JSON,
label: 'Version steps',
description: 'Json object to provide steps',
})
@WorkspaceIsNullable()
steps: WorkflowStep[] | null;
// Relations // Relations
@WorkspaceRelation({ @WorkspaceRelation({
standardId: WORKFLOW_VERSION_STANDARD_FIELD_IDS.workflow, standardId: WORKFLOW_VERSION_STANDARD_FIELD_IDS.workflow,

View File

@ -1,20 +0,0 @@
import { WorkflowCodeSettingsType } from 'src/modules/workflow/common/types/workflow-settings.type';
export enum WorkflowActionType {
CODE = 'CODE',
}
type CommonWorkflowAction = {
name: string;
displayName: string;
valid: boolean;
};
type WorkflowCodeAction = CommonWorkflowAction & {
type: WorkflowActionType.CODE;
settings: WorkflowCodeSettingsType;
};
export type WorkflowAction = WorkflowCodeAction & {
nextAction: WorkflowAction;
};

View File

@ -1,4 +1,4 @@
type WorkflowBaseSettingsType = { type BaseWorkflowSettings = {
errorHandlingOptions: { errorHandlingOptions: {
retryOnFailure: { retryOnFailure: {
value: boolean; value: boolean;
@ -9,6 +9,6 @@ type WorkflowBaseSettingsType = {
}; };
}; };
export type WorkflowCodeSettingsType = WorkflowBaseSettingsType & { export type WorkflowCodeSettings = BaseWorkflowSettings & {
serverlessFunctionId: string; serverlessFunctionId: string;
}; };

View File

@ -0,0 +1,18 @@
import { WorkflowCodeSettings } from 'src/modules/workflow/common/types/workflow-settings.type';
export enum WorkflowStepType {
CODE_ACTION = 'CODE_ACTION',
}
type BaseWorkflowStep = {
id: string;
name: string;
valid: boolean;
};
export type WorkflowCodeStep = BaseWorkflowStep & {
type: WorkflowStepType.CODE_ACTION;
settings: WorkflowCodeSettings;
};
export type WorkflowStep = WorkflowCodeStep;

View File

@ -1,20 +1,17 @@
import { WorkflowAction } from 'src/modules/workflow/common/types/workflow-action.type';
export enum WorkflowTriggerType { export enum WorkflowTriggerType {
DATABASE_EVENT = 'DATABASE_EVENT', DATABASE_EVENT = 'DATABASE_EVENT',
} }
type BaseTrigger = { type BaseTrigger = {
name: string;
type: WorkflowTriggerType; type: WorkflowTriggerType;
input?: object; input?: object;
nextAction?: WorkflowAction;
}; };
export type WorkflowDatabaseEventTrigger = BaseTrigger & { export type WorkflowDatabaseEventTrigger = BaseTrigger & {
type: WorkflowTriggerType.DATABASE_EVENT; type: WorkflowTriggerType.DATABASE_EVENT;
settings: { settings: {
eventName: string; eventName: string;
triggerName: string;
}; };
}; };

View File

@ -1,12 +0,0 @@
import { CustomException } from 'src/utils/custom-exception';
export class WorkflowActionExecutorException extends CustomException {
code: WorkflowActionExecutorExceptionCode;
constructor(message: string, code: WorkflowActionExecutorExceptionCode) {
super(message, code);
}
}
export enum WorkflowActionExecutorExceptionCode {
SCOPED_WORKSPACE_NOT_FOUND = 'SCOPED_WORKSPACE_NOT_FOUND',
}

View File

@ -1,23 +0,0 @@
import { Injectable } from '@nestjs/common';
import { WorkflowActionType } from 'src/modules/workflow/common/types/workflow-action.type';
import { WorkflowActionExecutor } from 'src/modules/workflow/workflow-action-executor/workflow-action-executor.interface';
import { CodeWorkflowActionExecutor } from 'src/modules/workflow/workflow-action-executor/workflow-action-executors/code-workflow-action-executor';
@Injectable()
export class WorkflowActionExecutorFactory {
constructor(
private readonly codeWorkflowActionExecutor: CodeWorkflowActionExecutor,
) {}
get(actionType: WorkflowActionType): WorkflowActionExecutor {
switch (actionType) {
case WorkflowActionType.CODE:
return this.codeWorkflowActionExecutor;
default:
throw new Error(
`Workflow action executor not found for action type '${actionType}'`,
);
}
}
}

View File

@ -1,17 +0,0 @@
import { Module } from '@nestjs/common';
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 { WorkflowActionExecutorFactory } from 'src/modules/workflow/workflow-action-executor/workflow-action-executor.factory';
import { CodeWorkflowActionExecutor } from 'src/modules/workflow/workflow-action-executor/workflow-action-executors/code-workflow-action-executor';
@Module({
imports: [ServerlessFunctionModule],
providers: [
WorkflowActionExecutorFactory,
CodeWorkflowActionExecutor,
ScopedWorkspaceContextFactory,
],
exports: [WorkflowActionExecutorFactory],
})
export class WorkflowActionExecutorModule {}

View File

@ -1,11 +1,11 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module'; import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
import { WorkflowActionExecutorModule } from 'src/modules/workflow/workflow-action-executor/workflow-action-executor.module';
import { WorkflowExecutorWorkspaceService } from 'src/modules/workflow/workflow-executor/workflow-executor.workspace-service'; import { WorkflowExecutorWorkspaceService } from 'src/modules/workflow/workflow-executor/workflow-executor.workspace-service';
import { WorkflowStepExecutorModule } from 'src/modules/workflow/workflow-step-executor/workflow-step-executor.module';
@Module({ @Module({
imports: [WorkflowCommonModule, WorkflowActionExecutorModule], imports: [WorkflowCommonModule, WorkflowStepExecutorModule],
providers: [WorkflowExecutorWorkspaceService], providers: [WorkflowExecutorWorkspaceService],
exports: [WorkflowExecutorWorkspaceService], exports: [WorkflowExecutorWorkspaceService],
}) })

View File

@ -1,11 +1,11 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { WorkflowAction } from 'src/modules/workflow/common/types/workflow-action.type'; import { WorkflowStep } from 'src/modules/workflow/common/types/workflow-step.type';
import { WorkflowActionExecutorFactory } from 'src/modules/workflow/workflow-action-executor/workflow-action-executor.factory';
import { import {
WorkflowExecutorException, WorkflowExecutorException,
WorkflowExecutorExceptionCode, WorkflowExecutorExceptionCode,
} from 'src/modules/workflow/workflow-executor/workflow-executor.exception'; } from 'src/modules/workflow/workflow-executor/workflow-executor.exception';
import { WorkflowStepExecutorFactory } from 'src/modules/workflow/workflow-step-executor/workflow-step-executor.factory';
const MAX_RETRIES_ON_FAILURE = 3; const MAX_RETRIES_ON_FAILURE = 3;
@ -17,36 +17,41 @@ export type WorkflowExecutionOutput = {
@Injectable() @Injectable()
export class WorkflowExecutorWorkspaceService { export class WorkflowExecutorWorkspaceService {
constructor( constructor(
private readonly workflowActionExecutorFactory: WorkflowActionExecutorFactory, private readonly workflowStepExecutorFactory: WorkflowStepExecutorFactory,
) {} ) {}
async execute({ async execute({
action, currentStepIndex,
steps,
payload, payload,
attemptCount = 1, attemptCount = 1,
}: { }: {
action?: WorkflowAction; currentStepIndex: number;
steps: WorkflowStep[];
payload?: object; payload?: object;
attemptCount?: number; attemptCount?: number;
}): Promise<WorkflowExecutionOutput> { }): Promise<WorkflowExecutionOutput> {
if (!action) { if (currentStepIndex >= steps.length) {
return { return {
data: payload, data: payload,
}; };
} }
const workflowActionExecutor = this.workflowActionExecutorFactory.get( const step = steps[currentStepIndex];
action.type,
const workflowStepExecutor = this.workflowStepExecutorFactory.get(
step.type,
); );
const result = await workflowActionExecutor.execute({ const result = await workflowStepExecutor.execute({
action, step,
payload, payload,
}); });
if (result.data) { if (result.data) {
return await this.execute({ return await this.execute({
action: action.nextAction, currentStepIndex: currentStepIndex + 1,
steps,
payload: result.data, payload: result.data,
}); });
} }
@ -58,19 +63,21 @@ export class WorkflowExecutorWorkspaceService {
); );
} }
if (action.settings.errorHandlingOptions.continueOnFailure.value) { if (step.settings.errorHandlingOptions.continueOnFailure.value) {
return await this.execute({ return await this.execute({
action: action.nextAction, currentStepIndex: currentStepIndex + 1,
steps,
payload, payload,
}); });
} }
if ( if (
action.settings.errorHandlingOptions.retryOnFailure.value && step.settings.errorHandlingOptions.retryOnFailure.value &&
attemptCount < MAX_RETRIES_ON_FAILURE attemptCount < MAX_RETRIES_ON_FAILURE
) { ) {
return await this.execute({ return await this.execute({
action, currentStepIndex,
steps,
payload, payload,
attemptCount: attemptCount + 1, attemptCount: attemptCount + 1,
}); });

View File

@ -38,7 +38,8 @@ export class RunWorkflowJob {
try { try {
await this.workflowExecutorWorkspaceService.execute({ await this.workflowExecutorWorkspaceService.execute({
action: workflowVersion.trigger.nextAction, currentStepIndex: 0,
steps: workflowVersion.steps || [],
payload, payload,
}); });

View File

@ -0,0 +1,13 @@
import { CustomException } from 'src/utils/custom-exception';
export class WorkflowStepExecutorException extends CustomException {
code: WorkflowStepExecutorExceptionCode;
constructor(message: string, code: WorkflowStepExecutorExceptionCode) {
super(message, code);
}
}
export enum WorkflowStepExecutorExceptionCode {
SCOPED_WORKSPACE_NOT_FOUND = 'SCOPED_WORKSPACE_NOT_FOUND',
INVALID_STEP_TYPE = 'INVALID_STEP_TYPE',
}

View File

@ -0,0 +1,26 @@
import { Injectable } from '@nestjs/common';
import { WorkflowStepType } from 'src/modules/workflow/common/types/workflow-step.type';
import {
WorkflowStepExecutorException,
WorkflowStepExecutorExceptionCode,
} from 'src/modules/workflow/workflow-step-executor/workflow-step-executor.exception';
import { WorkflowStepExecutor } from 'src/modules/workflow/workflow-step-executor/workflow-step-executor.interface';
import { CodeActionExecutor } from 'src/modules/workflow/workflow-step-executor/workflow-step-executors/code-action-executor';
@Injectable()
export class WorkflowStepExecutorFactory {
constructor(private readonly codeActionExecutor: CodeActionExecutor) {}
get(stepType: WorkflowStepType): WorkflowStepExecutor {
switch (stepType) {
case WorkflowStepType.CODE_ACTION:
return this.codeActionExecutor;
default:
throw new WorkflowStepExecutorException(
`Workflow step executor not found for step type '${stepType}'`,
WorkflowStepExecutorExceptionCode.INVALID_STEP_TYPE,
);
}
}
}

View File

@ -1,12 +1,12 @@
import { WorkflowAction } from 'src/modules/workflow/common/types/workflow-action.type';
import { WorkflowResult } from 'src/modules/workflow/common/types/workflow-result.type'; import { WorkflowResult } from 'src/modules/workflow/common/types/workflow-result.type';
import { WorkflowStep } from 'src/modules/workflow/common/types/workflow-step.type';
export interface WorkflowActionExecutor { export interface WorkflowStepExecutor {
execute({ execute({
action, step,
payload, payload,
}: { }: {
action: WorkflowAction; step: WorkflowStep;
payload?: object; payload?: object;
}): Promise<WorkflowResult>; }): Promise<WorkflowResult>;
} }

View File

@ -0,0 +1,17 @@
import { Module } from '@nestjs/common';
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 { WorkflowStepExecutorFactory } from 'src/modules/workflow/workflow-step-executor/workflow-step-executor.factory';
import { CodeActionExecutor } from 'src/modules/workflow/workflow-step-executor/workflow-step-executors/code-action-executor';
@Module({
imports: [ServerlessFunctionModule],
providers: [
WorkflowStepExecutorFactory,
CodeActionExecutor,
ScopedWorkspaceContextFactory,
],
exports: [WorkflowStepExecutorFactory],
})
export class WorkflowStepExecutorModule {}

View File

@ -2,39 +2,39 @@ import { Injectable } from '@nestjs/common';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; 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 { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
import { WorkflowAction } from 'src/modules/workflow/common/types/workflow-action.type';
import { WorkflowResult } from 'src/modules/workflow/common/types/workflow-result.type'; import { WorkflowResult } from 'src/modules/workflow/common/types/workflow-result.type';
import { WorkflowCodeStep } from 'src/modules/workflow/common/types/workflow-step.type';
import { import {
WorkflowActionExecutorException, WorkflowStepExecutorException,
WorkflowActionExecutorExceptionCode, WorkflowStepExecutorExceptionCode,
} from 'src/modules/workflow/workflow-action-executor/workflow-action-executor.exception'; } from 'src/modules/workflow/workflow-step-executor/workflow-step-executor.exception';
import { WorkflowActionExecutor } from 'src/modules/workflow/workflow-action-executor/workflow-action-executor.interface'; import { WorkflowStepExecutor } from 'src/modules/workflow/workflow-step-executor/workflow-step-executor.interface';
@Injectable() @Injectable()
export class CodeWorkflowActionExecutor implements WorkflowActionExecutor { export class CodeActionExecutor implements WorkflowStepExecutor {
constructor( constructor(
private readonly serverlessFunctionService: ServerlessFunctionService, private readonly serverlessFunctionService: ServerlessFunctionService,
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory, private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
) {} ) {}
async execute({ async execute({
action, step,
payload, payload,
}: { }: {
action: WorkflowAction; step: WorkflowCodeStep;
payload?: object; payload?: object;
}): Promise<WorkflowResult> { }): Promise<WorkflowResult> {
const { workspaceId } = this.scopedWorkspaceContextFactory.create(); const { workspaceId } = this.scopedWorkspaceContextFactory.create();
if (!workspaceId) { if (!workspaceId) {
throw new WorkflowActionExecutorException( throw new WorkflowStepExecutorException(
'Scoped workspace not found', 'Scoped workspace not found',
WorkflowActionExecutorExceptionCode.SCOPED_WORKSPACE_NOT_FOUND, WorkflowStepExecutorExceptionCode.SCOPED_WORKSPACE_NOT_FOUND,
); );
} }
const result = await this.serverlessFunctionService.executeOne( const result = await this.serverlessFunctionService.executeOne(
action.settings.serverlessFunctionId, step.settings.serverlessFunctionId,
workspaceId, workspaceId,
payload, payload,
); );

View File

@ -27,9 +27,9 @@ export function assertWorkflowVersionIsValid(
); );
} }
if (!workflowVersion.trigger.nextAction) { if (!workflowVersion.steps || workflowVersion.steps.length === 0) {
throw new WorkflowTriggerException( throw new WorkflowTriggerException(
'No next action provided in trigger', 'No steps provided in workflow version',
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER, WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
); );
} }