8839 workflow follow up code step (#8856)

- add readonly mode
- fix falsy stepOutput computation
This commit is contained in:
martmull
2024-12-05 14:26:28 +01:00
committed by GitHub
parent 081ecbcfaf
commit 455e548bea
17 changed files with 207 additions and 59 deletions

View File

@ -1571,6 +1571,8 @@ export type UpdateServerlessFunctionInput = {
export type UpdateWorkflowVersionStepInput = { export type UpdateWorkflowVersionStepInput = {
/** Step to update in JSON format */ /** Step to update in JSON format */
step: Scalars['JSON']['input']; step: Scalars['JSON']['input'];
/** Boolean to check if we need to update stepOutput */
shouldUpdateStepOutput?: InputMaybe<Scalars['Boolean']['input']>;
/** Workflow version ID */ /** Workflow version ID */
workflowVersionId: Scalars['String']['input']; workflowVersionId: Scalars['String']['input'];
}; };
@ -2249,4 +2251,4 @@ export const UpdateOneServerlessFunctionDocument = {"kind":"Document","definitio
export const FindManyAvailablePackagesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindManyAvailablePackages"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunctionIdInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getAvailablePackages"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<FindManyAvailablePackagesQuery, FindManyAvailablePackagesQueryVariables>; export const FindManyAvailablePackagesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindManyAvailablePackages"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunctionIdInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getAvailablePackages"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<FindManyAvailablePackagesQuery, FindManyAvailablePackagesQueryVariables>;
export const GetManyServerlessFunctionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetManyServerlessFunctions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findManyServerlessFunctions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersionInputSchema"}},{"kind":"Field","name":{"kind":"Name","value":"publishedVersions"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<GetManyServerlessFunctionsQuery, GetManyServerlessFunctionsQueryVariables>; export const GetManyServerlessFunctionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetManyServerlessFunctions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findManyServerlessFunctions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersionInputSchema"}},{"kind":"Field","name":{"kind":"Name","value":"publishedVersions"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<GetManyServerlessFunctionsQuery, GetManyServerlessFunctionsQueryVariables>;
export const GetOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunctionIdInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findOneServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersionInputSchema"}},{"kind":"Field","name":{"kind":"Name","value":"publishedVersions"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<GetOneServerlessFunctionQuery, GetOneServerlessFunctionQueryVariables>; export const GetOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunctionIdInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findOneServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersionInputSchema"}},{"kind":"Field","name":{"kind":"Name","value":"publishedVersions"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<GetOneServerlessFunctionQuery, GetOneServerlessFunctionQueryVariables>;
export const FindOneServerlessFunctionSourceCodeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindOneServerlessFunctionSourceCode"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"GetServerlessFunctionSourceCodeInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getServerlessFunctionSourceCode"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<FindOneServerlessFunctionSourceCodeQuery, FindOneServerlessFunctionSourceCodeQueryVariables>; export const FindOneServerlessFunctionSourceCodeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindOneServerlessFunctionSourceCode"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"GetServerlessFunctionSourceCodeInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getServerlessFunctionSourceCode"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<FindOneServerlessFunctionSourceCodeQuery, FindOneServerlessFunctionSourceCodeQueryVariables>;

View File

@ -1279,6 +1279,8 @@ export type UpdateServerlessFunctionInput = {
export type UpdateWorkflowVersionStepInput = { export type UpdateWorkflowVersionStepInput = {
/** Step to update in JSON format */ /** Step to update in JSON format */
step: Scalars['JSON']; step: Scalars['JSON'];
/** Boolean to check if we need to update stepOutput */
shouldUpdateStepOutput?: InputMaybe<Scalars['Boolean']>;
/** Workflow version ID */ /** Workflow version ID */
workflowVersionId: Scalars['String']; workflowVersionId: Scalars['String'];
}; };
@ -4416,4 +4418,4 @@ export function useGetWorkspaceFromInviteHashLazyQuery(baseOptions?: Apollo.Lazy
} }
export type GetWorkspaceFromInviteHashQueryHookResult = ReturnType<typeof useGetWorkspaceFromInviteHashQuery>; export type GetWorkspaceFromInviteHashQueryHookResult = ReturnType<typeof useGetWorkspaceFromInviteHashQuery>;
export type GetWorkspaceFromInviteHashLazyQueryHookResult = ReturnType<typeof useGetWorkspaceFromInviteHashLazyQuery>; export type GetWorkspaceFromInviteHashLazyQueryHookResult = ReturnType<typeof useGetWorkspaceFromInviteHashLazyQuery>;
export type GetWorkspaceFromInviteHashQueryResult = Apollo.QueryResult<GetWorkspaceFromInviteHashQuery, GetWorkspaceFromInviteHashQueryVariables>; export type GetWorkspaceFromInviteHashQueryResult = Apollo.QueryResult<GetWorkspaceFromInviteHashQuery, GetWorkspaceFromInviteHashQueryVariables>;

View File

@ -1,6 +1,7 @@
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient'; import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
import { UPDATE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/updateOneServerlessFunction'; import { UPDATE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/updateOneServerlessFunction';
import { FIND_MANY_SERVERLESS_FUNCTIONS } from '@/settings/serverless-functions/graphql/queries/findManyServerlessFunctions'; import { FIND_MANY_SERVERLESS_FUNCTIONS } from '@/settings/serverless-functions/graphql/queries/findManyServerlessFunctions';
import { FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE } from '@/settings/serverless-functions/graphql/queries/findOneServerlessFunctionSourceCode';
import { useMutation } from '@apollo/client'; import { useMutation } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities'; import { getOperationName } from '@apollo/client/utilities';
import { import {
@ -26,7 +27,10 @@ export const useUpdateOneServerlessFunction = () => {
input, input,
}, },
awaitRefetchQueries: true, awaitRefetchQueries: true,
refetchQueries: [getOperationName(FIND_MANY_SERVERLESS_FUNCTIONS) ?? ''], refetchQueries: [
getOperationName(FIND_MANY_SERVERLESS_FUNCTIONS) ?? '',
getOperationName(FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE) ?? '',
],
}); });
}; };

View File

@ -15,7 +15,6 @@ import {
IconTrash, IconTrash,
isDefined, isDefined,
} from 'twenty-ui'; } from 'twenty-ui';
import { capitalize } from '~/utils/string/capitalize';
import { assertWorkflowWithCurrentVersionIsDefined } from '../utils/assertWorkflowWithCurrentVersionIsDefined'; import { assertWorkflowWithCurrentVersionIsDefined } from '../utils/assertWorkflowWithCurrentVersionIsDefined';
export const RecordShowPageWorkflowHeader = ({ export const RecordShowPageWorkflowHeader = ({
@ -73,17 +72,6 @@ export const RecordShowPageWorkflowHeader = ({
workflowVersionId: workflowWithCurrentVersion.currentVersion.id, workflowVersionId: workflowWithCurrentVersion.currentVersion.id,
workflowName: workflowWithCurrentVersion.name, workflowName: workflowWithCurrentVersion.name,
}); });
enqueueSnackBar('', {
variant: SnackBarVariant.Success,
title: `${capitalize(workflowWithCurrentVersion.name)} starting...`,
icon: (
<IconSettingsAutomation
size={16}
color={theme.snackBar.success.color}
/>
),
});
}} }}
/> />

View File

@ -14,7 +14,10 @@ export const useUpdateStep = ({
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion(); const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
const { updateWorkflowVersionStep } = useUpdateWorkflowVersionStep(); const { updateWorkflowVersionStep } = useUpdateWorkflowVersionStep();
const updateStep = async <T extends WorkflowStep>(updatedStep: T) => { const updateStep = async <T extends WorkflowStep>(
updatedStep: T,
shouldUpdateStepOutput = true,
) => {
if (!isDefined(workflow.currentVersion)) { if (!isDefined(workflow.currentVersion)) {
throw new Error('Can not update an undefined workflow version.'); throw new Error('Can not update an undefined workflow version.');
} }
@ -23,6 +26,7 @@ export const useUpdateStep = ({
await updateWorkflowVersionStep({ await updateWorkflowVersionStep({
workflowVersionId: workflowVersion.id, workflowVersionId: workflowVersion.id,
step: updatedStep, step: updatedStep,
shouldUpdateStepOutput,
}); });
}; };

View File

@ -5,7 +5,7 @@ import { mergeDefaultFunctionInputAndFunctionInput } from '@/workflow/utils/merg
import { setNestedValue } from '@/workflow/utils/setNestedValue'; import { setNestedValue } from '@/workflow/utils/setNestedValue';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Fragment, ReactNode, useState } from 'react'; import { Fragment, ReactNode, useEffect, useState } from 'react';
import { import {
CodeEditor, CodeEditor,
HorizontalSeparator, HorizontalSeparator,
@ -63,7 +63,10 @@ type WorkflowEditActionFormServerlessFunctionProps = {
} }
| { | {
readonly?: false; readonly?: false;
onActionUpdate: (action: WorkflowCodeAction) => void; onActionUpdate: (
action: WorkflowCodeAction,
shouldUpdateStepOutput?: boolean,
) => void;
}; };
}; };
@ -88,9 +91,16 @@ export const WorkflowEditActionFormServerlessFunction = ({
id: serverlessFunctionId, id: serverlessFunctionId,
}); });
const [functionInput, setFunctionInput] =
useState<ServerlessFunctionInputFormData>(
action.settings.input.serverlessFunctionInput,
);
const { formValues, setFormValues, loading } = const { formValues, setFormValues, loading } =
useServerlessFunctionUpdateFormState(serverlessFunctionId); useServerlessFunctionUpdateFormState(serverlessFunctionId);
const headerTitle = action.name || 'Code - Serverless Function';
const save = async () => { const save = async () => {
try { try {
await updateOneServerlessFunction({ await updateOneServerlessFunction({
@ -112,6 +122,9 @@ export const WorkflowEditActionFormServerlessFunction = ({
const handleSave = usePreventOverlapCallback(save, 1000); const handleSave = usePreventOverlapCallback(save, 1000);
const onCodeChange = async (value: string) => { const onCodeChange = async (value: string) => {
if (actionOptions.readonly === true) {
return;
}
setFormValues((prevState) => ({ setFormValues((prevState) => ({
...prevState, ...prevState,
code: { ...prevState.code, [INDEX_FILE_PATH]: value }, code: { ...prevState.code, [INDEX_FILE_PATH]: value },
@ -121,6 +134,9 @@ export const WorkflowEditActionFormServerlessFunction = ({
}; };
const updateFunctionInputSchema = async () => { const updateFunctionInputSchema = async () => {
if (actionOptions.readonly === true) {
return;
}
const sourceCode = formValues.code?.[INDEX_FILE_PATH]; const sourceCode = formValues.code?.[INDEX_FILE_PATH];
if (!isDefined(sourceCode)) { if (!isDefined(sourceCode)) {
return; return;
@ -141,27 +157,25 @@ export const WorkflowEditActionFormServerlessFunction = ({
100, 100,
); );
const [functionInput, setFunctionInput] =
useState<ServerlessFunctionInputFormData>(
action.settings.input.serverlessFunctionInput,
);
const updateFunctionInput = useDebouncedCallback( const updateFunctionInput = useDebouncedCallback(
async (newFunctionInput: object) => { async (newFunctionInput: object, shouldUpdateStepOutput = true) => {
if (actionOptions.readonly === true) { if (actionOptions.readonly === true) {
return; return;
} }
actionOptions.onActionUpdate({ actionOptions.onActionUpdate(
...action, {
settings: { ...action,
...action.settings, settings: {
input: { ...action.settings,
...action.settings.input, input: {
serverlessFunctionInput: newFunctionInput, ...action.settings.input,
serverlessFunctionInput: newFunctionInput,
},
}, },
}, },
}); shouldUpdateStepOutput,
);
}, },
1_000, 1_000,
); );
@ -171,7 +185,7 @@ export const WorkflowEditActionFormServerlessFunction = ({
setFunctionInput(updatedFunctionInput); setFunctionInput(updatedFunctionInput);
await updateFunctionInput(updatedFunctionInput); await updateFunctionInput(updatedFunctionInput, false);
}; };
const renderFields = ( const renderFields = (
@ -230,10 +244,6 @@ export const WorkflowEditActionFormServerlessFunction = ({
}); });
}; };
const headerTitle = isDefined(action.name)
? action.name
: 'Code - Serverless Function';
const handleEditorDidMount = async ( const handleEditorDidMount = async (
editor: editor.IStandaloneCodeEditor, editor: editor.IStandaloneCodeEditor,
monaco: Monaco, monaco: Monaco,
@ -252,10 +262,13 @@ export const WorkflowEditActionFormServerlessFunction = ({
return; return;
} }
actionOptions?.onActionUpdate({ actionOptions?.onActionUpdate(
...action, {
...actionUpdate, ...action,
}); ...actionUpdate,
},
false,
);
}; };
const checkWorkflowUpdatable = async () => { const checkWorkflowUpdatable = async () => {
@ -265,6 +278,10 @@ export const WorkflowEditActionFormServerlessFunction = ({
await getUpdatableWorkflowVersion(workflow); await getUpdatableWorkflowVersion(workflow);
}; };
useEffect(() => {
setFunctionInput(action.settings.input.serverlessFunctionInput);
}, [action]);
return ( return (
!loading && ( !loading && (
<WorkflowEditGenericFormBase <WorkflowEditGenericFormBase
@ -285,6 +302,10 @@ export const WorkflowEditActionFormServerlessFunction = ({
await onCodeChange(value); await onCodeChange(value);
}} }}
onMount={handleEditorDidMount} onMount={handleEditorDidMount}
options={{
readOnly: actionOptions.readonly,
domReadOnly: actionOptions.readonly,
}}
/> />
{renderFields(functionInput)} {renderFields(functionInput)}
</WorkflowEditGenericFormBase> </WorkflowEditGenericFormBase>

View File

@ -319,6 +319,7 @@ export class LambdaDriver implements ServerlessDriver {
await this.waitFunctionUpdates(functionToExecute.id, 10); await this.waitFunctionUpdates(functionToExecute.id, 10);
const startTime = Date.now(); const startTime = Date.now();
const params: InvokeCommandInput = { const params: InvokeCommandInput = {
FunctionName: functionName, FunctionName: functionName,
Payload: JSON.stringify(payload), Payload: JSON.stringify(payload),

View File

@ -17,4 +17,11 @@ export class UpdateWorkflowVersionStepInput {
nullable: false, nullable: false,
}) })
step: WorkflowAction; step: WorkflowAction;
@Field(() => Boolean, {
description: 'Boolean to check if we need to update stepOutput',
nullable: true,
defaultValue: true,
})
shouldUpdateStepOutput: boolean;
} }

View File

@ -36,12 +36,18 @@ export class WorkflowVersionStepResolver {
@Mutation(() => WorkflowActionDTO) @Mutation(() => WorkflowActionDTO)
async updateWorkflowVersionStep( async updateWorkflowVersionStep(
@AuthWorkspace() { id: workspaceId }: Workspace, @AuthWorkspace() { id: workspaceId }: Workspace,
@Args('input') { step, workflowVersionId }: UpdateWorkflowVersionStepInput, @Args('input')
{
step,
workflowVersionId,
shouldUpdateStepOutput,
}: UpdateWorkflowVersionStepInput,
): Promise<WorkflowActionDTO> { ): Promise<WorkflowActionDTO> {
return this.workflowVersionStepWorkspaceService.updateWorkflowVersionStep({ return this.workflowVersionStepWorkspaceService.updateWorkflowVersionStep({
workspaceId, workspaceId,
workflowVersionId, workflowVersionId,
step, step,
shouldUpdateStepOutput,
}); });
} }

View File

@ -45,9 +45,9 @@ export class WorkspaceEventEmitter {
}); });
} }
public emitCustomBatchEvent( public emitCustomBatchEvent<T extends object>(
eventName: CustomEventName, eventName: CustomEventName,
events: object[], events: T[],
workspaceId: string, workspaceId: string,
) { ) {
if (!events.length) { if (!events.length) {

View File

@ -245,10 +245,12 @@ export class WorkflowVersionStepWorkspaceService {
workspaceId, workspaceId,
workflowVersionId, workflowVersionId,
step, step,
shouldUpdateStepOutput,
}: { }: {
workspaceId: string; workspaceId: string;
workflowVersionId: string; workflowVersionId: string;
step: WorkflowAction; step: WorkflowAction;
shouldUpdateStepOutput: boolean;
}): Promise<WorkflowAction> { }): Promise<WorkflowAction> {
const workflowVersionRepository = const workflowVersionRepository =
await this.twentyORMManager.getRepository<WorkflowVersionWorkspaceEntity>( await this.twentyORMManager.getRepository<WorkflowVersionWorkspaceEntity>(
@ -275,10 +277,12 @@ export class WorkflowVersionStepWorkspaceService {
); );
} }
const enrichedNewStep = await this.enrichOutputSchema({ const enrichedNewStep = shouldUpdateStepOutput
step, ? await this.enrichOutputSchema({
workspaceId, step,
}); workspaceId,
})
: step;
const updatedSteps = workflowVersion.steps.map((existingStep) => { const updatedSteps = workflowVersion.steps.map((existingStep) => {
if (existingStep.id === step.id) { if (existingStep.id === step.id) {

View File

@ -226,6 +226,7 @@ export class WorkflowBuilderWorkspaceService {
const inputSchema = const inputSchema =
codeIntrospectionService.getFunctionInputSchema(sourceCode); codeIntrospectionService.getFunctionInputSchema(sourceCode);
const fakeFunctionInput = const fakeFunctionInput =
codeIntrospectionService.generateInputData(inputSchema); codeIntrospectionService.generateInputData(inputSchema);
@ -233,7 +234,7 @@ export class WorkflowBuilderWorkspaceService {
await serverlessFunctionService.executeOneServerlessFunction( await serverlessFunctionService.executeOneServerlessFunction(
serverlessFunctionId, serverlessFunctionId,
workspaceId, workspaceId,
fakeFunctionInput, Object.values(fakeFunctionInput)?.[0] || {},
serverlessFunctionVersion, serverlessFunctionVersion,
); );

View File

@ -35,7 +35,8 @@ export class CodeWorkflowAction implements WorkflowAction {
await this.serverlessFunctionService.executeOneServerlessFunction( await this.serverlessFunctionService.executeOneServerlessFunction(
workflowActionInput.serverlessFunctionId, workflowActionInput.serverlessFunctionId,
workspaceId, workspaceId,
workflowActionInput.serverlessFunctionInput, Object.values(workflowActionInput.serverlessFunctionInput)?.[0] || {},
workflowActionInput.serverlessFunctionVersion,
); );
if (result.error) { if (result.error) {

View File

@ -8,6 +8,7 @@ import {
WorkflowVersionBatchEvent, WorkflowVersionBatchEvent,
WorkflowVersionEventType, WorkflowVersionEventType,
} from 'src/modules/workflow/workflow-status/jobs/workflow-statuses-update.job'; } from 'src/modules/workflow/workflow-status/jobs/workflow-statuses-update.job';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
describe('WorkflowStatusesUpdate', () => { describe('WorkflowStatusesUpdate', () => {
let job: WorkflowStatusesUpdateJob; let job: WorkflowStatusesUpdateJob;
@ -21,6 +22,10 @@ describe('WorkflowStatusesUpdate', () => {
getRepository: jest.fn().mockResolvedValue(mockWorkflowRepository), getRepository: jest.fn().mockResolvedValue(mockWorkflowRepository),
}; };
const mockServerlessFunctionService = {
publishOneServerlessFunction: jest.fn(),
};
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [ providers: [
@ -29,6 +34,10 @@ describe('WorkflowStatusesUpdate', () => {
provide: TwentyORMManager, provide: TwentyORMManager,
useValue: mockTwentyORMManager, useValue: mockTwentyORMManager,
}, },
{
provide: ServerlessFunctionService,
useValue: mockServerlessFunctionService,
},
], ],
}).compile(); }).compile();
@ -94,6 +103,7 @@ describe('WorkflowStatusesUpdate', () => {
statusUpdates: [ statusUpdates: [
{ {
workflowId: '1', workflowId: '1',
workflowVersionId: '1',
previousStatus: WorkflowVersionStatus.ACTIVE, previousStatus: WorkflowVersionStatus.ACTIVE,
newStatus: WorkflowVersionStatus.ACTIVE, newStatus: WorkflowVersionStatus.ACTIVE,
}, },
@ -108,7 +118,7 @@ describe('WorkflowStatusesUpdate', () => {
await job.handle(event); await job.handle(event);
expect(mockWorkflowRepository.findOneOrFail).toHaveBeenCalledTimes(1); expect(mockWorkflowRepository.findOneOrFail).toHaveBeenCalledTimes(2);
expect(mockWorkflowRepository.update).toHaveBeenCalledTimes(0); expect(mockWorkflowRepository.update).toHaveBeenCalledTimes(0);
}); });
@ -119,6 +129,7 @@ describe('WorkflowStatusesUpdate', () => {
statusUpdates: [ statusUpdates: [
{ {
workflowId: '1', workflowId: '1',
workflowVersionId: '1',
previousStatus: WorkflowVersionStatus.ACTIVE, previousStatus: WorkflowVersionStatus.ACTIVE,
newStatus: WorkflowVersionStatus.DRAFT, newStatus: WorkflowVersionStatus.DRAFT,
}, },
@ -133,7 +144,7 @@ describe('WorkflowStatusesUpdate', () => {
await job.handle(event); await job.handle(event);
expect(mockWorkflowRepository.findOneOrFail).toHaveBeenCalledTimes(1); expect(mockWorkflowRepository.findOneOrFail).toHaveBeenCalledTimes(2);
expect(mockWorkflowRepository.update).toHaveBeenCalledTimes(0); expect(mockWorkflowRepository.update).toHaveBeenCalledTimes(0);
}); });
@ -144,6 +155,7 @@ describe('WorkflowStatusesUpdate', () => {
statusUpdates: [ statusUpdates: [
{ {
workflowId: '1', workflowId: '1',
workflowVersionId: '1',
previousStatus: WorkflowVersionStatus.DEACTIVATED, previousStatus: WorkflowVersionStatus.DEACTIVATED,
newStatus: WorkflowVersionStatus.ACTIVE, newStatus: WorkflowVersionStatus.ACTIVE,
}, },
@ -158,7 +170,7 @@ describe('WorkflowStatusesUpdate', () => {
await job.handle(event); await job.handle(event);
expect(mockWorkflowRepository.findOneOrFail).toHaveBeenCalledTimes(1); expect(mockWorkflowRepository.findOneOrFail).toHaveBeenCalledTimes(2);
expect(mockWorkflowRepository.update).toHaveBeenCalledWith( expect(mockWorkflowRepository.update).toHaveBeenCalledWith(
{ id: '1' }, { id: '1' },
{ statuses: [WorkflowStatus.ACTIVE] }, { statuses: [WorkflowStatus.ACTIVE] },
@ -172,6 +184,7 @@ describe('WorkflowStatusesUpdate', () => {
statusUpdates: [ statusUpdates: [
{ {
workflowId: '1', workflowId: '1',
workflowVersionId: '1',
previousStatus: WorkflowVersionStatus.ACTIVE, previousStatus: WorkflowVersionStatus.ACTIVE,
newStatus: WorkflowVersionStatus.DEACTIVATED, newStatus: WorkflowVersionStatus.DEACTIVATED,
}, },
@ -186,7 +199,7 @@ describe('WorkflowStatusesUpdate', () => {
await job.handle(event); await job.handle(event);
expect(mockWorkflowRepository.findOneOrFail).toHaveBeenCalledTimes(1); expect(mockWorkflowRepository.findOneOrFail).toHaveBeenCalledTimes(2);
expect(mockWorkflowRepository.update).toHaveBeenCalledWith( expect(mockWorkflowRepository.update).toHaveBeenCalledWith(
{ id: '1' }, { id: '1' },
{ statuses: [WorkflowStatus.DEACTIVATED] }, { statuses: [WorkflowStatus.DEACTIVATED] },
@ -200,6 +213,7 @@ describe('WorkflowStatusesUpdate', () => {
statusUpdates: [ statusUpdates: [
{ {
workflowId: '1', workflowId: '1',
workflowVersionId: '1',
previousStatus: WorkflowVersionStatus.DRAFT, previousStatus: WorkflowVersionStatus.DRAFT,
newStatus: WorkflowVersionStatus.ACTIVE, newStatus: WorkflowVersionStatus.ACTIVE,
}, },
@ -214,7 +228,7 @@ describe('WorkflowStatusesUpdate', () => {
await job.handle(event); await job.handle(event);
expect(mockWorkflowRepository.findOneOrFail).toHaveBeenCalledTimes(1); expect(mockWorkflowRepository.findOneOrFail).toHaveBeenCalledTimes(2);
expect(mockWorkflowRepository.update).toHaveBeenCalledWith( expect(mockWorkflowRepository.update).toHaveBeenCalledWith(
{ id: '1' }, { id: '1' },
{ statuses: [WorkflowStatus.ACTIVE] }, { statuses: [WorkflowStatus.ACTIVE] },

View File

@ -4,11 +4,21 @@ import { Process } from 'src/engine/core-modules/message-queue/decorators/proces
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { WorkflowVersionStatus } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; import {
WorkflowVersionStatus,
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 { getStatusCombinationFromArray } from 'src/modules/workflow/workflow-status/utils/get-status-combination-from-array.util'; import { getStatusCombinationFromArray } from 'src/modules/workflow/workflow-status/utils/get-status-combination-from-array.util';
import { getStatusCombinationFromUpdate } from 'src/modules/workflow/workflow-status/utils/get-status-combination-from-update.util'; import { getStatusCombinationFromUpdate } from 'src/modules/workflow/workflow-status/utils/get-status-combination-from-update.util';
import { getWorkflowStatusesFromCombination } from 'src/modules/workflow/workflow-status/utils/get-statuses-from-combination.util'; import { getWorkflowStatusesFromCombination } from 'src/modules/workflow/workflow-status/utils/get-statuses-from-combination.util';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
import {
WorkflowAction,
WorkflowActionType,
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
import { isDefined } from 'src/utils/is-defined';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
export enum WorkflowVersionEventType { export enum WorkflowVersionEventType {
CREATE = 'CREATE', CREATE = 'CREATE',
@ -32,6 +42,7 @@ export type WorkflowVersionBatchCreateEvent = {
export type WorkflowVersionStatusUpdate = { export type WorkflowVersionStatusUpdate = {
workflowId: string; workflowId: string;
workflowVersionId: string;
previousStatus: WorkflowVersionStatus; previousStatus: WorkflowVersionStatus;
newStatus: WorkflowVersionStatus; newStatus: WorkflowVersionStatus;
}; };
@ -48,7 +59,10 @@ export type WorkflowVersionBatchDelete = {
@Processor({ queueName: MessageQueue.workflowQueue, scope: Scope.REQUEST }) @Processor({ queueName: MessageQueue.workflowQueue, scope: Scope.REQUEST })
export class WorkflowStatusesUpdateJob { export class WorkflowStatusesUpdateJob {
constructor(private readonly twentyORMManager: TwentyORMManager) {} constructor(
private readonly twentyORMManager: TwentyORMManager,
private readonly serverlessFunctionService: ServerlessFunctionService,
) {}
@Process(WorkflowStatusesUpdateJob.name) @Process(WorkflowStatusesUpdateJob.name)
async handle(event: WorkflowVersionBatchEvent): Promise<void> { async handle(event: WorkflowVersionBatchEvent): Promise<void> {
@ -63,7 +77,10 @@ export class WorkflowStatusesUpdateJob {
case WorkflowVersionEventType.STATUS_UPDATE: case WorkflowVersionEventType.STATUS_UPDATE:
await Promise.all( await Promise.all(
event.statusUpdates.map((statusUpdate) => event.statusUpdates.map((statusUpdate) =>
this.handleWorkflowVersionStatusUpdated(statusUpdate), this.handleWorkflowVersionStatusUpdated(
statusUpdate,
event.workspaceId,
),
), ),
); );
break; break;
@ -119,20 +136,92 @@ export class WorkflowStatusesUpdateJob {
); );
} }
private async handlePublishServerlessFunction({
statusUpdate,
workspaceId,
workflowVersion,
workflowVersionRepository,
}: {
statusUpdate: WorkflowVersionStatusUpdate;
workspaceId: string;
workflowVersion: WorkflowVersionWorkspaceEntity;
workflowVersionRepository: WorkspaceRepository<WorkflowVersionWorkspaceEntity>;
}) {
const shouldComputeNewSteps =
statusUpdate.newStatus === WorkflowVersionStatus.ACTIVE &&
isDefined(workflowVersion.steps) &&
workflowVersion.steps.filter(
(step) => step.type === WorkflowActionType.CODE,
).length > 0;
if (shouldComputeNewSteps) {
const newSteps: WorkflowAction[] = [];
for (const step of workflowVersion.steps || []) {
const newStep = { ...step };
if (step.type === WorkflowActionType.CODE) {
let serverlessFunction;
try {
serverlessFunction =
await this.serverlessFunctionService.publishOneServerlessFunction(
step.settings.input.serverlessFunctionId,
workspaceId,
);
} catch (e) {
serverlessFunction = null;
}
if (serverlessFunction) {
const newStepSettings = { ...step.settings };
newStepSettings.input.serverlessFunctionVersion =
serverlessFunction.latestVersion;
newStep.settings = newStepSettings;
}
}
newSteps.push(newStep);
}
await workflowVersionRepository.update(statusUpdate.workflowVersionId, {
steps: newSteps,
});
}
}
private async handleWorkflowVersionStatusUpdated( private async handleWorkflowVersionStatusUpdated(
statusUpdate: WorkflowVersionStatusUpdate, statusUpdate: WorkflowVersionStatusUpdate,
workspaceId: string,
): Promise<void> { ): Promise<void> {
const workflowRepository = const workflowRepository =
await this.twentyORMManager.getRepository<WorkflowWorkspaceEntity>( await this.twentyORMManager.getRepository<WorkflowWorkspaceEntity>(
'workflow', 'workflow',
); );
const workflowVersionRepository =
await this.twentyORMManager.getRepository<WorkflowVersionWorkspaceEntity>(
'workflowVersion',
);
const workflow = await workflowRepository.findOneOrFail({ const workflow = await workflowRepository.findOneOrFail({
where: { where: {
id: statusUpdate.workflowId, id: statusUpdate.workflowId,
}, },
}); });
const workflowVersion = await workflowVersionRepository.findOneOrFail({
where: { id: statusUpdate.workflowVersionId },
});
await this.handlePublishServerlessFunction({
workflowVersion,
workflowVersionRepository,
workspaceId,
statusUpdate,
});
const currentWorkflowStatusCombination = getStatusCombinationFromArray( const currentWorkflowStatusCombination = getStatusCombinationFromArray(
workflow.statuses || [], workflow.statuses || [],
); );

View File

@ -2,8 +2,10 @@ import { Module } from '@nestjs/common';
import { WorkflowStatusesUpdateJob } from 'src/modules/workflow/workflow-status/jobs/workflow-statuses-update.job'; import { WorkflowStatusesUpdateJob } from 'src/modules/workflow/workflow-status/jobs/workflow-statuses-update.job';
import { WorkflowVersionStatusListener } from 'src/modules/workflow/workflow-status/listeners/workflow-version-status.listener'; import { WorkflowVersionStatusListener } from 'src/modules/workflow/workflow-status/listeners/workflow-version-status.listener';
import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
@Module({ @Module({
imports: [ServerlessFunctionModule],
providers: [WorkflowStatusesUpdateJob, WorkflowVersionStatusListener], providers: [WorkflowStatusesUpdateJob, WorkflowVersionStatusListener],
}) })
export class WorkflowStatusModule {} export class WorkflowStatusModule {}

View File

@ -28,6 +28,7 @@ import { assertNever } from 'src/utils/assert';
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 { WORKFLOW_VERSION_STATUS_UPDATED } from 'src/modules/workflow/workflow-status/constants/workflow-version-status-updated.constants'; import { WORKFLOW_VERSION_STATUS_UPDATED } from 'src/modules/workflow/workflow-status/constants/workflow-version-status-updated.constants';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkflowVersionStatusUpdate } from 'src/modules/workflow/workflow-status/jobs/workflow-statuses-update.job';
@Injectable() @Injectable()
export class WorkflowTriggerWorkspaceService { export class WorkflowTriggerWorkspaceService {
@ -387,11 +388,12 @@ export class WorkflowTriggerWorkspaceService {
workspaceId, workspaceId,
}); });
this.workspaceEventEmitter.emitCustomBatchEvent( this.workspaceEventEmitter.emitCustomBatchEvent<WorkflowVersionStatusUpdate>(
WORKFLOW_VERSION_STATUS_UPDATED, WORKFLOW_VERSION_STATUS_UPDATED,
[ [
{ {
workflowId: workflowVersion.workflowId, workflowId: workflowVersion.workflowId,
workflowVersionId: workflowVersion.id,
previousStatus: workflowVersion.status, previousStatus: workflowVersion.status,
newStatus, newStatus,
}, },