22 branches 3 (#13181)

This PR does not produce any functional changes for our users. It
prepares the branches for workflows by:

- decommissioning `output` and `context` fields or `workflowRun` records
and use newly created `state` field from front-end and back-end
- use `stepStatus` computed by `back-end` in `front-end`
- add utils and types in `twenty-shared/workflow` (not completed, a
follow-up is scheduled
https://github.com/twentyhq/core-team-issues/issues/1211)
- add concurrency to `workflowQueue` message queue to avoid weird branch
execution when using forms in workflow branches
- add a WithLock decorator for better dev experience of
`CacheLockService.withLock` usage

Here is an example of such a workflow running (front branch display is
not yet done that's why it looks ugly) ->
https://discord.com/channels/1130383047699738754/1258024460238192691/1392897615171158098
This commit is contained in:
martmull
2025-07-16 11:16:04 +02:00
committed by GitHub
parent bf6330469b
commit 47386e92a3
47 changed files with 5124 additions and 6380 deletions

View File

@ -4,7 +4,6 @@ import { commandMenuWorkflowRunIdComponentState } from '@/command-menu/pages/wor
import { commandMenuWorkflowVersionIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowVersionIdComponentState'; import { commandMenuWorkflowVersionIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowVersionIdComponentState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useSetInitialWorkflowRunRightDrawerTab } from '@/workflow/workflow-diagram/hooks/useSetInitialWorkflowRunRightDrawerTab'; import { useSetInitialWorkflowRunRightDrawerTab } from '@/workflow/workflow-diagram/hooks/useSetInitialWorkflowRunRightDrawerTab';
import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { import {
@ -13,6 +12,7 @@ import {
IconSettingsAutomation, IconSettingsAutomation,
} from 'twenty-ui/display'; } from 'twenty-ui/display';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { WorkflowRunStepStatus } from '@/workflow/types/Workflow';
export const useWorkflowCommandMenu = () => { export const useWorkflowCommandMenu = () => {
const { navigateCommandMenu } = useNavigateCommandMenu(); const { navigateCommandMenu } = useNavigateCommandMenu();
@ -142,7 +142,7 @@ export const useWorkflowCommandMenu = () => {
title: string; title: string;
icon: IconComponent; icon: IconComponent;
workflowSelectedNode: string; workflowSelectedNode: string;
stepExecutionStatus: WorkflowDiagramRunStatus; stepExecutionStatus: WorkflowRunStepStatus;
}) => { }) => {
const pageId = v4(); const pageId = v4();

View File

@ -20,11 +20,11 @@ import {
WorkflowRunTabId, WorkflowRunTabId,
WorkflowRunTabIdType, WorkflowRunTabIdType,
} from '@/workflow/workflow-steps/types/WorkflowRunTabId'; } from '@/workflow/workflow-steps/types/WorkflowRunTabId';
import { getWorkflowRunStepExecutionStatus } from '@/workflow/workflow-steps/utils/getWorkflowRunStepExecutionStatus';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { isNull } from '@sniptt/guards'; import { isNull } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { IconLogin2, IconLogout, IconStepInto } from 'twenty-ui/display'; import { IconLogin2, IconLogout, IconStepInto } from 'twenty-ui/display';
import { getWorkflowRunStepExecutionStatus } from '@/workflow/workflow-steps/utils/getWorkflowRunStepExecutionStatus';
const StyledContainer = styled.div` const StyledContainer = styled.div`
display: flex; display: flex;
@ -65,9 +65,10 @@ export const CommandMenuWorkflowRunViewStepContent = () => {
} }
const stepExecutionStatus = getWorkflowRunStepExecutionStatus({ const stepExecutionStatus = getWorkflowRunStepExecutionStatus({
workflowRunOutput: workflowRun.output, workflowRunState: workflowRun.state,
stepId: workflowSelectedNode, stepId: workflowSelectedNode,
}); });
const stepDefinition = getStepDefinitionOrThrow({ const stepDefinition = getStepDefinitionOrThrow({
stepId: workflowSelectedNode, stepId: workflowSelectedNode,
trigger: flow.trigger, trigger: flow.trigger,

View File

@ -1,15 +1,15 @@
import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId'; import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
import { WorkflowRunStepStatus } from '@/workflow/types/Workflow';
export const getIsInputTabDisabled = ({ export const getIsInputTabDisabled = ({
stepExecutionStatus, stepExecutionStatus,
workflowSelectedNode, workflowSelectedNode,
}: { }: {
workflowSelectedNode: string; workflowSelectedNode: string;
stepExecutionStatus: WorkflowDiagramRunStatus; stepExecutionStatus: WorkflowRunStepStatus;
}) => { }) => {
return ( return (
workflowSelectedNode === TRIGGER_STEP_ID || workflowSelectedNode === TRIGGER_STEP_ID ||
stepExecutionStatus === 'not-executed' stepExecutionStatus === 'NOT_STARTED'
); );
}; };

View File

@ -1,11 +1,11 @@
import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; import { WorkflowRunStepStatus } from '@/workflow/types/Workflow';
export const getIsOutputTabDisabled = ({ export const getIsOutputTabDisabled = ({
stepExecutionStatus, stepExecutionStatus,
}: { }: {
stepExecutionStatus: WorkflowDiagramRunStatus; stepExecutionStatus: WorkflowRunStepStatus;
}) => { }) => {
return ( return (
stepExecutionStatus === 'running' || stepExecutionStatus === 'not-executed' stepExecutionStatus === 'RUNNING' || stepExecutionStatus === 'NOT_STARTED'
); );
}; };

View File

@ -1,12 +1,14 @@
import { WorkflowActionType } from '@/workflow/types/Workflow'; import {
import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; WorkflowActionType,
WorkflowRunStepStatus,
} from '@/workflow/types/Workflow';
export const getShouldFocusNodeTab = ({ export const getShouldFocusNodeTab = ({
stepExecutionStatus, stepExecutionStatus,
actionType, actionType,
}: { }: {
stepExecutionStatus: WorkflowDiagramRunStatus; stepExecutionStatus: WorkflowRunStepStatus;
actionType: WorkflowActionType | undefined; actionType: WorkflowActionType | undefined;
}) => { }) => {
return actionType === 'FORM' && stepExecutionStatus === 'running'; return actionType === 'FORM' && stepExecutionStatus === 'PENDING';
}; };

View File

@ -8,7 +8,7 @@ import { TimelineActivities } from '@/activities/timeline-activities/components/
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { FieldsCard } from '@/object-record/record-show/components/FieldsCard'; import { FieldsCard } from '@/object-record/record-show/components/FieldsCard';
import { CardType } from '@/object-record/record-show/types/CardType'; import { CardType } from '@/object-record/record-show/types/CardType';
import { ListenRecordUpdatesEffect } from '@/subscription/components/ListenUpdatesEffect'; import { ListenRecordUpdatesEffect } from '@/subscription/components/ListenRecordUpdatesEffect';
import { ShowPageActivityContainer } from '@/ui/layout/show-page/components/ShowPageActivityContainer'; import { ShowPageActivityContainer } from '@/ui/layout/show-page/components/ShowPageActivityContainer';
import { getWorkflowVisualizerComponentInstanceId } from '@/workflow/utils/getWorkflowVisualizerComponentInstanceId'; import { getWorkflowVisualizerComponentInstanceId } from '@/workflow/utils/getWorkflowVisualizerComponentInstanceId';
import { WorkflowRunVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect'; import { WorkflowRunVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect';
@ -192,7 +192,7 @@ export const CardComponents: Record<CardType, CardComponentType> = {
<ListenRecordUpdatesEffect <ListenRecordUpdatesEffect
objectNameSingular={targetableObject.targetObjectNameSingular} objectNameSingular={targetableObject.targetObjectNameSingular}
recordId={targetableObject.id} recordId={targetableObject.id}
listenedFields={['status', 'output']} listenedFields={['status', 'state']}
/> />
<Suspense fallback={<LoadingSkeleton />}> <Suspense fallback={<LoadingSkeleton />}>
<WorkflowRunVisualizer workflowRunId={targetableObject.id} /> <WorkflowRunVisualizer workflowRunId={targetableObject.id} />

View File

@ -8,7 +8,7 @@ import { RecordTableRowArrowKeysEffect } from '@/object-record/record-table/reco
import { RecordTableRowHotkeyEffect } from '@/object-record/record-table/record-table-row/components/RecordTableRowHotkeyEffect'; import { RecordTableRowHotkeyEffect } from '@/object-record/record-table/record-table-row/components/RecordTableRowHotkeyEffect';
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState'; import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState'; import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
import { ListenRecordUpdatesEffect } from '@/subscription/components/ListenUpdatesEffect'; import { ListenRecordUpdatesEffect } from '@/subscription/components/ListenRecordUpdatesEffect';
import { getDefaultRecordFieldsToListen } from '@/subscription/utils/getDefaultRecordFieldsToListen.util'; import { getDefaultRecordFieldsToListen } from '@/subscription/utils/getDefaultRecordFieldsToListen.util';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';

View File

@ -47,15 +47,15 @@ export const useRunWorkflowRunOpeningInCommandMenuSideEffects = () => {
objectPermissionsByObjectMetadataId, objectPermissionsByObjectMetadataId,
}); });
if ( if (
!(isDefined(workflowRunRecord) && isDefined(workflowRunRecord.output)) !(isDefined(workflowRunRecord) && isDefined(workflowRunRecord.state))
) { ) {
return; return;
} }
const { stepToOpenByDefault } = generateWorkflowRunDiagram({ const { stepToOpenByDefault } = generateWorkflowRunDiagram({
steps: workflowRunRecord.output.flow.steps, steps: workflowRunRecord.state.flow.steps,
stepsOutput: workflowRunRecord.output.stepsOutput, stepInfos: workflowRunRecord.state.stepInfos,
trigger: workflowRunRecord.output.flow.trigger, trigger: workflowRunRecord.state.flow.trigger,
}); });
if (!isDefined(stepToOpenByDefault)) { if (!isDefined(stepToOpenByDefault)) {
@ -86,8 +86,8 @@ export const useRunWorkflowRunOpeningInCommandMenuSideEffects = () => {
}), }),
{ {
workflowVersionId: workflowRunRecord.workflowVersionId, workflowVersionId: workflowRunRecord.workflowVersionId,
trigger: workflowRunRecord.output.flow.trigger, trigger: workflowRunRecord.state.flow.trigger,
steps: workflowRunRecord.output.flow.steps, steps: workflowRunRecord.state.flow.steps,
}, },
); );
set( set(

View File

@ -18,11 +18,12 @@ import {
workflowFormActionSettingsSchema, workflowFormActionSettingsSchema,
workflowHttpRequestActionSchema, workflowHttpRequestActionSchema,
workflowManualTriggerSchema, workflowManualTriggerSchema,
workflowRunContextSchema,
workflowRunOutputSchema, workflowRunOutputSchema,
workflowRunOutputStepsOutputSchema, workflowRunOutputStepsOutputSchema,
workflowRunSchema, workflowRunSchema,
workflowRunStateSchema,
workflowRunStatusSchema, workflowRunStatusSchema,
workflowRunStepStatusSchema,
workflowSendEmailActionSchema, workflowSendEmailActionSchema,
workflowSendEmailActionSettingsSchema, workflowSendEmailActionSettingsSchema,
workflowTriggerSchema, workflowTriggerSchema,
@ -150,14 +151,16 @@ export type WorkflowRunOutputStepsOutput = z.infer<
typeof workflowRunOutputStepsOutputSchema typeof workflowRunOutputStepsOutputSchema
>; >;
export type WorkflowRunContext = z.infer<typeof workflowRunContextSchema>;
export type WorkflowRunFlow = WorkflowRunOutput['flow'];
export type WorkflowRunStatus = z.infer<typeof workflowRunStatusSchema>; export type WorkflowRunStatus = z.infer<typeof workflowRunStatusSchema>;
export type WorkflowRun = z.infer<typeof workflowRunSchema>; export type WorkflowRun = z.infer<typeof workflowRunSchema>;
export type WorkflowRunState = z.infer<typeof workflowRunStateSchema>;
export type WorkflowRunStepStatus = z.infer<typeof workflowRunStepStatusSchema>;
export type WorkflowRunFlow = WorkflowRunState['flow'];
export type Workflow = { export type Workflow = {
__typename: 'Workflow'; __typename: 'Workflow';
id: string; id: string;

View File

@ -1,5 +1,6 @@
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { z } from 'zod'; import { z } from 'zod';
import { StepStatus } from 'twenty-shared/workflow';
// Base schemas // Base schemas
export const objectRecordSchema = z.record(z.any()); export const objectRecordSchema = z.record(z.any());
@ -317,6 +318,27 @@ export const workflowRunOutputSchema = z.object({
error: z.any().optional(), error: z.any().optional(),
}); });
export const workflowRunStepStatusSchema = z.nativeEnum(StepStatus);
export const workflowRunStateStepInfoSchema = z.object({
result: z.any().optional(),
error: z.any().optional(),
status: workflowRunStepStatusSchema,
});
export const workflowRunStateStepInfosSchema = z.record(
workflowRunStateStepInfoSchema,
);
export const workflowRunStateSchema = z.object({
flow: z.object({
trigger: workflowTriggerSchema,
steps: z.array(workflowActionSchema),
}),
stepInfos: workflowRunStateStepInfosSchema,
workflowRunError: z.any().optional(),
});
export const workflowRunContextSchema = z.record(z.any()); export const workflowRunContextSchema = z.record(z.any());
export const workflowRunStatusSchema = z.enum([ export const workflowRunStatusSchema = z.enum([
@ -335,6 +357,7 @@ export const workflowRunSchema = z
workflowId: z.string(), workflowId: z.string(),
output: workflowRunOutputSchema.nullable(), output: workflowRunOutputSchema.nullable(),
context: workflowRunContextSchema.nullable(), context: workflowRunContextSchema.nullable(),
state: workflowRunStateSchema.nullable(),
status: workflowRunStatusSchema, status: workflowRunStatusSchema,
createdAt: z.string(), createdAt: z.string(),
deletedAt: z.string().nullable(), deletedAt: z.string().nullable(),

View File

@ -1,27 +1,7 @@
import { WorkflowDiagramStepNodeBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase'; import { WorkflowDiagramStepNodeBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase';
import { WorkflowDiagramStepNodeIcon } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeIcon'; import { WorkflowDiagramStepNodeIcon } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeIcon';
import { import { WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
WorkflowDiagramRunStatus, import { getNodeVariantFromStepRunStatus } from '@/workflow/workflow-diagram/utils/getNodeVariantFromStepRunStatus';
WorkflowDiagramStepNodeData,
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { WorkflowDiagramNodeVariant } from '@/workflow/workflow-diagram/types/WorkflowDiagramNodeVariant';
const getNodeVariantFromRunStatus = (
runStatus: WorkflowDiagramRunStatus | undefined,
): WorkflowDiagramNodeVariant => {
switch (runStatus) {
case 'success':
return 'success';
case 'failure':
return 'failure';
case 'running':
return 'running';
case 'not-executed':
return 'not-executed';
default:
return 'default';
}
};
export const WorkflowDiagramStepNodeReadonly = ({ export const WorkflowDiagramStepNodeReadonly = ({
data, data,
@ -31,7 +11,7 @@ export const WorkflowDiagramStepNodeReadonly = ({
return ( return (
<WorkflowDiagramStepNodeBase <WorkflowDiagramStepNodeBase
name={data.name} name={data.name}
variant={getNodeVariantFromRunStatus(data.runStatus)} variant={getNodeVariantFromStepRunStatus(data.runStatus)}
nodeType={data.nodeType} nodeType={data.nodeType}
Icon={<WorkflowDiagramStepNodeIcon data={data} />} Icon={<WorkflowDiagramStepNodeIcon data={data} />}
/> />

View File

@ -9,7 +9,7 @@ import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion';
import { flowComponentState } from '@/workflow/states/flowComponentState'; import { flowComponentState } from '@/workflow/states/flowComponentState';
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
import { workflowVisualizerWorkflowRunIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowRunIdComponentState'; import { workflowVisualizerWorkflowRunIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowRunIdComponentState';
import { WorkflowRunOutput } from '@/workflow/types/Workflow'; import { WorkflowRunState } from '@/workflow/types/Workflow';
import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState'; import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
import { workflowDiagramStatusComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramStatusComponentState'; import { workflowDiagramStatusComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramStatusComponentState';
import { workflowRunDiagramAutomaticallyOpenedStepsComponentState } from '@/workflow/workflow-diagram/states/workflowRunDiagramAutomaticallyOpenedStepsComponentState'; import { workflowRunDiagramAutomaticallyOpenedStepsComponentState } from '@/workflow/workflow-diagram/states/workflowRunDiagramAutomaticallyOpenedStepsComponentState';
@ -82,15 +82,15 @@ export const WorkflowRunVisualizerEffect = ({
const handleWorkflowRunDiagramGeneration = useRecoilCallback( const handleWorkflowRunDiagramGeneration = useRecoilCallback(
({ snapshot, set }) => ({ snapshot, set }) =>
({ ({
workflowRunOutput, workflowRunState,
workflowVersionId, workflowVersionId,
isInRightDrawer, isInRightDrawer,
}: { }: {
workflowRunOutput: WorkflowRunOutput | undefined; workflowRunState: WorkflowRunState | undefined;
workflowVersionId: string | undefined; workflowVersionId: string | undefined;
isInRightDrawer: boolean; isInRightDrawer: boolean;
}) => { }) => {
if (!(isDefined(workflowRunOutput) && isDefined(workflowVersionId))) { if (!(isDefined(workflowRunState) && isDefined(workflowVersionId))) {
set(flowState, undefined); set(flowState, undefined);
set(workflowDiagramState, undefined); set(workflowDiagramState, undefined);
@ -108,15 +108,15 @@ export const WorkflowRunVisualizerEffect = ({
set(flowState, { set(flowState, {
workflowVersionId, workflowVersionId,
trigger: workflowRunOutput.flow.trigger, trigger: workflowRunState.flow.trigger,
steps: workflowRunOutput.flow.steps, steps: workflowRunState.flow.steps,
}); });
const { diagram: baseWorkflowRunDiagram, stepToOpenByDefault } = const { diagram: baseWorkflowRunDiagram, stepToOpenByDefault } =
generateWorkflowRunDiagram({ generateWorkflowRunDiagram({
trigger: workflowRunOutput.flow.trigger, trigger: workflowRunState.flow.trigger,
steps: workflowRunOutput.flow.steps, steps: workflowRunState.flow.steps,
stepsOutput: workflowRunOutput.stepsOutput, stepInfos: workflowRunState.stepInfos,
}); });
if (isDefined(stepToOpenByDefault)) { if (isDefined(stepToOpenByDefault)) {
@ -197,14 +197,14 @@ export const WorkflowRunVisualizerEffect = ({
useEffect(() => { useEffect(() => {
handleWorkflowRunDiagramGeneration({ handleWorkflowRunDiagramGeneration({
workflowRunOutput: workflowRun?.output ?? undefined, workflowRunState: workflowRun?.state ?? undefined,
workflowVersionId: workflowRun?.workflowVersionId, workflowVersionId: workflowRun?.workflowVersionId,
isInRightDrawer, isInRightDrawer,
}); });
}, [ }, [
handleWorkflowRunDiagramGeneration, handleWorkflowRunDiagramGeneration,
isInRightDrawer, isInRightDrawer,
workflowRun?.output, workflowRun?.state,
workflowRun?.workflowVersionId, workflowRun?.workflowVersionId,
]); ]);

View File

@ -3,10 +3,10 @@ import { getIsOutputTabDisabled } from '@/command-menu/pages/workflow/step/view-
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState'; import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { WorkflowRunTabId } from '@/workflow/workflow-steps/types/WorkflowRunTabId'; import { WorkflowRunTabId } from '@/workflow/workflow-steps/types/WorkflowRunTabId';
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { WorkflowRunStepStatus } from '@/workflow/types/Workflow';
export const useSetInitialWorkflowRunRightDrawerTab = () => { export const useSetInitialWorkflowRunRightDrawerTab = () => {
const setInitialWorkflowRunRightDrawerTab = useRecoilCallback( const setInitialWorkflowRunRightDrawerTab = useRecoilCallback(
@ -16,7 +16,7 @@ export const useSetInitialWorkflowRunRightDrawerTab = () => {
stepExecutionStatus, stepExecutionStatus,
}: { }: {
workflowSelectedNode: string; workflowSelectedNode: string;
stepExecutionStatus: WorkflowDiagramRunStatus; stepExecutionStatus: WorkflowRunStepStatus;
}) => { }) => {
const commandMenuPageInfo = getSnapshotValue( const commandMenuPageInfo = getSnapshotValue(
snapshot, snapshot,

View File

@ -1,5 +1,6 @@
import { import {
WorkflowActionType, WorkflowActionType,
WorkflowRunStepStatus,
WorkflowTriggerType, WorkflowTriggerType,
} from '@/workflow/types/Workflow'; } from '@/workflow/types/Workflow';
import { Edge, Node } from '@xyflow/react'; import { Edge, Node } from '@xyflow/react';
@ -21,32 +22,26 @@ export type WorkflowDiagram = {
edges: Array<WorkflowDiagramEdge>; edges: Array<WorkflowDiagramEdge>;
}; };
export type WorkflowDiagramRunStatus =
| 'running'
| 'success'
| 'failure'
| 'not-executed';
export type WorkflowDiagramStepNodeData = export type WorkflowDiagramStepNodeData =
| { | {
nodeType: 'trigger'; nodeType: 'trigger';
triggerType: WorkflowTriggerType; triggerType: WorkflowTriggerType;
name: string; name: string;
icon?: string; icon?: string;
runStatus?: WorkflowDiagramRunStatus; runStatus?: WorkflowRunStepStatus;
} }
| { | {
nodeType: 'action'; nodeType: 'action';
actionType: WorkflowActionType; actionType: WorkflowActionType;
name: string; name: string;
runStatus?: WorkflowDiagramRunStatus; runStatus?: WorkflowRunStepStatus;
}; };
export type WorkflowRunDiagramStepNodeData = Exclude< export type WorkflowRunDiagramStepNodeData = Exclude<
WorkflowDiagramStepNodeData, WorkflowDiagramStepNodeData,
'runStatus' 'runStatus'
> & { > & {
runStatus: WorkflowDiagramRunStatus; runStatus: WorkflowRunStepStatus;
}; };
export type WorkflowDiagramCreateStepNodeData = { export type WorkflowDiagramCreateStepNodeData = {
@ -66,7 +61,7 @@ export type WorkflowDiagramNodeData =
export type WorkflowRunDiagramNodeData = Exclude< export type WorkflowRunDiagramNodeData = Exclude<
WorkflowDiagramStepNodeData, WorkflowDiagramStepNodeData,
'runStatus' 'runStatus'
> & { runStatus: WorkflowDiagramRunStatus }; > & { runStatus: WorkflowRunStepStatus };
export type EdgeData = { export type EdgeData = {
stepId?: string; stepId?: string;

View File

@ -1,11 +1,8 @@
import { import { WorkflowStep, WorkflowTrigger } from '@/workflow/types/Workflow';
WorkflowRunOutputStepsOutput,
WorkflowStep,
WorkflowTrigger,
} from '@/workflow/types/Workflow';
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { getUuidV4Mock } from '~/testing/utils/getUuidV4Mock'; import { getUuidV4Mock } from '~/testing/utils/getUuidV4Mock';
import { generateWorkflowRunDiagram } from '../generateWorkflowRunDiagram'; import { generateWorkflowRunDiagram } from '../generateWorkflowRunDiagram';
import { StepStatus, WorkflowRunStepInfos } from 'twenty-shared/workflow';
jest.mock('uuid', () => ({ jest.mock('uuid', () => ({
v4: getUuidV4Mock(), v4: getUuidV4Mock(),
@ -82,14 +79,28 @@ describe('generateWorkflowRunDiagram', () => {
}, },
]; ];
const stepsOutput: WorkflowRunOutputStepsOutput = { const stepInfos: WorkflowRunStepInfos = {
trigger: {
result: {},
status: StepStatus.SUCCESS,
},
step1: { step1: {
result: undefined,
error: '', error: '',
status: StepStatus.FAILED,
},
step2: {
status: StepStatus.NOT_STARTED,
},
step3: {
status: StepStatus.NOT_STARTED,
}, },
}; };
const result = generateWorkflowRunDiagram({ trigger, steps, stepsOutput }); const result = generateWorkflowRunDiagram({
trigger,
steps,
stepInfos,
});
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
{ {
@ -130,7 +141,7 @@ describe('generateWorkflowRunDiagram', () => {
"icon": "IconPlaylistAdd", "icon": "IconPlaylistAdd",
"name": "Company created", "name": "Company created",
"nodeType": "trigger", "nodeType": "trigger",
"runStatus": "success", "runStatus": "SUCCESS",
"triggerType": "DATABASE_EVENT", "triggerType": "DATABASE_EVENT",
}, },
"id": "trigger", "id": "trigger",
@ -144,7 +155,7 @@ describe('generateWorkflowRunDiagram', () => {
"actionType": "CODE", "actionType": "CODE",
"name": "Step 1", "name": "Step 1",
"nodeType": "action", "nodeType": "action",
"runStatus": "failure", "runStatus": "FAILED",
}, },
"id": "step1", "id": "step1",
"position": { "position": {
@ -157,7 +168,7 @@ describe('generateWorkflowRunDiagram', () => {
"actionType": "CODE", "actionType": "CODE",
"name": "Step 2", "name": "Step 2",
"nodeType": "action", "nodeType": "action",
"runStatus": "not-executed", "runStatus": "NOT_STARTED",
}, },
"id": "step2", "id": "step2",
"position": { "position": {
@ -170,7 +181,7 @@ describe('generateWorkflowRunDiagram', () => {
"actionType": "CODE", "actionType": "CODE",
"name": "Step 3", "name": "Step 3",
"nodeType": "action", "nodeType": "action",
"runStatus": "not-executed", "runStatus": "NOT_STARTED",
}, },
"id": "step3", "id": "step3",
"position": { "position": {
@ -255,22 +266,30 @@ describe('generateWorkflowRunDiagram', () => {
}, },
]; ];
const stepsOutput: WorkflowRunOutputStepsOutput = { const stepInfos: WorkflowRunStepInfos = {
trigger: {
result: {},
status: StepStatus.SUCCESS,
},
step1: { step1: {
result: {}, result: {},
error: undefined, status: StepStatus.SUCCESS,
}, },
step2: { step2: {
result: {}, result: {},
error: undefined, status: StepStatus.SUCCESS,
}, },
step3: { step3: {
result: {}, result: {},
error: undefined, status: StepStatus.SUCCESS,
}, },
}; };
const result = generateWorkflowRunDiagram({ trigger, steps, stepsOutput }); const result = generateWorkflowRunDiagram({
trigger,
steps,
stepInfos,
});
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
{ {
@ -313,7 +332,7 @@ describe('generateWorkflowRunDiagram', () => {
"icon": "IconPlaylistAdd", "icon": "IconPlaylistAdd",
"name": "Company created", "name": "Company created",
"nodeType": "trigger", "nodeType": "trigger",
"runStatus": "success", "runStatus": "SUCCESS",
"triggerType": "DATABASE_EVENT", "triggerType": "DATABASE_EVENT",
}, },
"id": "trigger", "id": "trigger",
@ -327,7 +346,7 @@ describe('generateWorkflowRunDiagram', () => {
"actionType": "CODE", "actionType": "CODE",
"name": "Step 1", "name": "Step 1",
"nodeType": "action", "nodeType": "action",
"runStatus": "success", "runStatus": "SUCCESS",
}, },
"id": "step1", "id": "step1",
"position": { "position": {
@ -340,7 +359,7 @@ describe('generateWorkflowRunDiagram', () => {
"actionType": "CODE", "actionType": "CODE",
"name": "Step 2", "name": "Step 2",
"nodeType": "action", "nodeType": "action",
"runStatus": "success", "runStatus": "SUCCESS",
}, },
"id": "step2", "id": "step2",
"position": { "position": {
@ -353,7 +372,7 @@ describe('generateWorkflowRunDiagram', () => {
"actionType": "CODE", "actionType": "CODE",
"name": "Step 3", "name": "Step 3",
"nodeType": "action", "nodeType": "action",
"runStatus": "success", "runStatus": "SUCCESS",
}, },
"id": "step3", "id": "step3",
"position": { "position": {
@ -438,9 +457,30 @@ describe('generateWorkflowRunDiagram', () => {
}, },
]; ];
const stepsOutput = undefined; const stepInfos: WorkflowRunStepInfos = {
trigger: {
result: {},
status: StepStatus.SUCCESS,
},
step1: {
error: '',
status: StepStatus.RUNNING,
},
step2: {
error: '',
status: StepStatus.NOT_STARTED,
},
step3: {
error: '',
status: StepStatus.NOT_STARTED,
},
};
const result = generateWorkflowRunDiagram({ trigger, steps, stepsOutput }); const result = generateWorkflowRunDiagram({
trigger,
steps,
stepInfos,
});
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
{ {
@ -481,7 +521,7 @@ describe('generateWorkflowRunDiagram', () => {
"icon": "IconPlaylistAdd", "icon": "IconPlaylistAdd",
"name": "Company created", "name": "Company created",
"nodeType": "trigger", "nodeType": "trigger",
"runStatus": "success", "runStatus": "SUCCESS",
"triggerType": "DATABASE_EVENT", "triggerType": "DATABASE_EVENT",
}, },
"id": "trigger", "id": "trigger",
@ -495,7 +535,7 @@ describe('generateWorkflowRunDiagram', () => {
"actionType": "CODE", "actionType": "CODE",
"name": "Step 1", "name": "Step 1",
"nodeType": "action", "nodeType": "action",
"runStatus": "running", "runStatus": "RUNNING",
}, },
"id": "step1", "id": "step1",
"position": { "position": {
@ -508,7 +548,7 @@ describe('generateWorkflowRunDiagram', () => {
"actionType": "CODE", "actionType": "CODE",
"name": "Step 2", "name": "Step 2",
"nodeType": "action", "nodeType": "action",
"runStatus": "not-executed", "runStatus": "NOT_STARTED",
}, },
"id": "step2", "id": "step2",
"position": { "position": {
@ -521,7 +561,7 @@ describe('generateWorkflowRunDiagram', () => {
"actionType": "CODE", "actionType": "CODE",
"name": "Step 3", "name": "Step 3",
"nodeType": "action", "nodeType": "action",
"runStatus": "not-executed", "runStatus": "NOT_STARTED",
}, },
"id": "step3", "id": "step3",
"position": { "position": {
@ -625,14 +665,30 @@ describe('generateWorkflowRunDiagram', () => {
}, },
]; ];
const stepsOutput: WorkflowRunOutputStepsOutput = { const stepInfos: WorkflowRunStepInfos = {
trigger: {
result: {},
status: StepStatus.SUCCESS,
},
step1: { step1: {
result: {}, result: {},
error: undefined, status: StepStatus.SUCCESS,
},
step2: {
result: {},
status: StepStatus.RUNNING,
},
step3: {
result: {},
status: StepStatus.NOT_STARTED,
}, },
}; };
const result = generateWorkflowRunDiagram({ trigger, steps, stepsOutput }); const result = generateWorkflowRunDiagram({
trigger,
steps,
stepInfos,
});
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
{ {
@ -683,7 +739,7 @@ describe('generateWorkflowRunDiagram', () => {
"icon": "IconPlaylistAdd", "icon": "IconPlaylistAdd",
"name": "Company created", "name": "Company created",
"nodeType": "trigger", "nodeType": "trigger",
"runStatus": "success", "runStatus": "SUCCESS",
"triggerType": "DATABASE_EVENT", "triggerType": "DATABASE_EVENT",
}, },
"id": "trigger", "id": "trigger",
@ -697,7 +753,7 @@ describe('generateWorkflowRunDiagram', () => {
"actionType": "CODE", "actionType": "CODE",
"name": "Step 1", "name": "Step 1",
"nodeType": "action", "nodeType": "action",
"runStatus": "success", "runStatus": "SUCCESS",
}, },
"id": "step1", "id": "step1",
"position": { "position": {
@ -710,7 +766,7 @@ describe('generateWorkflowRunDiagram', () => {
"actionType": "CODE", "actionType": "CODE",
"name": "Step 2", "name": "Step 2",
"nodeType": "action", "nodeType": "action",
"runStatus": "running", "runStatus": "RUNNING",
}, },
"id": "step2", "id": "step2",
"position": { "position": {
@ -723,7 +779,7 @@ describe('generateWorkflowRunDiagram', () => {
"actionType": "CODE", "actionType": "CODE",
"name": "Step 3", "name": "Step 3",
"nodeType": "action", "nodeType": "action",
"runStatus": "not-executed", "runStatus": "NOT_STARTED",
}, },
"id": "step3", "id": "step3",
"position": { "position": {
@ -736,7 +792,7 @@ describe('generateWorkflowRunDiagram', () => {
"actionType": "CODE", "actionType": "CODE",
"name": "Step 4", "name": "Step 4",
"nodeType": "action", "nodeType": "action",
"runStatus": "not-executed", "runStatus": "NOT_STARTED",
}, },
"id": "step4", "id": "step4",
"position": { "position": {
@ -786,15 +842,31 @@ describe('generateWorkflowRunDiagram', () => {
nextStepIds: undefined, nextStepIds: undefined,
}, },
]; ];
const stepsOutput = {
const stepInfos: WorkflowRunStepInfos = {
trigger: {
result: {},
status: StepStatus.SUCCESS,
},
step1: { step1: {
result: undefined, result: {},
error: undefined, status: StepStatus.PENDING,
pendingEvent: true, },
step2: {
result: {},
status: StepStatus.NOT_STARTED,
},
step3: {
result: {},
status: StepStatus.NOT_STARTED,
}, },
}; };
const result = generateWorkflowRunDiagram({ trigger, steps, stepsOutput }); const result = generateWorkflowRunDiagram({
trigger,
steps,
stepInfos,
});
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
{ {
@ -817,7 +889,7 @@ describe('generateWorkflowRunDiagram', () => {
"icon": "IconPlaylistAdd", "icon": "IconPlaylistAdd",
"name": "Company created", "name": "Company created",
"nodeType": "trigger", "nodeType": "trigger",
"runStatus": "success", "runStatus": "SUCCESS",
"triggerType": "DATABASE_EVENT", "triggerType": "DATABASE_EVENT",
}, },
"id": "trigger", "id": "trigger",
@ -831,7 +903,7 @@ describe('generateWorkflowRunDiagram', () => {
"actionType": "FORM", "actionType": "FORM",
"name": "Step 1", "name": "Step 1",
"nodeType": "action", "nodeType": "action",
"runStatus": "running", "runStatus": "PENDING",
}, },
"id": "step1", "id": "step1",
"position": { "position": {
@ -846,7 +918,7 @@ describe('generateWorkflowRunDiagram', () => {
"actionType": "FORM", "actionType": "FORM",
"name": "Step 1", "name": "Step 1",
"nodeType": "action", "nodeType": "action",
"runStatus": "running", "runStatus": "PENDING",
}, },
"id": "step1", "id": "step1",
}, },

View File

@ -1,11 +1,6 @@
import { import { WorkflowStep, WorkflowTrigger } from '@/workflow/types/Workflow';
WorkflowRunOutputStepsOutput,
WorkflowStep,
WorkflowTrigger,
} from '@/workflow/types/Workflow';
import { WORKFLOW_VISUALIZER_EDGE_SUCCESS_CONFIGURATION } from '@/workflow/workflow-diagram/constants/WorkflowVisualizerEdgeSuccessConfiguration'; import { WORKFLOW_VISUALIZER_EDGE_SUCCESS_CONFIGURATION } from '@/workflow/workflow-diagram/constants/WorkflowVisualizerEdgeSuccessConfiguration';
import { import {
WorkflowDiagramRunStatus,
WorkflowRunDiagram, WorkflowRunDiagram,
WorkflowRunDiagramNode, WorkflowRunDiagramNode,
WorkflowRunDiagramStepNodeData, WorkflowRunDiagramStepNodeData,
@ -14,15 +9,16 @@ import { generateWorkflowDiagram } from '@/workflow/workflow-diagram/utils/gener
import { isStepNode } from '@/workflow/workflow-diagram/utils/isStepNode'; import { isStepNode } from '@/workflow/workflow-diagram/utils/isStepNode';
import { transformFilterNodesAsEdges } from '@/workflow/workflow-diagram/utils/transformFilterNodesAsEdges'; import { transformFilterNodesAsEdges } from '@/workflow/workflow-diagram/utils/transformFilterNodesAsEdges';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { WorkflowRunStepInfos, StepStatus } from 'twenty-shared/workflow';
export const generateWorkflowRunDiagram = ({ export const generateWorkflowRunDiagram = ({
trigger, trigger,
steps, steps,
stepsOutput, stepInfos,
}: { }: {
trigger: WorkflowTrigger; trigger: WorkflowTrigger;
steps: Array<WorkflowStep>; steps: Array<WorkflowStep>;
stepsOutput: WorkflowRunOutputStepsOutput | undefined; stepInfos: WorkflowRunStepInfos | undefined;
}): { }): {
diagram: WorkflowRunDiagram; diagram: WorkflowRunDiagram;
stepToOpenByDefault: stepToOpenByDefault:
@ -43,50 +39,29 @@ export const generateWorkflowRunDiagram = ({
generateWorkflowDiagram({ trigger, steps }), generateWorkflowDiagram({ trigger, steps }),
); );
let skippedExecution = false;
const workflowRunDiagramNodes: WorkflowRunDiagramNode[] = const workflowRunDiagramNodes: WorkflowRunDiagramNode[] =
workflowDiagram.nodes.filter(isStepNode).map((node) => { workflowDiagram.nodes.filter(isStepNode).map((node) => {
if (node.data.nodeType === 'trigger') { const nodeId = node.id;
const stepInfo = stepInfos?.[nodeId];
if (!isDefined(stepInfo)) {
return { return {
...node, ...node,
data: { data: {
...node.data, ...node.data,
runStatus: 'success', runStatus: StepStatus.NOT_STARTED,
}, },
}; };
} }
const nodeId = node.id; const nodeData = {
...node.data,
runStatus: stepInfo.status,
};
const runResult = stepsOutput?.[nodeId]; if (!isDefined(stepToOpenByDefault) && stepInfo.status === 'PENDING') {
stepToOpenByDefault = { id: nodeId, data: nodeData };
const isPendingFormAction =
node.data.nodeType === 'action' &&
node.data.actionType === 'FORM' &&
isDefined(runResult?.pendingEvent) &&
runResult.pendingEvent;
let runStatus: WorkflowDiagramRunStatus = 'success';
if (skippedExecution) {
runStatus = 'not-executed';
} else if (!isDefined(runResult) || isPendingFormAction) {
runStatus = 'running';
} else if (isDefined(runResult.error)) {
runStatus = 'failure';
}
skippedExecution =
skippedExecution || runStatus === 'failure' || runStatus === 'running';
const nodeData = { ...node.data, runStatus };
if (isPendingFormAction) {
stepToOpenByDefault = {
id: nodeId,
data: nodeData,
};
} }
return { return {
@ -100,7 +75,17 @@ export const generateWorkflowRunDiagram = ({
(node) => node.id === edge.source, (node) => node.id === edge.source,
); );
if (isDefined(parentNode) && parentNode.data.runStatus === 'success') { if (!isDefined(parentNode)) {
return edge;
}
const stepInfo = stepInfos?.[parentNode.id];
if (!isDefined(stepInfo)) {
return edge;
}
if (stepInfo.status === 'SUCCESS') {
return { return {
...edge, ...edge,
...WORKFLOW_VISUALIZER_EDGE_SUCCESS_CONFIGURATION, ...WORKFLOW_VISUALIZER_EDGE_SUCCESS_CONFIGURATION,

View File

@ -0,0 +1,20 @@
import { WorkflowRunStepStatus } from '@/workflow/types/Workflow';
import { WorkflowDiagramNodeVariant } from '@/workflow/workflow-diagram/types/WorkflowDiagramNodeVariant';
export const getNodeVariantFromStepRunStatus = (
runStatus: WorkflowRunStepStatus | undefined,
): WorkflowDiagramNodeVariant => {
switch (runStatus) {
case 'SUCCESS':
return 'success';
case 'FAILED':
return 'failure';
case 'RUNNING':
case 'PENDING':
return 'running';
case 'NOT_STARTED':
return 'not-executed';
default:
return 'default';
}
};

View File

@ -20,6 +20,7 @@ import {
ShouldExpandNodeInitiallyProps, ShouldExpandNodeInitiallyProps,
} from 'twenty-ui/json-visualizer'; } from 'twenty-ui/json-visualizer';
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard'; import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
import { JsonValue } from 'type-fest';
export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => { export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
const { t, i18n } = useLingui(); const { t, i18n } = useLingui();
@ -29,15 +30,15 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
const workflowRunId = useWorkflowRunIdOrThrow(); const workflowRunId = useWorkflowRunIdOrThrow();
const workflowRun = useWorkflowRun({ workflowRunId }); const workflowRun = useWorkflowRun({ workflowRunId });
const step = workflowRun?.output?.flow.steps.find( const step = workflowRun?.state?.flow.steps.find(
(step) => step.id === stepId, (step) => step.id === stepId,
); );
if ( if (
!( !(
isDefined(workflowRun) && isDefined(workflowRun) &&
isDefined(workflowRun.context) && isDefined(workflowRun.state?.stepInfos) &&
isDefined(workflowRun.output?.flow) && isDefined(workflowRun.state?.flow) &&
isDefined(step) isDefined(step)
) )
) { ) {
@ -46,7 +47,7 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
const previousStepId = getWorkflowPreviousStepId({ const previousStepId = getWorkflowPreviousStepId({
stepId, stepId,
steps: workflowRun.output.flow.steps, steps: workflowRun.state.flow.steps,
}); });
if (previousStepId === undefined) { if (previousStepId === undefined) {
@ -55,8 +56,8 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
const stepDefinition = getStepDefinitionOrThrow({ const stepDefinition = getStepDefinitionOrThrow({
stepId, stepId,
trigger: workflowRun.output.flow.trigger, trigger: workflowRun.state.flow.trigger,
steps: workflowRun.output.flow.steps, steps: workflowRun.state.flow.steps,
}); });
if (stepDefinition?.type !== 'action') { if (stepDefinition?.type !== 'action') {
throw new Error('The input tab must be rendered with an action step.'); throw new Error('The input tab must be rendered with an action step.');
@ -76,10 +77,11 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
const allVariablesUsedInStep = Array.from(variablesUsedInStep); const allVariablesUsedInStep = Array.from(variablesUsedInStep);
const stepContext = getWorkflowRunStepContext({ const stepContext = getWorkflowRunStepContext({
context: workflowRun.context, stepInfos: workflowRun.state.stepInfos,
flow: workflowRun.output.flow, flow: workflowRun.state.flow,
stepId, stepId,
}); });
if (stepContext.length === 0) { if (stepContext.length === 0) {
throw new Error('The input tab must be rendered with a non-empty context.'); throw new Error('The input tab must be rendered with a non-empty context.');
} }
@ -132,7 +134,7 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
elements={stepContext.map(({ id, name, context }) => ({ elements={stepContext.map(({ id, name, context }) => ({
id, id,
label: name, label: name,
value: context, value: context as JsonValue,
}))} }))}
Icon={IconBrackets} Icon={IconBrackets}
depth={0} depth={0}

View File

@ -1,7 +1,10 @@
import { WorkflowAction, WorkflowTrigger } from '@/workflow/types/Workflow'; import {
WorkflowAction,
WorkflowRunStepStatus,
WorkflowTrigger,
} from '@/workflow/types/Workflow';
import { assertUnreachable } from '@/workflow/utils/assertUnreachable'; import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow'; import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { WorkflowEditActionAiAgent } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowEditActionAiAgent'; import { WorkflowEditActionAiAgent } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowEditActionAiAgent';
import { WorkflowActionServerlessFunction } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowActionServerlessFunction'; import { WorkflowActionServerlessFunction } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowActionServerlessFunction';
import { WorkflowEditActionCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord'; import { WorkflowEditActionCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord';
@ -21,7 +24,7 @@ type WorkflowRunStepNodeDetailProps = {
stepId: string; stepId: string;
trigger: WorkflowTrigger | null; trigger: WorkflowTrigger | null;
steps: Array<WorkflowAction> | null; steps: Array<WorkflowAction> | null;
stepExecutionStatus?: WorkflowDiagramRunStatus; stepExecutionStatus?: WorkflowRunStepStatus;
}; };
export const WorkflowRunStepNodeDetail = ({ export const WorkflowRunStepNodeDetail = ({
@ -172,7 +175,7 @@ export const WorkflowRunStepNodeDetail = ({
key={stepId} key={stepId}
action={stepDefinition.definition} action={stepDefinition.definition}
actionOptions={{ actionOptions={{
readonly: stepExecutionStatus !== 'running', readonly: stepExecutionStatus !== 'PENDING',
}} }}
/> />
); );

View File

@ -1,6 +1,5 @@
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun'; import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow'; import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
import { WorkflowExecutorOutput } from '@/workflow/types/Workflow';
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow'; import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
import { WorkflowRunStepJsonContainer } from '@/workflow/workflow-steps/components/WorkflowRunStepJsonContainer'; import { WorkflowRunStepJsonContainer } from '@/workflow/workflow-steps/components/WorkflowRunStepJsonContainer';
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader'; import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
@ -30,18 +29,16 @@ export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
const workflowRunId = useWorkflowRunIdOrThrow(); const workflowRunId = useWorkflowRunIdOrThrow();
const workflowRun = useWorkflowRun({ workflowRunId }); const workflowRun = useWorkflowRun({ workflowRunId });
if (!isDefined(workflowRun?.output?.stepsOutput)) { if (!isDefined(workflowRun?.state?.stepInfos)) {
return null; return null;
} }
const stepOutput = workflowRun.output.stepsOutput[stepId] as const stepInfo = workflowRun.state.stepInfos[stepId];
| WorkflowExecutorOutput
| undefined;
const stepDefinition = getStepDefinitionOrThrow({ const stepDefinition = getStepDefinitionOrThrow({
stepId, stepId,
trigger: workflowRun.output.flow.trigger, trigger: workflowRun.state.flow.trigger,
steps: workflowRun.output.flow.steps, steps: workflowRun.state.flow.steps,
}); });
if ( if (
!isDefined(stepDefinition?.definition) || !isDefined(stepDefinition?.definition) ||
@ -87,7 +84,7 @@ export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
<WorkflowRunStepJsonContainer> <WorkflowRunStepJsonContainer>
<JsonTree <JsonTree
value={stepOutput ?? t`No output available`} value={stepInfo ?? t`No output available`}
shouldExpandNodeInitially={isTwoFirstDepths} shouldExpandNodeInitially={isTwoFirstDepths}
emptyArrayLabel={t`Empty Array`} emptyArrayLabel={t`Empty Array`}
emptyObjectLabel={t`Empty Object`} emptyObjectLabel={t`Empty Object`}
@ -95,7 +92,7 @@ export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
arrowButtonCollapsedLabel={t`Expand`} arrowButtonCollapsedLabel={t`Expand`}
arrowButtonExpandedLabel={t`Collapse`} arrowButtonExpandedLabel={t`Collapse`}
getNodeHighlighting={ getNodeHighlighting={
isDefined(stepOutput?.error) isDefined(stepInfo?.error)
? setRedHighlightingForEveryNode ? setRedHighlightingForEveryNode
: undefined : undefined
} }

View File

@ -49,18 +49,18 @@ export const useUpdateWorkflowRunStep = () => {
if ( if (
!isDefined(cachedRecord) || !isDefined(cachedRecord) ||
!isDefined(cachedRecord?.output?.flow?.steps) !isDefined(cachedRecord?.state?.flow?.steps)
) { ) {
return; return;
} }
const newCachedRecord = { const newCachedRecord = {
...cachedRecord, ...cachedRecord,
output: { state: {
...cachedRecord.output, ...cachedRecord.state,
flow: { flow: {
...cachedRecord.output.flow, ...cachedRecord.state.flow,
steps: cachedRecord.output.flow.steps.map((step: WorkflowAction) => { steps: cachedRecord.state.flow.steps.map((step: WorkflowAction) => {
if (step.id === updatedStep.id) { if (step.id === updatedStep.id) {
return updatedStep; return updatedStep;
} }
@ -71,7 +71,7 @@ export const useUpdateWorkflowRunStep = () => {
}; };
const recordGqlFields = { const recordGqlFields = {
output: true, state: true,
}; };
updateRecordFromCache({ updateRecordFromCache({
objectMetadataItems, objectMetadataItems,

View File

@ -1,6 +1,7 @@
import { WorkflowRunFlow } from '@/workflow/types/Workflow'; import { WorkflowRunFlow } from '@/workflow/types/Workflow';
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId'; import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
import { getWorkflowRunStepContext } from '../getWorkflowRunStepContext'; import { getWorkflowRunStepContext } from '../getWorkflowRunStepContext';
import { StepStatus } from 'twenty-shared/workflow';
describe('getWorkflowRunStepContext', () => { describe('getWorkflowRunStepContext', () => {
it('should return an empty array for trigger step', () => { it('should return an empty array for trigger step', () => {
@ -15,14 +16,17 @@ describe('getWorkflowRunStepContext', () => {
}, },
steps: [], steps: [],
} satisfies WorkflowRunFlow; } satisfies WorkflowRunFlow;
const context = { const stepInfos = {
[TRIGGER_STEP_ID]: { company: { id: '123' } }, [TRIGGER_STEP_ID]: {
result: { company: { id: '123' } },
status: StepStatus.SUCCESS,
},
}; };
const result = getWorkflowRunStepContext({ const result = getWorkflowRunStepContext({
stepId: TRIGGER_STEP_ID, stepId: TRIGGER_STEP_ID,
flow, flow,
context, stepInfos,
}); });
expect(result).toEqual([]); expect(result).toEqual([]);
@ -77,15 +81,20 @@ describe('getWorkflowRunStepContext', () => {
}, },
], ],
} satisfies WorkflowRunFlow; } satisfies WorkflowRunFlow;
const context = {
[TRIGGER_STEP_ID]: { company: { id: '123' } }, const stepInfos = {
step1: { taskId: '456' }, [TRIGGER_STEP_ID]: {
result: { company: { id: '123' } },
status: StepStatus.SUCCESS,
},
step1: { result: { taskId: '456' }, status: StepStatus.SUCCESS },
step2: { result: { taskId: '456' }, status: StepStatus.SUCCESS },
}; };
const result = getWorkflowRunStepContext({ const result = getWorkflowRunStepContext({
stepId: 'step2', stepId: 'step2',
flow, flow,
context, stepInfos,
}); });
expect(result).toEqual([ expect(result).toEqual([
@ -149,16 +158,19 @@ describe('getWorkflowRunStepContext', () => {
}, },
], ],
} satisfies WorkflowRunFlow; } satisfies WorkflowRunFlow;
const context = { const stepInfos = {
[TRIGGER_STEP_ID]: { company: { id: '123' } }, [TRIGGER_STEP_ID]: {
step1: { taskId: '456' }, result: { company: { id: '123' } },
step2: { emailId: '789' }, status: StepStatus.SUCCESS,
},
step1: { result: { taskId: '456' }, status: StepStatus.SUCCESS },
step2: { result: { emailId: '789' }, status: StepStatus.SUCCESS },
}; };
const result = getWorkflowRunStepContext({ const result = getWorkflowRunStepContext({
stepId: 'step1', stepId: 'step1',
flow, flow,
context, stepInfos,
}); });
expect(result).toEqual([ expect(result).toEqual([
@ -237,17 +249,20 @@ describe('getWorkflowRunStepContext', () => {
}, },
], ],
} satisfies WorkflowRunFlow; } satisfies WorkflowRunFlow;
const context = { const stepInfos = {
[TRIGGER_STEP_ID]: { company: { id: '123' } }, [TRIGGER_STEP_ID]: {
step1: { noteId: '456' }, result: { company: { id: '123' } },
step2: { noteId: '789' }, status: StepStatus.SUCCESS,
step3: { noteId: '101' }, },
step1: { result: { noteId: '456' }, status: StepStatus.SUCCESS },
step2: { result: { noteId: '789' }, status: StepStatus.SUCCESS },
step3: { result: { noteId: '101' }, status: StepStatus.SUCCESS },
}; };
const result = getWorkflowRunStepContext({ const result = getWorkflowRunStepContext({
stepId: 'step3', stepId: 'step3',
flow, flow,
context, stepInfos,
}); });
expect(result).toEqual([ expect(result).toEqual([

View File

@ -1,4 +1,5 @@
import { getWorkflowRunStepExecutionStatus } from '../getWorkflowRunStepExecutionStatus'; import { getWorkflowRunStepExecutionStatus } from '../getWorkflowRunStepExecutionStatus';
import { StepStatus } from 'twenty-shared/workflow';
describe('getWorkflowRunStepExecutionStatus', () => { describe('getWorkflowRunStepExecutionStatus', () => {
const stepId = '453e0084-aca2-45b9-8d1c-458a2b8ac70a'; const stepId = '453e0084-aca2-45b9-8d1c-458a2b8ac70a';
@ -6,16 +7,16 @@ describe('getWorkflowRunStepExecutionStatus', () => {
it('should return not-executed when the output is null', () => { it('should return not-executed when the output is null', () => {
expect( expect(
getWorkflowRunStepExecutionStatus({ getWorkflowRunStepExecutionStatus({
workflowRunOutput: null, workflowRunState: null,
stepId, stepId,
}), }),
).toBe('not-executed'); ).toBe(StepStatus.NOT_STARTED);
}); });
it('should return success when step has result', () => { it('should return success when step has result', () => {
expect( expect(
getWorkflowRunStepExecutionStatus({ getWorkflowRunStepExecutionStatus({
workflowRunOutput: { workflowRunState: {
flow: { flow: {
steps: [ steps: [
{ {
@ -60,15 +61,20 @@ describe('getWorkflowRunStepExecutionStatus', () => {
}, },
}, },
}, },
stepsOutput: { stepInfos: {
trigger: {
result: {},
status: StepStatus.SUCCESS,
},
[stepId]: { [stepId]: {
result: {}, result: {},
status: StepStatus.SUCCESS,
}, },
}, },
}, },
stepId, stepId,
}), }),
).toBe('success'); ).toBe(StepStatus.SUCCESS);
}); });
it('should return failure when workflow has error', () => { it('should return failure when workflow has error', () => {
@ -76,7 +82,7 @@ describe('getWorkflowRunStepExecutionStatus', () => {
expect( expect(
getWorkflowRunStepExecutionStatus({ getWorkflowRunStepExecutionStatus({
workflowRunOutput: { workflowRunState: {
flow: { flow: {
steps: [ steps: [
{ {
@ -121,16 +127,21 @@ describe('getWorkflowRunStepExecutionStatus', () => {
}, },
}, },
}, },
error, workflowRunError: error,
stepsOutput: { stepInfos: {
trigger: {
result: {},
status: StepStatus.SUCCESS,
},
[stepId]: { [stepId]: {
error, error,
status: StepStatus.FAILED,
}, },
}, },
}, },
stepId, stepId,
}), }),
).toBe('failure'); ).toBe(StepStatus.FAILED);
}); });
it('should return not-executed when step has no output', () => { it('should return not-executed when step has no output', () => {
@ -138,7 +149,7 @@ describe('getWorkflowRunStepExecutionStatus', () => {
expect( expect(
getWorkflowRunStepExecutionStatus({ getWorkflowRunStepExecutionStatus({
workflowRunOutput: { workflowRunState: {
flow: { flow: {
steps: [ steps: [
{ {
@ -217,14 +228,23 @@ describe('getWorkflowRunStepExecutionStatus', () => {
}, },
}, },
}, },
stepsOutput: { stepInfos: {
trigger: {
result: {},
status: StepStatus.SUCCESS,
},
[stepId]: { [stepId]: {
result: {}, result: {},
status: StepStatus.SUCCESS,
},
[secondStepId]: {
result: {},
status: StepStatus.NOT_STARTED,
}, },
}, },
}, },
stepId: secondStepId, stepId: secondStepId,
}), }),
).toBe('not-executed'); ).toBe(StepStatus.NOT_STARTED);
}); });
}); });

View File

@ -1,14 +1,18 @@
import { WorkflowRunContext, WorkflowRunFlow } from '@/workflow/types/Workflow'; import { WorkflowRunFlow } from '@/workflow/types/Workflow';
import { getPreviousSteps } from '@/workflow/workflow-steps/utils/getWorkflowPreviousSteps'; import { getPreviousSteps } from '@/workflow/workflow-steps/utils/getWorkflowPreviousSteps';
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId'; import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
import {
getWorkflowRunContext,
WorkflowRunStepInfos,
} from 'twenty-shared/workflow';
export const getWorkflowRunStepContext = ({ export const getWorkflowRunStepContext = ({
stepId, stepId,
flow, flow,
context, stepInfos,
}: { }: {
stepId: string; stepId: string;
context: WorkflowRunContext; stepInfos: WorkflowRunStepInfos;
flow: WorkflowRunFlow; flow: WorkflowRunFlow;
}) => { }) => {
if (stepId === TRIGGER_STEP_ID) { if (stepId === TRIGGER_STEP_ID) {
@ -17,6 +21,8 @@ export const getWorkflowRunStepContext = ({
const previousSteps = getPreviousSteps(flow.steps, stepId); const previousSteps = getPreviousSteps(flow.steps, stepId);
const context = getWorkflowRunContext(stepInfos);
const previousStepsContext = previousSteps.map((step) => { const previousStepsContext = previousSteps.map((step) => {
return { return {
id: step.id, id: step.id,

View File

@ -1,37 +1,22 @@
import { WorkflowRunOutput } from '@/workflow/types/Workflow'; import {
import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; WorkflowRunState,
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId'; WorkflowRunStepStatus,
import { isNull } from '@sniptt/guards'; } from '@/workflow/types/Workflow';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { StepStatus } from 'twenty-shared/workflow';
export const getWorkflowRunStepExecutionStatus = ({ export const getWorkflowRunStepExecutionStatus = ({
workflowRunOutput, workflowRunState,
stepId, stepId,
}: { }: {
workflowRunOutput: WorkflowRunOutput | null; workflowRunState: WorkflowRunState | null;
stepId: string; stepId: string;
}): WorkflowDiagramRunStatus => { }): WorkflowRunStepStatus => {
if (isNull(workflowRunOutput)) { const stepOutput = workflowRunState?.stepInfos?.[stepId];
return 'not-executed';
if (isDefined(stepOutput)) {
return stepOutput.status;
} }
if (stepId === TRIGGER_STEP_ID) { return StepStatus.NOT_STARTED;
return 'success';
}
const stepOutput = workflowRunOutput.stepsOutput?.[stepId];
if (isDefined(stepOutput?.error)) {
return 'failure';
}
if (isDefined(stepOutput?.pendingEvent)) {
return 'running';
}
if (isDefined(stepOutput?.result)) {
return 'success';
}
return 'not-executed';
}; };

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { MoreThan, Repository } from 'typeorm'; import { MoreThan, Repository } from 'typeorm';
import { Command, Option } from 'nest-commander'; import { Command, Option } from 'nest-commander';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { StepStatus, WorkflowRunStepInfos } from 'twenty-shared/workflow';
import { import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner, ActiveOrSuspendedWorkspacesMigrationCommandRunner,
@ -15,10 +16,6 @@ import {
WorkflowRunOutput, WorkflowRunOutput,
WorkflowRunWorkspaceEntity, WorkflowRunWorkspaceEntity,
} from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity'; } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
import {
StepStatus,
WorkflowRunStepInfo,
} from 'src/modules/workflow/workflow-executor/types/workflow-run-step-info.type';
const DEFAULT_CHUNK_SIZE = 500; const DEFAULT_CHUNK_SIZE = 500;
@ -121,7 +118,7 @@ export class MigrateWorkflowRunStatesCommand extends ActiveOrSuspendedWorkspaces
} }
private buildRunStateFromOutput(output: WorkflowRunOutput): WorkflowRunState { private buildRunStateFromOutput(output: WorkflowRunOutput): WorkflowRunState {
const stepInfos: Record<string, WorkflowRunStepInfo> = Object.fromEntries( const stepInfos: WorkflowRunStepInfos = Object.fromEntries(
output.flow.steps.map((step) => { output.flow.steps.map((step) => {
const stepOutput = output.stepsOutput?.[step.id]; const stepOutput = output.stepsOutput?.[step.id];
const status = stepOutput?.pendingEvent const status = stepOutput?.pendingEvent

View File

@ -0,0 +1,48 @@
import { Inject } from '@nestjs/common';
import {
CacheLockOptions,
CacheLockService,
} from 'src/engine/core-modules/cache-lock/cache-lock.service';
export const WithLock = (
lockKeyParamPath: string,
options?: CacheLockOptions,
): MethodDecorator => {
const injectCacheLockService = Inject(CacheLockService);
return function (target, propertyKey, descriptor: PropertyDescriptor) {
injectCacheLockService(target, 'cacheLockService');
const originalMethod = descriptor.value;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
descriptor.value = async function (...args: any[]) {
const self = this as { cacheLockService: CacheLockService };
if (!self.cacheLockService) {
throw new Error('cacheLockService not available on instance');
}
if (typeof args[0] !== 'object') {
throw new Error(
`You must use one object parameter to use @WithLock decorator. Received ${args}`,
);
}
const key = args[0][lockKeyParamPath];
if (typeof key !== 'string') {
throw new Error(
`Could not resolve lock key from path "${lockKeyParamPath}" on first argument`,
);
}
return await self.cacheLockService.withLock(
() => originalMethod.apply(self, args),
key,
options,
);
};
};
};

View File

@ -1,21 +1,11 @@
import { SetMetadata } from '@nestjs/common'; import { SetMetadata } from '@nestjs/common';
import { isString } from '@nestjs/common/utils/shared.utils';
import { PROCESS_METADATA } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { PROCESS_METADATA } from 'src/engine/core-modules/message-queue/message-queue.constants';
export interface MessageQueueProcessOptions { export interface MessageQueueProcessOptions {
jobName: string; jobName: string;
concurrency?: number;
} }
export function Process(jobName: string): MethodDecorator; export function Process(jobName: string): MethodDecorator {
export function Process(options: MessageQueueProcessOptions): MethodDecorator; return SetMetadata(PROCESS_METADATA, { jobName });
export function Process(
nameOrOptions: string | MessageQueueProcessOptions,
): MethodDecorator {
const options = isString(nameOrOptions)
? { jobName: nameOrOptions }
: nameOrOptions;
return SetMetadata(PROCESS_METADATA, options || {});
} }

View File

@ -1,12 +1,9 @@
import { Scope, SetMetadata } from '@nestjs/common'; import { Scope, SetMetadata } from '@nestjs/common';
import { SCOPE_OPTIONS_METADATA } from '@nestjs/common/constants'; import { SCOPE_OPTIONS_METADATA } from '@nestjs/common/constants';
import { MessageQueueWorkerOptions } from 'src/engine/core-modules/message-queue/interfaces/message-queue-worker-options.interface';
import { import {
MessageQueue, MessageQueue,
PROCESSOR_METADATA, PROCESSOR_METADATA,
WORKER_METADATA,
} from 'src/engine/core-modules/message-queue/message-queue.constants'; } from 'src/engine/core-modules/message-queue/message-queue.constants';
export interface MessageQueueProcessorOptions { export interface MessageQueueProcessorOptions {
@ -24,16 +21,7 @@ export interface MessageQueueProcessorOptions {
* Represents a worker that is able to process jobs from the queue. * Represents a worker that is able to process jobs from the queue.
* @param queueName name of the queue to process * @param queueName name of the queue to process
*/ */
export function Processor(queueName: string): ClassDecorator; export function Processor(queueName: MessageQueue): ClassDecorator;
/**
* Represents a worker that is able to process jobs from the queue.
* @param queueName name of the queue to process
* @param workerOptions additional worker options
*/
export function Processor(
queueName: string,
workerOptions: MessageQueueWorkerOptions,
): ClassDecorator;
/** /**
* Represents a worker that is able to process jobs from the queue. * Represents a worker that is able to process jobs from the queue.
* @param processorOptions processor options * @param processorOptions processor options
@ -41,21 +29,11 @@ export function Processor(
export function Processor( export function Processor(
processorOptions: MessageQueueProcessorOptions, processorOptions: MessageQueueProcessorOptions,
): ClassDecorator; ): ClassDecorator;
/**
* Represents a worker that is able to process jobs from the queue.
* @param processorOptions processor options (Nest-specific)
* @param workerOptions additional Bull worker options
*/
export function Processor( export function Processor(
processorOptions: MessageQueueProcessorOptions, queueNameOrOptions: string | MessageQueueProcessorOptions,
workerOptions: MessageQueueWorkerOptions,
): ClassDecorator;
export function Processor(
queueNameOrOptions?: string | MessageQueueProcessorOptions,
maybeWorkerOptions?: MessageQueueWorkerOptions,
): ClassDecorator { ): ClassDecorator {
const options = const options =
queueNameOrOptions && typeof queueNameOrOptions === 'object' typeof queueNameOrOptions === 'object'
? queueNameOrOptions ? queueNameOrOptions
: { queueName: queueNameOrOptions }; : { queueName: queueNameOrOptions };
@ -63,7 +41,5 @@ export function Processor(
return (target: Function) => { return (target: Function) => {
SetMetadata(SCOPE_OPTIONS_METADATA, options)(target); SetMetadata(SCOPE_OPTIONS_METADATA, options)(target);
SetMetadata(PROCESSOR_METADATA, options)(target); SetMetadata(PROCESSOR_METADATA, options)(target);
maybeWorkerOptions &&
SetMetadata(WORKER_METADATA, maybeWorkerOptions)(target);
}; };
} }

View File

@ -1,6 +1,5 @@
export const PROCESSOR_METADATA = Symbol('message-queue:processor_metadata'); export const PROCESSOR_METADATA = Symbol('message-queue:processor_metadata');
export const PROCESS_METADATA = Symbol('message-queue:process_metadata'); export const PROCESS_METADATA = Symbol('message-queue:process_metadata');
export const WORKER_METADATA = Symbol('bullmq:worker_metadata');
export const QUEUE_DRIVER = Symbol('message-queue:queue_driver'); export const QUEUE_DRIVER = Symbol('message-queue:queue_driver');
export enum MessageQueue { export enum MessageQueue {
@ -13,11 +12,8 @@ export enum MessageQueue {
contactCreationQueue = 'contact-creation-queue', contactCreationQueue = 'contact-creation-queue',
billingQueue = 'billing-queue', billingQueue = 'billing-queue',
workspaceQueue = 'workspace-queue', workspaceQueue = 'workspace-queue',
recordPositionBackfillQueue = 'record-position-backfill-queue',
entityEventsToDbQueue = 'entity-events-to-db-queue', entityEventsToDbQueue = 'entity-events-to-db-queue',
testQueue = 'test-queue',
workflowQueue = 'workflow-queue', workflowQueue = 'workflow-queue',
serverlessFunctionQueue = 'serverless-function-queue',
deleteCascadeQueue = 'delete-cascade-queue', deleteCascadeQueue = 'delete-cascade-queue',
subscriptionsQueue = 'subscriptions-queue', subscriptionsQueue = 'subscriptions-queue',
} }

View File

@ -1,5 +1,6 @@
import { msg } from '@lingui/core/macro'; import { msg } from '@lingui/core/macro';
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { WorkflowRunStepInfos } from 'twenty-shared/workflow';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface'; import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
@ -29,7 +30,6 @@ import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-o
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.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 { WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type'; import { WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type';
import { WorkflowRunStepInfo } from 'src/modules/workflow/workflow-executor/types/workflow-run-step-info.type';
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
import { WorkflowTrigger } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type'; import { WorkflowTrigger } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
@ -60,7 +60,7 @@ export type WorkflowRunState = {
trigger: WorkflowTrigger; trigger: WorkflowTrigger;
steps: WorkflowAction[]; steps: WorkflowAction[];
}; };
stepInfos: Record<string, WorkflowRunStepInfo>; stepInfos: WorkflowRunStepInfos;
workflowRunError?: string; workflowRunError?: string;
}; };

View File

@ -6,6 +6,7 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined, isValidUuid } from 'twenty-shared/utils'; import { isDefined, isValidUuid } from 'twenty-shared/utils';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { StepStatus } from 'twenty-shared/workflow';
import { BASE_TYPESCRIPT_PROJECT_INPUT_SCHEMA } from 'src/engine/core-modules/serverless/drivers/constants/base-typescript-project-input-schema'; import { BASE_TYPESCRIPT_PROJECT_INPUT_SCHEMA } from 'src/engine/core-modules/serverless/drivers/constants/base-typescript-project-input-schema';
import { CreateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/create-workflow-version-step-input.dto'; import { CreateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/create-workflow-version-step-input.dto';
@ -27,7 +28,6 @@ import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/work
import { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.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 { 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 { removeStep } from 'src/modules/workflow/workflow-builder/workflow-step/utils/remove-step';
import { StepStatus } from 'src/modules/workflow/workflow-executor/types/workflow-run-step-info.type';
import { BaseWorkflowActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type'; import { BaseWorkflowActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type';
import { import {
WorkflowAction, WorkflowAction,
@ -302,7 +302,7 @@ export class WorkflowVersionStepWorkspaceService {
workspaceId, workspaceId,
}); });
const step = workflowRun.output?.flow?.steps?.find( const step = workflowRun.state?.flow?.steps?.find(
(step) => step.id === stepId, (step) => step.id === stepId,
); );

View File

@ -1,8 +1,10 @@
import { StepStatus } from 'twenty-shared/workflow';
import { import {
WorkflowAction, WorkflowAction,
WorkflowActionType, WorkflowActionType,
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
import { canExecuteStep } from 'src/modules/workflow/workflow-executor/utils/can-execute-step.utils'; import { canExecuteStep } from 'src/modules/workflow/workflow-executor/utils/can-execute-step.util';
describe('canExecuteStep', () => { describe('canExecuteStep', () => {
const steps = [ const steps = [
@ -42,13 +44,19 @@ describe('canExecuteStep', () => {
] as WorkflowAction[]; ] as WorkflowAction[];
it('should return true if all parents succeeded', () => { it('should return true if all parents succeeded', () => {
const context = { const stepInfos = {
trigger: 'trigger result', 'step-1': {
'step-1': 'step-1 result', status: StepStatus.SUCCESS,
'step-2': 'step-2 result', },
'step-2': {
status: StepStatus.SUCCESS,
},
'step-3': {
status: StepStatus.NOT_STARTED,
},
}; };
const result = canExecuteStep({ context, steps, stepId: 'step-3' }); const result = canExecuteStep({ stepInfos, steps, stepId: 'step-3' });
expect(result).toBe(true); expect(result).toBe(true);
}); });
@ -56,9 +64,16 @@ describe('canExecuteStep', () => {
it('should return false if one parent is not succeeded', () => { it('should return false if one parent is not succeeded', () => {
expect( expect(
canExecuteStep({ canExecuteStep({
context: { stepInfos: {
trigger: 'trigger result', 'step-1': {
'step-2': 'step-2 result', status: StepStatus.NOT_STARTED,
},
'step-2': {
status: StepStatus.SUCCESS,
},
'step-3': {
status: StepStatus.NOT_STARTED,
},
}, },
steps, steps,
stepId: 'step-3', stepId: 'step-3',
@ -67,9 +82,16 @@ describe('canExecuteStep', () => {
expect( expect(
canExecuteStep({ canExecuteStep({
context: { stepInfos: {
trigger: 'trigger result', 'step-1': {
'step-1': 'step-1 result', status: StepStatus.SUCCESS,
},
'step-2': {
status: StepStatus.NOT_STARTED,
},
'step-3': {
status: StepStatus.NOT_STARTED,
},
}, },
steps, steps,
stepId: 'step-3', stepId: 'step-3',
@ -78,9 +100,90 @@ describe('canExecuteStep', () => {
expect( expect(
canExecuteStep({ canExecuteStep({
context: { stepInfos: {
trigger: 'trigger result', 'step-1': {
'step-1': {}, status: StepStatus.NOT_STARTED,
},
'step-2': {
status: StepStatus.NOT_STARTED,
},
'step-3': {
status: StepStatus.NOT_STARTED,
},
},
steps,
stepId: 'step-3',
}),
).toBe(false);
});
it('should return false if step has already ran', () => {
expect(
canExecuteStep({
stepInfos: {
'step-1': {
status: StepStatus.SUCCESS,
},
'step-2': {
status: StepStatus.SUCCESS,
},
'step-3': {
status: StepStatus.SUCCESS,
},
},
steps,
stepId: 'step-3',
}),
).toBe(false);
expect(
canExecuteStep({
stepInfos: {
'step-1': {
status: StepStatus.SUCCESS,
},
'step-2': {
status: StepStatus.SUCCESS,
},
'step-3': {
status: StepStatus.PENDING,
},
},
steps,
stepId: 'step-3',
}),
).toBe(false);
expect(
canExecuteStep({
stepInfos: {
'step-1': {
status: StepStatus.SUCCESS,
},
'step-2': {
status: StepStatus.SUCCESS,
},
'step-3': {
status: StepStatus.FAILED,
},
},
steps,
stepId: 'step-3',
}),
).toBe(false);
expect(
canExecuteStep({
stepInfos: {
'step-1': {
status: StepStatus.SUCCESS,
},
'step-2': {
status: StepStatus.SUCCESS,
},
'step-3': {
status: StepStatus.RUNNING,
},
}, },
steps, steps,
stepId: 'step-3', stepId: 'step-3',

View File

@ -1,23 +1,30 @@
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { StepStatus, WorkflowRunStepInfos } from 'twenty-shared/workflow';
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
export const canExecuteStep = ({ export const canExecuteStep = ({
context,
stepId, stepId,
steps, steps,
stepInfos,
}: { }: {
steps: WorkflowAction[]; steps: WorkflowAction[];
context: Record<string, unknown>; stepInfos: WorkflowRunStepInfos;
stepId: string; stepId: string;
}) => { }) => {
if (
isDefined(stepInfos[stepId]?.status) &&
stepInfos[stepId].status !== StepStatus.NOT_STARTED
) {
return false;
}
const parentSteps = steps.filter( const parentSteps = steps.filter(
(parentStep) => (parentStep) =>
isDefined(parentStep) && parentStep.nextStepIds?.includes(stepId), isDefined(parentStep) && parentStep.nextStepIds?.includes(stepId),
); );
// TODO use workflowRun.state to check if step status is not COMPLETED. Return false in this case return parentSteps.every(
return parentSteps.every((parentStep) => (parentStep) => stepInfos[parentStep.id]?.status === StepStatus.SUCCESS,
Object.keys(context).includes(parentStep.id),
); );
}; };

View File

@ -1,5 +1,7 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { getWorkflowRunContext, StepStatus } from 'twenty-shared/workflow';
import { BILLING_FEATURE_USED } from 'src/engine/core-modules/billing/constants/billing-feature-used.constant'; import { BILLING_FEATURE_USED } from 'src/engine/core-modules/billing/constants/billing-feature-used.constant';
import { BILLING_WORKFLOW_EXECUTION_ERROR_MESSAGE } from 'src/engine/core-modules/billing/constants/billing-workflow-execution-error-message.constant'; import { BILLING_WORKFLOW_EXECUTION_ERROR_MESSAGE } from 'src/engine/core-modules/billing/constants/billing-workflow-execution-error-message.constant';
import { BillingMeterEventName } from 'src/engine/core-modules/billing/enums/billing-meter-event-names'; import { BillingMeterEventName } from 'src/engine/core-modules/billing/enums/billing-meter-event-names';
@ -12,15 +14,14 @@ import {
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
import { WorkflowExecutorWorkspaceService } from 'src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service'; import { WorkflowExecutorWorkspaceService } from 'src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service';
import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service'; import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service';
import { StepStatus } from 'src/modules/workflow/workflow-executor/types/workflow-run-step-info.type';
import { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity'; import { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
import { canExecuteStep } from 'src/modules/workflow/workflow-executor/utils/can-execute-step.utils'; import { canExecuteStep } from 'src/modules/workflow/workflow-executor/utils/can-execute-step.util';
jest.mock( jest.mock(
'src/modules/workflow/workflow-executor/utils/can-execute-step.utils', 'src/modules/workflow/workflow-executor/utils/can-execute-step.util',
() => { () => {
const actual = jest.requireActual( const actual = jest.requireActual(
'src/modules/workflow/workflow-executor/utils/can-execute-step.utils', 'src/modules/workflow/workflow-executor/utils/can-execute-step.util',
); );
return { return {
@ -100,7 +101,6 @@ describe('WorkflowExecutorWorkspaceService', () => {
describe('execute', () => { describe('execute', () => {
const mockWorkflowRunId = 'workflow-run-id'; const mockWorkflowRunId = 'workflow-run-id';
const mockWorkspaceId = 'workspace-id'; const mockWorkspaceId = 'workspace-id';
const mockContext = { trigger: 'trigger-result' };
const mockSteps = [ const mockSteps = [
{ {
id: 'step-1', id: 'step-1',
@ -125,10 +125,12 @@ describe('WorkflowExecutorWorkspaceService', () => {
nextStepIds: [], nextStepIds: [],
}, },
] as WorkflowAction[]; ] as WorkflowAction[];
const mockStepInfos = {
trigger: { result: {}, status: StepStatus.SUCCESS },
};
mockWorkflowRunWorkspaceService.getWorkflowRun.mockReturnValue({ mockWorkflowRunWorkspaceService.getWorkflowRun.mockReturnValue({
output: { flow: { steps: mockSteps } }, state: { flow: { steps: mockSteps }, stepInfos: mockStepInfos },
context: mockContext,
}); });
it('should execute a step and continue to the next step on success', async () => { it('should execute a step and continue to the next step on success', async () => {
@ -151,7 +153,7 @@ describe('WorkflowExecutorWorkspaceService', () => {
expect(mockWorkflowExecutor.execute).toHaveBeenCalledWith({ expect(mockWorkflowExecutor.execute).toHaveBeenCalledWith({
currentStepId: 'step-1', currentStepId: 'step-1',
steps: mockSteps, steps: mockSteps,
context: mockContext, context: getWorkflowRunContext(mockStepInfos),
}); });
expect(workspaceEventEmitter.emitCustomBatchEvent).toHaveBeenCalledWith( expect(workspaceEventEmitter.emitCustomBatchEvent).toHaveBeenCalledWith(
@ -319,8 +321,10 @@ describe('WorkflowExecutorWorkspaceService', () => {
] as WorkflowAction[]; ] as WorkflowAction[];
mockWorkflowRunWorkspaceService.getWorkflowRun.mockReturnValueOnce({ mockWorkflowRunWorkspaceService.getWorkflowRun.mockReturnValueOnce({
output: { flow: { steps: stepsWithContinueOnFailure } }, state: {
context: mockContext, flow: { steps: stepsWithContinueOnFailure },
stepInfos: mockStepInfos,
},
}); });
mockWorkflowExecutor.execute.mockResolvedValueOnce({ mockWorkflowExecutor.execute.mockResolvedValueOnce({
@ -385,8 +389,10 @@ describe('WorkflowExecutorWorkspaceService', () => {
] as WorkflowAction[]; ] as WorkflowAction[];
mockWorkflowRunWorkspaceService.getWorkflowRun.mockReturnValue({ mockWorkflowRunWorkspaceService.getWorkflowRun.mockReturnValue({
output: { flow: { steps: stepsWithRetryOnFailure } }, state: {
context: mockContext, flow: { steps: stepsWithRetryOnFailure },
stepInfos: mockStepInfos,
},
}); });
mockWorkflowExecutor.execute.mockResolvedValue({ mockWorkflowExecutor.execute.mockResolvedValue({

View File

@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { getWorkflowRunContext, StepStatus } from 'twenty-shared/workflow';
import { BILLING_FEATURE_USED } from 'src/engine/core-modules/billing/constants/billing-feature-used.constant'; import { BILLING_FEATURE_USED } from 'src/engine/core-modules/billing/constants/billing-feature-used.constant';
import { BILLING_WORKFLOW_EXECUTION_ERROR_MESSAGE } from 'src/engine/core-modules/billing/constants/billing-workflow-execution-error-message.constant'; import { BILLING_WORKFLOW_EXECUTION_ERROR_MESSAGE } from 'src/engine/core-modules/billing/constants/billing-workflow-execution-error-message.constant';
@ -19,8 +20,7 @@ import {
WorkflowBranchExecutorInput, WorkflowBranchExecutorInput,
WorkflowExecutorInput, WorkflowExecutorInput,
} from 'src/modules/workflow/workflow-executor/types/workflow-executor-input'; } from 'src/modules/workflow/workflow-executor/types/workflow-executor-input';
import { StepStatus } from 'src/modules/workflow/workflow-executor/types/workflow-run-step-info.type'; import { canExecuteStep } from 'src/modules/workflow/workflow-executor/utils/can-execute-step.util';
import { canExecuteStep } from 'src/modules/workflow/workflow-executor/utils/can-execute-step.utils';
import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service'; import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service';
const MAX_RETRIES_ON_FAILURE = 3; const MAX_RETRIES_ON_FAILURE = 3;
@ -57,7 +57,7 @@ export class WorkflowExecutorWorkspaceService {
workspaceId, workspaceId,
}: WorkflowBranchExecutorInput) { }: WorkflowBranchExecutorInput) {
const workflowRunInfo = await this.getWorkflowRunInfoOrEndWorkflowRun({ const workflowRunInfo = await this.getWorkflowRunInfoOrEndWorkflowRun({
stepId: stepId, stepId,
workflowRunId, workflowRunId,
workspaceId, workspaceId,
}); });
@ -66,9 +66,9 @@ export class WorkflowExecutorWorkspaceService {
return; return;
} }
const { stepToExecute, steps, context } = workflowRunInfo; const { stepToExecute, steps, stepInfos } = workflowRunInfo;
if (!canExecuteStep({ stepId: stepToExecute.id, steps, context })) { if (!canExecuteStep({ stepId, steps, stepInfos })) {
return; return;
} }
@ -98,7 +98,7 @@ export class WorkflowExecutorWorkspaceService {
actionOutput = await workflowAction.execute({ actionOutput = await workflowAction.execute({
currentStepId: stepId, currentStepId: stepId,
steps, steps,
context, context: getWorkflowRunContext(stepInfos),
}); });
} catch (error) { } catch (error) {
actionOutput = { actionOutput = {
@ -219,31 +219,18 @@ export class WorkflowExecutorWorkspaceService {
return; return;
} }
const steps = workflowRun.output?.flow.steps; if (!isDefined(workflowRun?.state)) {
const context = workflowRun.context;
if (!isDefined(steps)) {
await this.workflowRunWorkspaceService.endWorkflowRun({ await this.workflowRunWorkspaceService.endWorkflowRun({
workflowRunId, workflowRunId,
workspaceId, workspaceId,
status: WorkflowRunStatus.FAILED, status: WorkflowRunStatus.FAILED,
error: 'Steps undefined', error: `WorkflowRun ${workflowRunId} doesn't have any state`,
}); });
return; return;
} }
if (!isDefined(context)) { const steps = workflowRun.state.flow.steps;
await this.workflowRunWorkspaceService.endWorkflowRun({
workflowRunId,
workspaceId,
status: WorkflowRunStatus.FAILED,
error: 'Context not found',
});
return;
}
const stepToExecute = steps.find((step) => step.id === stepId); const stepToExecute = steps.find((step) => step.id === stepId);
@ -258,7 +245,11 @@ export class WorkflowExecutorWorkspaceService {
return; return;
} }
return { stepToExecute, steps, context }; return {
stepToExecute,
steps,
stepInfos: workflowRun.state.stepInfos,
};
} }
private sendWorkflowNodeRunEvent(workspaceId: string) { private sendWorkflowNodeRunEvent(workspaceId: string) {

View File

@ -144,7 +144,7 @@ export class RunWorkflowJob {
); );
} }
const lastExecutedStep = workflowRun.output?.flow?.steps?.find( const lastExecutedStep = workflowRun.state?.flow?.steps?.find(
(step) => step.id === lastExecutedStepId, (step) => step.id === lastExecutedStepId,
); );

View File

@ -5,6 +5,7 @@ import { Repository } from 'typeorm';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import { StepStatus } from 'twenty-shared/workflow';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { objectRecordChangedValues } from 'src/engine/core-modules/event-emitter/utils/object-record-changed-values'; import { objectRecordChangedValues } from 'src/engine/core-modules/event-emitter/utils/object-record-changed-values';
@ -28,9 +29,8 @@ import {
WorkflowRunException, WorkflowRunException,
WorkflowRunExceptionCode, WorkflowRunExceptionCode,
} from 'src/modules/workflow/workflow-runner/exceptions/workflow-run.exception'; } from 'src/modules/workflow/workflow-runner/exceptions/workflow-run.exception';
import { StepStatus } from 'src/modules/workflow/workflow-executor/types/workflow-run-step-info.type';
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { CacheLockService } from 'src/engine/core-modules/cache-lock/cache-lock.service'; import { WithLock } from 'src/engine/core-modules/cache-lock/with-lock.decorator';
@Injectable() @Injectable()
export class WorkflowRunWorkspaceService { export class WorkflowRunWorkspaceService {
@ -43,7 +43,6 @@ export class WorkflowRunWorkspaceService {
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>, private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly recordPositionService: RecordPositionService, private readonly recordPositionService: RecordPositionService,
private readonly metricsService: MetricsService, private readonly metricsService: MetricsService,
private readonly cacheLockService: CacheLockService,
) {} ) {}
async createWorkflowRun({ async createWorkflowRun({
@ -141,18 +140,8 @@ export class WorkflowRunWorkspaceService {
return workflowRun.id; return workflowRun.id;
} }
async startWorkflowRun(params: { @WithLock('workflowRunId')
workflowRunId: string; async startWorkflowRun({
workspaceId: string;
payload: object;
}) {
await this.cacheLockService.withLock(
async () => await this.startWorkflowRunWithoutLock(params),
params.workflowRunId,
);
}
private async startWorkflowRunWithoutLock({
workflowRunId, workflowRunId,
workspaceId, workspaceId,
payload, payload,
@ -207,19 +196,8 @@ export class WorkflowRunWorkspaceService {
await this.updateWorkflowRun({ workflowRunId, workspaceId, partialUpdate }); await this.updateWorkflowRun({ workflowRunId, workspaceId, partialUpdate });
} }
async endWorkflowRun(params: { @WithLock('workflowRunId')
workflowRunId: string; async endWorkflowRun({
workspaceId: string;
status: WorkflowRunStatus;
error?: string;
}) {
await this.cacheLockService.withLock(
async () => await this.endWorkflowRunWithoutLock(params),
params.workflowRunId,
);
}
private async endWorkflowRunWithoutLock({
workflowRunId, workflowRunId,
workspaceId, workspaceId,
status, status,
@ -259,19 +237,8 @@ export class WorkflowRunWorkspaceService {
}); });
} }
async updateWorkflowRunStepStatus(params: { @WithLock('workflowRunId')
workflowRunId: string; async updateWorkflowRunStepStatus({
stepId: string;
workspaceId: string;
stepStatus: StepStatus;
}) {
await this.cacheLockService.withLock(
async () => await this.updateWorkflowRunStepStatusWithoutLock(params),
params.workflowRunId,
);
}
private async updateWorkflowRunStepStatusWithoutLock({
workflowRunId, workflowRunId,
workspaceId, workspaceId,
stepId, stepId,
@ -303,19 +270,8 @@ export class WorkflowRunWorkspaceService {
await this.updateWorkflowRun({ workflowRunId, workspaceId, partialUpdate }); await this.updateWorkflowRun({ workflowRunId, workspaceId, partialUpdate });
} }
async saveWorkflowRunState(params: { @WithLock('workflowRunId')
workflowRunId: string; async saveWorkflowRunState({
stepOutput: StepOutput;
workspaceId: string;
stepStatus: StepStatus;
}) {
await this.cacheLockService.withLock(
async () => await this.saveWorkflowRunStateWithoutLock(params),
params.workflowRunId,
);
}
private async saveWorkflowRunStateWithoutLock({
workflowRunId, workflowRunId,
stepOutput, stepOutput,
workspaceId, workspaceId,
@ -367,18 +323,8 @@ export class WorkflowRunWorkspaceService {
await this.updateWorkflowRun({ workflowRunId, workspaceId, partialUpdate }); await this.updateWorkflowRun({ workflowRunId, workspaceId, partialUpdate });
} }
async updateWorkflowRunStep(params: { @WithLock('workflowRunId')
workflowRunId: string; async updateWorkflowRunStep({
step: WorkflowAction;
workspaceId: string;
}) {
await this.cacheLockService.withLock(
async () => await this.updateWorkflowRunStepWithoutLock(params),
params.workflowRunId,
);
}
private async updateWorkflowRunStepWithoutLock({
workflowRunId, workflowRunId,
step, step,
workspaceId, workspaceId,

View File

@ -34,6 +34,7 @@
"./translations/index.ts", "./translations/index.ts",
"./types/index.ts", "./types/index.ts",
"./utils/index.ts", "./utils/index.ts",
"./workflow/index.ts",
"./workspace/index.ts" "./workspace/index.ts"
] ]
}, },
@ -44,6 +45,7 @@
"translations", "translations",
"types", "types",
"utils", "utils",
"workflow",
"workspace" "workspace"
] ]
} }

View File

@ -24,6 +24,8 @@
"{projectRoot}/types/dist", "{projectRoot}/types/dist",
"{projectRoot}/utils/package.json", "{projectRoot}/utils/package.json",
"{projectRoot}/utils/dist", "{projectRoot}/utils/dist",
"{projectRoot}/workflow/package.json",
"{projectRoot}/workflow/dist",
"{projectRoot}/workspace/package.json", "{projectRoot}/workspace/package.json",
"{projectRoot}/workspace/dist" "{projectRoot}/workspace/dist"
] ]

View File

@ -0,0 +1,15 @@
/*
* _____ _
*|_ _|_ _____ _ __ | |_ _ _
* | | \ \ /\ / / _ \ '_ \| __| | | | Auto-generated file
* | | \ V V / __/ | | | |_| |_| | Any edits to this will be overridden
* |_| \_/\_/ \___|_| |_|\__|\__, |
* |___/
*/
export type {
WorkflowRunStepInfo,
WorkflowRunStepInfos,
} from './types/WorkflowRunStateStepInfos';
export { StepStatus } from './types/WorkflowRunStateStepInfos';
export { getWorkflowRunContext } from './utils/getWorkflowRunContext';

View File

@ -11,3 +11,5 @@ export type WorkflowRunStepInfo = {
error?: string; error?: string;
status: StepStatus; status: StepStatus;
}; };
export type WorkflowRunStepInfos = Record<string, WorkflowRunStepInfo>;

View File

@ -0,0 +1,37 @@
import { getWorkflowRunContext } from '@/workflow/utils/getWorkflowRunContext';
import {
StepStatus,
WorkflowRunStepInfos,
} from '@/workflow/types/WorkflowRunStateStepInfos';
describe('getWorkflowRunContext', () => {
it('returns a context with only steps that have a defined result', () => {
const stepInfos: WorkflowRunStepInfos = {
step1: { result: { res: 'value1' }, status: StepStatus.SUCCESS },
step2: { result: {}, status: StepStatus.SUCCESS },
step3: { status: StepStatus.NOT_STARTED },
step4: { result: { res: 0 }, status: StepStatus.SUCCESS },
step5: { result: { res: undefined }, status: StepStatus.SUCCESS },
};
const context = getWorkflowRunContext(stepInfos);
expect(context).toEqual({
step1: { res: 'value1' },
step2: {},
step4: { res: 0 },
step5: { res: undefined },
});
});
it('returns an empty object when no step has a defined result', () => {
const stepInfos: WorkflowRunStepInfos = {
step1: { status: StepStatus.NOT_STARTED },
step2: { status: StepStatus.NOT_STARTED },
};
const context = getWorkflowRunContext(stepInfos);
expect(context).toEqual({});
});
});

View File

@ -0,0 +1,12 @@
import { isDefined } from '@/utils';
import { WorkflowRunStepInfos } from '@/workflow/types/WorkflowRunStateStepInfos';
export const getWorkflowRunContext = (
stepInfos: WorkflowRunStepInfos,
): Record<string, unknown> => {
return Object.fromEntries(
Object.entries(stepInfos)
.filter(([, value]) => isDefined(value?.['result']))
.map(([key, value]) => [key, value?.['result']]),
);
};

View File

@ -0,0 +1,4 @@
{
"main": "dist/twenty-shared-workflow.cjs.js",
"module": "dist/twenty-shared-workflow.esm.js"
}