Compute output schema on frontend (#16530)

Fixes https://github.com/twentyhq/core-team-issues/issues/1382

Current issue : all step output schemas are computed and stored on
backend side. Which means that, when the database schema is updated -
like a field creation - steps needs to be deleted an recreated. Which is
invisible to users.

Solution : schema generation is moved on frontend side

1. Coming on the page the first time, the schema is populated for all
steps except a few ones that are handled differently (Code, Webhook,
http node, Agent)

2. A separated state allow to determine if a step needs a recomputation.

3. The user only needs a refresh to see the whole schema re-computed

Follow-up:
- check if remaining backend steps could be moved to runtime
computation. But Code will still require storage.
- Clean backend service that is not used anymore
This commit is contained in:
Thomas Trompette
2025-12-15 16:20:35 +01:00
committed by GitHub
parent 2e104c8e76
commit 95e0793f81
43 changed files with 3152 additions and 209 deletions
@@ -25,7 +25,7 @@ jest.mock('@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow', () => ({
}),
}));
jest.mock('@/workflow/hooks/useStepsOutputSchema', () => ({
jest.mock('@/workflow/workflow-variables/hooks/useStepsOutputSchema', () => ({
useStepsOutputSchema: () => ({
deleteStepsOutputSchema: mockDeleteStepsOutputSchema,
}),
@@ -1,8 +1,10 @@
import { useUpdateStep } from '@/workflow/workflow-steps/hooks/useUpdateStep';
import { renderHook } from '@testing-library/react';
import { act, renderHook } from '@testing-library/react';
import { type WorkflowAction } from '~/generated/graphql';
const mockUpdateWorkflowVersionStep = jest.fn();
const mockGetUpdatableWorkflowVersion = jest.fn();
const mockMarkStepForRecomputation = jest.fn();
jest.mock(
'@/workflow/workflow-steps/hooks/useUpdateWorkflowVersionStep',
@@ -19,6 +21,12 @@ jest.mock('@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow', () => ({
}),
}));
jest.mock('@/workflow/workflow-variables/hooks/useStepsOutputSchema', () => ({
useStepsOutputSchema: jest.fn(() => ({
markStepForRecomputation: mockMarkStepForRecomputation,
})),
}));
describe('useUpdateStep', () => {
beforeEach(() => {
jest.clearAllMocks();
@@ -52,7 +60,9 @@ describe('useUpdateStep', () => {
mockGetUpdatableWorkflowVersion.mockResolvedValue(mockWorkflowVersionId);
const { result } = renderHook(() => useUpdateStep());
await result.current.updateStep(mockStep);
await act(async () => {
await result.current.updateStep(mockStep);
});
expect(mockGetUpdatableWorkflowVersion).toHaveBeenCalled();
expect(mockUpdateWorkflowVersionStep).toHaveBeenCalledWith({
@@ -60,4 +70,69 @@ describe('useUpdateStep', () => {
step: mockStep,
});
});
it('should mark step for recomputation after update', async () => {
const mockWorkflowVersionId = 'version-123';
const mockStep = {
id: 'step-1',
name: 'Create Record',
valid: true,
type: 'CREATE_RECORD' as const,
settings: {
input: {
objectName: 'company',
},
errorHandlingOptions: {
retryOnFailure: { value: false },
continueOnFailure: { value: false },
},
},
};
mockGetUpdatableWorkflowVersion.mockResolvedValue(mockWorkflowVersionId);
const { result } = renderHook(() => useUpdateStep());
await act(async () => {
await result.current.updateStep(mockStep as WorkflowAction);
});
expect(mockMarkStepForRecomputation).toHaveBeenCalledWith({
stepId: 'step-1',
workflowVersionId: mockWorkflowVersionId,
});
});
it('should mark step for recomputation for all step types', async () => {
const mockWorkflowVersionId = 'version-123';
const stepTypes = ['CODE', 'HTTP_REQUEST', 'CREATE_RECORD', 'SEND_EMAIL'];
for (const stepType of stepTypes) {
mockMarkStepForRecomputation.mockClear();
mockGetUpdatableWorkflowVersion.mockResolvedValue(mockWorkflowVersionId);
const mockStep = {
id: `step-${stepType}`,
name: `${stepType} Step`,
valid: true,
type: stepType as any,
settings: {
input: {},
errorHandlingOptions: {
retryOnFailure: { value: false },
continueOnFailure: { value: false },
},
},
};
const { result } = renderHook(() => useUpdateStep());
await act(async () => {
await result.current.updateStep(mockStep as WorkflowAction);
});
expect(mockMarkStepForRecomputation).toHaveBeenCalledWith({
stepId: `step-${stepType}`,
workflowVersionId: mockWorkflowVersionId,
});
}
});
});
@@ -1,11 +1,11 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow';
import { useStepsOutputSchema } from '@/workflow/hooks/useStepsOutputSchema';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
import { useDeleteWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useDeleteWorkflowVersionStep';
import { useResetWorkflowAiAgentPermissionsStateOnCommandMenuClose } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/hooks/useResetWorkflowAiAgentPermissionsStateOnCommandMenuClose';
import { useStepsOutputSchema } from '@/workflow/workflow-variables/hooks/useStepsOutputSchema';
import { isDefined } from 'twenty-shared/utils';
export const useDeleteStep = () => {
@@ -1,11 +1,13 @@
import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow';
import { type WorkflowAction } from '@/workflow/types/Workflow';
import { useUpdateWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowVersionStep';
import { useStepsOutputSchema } from '@/workflow/workflow-variables/hooks/useStepsOutputSchema';
export const useUpdateStep = () => {
const { getUpdatableWorkflowVersion } =
useGetUpdatableWorkflowVersionOrThrow();
const { updateWorkflowVersionStep } = useUpdateWorkflowVersionStep();
const { markStepForRecomputation } = useStepsOutputSchema();
const updateStep = async (updatedStep: WorkflowAction) => {
const workflowVersionId = await getUpdatableWorkflowVersion();
@@ -15,6 +17,11 @@ export const useUpdateStep = () => {
step: updatedStep,
});
markStepForRecomputation({
stepId: updatedStep.id,
workflowVersionId,
});
return {
updatedStep: result?.data?.updateWorkflowVersionStep,
};