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

@ -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

@ -0,0 +1,15 @@
export enum StepStatus {
NOT_STARTED = 'NOT_STARTED',
RUNNING = 'RUNNING',
SUCCESS = 'SUCCESS',
FAILED = 'FAILED',
PENDING = 'PENDING',
}
export type WorkflowRunStepInfo = {
result?: object;
error?: string;
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']]),
);
};