Compare commits

..
Author SHA1 Message Date
Charles Bochet 3e0e1ccb86 refactor(server): extract static step builders from workflow-version-step-operations
The `runStepCreationSideEffectsAndBuildStep` method mixed 12 pure/static
step builders with 5 async ones that have side effects. Extract the static
builders into a `buildStaticWorkflowStep` utility and shared constants.

| File | Before | After |
|---|---|---|
| `workflow-version-step-operations.workspace-service.ts` | 1005 | 782 |
| `build-static-workflow-step.util.ts` | — | 237 |
| `workflow-step.constants.ts` | — | 21 |

No behavior change.
2026-06-04 03:03:19 +02:00
5 changed files with 1289 additions and 831 deletions
File diff suppressed because it is too large Load Diff
@@ -1,106 +0,0 @@
import { type OpenAPIV3_1 } from 'openapi-types';
import { capitalize } from 'twenty-shared/utils';
type SchemaOrRef = OpenAPIV3_1.SchemaObject | OpenAPIV3_1.ReferenceObject;
export type MetadataSchemaConfig = {
nameSingular: string;
namePlural: string;
description: string;
properties: Record<string, SchemaOrRef>;
requiredFields?: string[];
updateDescription?: string;
updateProperties: Record<string, SchemaOrRef>;
responseExtraProperties?: Record<string, SchemaOrRef>;
responseCommonProperties?: Record<string, SchemaOrRef>;
extraSchemas?: Record<string, OpenAPIV3_1.SchemaObject>;
};
const DEFAULT_RESPONSE_COMMON_PROPERTIES: Record<string, SchemaOrRef> = {
workspaceId: { type: 'string', format: 'uuid' },
createdAt: { type: 'string', format: 'date-time' },
updatedAt: { type: 'string', format: 'date-time' },
deletedAt: { type: 'string', format: 'date-time' },
};
const stripDefaults = (
properties: Record<string, SchemaOrRef>,
): Record<string, SchemaOrRef> => {
return Object.fromEntries(
Object.entries(properties).map(([key, value]) => {
if ('default' in value) {
const { default: _, ...rest } = value;
return [key, rest];
}
return [key, value];
}),
);
};
export const computeMetadataSchemaForEntity = (
config: MetadataSchemaConfig,
): Record<string, OpenAPIV3_1.SchemaObject> => {
const {
nameSingular,
namePlural,
description,
properties,
requiredFields,
updateDescription,
updateProperties,
responseExtraProperties = {},
responseCommonProperties = DEFAULT_RESPONSE_COMMON_PROPERTIES,
extraSchemas = {},
} = config;
const singularKey = capitalize(nameSingular);
const pluralKey = capitalize(namePlural);
const schemas: Record<string, OpenAPIV3_1.SchemaObject> = {
...extraSchemas,
};
schemas[singularKey] = {
type: 'object',
description,
properties,
...(requiredFields?.length ? { required: requiredFields } : {}),
};
schemas[pluralKey] = {
type: 'array',
description: `A list of ${namePlural}`,
items: {
$ref: `#/components/schemas/${singularKey}`,
},
};
schemas[`${singularKey}ForUpdate`] = {
type: 'object',
description: updateDescription ?? `${description} for update`,
properties: updateProperties,
};
schemas[`${singularKey}ForResponse`] = {
type: 'object',
description,
properties: {
id: { type: 'string', format: 'uuid' },
...stripDefaults(properties),
...responseExtraProperties,
...responseCommonProperties,
},
};
schemas[`${pluralKey}ForResponse`] = {
type: 'array',
description: `A list of ${namePlural}`,
items: {
$ref: `#/components/schemas/${singularKey}ForResponse`,
},
};
return schemas;
};
@@ -0,0 +1,20 @@
import { type BaseWorkflowActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type';
export const BASE_STEP_DEFINITION: BaseWorkflowActionSettings = {
outputSchema: {},
errorHandlingOptions: {
continueOnFailure: {
value: false,
},
retryOnFailure: {
value: false,
},
},
};
export const DUPLICATED_STEP_POSITION_OFFSET = 50;
export const ITERATOR_EMPTY_STEP_POSITION_OFFSET = {
x: 174,
y: 83,
};
@@ -0,0 +1,236 @@
import { type WorkflowStepPositionInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-step-position.input';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { BASE_STEP_DEFINITION } from 'src/modules/workflow/workflow-builder/workflow-version-step/constants/workflow-step.constants';
import {
WorkflowActionType,
type WorkflowAction,
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
export const buildStaticWorkflowStep = ({
baseStep,
type,
activeObjectMetadataItem,
}: {
baseStep: {
id: string;
position?: WorkflowStepPositionInput;
valid: boolean;
nextStepIds: string[];
};
type: WorkflowActionType;
activeObjectMetadataItem?: ObjectMetadataEntity | null;
}): { builtStep: WorkflowAction } | undefined => {
switch (type) {
case WorkflowActionType.SEND_EMAIL: {
return {
builtStep: {
...baseStep,
name: 'Send Email',
type: WorkflowActionType.SEND_EMAIL,
settings: {
...BASE_STEP_DEFINITION,
input: {
connectedAccountId: '',
recipients: {
to: '',
cc: '',
bcc: '',
},
subject: '',
body: '',
},
},
},
};
}
case WorkflowActionType.DRAFT_EMAIL: {
return {
builtStep: {
...baseStep,
name: 'Draft Email',
type: WorkflowActionType.DRAFT_EMAIL,
settings: {
...BASE_STEP_DEFINITION,
input: {
connectedAccountId: '',
recipients: {
to: '',
cc: '',
bcc: '',
},
subject: '',
body: '',
},
},
},
};
}
case WorkflowActionType.CREATE_RECORD: {
return {
builtStep: {
...baseStep,
name: 'Create Record',
type: WorkflowActionType.CREATE_RECORD,
settings: {
...BASE_STEP_DEFINITION,
input: {
objectName: activeObjectMetadataItem?.nameSingular || '',
objectRecord: {},
},
},
},
};
}
case WorkflowActionType.UPDATE_RECORD: {
return {
builtStep: {
...baseStep,
name: 'Update Record',
type: WorkflowActionType.UPDATE_RECORD,
settings: {
...BASE_STEP_DEFINITION,
input: {
objectName: activeObjectMetadataItem?.nameSingular || '',
objectRecord: {},
objectRecordId: '',
fieldsToUpdate: [],
},
},
},
};
}
case WorkflowActionType.DELETE_RECORD: {
return {
builtStep: {
...baseStep,
name: 'Delete Record',
type: WorkflowActionType.DELETE_RECORD,
settings: {
...BASE_STEP_DEFINITION,
input: {
objectName: activeObjectMetadataItem?.nameSingular || '',
objectRecordId: '',
},
},
},
};
}
case WorkflowActionType.UPSERT_RECORD: {
return {
builtStep: {
...baseStep,
name: 'Create or Update Record',
type: WorkflowActionType.UPSERT_RECORD,
settings: {
...BASE_STEP_DEFINITION,
input: {
objectName: activeObjectMetadataItem?.nameSingular || '',
objectRecord: {},
fieldsToUpdate: [],
},
},
},
};
}
case WorkflowActionType.FIND_RECORDS: {
return {
builtStep: {
...baseStep,
name: 'Search Records',
type: WorkflowActionType.FIND_RECORDS,
settings: {
...BASE_STEP_DEFINITION,
input: {
objectName: activeObjectMetadataItem?.nameSingular || '',
limit: 1,
},
},
},
};
}
case WorkflowActionType.FORM: {
return {
builtStep: {
...baseStep,
name: 'Form',
type: WorkflowActionType.FORM,
settings: {
...BASE_STEP_DEFINITION,
input: [],
},
},
};
}
case WorkflowActionType.FILTER: {
return {
builtStep: {
...baseStep,
name: 'Filter',
type: WorkflowActionType.FILTER,
settings: {
...BASE_STEP_DEFINITION,
input: {
stepFilterGroups: [],
stepFilters: [],
},
},
},
};
}
case WorkflowActionType.HTTP_REQUEST: {
return {
builtStep: {
...baseStep,
name: 'HTTP Request',
type: WorkflowActionType.HTTP_REQUEST,
settings: {
...BASE_STEP_DEFINITION,
input: {
url: '',
method: 'GET',
headers: {},
body: {},
},
},
},
};
}
case WorkflowActionType.DELAY: {
return {
builtStep: {
...baseStep,
name: 'Delay',
type: WorkflowActionType.DELAY,
settings: {
...BASE_STEP_DEFINITION,
input: {
delayType: 'DURATION',
duration: {
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
},
},
},
},
};
}
case WorkflowActionType.EMPTY: {
return {
builtStep: {
...baseStep,
name: 'Add an Action',
type: WorkflowActionType.EMPTY,
valid: true,
settings: {
...BASE_STEP_DEFINITION,
input: {},
},
},
};
}
default:
return undefined;
}
};
@@ -38,7 +38,12 @@ import { type WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
import { type OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type';
import { CodeStepBuildService } from 'src/modules/workflow/workflow-builder/workflow-version-step/code-step/services/code-step-build.service';
import { type BaseWorkflowActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type';
import {
BASE_STEP_DEFINITION,
DUPLICATED_STEP_POSITION_OFFSET,
ITERATOR_EMPTY_STEP_POSITION_OFFSET,
} from 'src/modules/workflow/workflow-builder/workflow-version-step/constants/workflow-step.constants';
import { buildStaticWorkflowStep } from 'src/modules/workflow/workflow-builder/workflow-version-step/utils/build-static-workflow-step.util';
import {
WorkflowActionType,
type WorkflowAction,
@@ -46,24 +51,6 @@ import {
type WorkflowFormAction,
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
import { AUTO_SELECT_SMART_MODEL_ID } from 'twenty-shared/constants';
const BASE_STEP_DEFINITION: BaseWorkflowActionSettings = {
outputSchema: {},
errorHandlingOptions: {
continueOnFailure: {
value: false,
},
retryOnFailure: {
value: false,
},
},
};
const DUPLICATED_STEP_POSITION_OFFSET = 50;
const ITERATOR_EMPTY_STEP_POSITION_OFFSET = {
x: 174,
y: 83,
};
@Injectable()
export class WorkflowVersionStepOperationsWorkspaceService {
@@ -153,6 +140,30 @@ export class WorkflowVersionStepOperationsWorkspaceService {
nextStepIds: [],
};
const recordRelatedTypes = new Set([
WorkflowActionType.CREATE_RECORD,
WorkflowActionType.UPDATE_RECORD,
WorkflowActionType.DELETE_RECORD,
WorkflowActionType.UPSERT_RECORD,
WorkflowActionType.FIND_RECORDS,
]);
const activeObjectMetadataItem = recordRelatedTypes.has(type)
? await this.objectMetadataRepository.findOne({
where: { workspaceId, isActive: true, isSystem: false },
})
: undefined;
const staticResult = buildStaticWorkflowStep({
baseStep,
type,
activeObjectMetadataItem,
});
if (isDefined(staticResult)) {
return staticResult;
}
switch (type) {
case WorkflowActionType.CODE: {
const logicFunctionId = id ?? v4();
@@ -268,205 +279,6 @@ export class WorkflowVersionStepOperationsWorkspaceService {
},
};
}
case WorkflowActionType.SEND_EMAIL: {
return {
builtStep: {
...baseStep,
name: 'Send Email',
type: WorkflowActionType.SEND_EMAIL,
settings: {
...BASE_STEP_DEFINITION,
input: {
connectedAccountId: '',
recipients: {
to: '',
cc: '',
bcc: '',
},
subject: '',
body: '',
},
},
},
};
}
case WorkflowActionType.DRAFT_EMAIL: {
return {
builtStep: {
...baseStep,
name: 'Draft Email',
type: WorkflowActionType.DRAFT_EMAIL,
settings: {
...BASE_STEP_DEFINITION,
input: {
connectedAccountId: '',
recipients: {
to: '',
cc: '',
bcc: '',
},
subject: '',
body: '',
},
},
},
};
}
case WorkflowActionType.CREATE_RECORD: {
const activeObjectMetadataItem =
await this.objectMetadataRepository.findOne({
where: { workspaceId, isActive: true, isSystem: false },
});
return {
builtStep: {
...baseStep,
name: 'Create Record',
type: WorkflowActionType.CREATE_RECORD,
settings: {
...BASE_STEP_DEFINITION,
input: {
objectName: activeObjectMetadataItem?.nameSingular || '',
objectRecord: {},
},
},
},
};
}
case WorkflowActionType.UPDATE_RECORD: {
const activeObjectMetadataItem =
await this.objectMetadataRepository.findOne({
where: { workspaceId, isActive: true, isSystem: false },
});
return {
builtStep: {
...baseStep,
name: 'Update Record',
type: WorkflowActionType.UPDATE_RECORD,
settings: {
...BASE_STEP_DEFINITION,
input: {
objectName: activeObjectMetadataItem?.nameSingular || '',
objectRecord: {},
objectRecordId: '',
fieldsToUpdate: [],
},
},
},
};
}
case WorkflowActionType.DELETE_RECORD: {
const activeObjectMetadataItem =
await this.objectMetadataRepository.findOne({
where: { workspaceId, isActive: true, isSystem: false },
});
return {
builtStep: {
...baseStep,
name: 'Delete Record',
type: WorkflowActionType.DELETE_RECORD,
settings: {
...BASE_STEP_DEFINITION,
input: {
objectName: activeObjectMetadataItem?.nameSingular || '',
objectRecordId: '',
},
},
},
};
}
case WorkflowActionType.UPSERT_RECORD: {
const activeObjectMetadataItem =
await this.objectMetadataRepository.findOne({
where: { workspaceId, isActive: true, isSystem: false },
});
return {
builtStep: {
...baseStep,
name: 'Create or Update Record',
type: WorkflowActionType.UPSERT_RECORD,
settings: {
...BASE_STEP_DEFINITION,
input: {
objectName: activeObjectMetadataItem?.nameSingular || '',
objectRecord: {},
fieldsToUpdate: [],
},
},
},
};
}
case WorkflowActionType.FIND_RECORDS: {
const activeObjectMetadataItem =
await this.objectMetadataRepository.findOne({
where: { workspaceId, isActive: true, isSystem: false },
});
return {
builtStep: {
...baseStep,
name: 'Search Records',
type: WorkflowActionType.FIND_RECORDS,
settings: {
...BASE_STEP_DEFINITION,
input: {
objectName: activeObjectMetadataItem?.nameSingular || '',
limit: 1,
},
},
},
};
}
case WorkflowActionType.FORM: {
return {
builtStep: {
...baseStep,
name: 'Form',
type: WorkflowActionType.FORM,
settings: {
...BASE_STEP_DEFINITION,
input: [],
},
},
};
}
case WorkflowActionType.FILTER: {
return {
builtStep: {
...baseStep,
name: 'Filter',
type: WorkflowActionType.FILTER,
settings: {
...BASE_STEP_DEFINITION,
input: {
stepFilterGroups: [],
stepFilters: [],
},
},
},
};
}
case WorkflowActionType.HTTP_REQUEST: {
return {
builtStep: {
...baseStep,
name: 'HTTP Request',
type: WorkflowActionType.HTTP_REQUEST,
settings: {
...BASE_STEP_DEFINITION,
input: {
url: '',
method: 'GET',
headers: {},
body: {},
},
},
},
};
}
case WorkflowActionType.AI_AGENT: {
const newAgent = await this.agentService.createOneAgent(
{
@@ -564,41 +376,6 @@ export class WorkflowVersionStepOperationsWorkspaceService {
additionalCreatedSteps: [ifEmptyNode, elseEmptyNode],
};
}
case WorkflowActionType.DELAY: {
return {
builtStep: {
...baseStep,
name: 'Delay',
type: WorkflowActionType.DELAY,
settings: {
...BASE_STEP_DEFINITION,
input: {
delayType: 'DURATION',
duration: {
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
},
},
},
},
};
}
case WorkflowActionType.EMPTY: {
return {
builtStep: {
...baseStep,
name: 'Add an Action',
type: WorkflowActionType.EMPTY,
valid: true,
settings: {
...BASE_STEP_DEFINITION,
input: {},
},
},
};
}
default:
throw new WorkflowVersionStepException(
`WorkflowActionType '${type}' unknown`,