mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-23 12:02:10 +03:00
Execute variables in action input (#7715)
- send context from all previous steps rather than unique payload - wrap input data in settings into input field - add email into send email action settings - update output shape <img width="553" alt="Capture d’écran 2024-10-15 à 15 21 32" src="https://github.com/user-attachments/assets/0f5ed004-0d6e-4a59-969b-a5710f3f3985"> <img width="761" alt="Capture d’écran 2024-10-15 à 15 20 09" src="https://github.com/user-attachments/assets/ac140846-c383-483b-968a-eab469b76785">
This commit is contained in:
parent
a88c2fa453
commit
e811bae10e
@ -1,21 +1,21 @@
|
|||||||
|
import { GMAIL_SEND_SCOPE } from '@/accounts/constants/GmailSendScope';
|
||||||
|
import { ConnectedAccount } from '@/accounts/types/ConnectedAccount';
|
||||||
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
|
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||||
|
import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth';
|
||||||
|
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||||
import { TextArea } from '@/ui/input/components/TextArea';
|
import { TextArea } from '@/ui/input/components/TextArea';
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
import { WorkflowEditActionFormBase } from '@/workflow/components/WorkflowEditActionFormBase';
|
import { WorkflowEditActionFormBase } from '@/workflow/components/WorkflowEditActionFormBase';
|
||||||
|
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||||
import { WorkflowSendEmailStep } from '@/workflow/types/Workflow';
|
import { WorkflowSendEmailStep } from '@/workflow/types/Workflow';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import React, { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
import { IconMail, IconPlus, isDefined } from 'twenty-ui';
|
import { IconMail, IconPlus, isDefined } from 'twenty-ui';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
import { Select, SelectOption } from '@/ui/input/components/Select';
|
|
||||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
|
||||||
import { ConnectedAccount } from '@/accounts/types/ConnectedAccount';
|
|
||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
|
||||||
import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth';
|
|
||||||
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
|
||||||
import { GMAIL_SEND_SCOPE } from '@/accounts/constants/GmailSendScope';
|
|
||||||
|
|
||||||
const StyledTriggerSettings = styled.div`
|
const StyledTriggerSettings = styled.div`
|
||||||
padding: ${({ theme }) => theme.spacing(6)};
|
padding: ${({ theme }) => theme.spacing(6)};
|
||||||
@ -37,6 +37,7 @@ type WorkflowEditActionFormSendEmailProps =
|
|||||||
|
|
||||||
type SendEmailFormData = {
|
type SendEmailFormData = {
|
||||||
connectedAccountId: string;
|
connectedAccountId: string;
|
||||||
|
email: string;
|
||||||
subject: string;
|
subject: string;
|
||||||
body: string;
|
body: string;
|
||||||
};
|
};
|
||||||
@ -53,6 +54,7 @@ export const WorkflowEditActionFormSendEmail = (
|
|||||||
const form = useForm<SendEmailFormData>({
|
const form = useForm<SendEmailFormData>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
connectedAccountId: '',
|
connectedAccountId: '',
|
||||||
|
email: '',
|
||||||
subject: '',
|
subject: '',
|
||||||
body: '',
|
body: '',
|
||||||
},
|
},
|
||||||
@ -83,10 +85,11 @@ export const WorkflowEditActionFormSendEmail = (
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.setValue(
|
form.setValue(
|
||||||
'connectedAccountId',
|
'connectedAccountId',
|
||||||
props.action.settings.connectedAccountId ?? '',
|
props.action.settings.input.connectedAccountId ?? '',
|
||||||
);
|
);
|
||||||
form.setValue('subject', props.action.settings.subject ?? '');
|
form.setValue('email', props.action.settings.input.email ?? '');
|
||||||
form.setValue('body', props.action.settings.body ?? '');
|
form.setValue('subject', props.action.settings.input.subject ?? '');
|
||||||
|
form.setValue('body', props.action.settings.input.body ?? '');
|
||||||
}, [props.action.settings, form]);
|
}, [props.action.settings, form]);
|
||||||
|
|
||||||
const saveAction = useDebouncedCallback(
|
const saveAction = useDebouncedCallback(
|
||||||
@ -99,10 +102,13 @@ export const WorkflowEditActionFormSendEmail = (
|
|||||||
...props.action,
|
...props.action,
|
||||||
settings: {
|
settings: {
|
||||||
...props.action.settings,
|
...props.action.settings,
|
||||||
|
input: {
|
||||||
connectedAccountId: formData.connectedAccountId,
|
connectedAccountId: formData.connectedAccountId,
|
||||||
|
email: formData.email,
|
||||||
subject: formData.subject,
|
subject: formData.subject,
|
||||||
body: formData.body,
|
body: formData.body,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (checkScopes === true) {
|
if (checkScopes === true) {
|
||||||
@ -134,12 +140,12 @@ export const WorkflowEditActionFormSendEmail = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isDefined(props.action.settings.connectedAccountId) &&
|
isDefined(props.action.settings.input.connectedAccountId) &&
|
||||||
props.action.settings.connectedAccountId !== ''
|
props.action.settings.input.connectedAccountId !== ''
|
||||||
) {
|
) {
|
||||||
filter.or.push({
|
filter.or.push({
|
||||||
id: {
|
id: {
|
||||||
eq: props.action.settings.connectedAccountId,
|
eq: props.action.settings.input.connectedAccountId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -198,6 +204,21 @@ export const WorkflowEditActionFormSendEmail = (
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<Controller
|
||||||
|
name="email"
|
||||||
|
control={form.control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextInput
|
||||||
|
label="Email"
|
||||||
|
placeholder="Enter receiver email (use {{variable}} for dynamic content)"
|
||||||
|
value={field.value}
|
||||||
|
onChange={(email) => {
|
||||||
|
field.onChange(email);
|
||||||
|
handleSave();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
name="subject"
|
name="subject"
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
@ -54,7 +54,7 @@ export const WorkflowEditActionFormServerlessFunction = (
|
|||||||
dropdownId="workflow-edit-action-function"
|
dropdownId="workflow-edit-action-function"
|
||||||
label="Function"
|
label="Function"
|
||||||
fullWidth
|
fullWidth
|
||||||
value={props.action.settings.serverlessFunctionId}
|
value={props.action.settings.input.serverlessFunctionId}
|
||||||
options={availableFunctions}
|
options={availableFunctions}
|
||||||
disabled={props.readonly}
|
disabled={props.readonly}
|
||||||
onChange={(updatedFunction) => {
|
onChange={(updatedFunction) => {
|
||||||
@ -66,8 +66,10 @@ export const WorkflowEditActionFormServerlessFunction = (
|
|||||||
...props.action,
|
...props.action,
|
||||||
settings: {
|
settings: {
|
||||||
...props.action.settings,
|
...props.action.settings,
|
||||||
|
input: {
|
||||||
serverlessFunctionId: updatedFunction,
|
serverlessFunctionId: updatedFunction,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -10,14 +10,19 @@ type BaseWorkflowStepSettings = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type WorkflowCodeStepSettings = BaseWorkflowStepSettings & {
|
export type WorkflowCodeStepSettings = BaseWorkflowStepSettings & {
|
||||||
|
input: {
|
||||||
serverlessFunctionId: string;
|
serverlessFunctionId: string;
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type WorkflowSendEmailStepSettings = BaseWorkflowStepSettings & {
|
export type WorkflowSendEmailStepSettings = BaseWorkflowStepSettings & {
|
||||||
|
input: {
|
||||||
connectedAccountId: string;
|
connectedAccountId: string;
|
||||||
|
email: string;
|
||||||
subject?: string;
|
subject?: string;
|
||||||
body?: string;
|
body?: string;
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
type BaseWorkflowStep = {
|
type BaseWorkflowStep = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -21,9 +21,11 @@ describe('addCreateStepNodes', () => {
|
|||||||
retryOnFailure: { value: true },
|
retryOnFailure: { value: true },
|
||||||
continueOnFailure: { value: false },
|
continueOnFailure: { value: false },
|
||||||
},
|
},
|
||||||
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'step2',
|
id: 'step2',
|
||||||
name: 'Step 2',
|
name: 'Step 2',
|
||||||
@ -34,9 +36,11 @@ describe('addCreateStepNodes', () => {
|
|||||||
retryOnFailure: { value: true },
|
retryOnFailure: { value: true },
|
||||||
continueOnFailure: { value: false },
|
continueOnFailure: { value: false },
|
||||||
},
|
},
|
||||||
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const diagramInitial = generateWorkflowDiagram({ trigger, steps });
|
const diagramInitial = generateWorkflowDiagram({ trigger, steps });
|
||||||
|
@ -42,9 +42,11 @@ describe('generateWorkflowDiagram', () => {
|
|||||||
retryOnFailure: { value: true },
|
retryOnFailure: { value: true },
|
||||||
continueOnFailure: { value: false },
|
continueOnFailure: { value: false },
|
||||||
},
|
},
|
||||||
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'step2',
|
id: 'step2',
|
||||||
name: 'Step 2',
|
name: 'Step 2',
|
||||||
@ -55,9 +57,11 @@ describe('generateWorkflowDiagram', () => {
|
|||||||
retryOnFailure: { value: true },
|
retryOnFailure: { value: true },
|
||||||
continueOnFailure: { value: false },
|
continueOnFailure: { value: false },
|
||||||
},
|
},
|
||||||
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = generateWorkflowDiagram({ trigger, steps });
|
const result = generateWorkflowDiagram({ trigger, steps });
|
||||||
@ -96,9 +100,11 @@ describe('generateWorkflowDiagram', () => {
|
|||||||
retryOnFailure: { value: true },
|
retryOnFailure: { value: true },
|
||||||
continueOnFailure: { value: false },
|
continueOnFailure: { value: false },
|
||||||
},
|
},
|
||||||
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'step2',
|
id: 'step2',
|
||||||
name: 'Step 2',
|
name: 'Step 2',
|
||||||
@ -109,9 +115,11 @@ describe('generateWorkflowDiagram', () => {
|
|||||||
retryOnFailure: { value: true },
|
retryOnFailure: { value: true },
|
||||||
continueOnFailure: { value: false },
|
continueOnFailure: { value: false },
|
||||||
},
|
},
|
||||||
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = generateWorkflowDiagram({ trigger, steps });
|
const result = generateWorkflowDiagram({ trigger, steps });
|
||||||
|
@ -80,8 +80,10 @@ describe('getWorkflowVersionDiagram', () => {
|
|||||||
retryOnFailure: { value: true },
|
retryOnFailure: { value: true },
|
||||||
continueOnFailure: { value: false },
|
continueOnFailure: { value: false },
|
||||||
},
|
},
|
||||||
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
type: 'CODE',
|
type: 'CODE',
|
||||||
valid: true,
|
valid: true,
|
||||||
},
|
},
|
||||||
|
@ -25,8 +25,10 @@ describe('insertStep', () => {
|
|||||||
retryOnFailure: { value: true },
|
retryOnFailure: { value: true },
|
||||||
continueOnFailure: { value: false },
|
continueOnFailure: { value: false },
|
||||||
},
|
},
|
||||||
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
type: 'CODE',
|
type: 'CODE',
|
||||||
valid: true,
|
valid: true,
|
||||||
};
|
};
|
||||||
@ -63,8 +65,10 @@ describe('insertStep', () => {
|
|||||||
retryOnFailure: { value: true },
|
retryOnFailure: { value: true },
|
||||||
continueOnFailure: { value: false },
|
continueOnFailure: { value: false },
|
||||||
},
|
},
|
||||||
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
type: 'CODE',
|
type: 'CODE',
|
||||||
valid: true,
|
valid: true,
|
||||||
};
|
};
|
||||||
@ -95,8 +99,10 @@ describe('insertStep', () => {
|
|||||||
retryOnFailure: { value: true },
|
retryOnFailure: { value: true },
|
||||||
continueOnFailure: { value: false },
|
continueOnFailure: { value: false },
|
||||||
},
|
},
|
||||||
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
type: 'CODE',
|
type: 'CODE',
|
||||||
valid: true,
|
valid: true,
|
||||||
},
|
},
|
||||||
@ -108,8 +114,10 @@ describe('insertStep', () => {
|
|||||||
retryOnFailure: { value: true },
|
retryOnFailure: { value: true },
|
||||||
continueOnFailure: { value: false },
|
continueOnFailure: { value: false },
|
||||||
},
|
},
|
||||||
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
type: 'CODE',
|
type: 'CODE',
|
||||||
valid: true,
|
valid: true,
|
||||||
},
|
},
|
||||||
@ -129,8 +137,10 @@ describe('insertStep', () => {
|
|||||||
retryOnFailure: { value: true },
|
retryOnFailure: { value: true },
|
||||||
continueOnFailure: { value: false },
|
continueOnFailure: { value: false },
|
||||||
},
|
},
|
||||||
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
type: 'CODE',
|
type: 'CODE',
|
||||||
valid: true,
|
valid: true,
|
||||||
};
|
};
|
||||||
@ -165,8 +175,10 @@ describe('insertStep', () => {
|
|||||||
retryOnFailure: { value: true },
|
retryOnFailure: { value: true },
|
||||||
continueOnFailure: { value: false },
|
continueOnFailure: { value: false },
|
||||||
},
|
},
|
||||||
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
type: 'CODE',
|
type: 'CODE',
|
||||||
valid: true,
|
valid: true,
|
||||||
},
|
},
|
||||||
@ -178,8 +190,10 @@ describe('insertStep', () => {
|
|||||||
retryOnFailure: { value: true },
|
retryOnFailure: { value: true },
|
||||||
continueOnFailure: { value: false },
|
continueOnFailure: { value: false },
|
||||||
},
|
},
|
||||||
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
type: 'CODE',
|
type: 'CODE',
|
||||||
valid: true,
|
valid: true,
|
||||||
},
|
},
|
||||||
@ -199,8 +213,10 @@ describe('insertStep', () => {
|
|||||||
retryOnFailure: { value: true },
|
retryOnFailure: { value: true },
|
||||||
continueOnFailure: { value: false },
|
continueOnFailure: { value: false },
|
||||||
},
|
},
|
||||||
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
type: 'CODE',
|
type: 'CODE',
|
||||||
valid: true,
|
valid: true,
|
||||||
};
|
};
|
||||||
|
@ -10,8 +10,10 @@ it('returns a deep copy of the provided steps array instead of mutating it', ()
|
|||||||
retryOnFailure: { value: true },
|
retryOnFailure: { value: true },
|
||||||
continueOnFailure: { value: false },
|
continueOnFailure: { value: false },
|
||||||
},
|
},
|
||||||
|
input: {
|
||||||
serverlessFunctionId: 'first',
|
serverlessFunctionId: 'first',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
type: 'CODE',
|
type: 'CODE',
|
||||||
valid: true,
|
valid: true,
|
||||||
} satisfies WorkflowStep;
|
} satisfies WorkflowStep;
|
||||||
@ -47,8 +49,10 @@ it('removes a step in a non-empty steps array', () => {
|
|||||||
retryOnFailure: { value: true },
|
retryOnFailure: { value: true },
|
||||||
continueOnFailure: { value: false },
|
continueOnFailure: { value: false },
|
||||||
},
|
},
|
||||||
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
type: 'CODE',
|
type: 'CODE',
|
||||||
valid: true,
|
valid: true,
|
||||||
};
|
};
|
||||||
@ -67,8 +71,10 @@ it('removes a step in a non-empty steps array', () => {
|
|||||||
retryOnFailure: { value: true },
|
retryOnFailure: { value: true },
|
||||||
continueOnFailure: { value: false },
|
continueOnFailure: { value: false },
|
||||||
},
|
},
|
||||||
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
type: 'CODE',
|
type: 'CODE',
|
||||||
valid: true,
|
valid: true,
|
||||||
},
|
},
|
||||||
@ -81,8 +87,10 @@ it('removes a step in a non-empty steps array', () => {
|
|||||||
retryOnFailure: { value: true },
|
retryOnFailure: { value: true },
|
||||||
continueOnFailure: { value: false },
|
continueOnFailure: { value: false },
|
||||||
},
|
},
|
||||||
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
type: 'CODE',
|
type: 'CODE',
|
||||||
valid: true,
|
valid: true,
|
||||||
},
|
},
|
||||||
|
@ -11,8 +11,10 @@ describe('replaceStep', () => {
|
|||||||
retryOnFailure: { value: true },
|
retryOnFailure: { value: true },
|
||||||
continueOnFailure: { value: false },
|
continueOnFailure: { value: false },
|
||||||
},
|
},
|
||||||
|
input: {
|
||||||
serverlessFunctionId: 'first',
|
serverlessFunctionId: 'first',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
type: 'CODE',
|
type: 'CODE',
|
||||||
valid: true,
|
valid: true,
|
||||||
} satisfies WorkflowStep;
|
} satisfies WorkflowStep;
|
||||||
@ -39,9 +41,11 @@ describe('replaceStep', () => {
|
|||||||
retryOnFailure: { value: true },
|
retryOnFailure: { value: true },
|
||||||
continueOnFailure: { value: false },
|
continueOnFailure: { value: false },
|
||||||
},
|
},
|
||||||
|
input: {
|
||||||
serverlessFunctionId: 'second',
|
serverlessFunctionId: 'second',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
stepId: stepToBeReplaced.id,
|
stepId: stepToBeReplaced.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -57,8 +61,10 @@ describe('replaceStep', () => {
|
|||||||
retryOnFailure: { value: true },
|
retryOnFailure: { value: true },
|
||||||
continueOnFailure: { value: false },
|
continueOnFailure: { value: false },
|
||||||
},
|
},
|
||||||
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
type: 'CODE',
|
type: 'CODE',
|
||||||
valid: true,
|
valid: true,
|
||||||
};
|
};
|
||||||
@ -77,8 +83,10 @@ describe('replaceStep', () => {
|
|||||||
retryOnFailure: { value: true },
|
retryOnFailure: { value: true },
|
||||||
continueOnFailure: { value: false },
|
continueOnFailure: { value: false },
|
||||||
},
|
},
|
||||||
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
type: 'CODE',
|
type: 'CODE',
|
||||||
valid: true,
|
valid: true,
|
||||||
},
|
},
|
||||||
@ -91,8 +99,10 @@ describe('replaceStep', () => {
|
|||||||
retryOnFailure: { value: true },
|
retryOnFailure: { value: true },
|
||||||
continueOnFailure: { value: false },
|
continueOnFailure: { value: false },
|
||||||
},
|
},
|
||||||
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
type: 'CODE',
|
type: 'CODE',
|
||||||
valid: true,
|
valid: true,
|
||||||
},
|
},
|
||||||
|
@ -14,7 +14,9 @@ export const getStepDefaultDefinition = (
|
|||||||
type: 'CODE',
|
type: 'CODE',
|
||||||
valid: false,
|
valid: false,
|
||||||
settings: {
|
settings: {
|
||||||
|
input: {
|
||||||
serverlessFunctionId: '',
|
serverlessFunctionId: '',
|
||||||
|
},
|
||||||
errorHandlingOptions: {
|
errorHandlingOptions: {
|
||||||
continueOnFailure: {
|
continueOnFailure: {
|
||||||
value: false,
|
value: false,
|
||||||
@ -33,9 +35,12 @@ export const getStepDefaultDefinition = (
|
|||||||
type: 'SEND_EMAIL',
|
type: 'SEND_EMAIL',
|
||||||
valid: false,
|
valid: false,
|
||||||
settings: {
|
settings: {
|
||||||
|
input: {
|
||||||
connectedAccountId: '',
|
connectedAccountId: '',
|
||||||
|
email: '',
|
||||||
subject: '',
|
subject: '',
|
||||||
body: '',
|
body: '',
|
||||||
|
},
|
||||||
errorHandlingOptions: {
|
errorHandlingOptions: {
|
||||||
continueOnFailure: {
|
continueOnFailure: {
|
||||||
value: false,
|
value: false,
|
||||||
|
@ -1,40 +1,37 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
import { z } from 'zod';
|
|
||||||
import Handlebars from 'handlebars';
|
|
||||||
import { JSDOM } from 'jsdom';
|
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
|
import { JSDOM } from 'jsdom';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface';
|
||||||
|
|
||||||
import { WorkflowActionResult } from 'src/modules/workflow/workflow-executor/types/workflow-action-result.type';
|
|
||||||
import { WorkflowSendEmailStep } from 'src/modules/workflow/workflow-executor/types/workflow-action.type';
|
|
||||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
|
||||||
import { EmailService } from 'src/engine/core-modules/email/email.service';
|
|
||||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
|
||||||
import {
|
|
||||||
WorkflowStepExecutorException,
|
|
||||||
WorkflowStepExecutorExceptionCode,
|
|
||||||
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
|
|
||||||
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
||||||
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
|
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||||
import {
|
import {
|
||||||
MailSenderException,
|
MailSenderException,
|
||||||
MailSenderExceptionCode,
|
MailSenderExceptionCode,
|
||||||
} from 'src/modules/mail-sender/exceptions/mail-sender.exception';
|
} from 'src/modules/mail-sender/exceptions/mail-sender.exception';
|
||||||
import { GmailClientProvider } from 'src/modules/messaging/message-import-manager/drivers/gmail/providers/gmail-client.provider';
|
import { GmailClientProvider } from 'src/modules/messaging/message-import-manager/drivers/gmail/providers/gmail-client.provider';
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
import {
|
||||||
|
WorkflowStepExecutorException,
|
||||||
|
WorkflowStepExecutorExceptionCode,
|
||||||
|
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
|
||||||
|
import { WorkflowActionResult } from 'src/modules/workflow/workflow-executor/types/workflow-action-result.type';
|
||||||
|
import { WorkflowSendEmailStepInput } from 'src/modules/workflow/workflow-executor/types/workflow-step-settings.type';
|
||||||
import { isDefined } from 'src/utils/is-defined';
|
import { isDefined } from 'src/utils/is-defined';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SendEmailWorkflowAction {
|
export class SendEmailWorkflowAction implements WorkflowAction {
|
||||||
private readonly logger = new Logger(SendEmailWorkflowAction.name);
|
private readonly logger = new Logger(SendEmailWorkflowAction.name);
|
||||||
constructor(
|
constructor(
|
||||||
private readonly environmentService: EnvironmentService,
|
|
||||||
private readonly emailService: EmailService,
|
|
||||||
private readonly gmailClientProvider: GmailClientProvider,
|
private readonly gmailClientProvider: GmailClientProvider,
|
||||||
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
|
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
|
||||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private async getEmailClient(step: WorkflowSendEmailStep) {
|
private async getEmailClient(connectedAccountId: string) {
|
||||||
const { workspaceId } = this.scopedWorkspaceContextFactory.create();
|
const { workspaceId } = this.scopedWorkspaceContextFactory.create();
|
||||||
|
|
||||||
if (!workspaceId) {
|
if (!workspaceId) {
|
||||||
@ -50,12 +47,12 @@ export class SendEmailWorkflowAction {
|
|||||||
'connectedAccount',
|
'connectedAccount',
|
||||||
);
|
);
|
||||||
const connectedAccount = await connectedAccountRepository.findOneBy({
|
const connectedAccount = await connectedAccountRepository.findOneBy({
|
||||||
id: step.settings.connectedAccountId,
|
id: connectedAccountId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isDefined(connectedAccount)) {
|
if (!isDefined(connectedAccount)) {
|
||||||
throw new MailSenderException(
|
throw new MailSenderException(
|
||||||
`Connected Account '${step.settings.connectedAccountId}' not found`,
|
`Connected Account '${connectedAccountId}' not found`,
|
||||||
MailSenderExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND,
|
MailSenderExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -71,39 +68,32 @@ export class SendEmailWorkflowAction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute({
|
async execute(
|
||||||
step,
|
workflowStepInput: WorkflowSendEmailStepInput,
|
||||||
payload,
|
): Promise<WorkflowActionResult> {
|
||||||
}: {
|
const emailProvider = await this.getEmailClient(
|
||||||
step: WorkflowSendEmailStep;
|
workflowStepInput.connectedAccountId,
|
||||||
payload: {
|
);
|
||||||
email: string;
|
const { email, body, subject } = workflowStepInput;
|
||||||
[key: string]: string;
|
|
||||||
};
|
|
||||||
}): Promise<WorkflowActionResult> {
|
|
||||||
const emailProvider = await this.getEmailClient(step);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const emailSchema = z.string().trim().email('Invalid email');
|
const emailSchema = z.string().trim().email('Invalid email');
|
||||||
|
|
||||||
const result = emailSchema.safeParse(payload.email);
|
const result = emailSchema.safeParse(email);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
this.logger.warn(`Email '${payload.email}' invalid`);
|
this.logger.warn(`Email '${email}' invalid`);
|
||||||
|
|
||||||
return { result: { success: false } };
|
return { result: { success: false } };
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = Handlebars.compile(step.settings.body)(payload);
|
|
||||||
const subject = Handlebars.compile(step.settings.subject)(payload);
|
|
||||||
|
|
||||||
const window = new JSDOM('').window;
|
const window = new JSDOM('').window;
|
||||||
const purify = DOMPurify(window);
|
const purify = DOMPurify(window);
|
||||||
const safeBody = purify.sanitize(body || '');
|
const safeBody = purify.sanitize(body || '');
|
||||||
const safeSubject = purify.sanitize(subject || '');
|
const safeSubject = purify.sanitize(subject || '');
|
||||||
|
|
||||||
const message = [
|
const message = [
|
||||||
`To: ${payload.email}`,
|
`To: ${email}`,
|
||||||
`Subject: ${safeSubject || ''}`,
|
`Subject: ${safeSubject || ''}`,
|
||||||
'MIME-Version: 1.0',
|
'MIME-Version: 1.0',
|
||||||
'Content-Type: text/plain; charset="UTF-8"',
|
'Content-Type: text/plain; charset="UTF-8"',
|
||||||
|
@ -1,28 +1,26 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface';
|
||||||
|
|
||||||
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||||
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
||||||
import { WorkflowActionResult } from 'src/modules/workflow/workflow-executor/types/workflow-action-result.type';
|
|
||||||
import { WorkflowCodeStep } from 'src/modules/workflow/workflow-executor/types/workflow-action.type';
|
|
||||||
import {
|
import {
|
||||||
WorkflowStepExecutorException,
|
WorkflowStepExecutorException,
|
||||||
WorkflowStepExecutorExceptionCode,
|
WorkflowStepExecutorExceptionCode,
|
||||||
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
|
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
|
||||||
|
import { WorkflowActionResult } from 'src/modules/workflow/workflow-executor/types/workflow-action-result.type';
|
||||||
|
import { WorkflowCodeStepInput } from 'src/modules/workflow/workflow-executor/types/workflow-step-settings.type';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CodeWorkflowAction {
|
export class CodeWorkflowAction implements WorkflowAction {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly serverlessFunctionService: ServerlessFunctionService,
|
private readonly serverlessFunctionService: ServerlessFunctionService,
|
||||||
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
|
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async execute({
|
async execute(
|
||||||
step,
|
workflowStepInput: WorkflowCodeStepInput,
|
||||||
payload,
|
): Promise<WorkflowActionResult> {
|
||||||
}: {
|
|
||||||
step: WorkflowCodeStep;
|
|
||||||
payload?: object;
|
|
||||||
}): Promise<WorkflowActionResult> {
|
|
||||||
const { workspaceId } = this.scopedWorkspaceContextFactory.create();
|
const { workspaceId } = this.scopedWorkspaceContextFactory.create();
|
||||||
|
|
||||||
if (!workspaceId) {
|
if (!workspaceId) {
|
||||||
@ -34,9 +32,9 @@ export class CodeWorkflowAction {
|
|||||||
|
|
||||||
const result =
|
const result =
|
||||||
await this.serverlessFunctionService.executeOneServerlessFunction(
|
await this.serverlessFunctionService.executeOneServerlessFunction(
|
||||||
step.settings.serverlessFunctionId,
|
workflowStepInput.serverlessFunctionId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
payload || {},
|
{}, // TODO: input will be dynamically calculated from function input
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
|
@ -32,17 +32,21 @@ export enum WorkflowRunStatus {
|
|||||||
FAILED = 'FAILED',
|
FAILED = 'FAILED',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkflowRunOutput = {
|
type StepRunOutput = {
|
||||||
steps: {
|
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
|
outputs: {
|
||||||
attemptCount: number;
|
attemptCount: number;
|
||||||
result: object | undefined;
|
result: object | undefined;
|
||||||
error: string | undefined;
|
error: string | undefined;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WorkflowRunOutput = {
|
||||||
|
steps: Record<string, StepRunOutput>;
|
||||||
|
};
|
||||||
|
|
||||||
@WorkspaceEntity({
|
@WorkspaceEntity({
|
||||||
standardId: STANDARD_OBJECT_IDS.workflowRun,
|
standardId: STANDARD_OBJECT_IDS.workflowRun,
|
||||||
namePlural: 'workflowRuns',
|
namePlural: 'workflowRuns',
|
||||||
|
@ -8,4 +8,5 @@ export class WorkflowExecutorException extends CustomException {
|
|||||||
|
|
||||||
export enum WorkflowExecutorExceptionCode {
|
export enum WorkflowExecutorExceptionCode {
|
||||||
WORKFLOW_FAILED = 'WORKFLOW_FAILED',
|
WORKFLOW_FAILED = 'WORKFLOW_FAILED',
|
||||||
|
VARIABLE_EVALUATION_FAILED = 'VARIABLE_EVALUATION_FAILED',
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,5 @@
|
|||||||
import { WorkflowActionResult } from 'src/modules/workflow/workflow-executor/types/workflow-action-result.type';
|
import { WorkflowActionResult } from 'src/modules/workflow/workflow-executor/types/workflow-action-result.type';
|
||||||
import { WorkflowStep } from 'src/modules/workflow/workflow-executor/types/workflow-action.type';
|
|
||||||
|
|
||||||
export interface WorkflowAction {
|
export interface WorkflowAction {
|
||||||
execute({
|
execute(workflowStepInput: unknown): Promise<WorkflowActionResult>;
|
||||||
step,
|
|
||||||
payload,
|
|
||||||
}: {
|
|
||||||
step: WorkflowStep;
|
|
||||||
payload?: object;
|
|
||||||
}): Promise<WorkflowActionResult>;
|
|
||||||
}
|
}
|
||||||
|
@ -9,12 +9,21 @@ type BaseWorkflowStepSettings = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkflowCodeStepSettings = BaseWorkflowStepSettings & {
|
export type WorkflowCodeStepInput = {
|
||||||
serverlessFunctionId: string;
|
serverlessFunctionId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkflowSendEmailStepSettings = BaseWorkflowStepSettings & {
|
export type WorkflowCodeStepSettings = BaseWorkflowStepSettings & {
|
||||||
|
input: WorkflowCodeStepInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkflowSendEmailStepInput = {
|
||||||
connectedAccountId: string;
|
connectedAccountId: string;
|
||||||
|
email: string;
|
||||||
subject?: string;
|
subject?: string;
|
||||||
body?: string;
|
body?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WorkflowSendEmailStepSettings = BaseWorkflowStepSettings & {
|
||||||
|
input: WorkflowSendEmailStepInput;
|
||||||
|
};
|
||||||
|
@ -0,0 +1,81 @@
|
|||||||
|
import { resolveInput } from 'src/modules/workflow/workflow-executor/utils/variable-resolver.util';
|
||||||
|
|
||||||
|
describe('resolveInput', () => {
|
||||||
|
const context = {
|
||||||
|
user: {
|
||||||
|
name: 'John Doe',
|
||||||
|
age: 30,
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
theme: 'dark',
|
||||||
|
notifications: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should return null for null input', () => {
|
||||||
|
expect(resolveInput(null, context)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined for undefined input', () => {
|
||||||
|
expect(resolveInput(undefined, context)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve a simple string variable', () => {
|
||||||
|
expect(resolveInput('{{user.name}}', context)).toBe('John Doe');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve multiple variables in a string', () => {
|
||||||
|
expect(
|
||||||
|
resolveInput('Name: {{user.name}}, Age: {{user.age}}', context),
|
||||||
|
).toBe('Name: John Doe, Age: 30');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle non-existent variables', () => {
|
||||||
|
expect(resolveInput('{{user.email}}', context)).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve variables in an array', () => {
|
||||||
|
const input = ['{{user.name}}', '{{settings.theme}}', 'static'];
|
||||||
|
const expected = ['John Doe', 'dark', 'static'];
|
||||||
|
|
||||||
|
expect(resolveInput(input, context)).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve variables in an object', () => {
|
||||||
|
const input = {
|
||||||
|
name: '{{user.name}}',
|
||||||
|
theme: '{{settings.theme}}',
|
||||||
|
static: 'value',
|
||||||
|
};
|
||||||
|
const expected = {
|
||||||
|
name: 'John Doe',
|
||||||
|
theme: 'dark',
|
||||||
|
static: 'value',
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(resolveInput(input, context)).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle nested objects and arrays', () => {
|
||||||
|
const input = {
|
||||||
|
user: {
|
||||||
|
displayName: '{{user.name}}',
|
||||||
|
preferences: ['{{settings.theme}}', '{{settings.notifications}}'],
|
||||||
|
},
|
||||||
|
staticData: [1, 2, 3],
|
||||||
|
};
|
||||||
|
const expected = {
|
||||||
|
user: {
|
||||||
|
displayName: 'John Doe',
|
||||||
|
preferences: ['dark', 'true'],
|
||||||
|
},
|
||||||
|
staticData: [1, 2, 3],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(resolveInput(input, context)).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error for invalid expressions', () => {
|
||||||
|
expect(() => resolveInput('{{invalidFunction()}}', context)).toThrow();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,98 @@
|
|||||||
|
import { isNil, isString } from '@nestjs/common/utils/shared.utils';
|
||||||
|
|
||||||
|
import Handlebars from 'handlebars';
|
||||||
|
|
||||||
|
import {
|
||||||
|
WorkflowExecutorException,
|
||||||
|
WorkflowExecutorExceptionCode,
|
||||||
|
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-executor.exception';
|
||||||
|
|
||||||
|
const VARIABLE_PATTERN = RegExp('\\{\\{(.*?)\\}\\}', 'g');
|
||||||
|
|
||||||
|
export const resolveInput = (
|
||||||
|
unresolvedInput: unknown,
|
||||||
|
context: Record<string, unknown>,
|
||||||
|
): unknown => {
|
||||||
|
if (isNil(unresolvedInput)) {
|
||||||
|
return unresolvedInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isString(unresolvedInput)) {
|
||||||
|
return resolveString(unresolvedInput, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(unresolvedInput)) {
|
||||||
|
return resolveArray(unresolvedInput, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof unresolvedInput === 'object' && unresolvedInput !== null) {
|
||||||
|
return resolveObject(unresolvedInput, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return unresolvedInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveArray = (
|
||||||
|
input: unknown[],
|
||||||
|
context: Record<string, unknown>,
|
||||||
|
): unknown[] => {
|
||||||
|
const resolvedArray = input;
|
||||||
|
|
||||||
|
for (let i = 0; i < input.length; ++i) {
|
||||||
|
resolvedArray[i] = resolveInput(input[i], context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedArray;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveObject = (
|
||||||
|
input: object,
|
||||||
|
context: Record<string, unknown>,
|
||||||
|
): object => {
|
||||||
|
const resolvedObject = input;
|
||||||
|
|
||||||
|
const entries = Object.entries(resolvedObject);
|
||||||
|
|
||||||
|
for (const [key, value] of entries) {
|
||||||
|
resolvedObject[key] = resolveInput(value, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedObject;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveString = (
|
||||||
|
input: string,
|
||||||
|
context: Record<string, unknown>,
|
||||||
|
): string => {
|
||||||
|
const matchedTokens = input.match(VARIABLE_PATTERN);
|
||||||
|
|
||||||
|
if (!matchedTokens || matchedTokens.length === 0) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchedTokens.length === 1 && matchedTokens[0] === input) {
|
||||||
|
return evalFromContext(input, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return input.replace(VARIABLE_PATTERN, (matchedToken, _) => {
|
||||||
|
const processedToken = evalFromContext(matchedToken, context);
|
||||||
|
|
||||||
|
return processedToken;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const evalFromContext = (
|
||||||
|
input: string,
|
||||||
|
context: Record<string, unknown>,
|
||||||
|
): string => {
|
||||||
|
try {
|
||||||
|
const inferredInput = Handlebars.compile(input)(context);
|
||||||
|
|
||||||
|
return inferredInput ?? '';
|
||||||
|
} catch (exception) {
|
||||||
|
throw new WorkflowExecutorException(
|
||||||
|
`Failed to evaluate variable ${input}: ${exception}`,
|
||||||
|
WorkflowExecutorExceptionCode.VARIABLE_EVALUATION_FAILED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
@ -1,13 +1,13 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
|
|
||||||
import { WorkflowExecutorWorkspaceService } from 'src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service';
|
|
||||||
import { WorkflowActionFactory } from 'src/modules/workflow/workflow-executor/factories/workflow-action.factory';
|
|
||||||
import { CodeWorkflowAction } from 'src/modules/serverless/workflow-actions/code.workflow-action';
|
|
||||||
import { SendEmailWorkflowAction } from 'src/modules/mail-sender/workflow-actions/send-email.workflow-action';
|
|
||||||
import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
|
import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
|
||||||
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
||||||
|
import { SendEmailWorkflowAction } from 'src/modules/mail-sender/workflow-actions/send-email.workflow-action';
|
||||||
import { MessagingGmailDriverModule } from 'src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module';
|
import { MessagingGmailDriverModule } from 'src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module';
|
||||||
|
import { CodeWorkflowAction } from 'src/modules/serverless/workflow-actions/code.workflow-action';
|
||||||
|
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
|
||||||
|
import { WorkflowActionFactory } from 'src/modules/workflow/workflow-executor/factories/workflow-action.factory';
|
||||||
|
import { WorkflowExecutorWorkspaceService } from 'src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
} from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
|
} from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
|
||||||
import { WorkflowActionFactory } from 'src/modules/workflow/workflow-executor/factories/workflow-action.factory';
|
import { WorkflowActionFactory } from 'src/modules/workflow/workflow-executor/factories/workflow-action.factory';
|
||||||
import { WorkflowStep } from 'src/modules/workflow/workflow-executor/types/workflow-action.type';
|
import { WorkflowStep } from 'src/modules/workflow/workflow-executor/types/workflow-action.type';
|
||||||
|
import { resolveInput } from 'src/modules/workflow/workflow-executor/utils/variable-resolver.util';
|
||||||
|
|
||||||
const MAX_RETRIES_ON_FAILURE = 3;
|
const MAX_RETRIES_ON_FAILURE = 3;
|
||||||
|
|
||||||
@ -21,14 +22,14 @@ export class WorkflowExecutorWorkspaceService {
|
|||||||
async execute({
|
async execute({
|
||||||
currentStepIndex,
|
currentStepIndex,
|
||||||
steps,
|
steps,
|
||||||
payload,
|
context,
|
||||||
output,
|
output,
|
||||||
attemptCount = 1,
|
attemptCount = 1,
|
||||||
}: {
|
}: {
|
||||||
currentStepIndex: number;
|
currentStepIndex: number;
|
||||||
steps: WorkflowStep[];
|
steps: WorkflowStep[];
|
||||||
output: WorkflowExecutorOutput;
|
output: WorkflowExecutorOutput;
|
||||||
payload?: object;
|
context: Record<string, unknown>;
|
||||||
attemptCount?: number;
|
attemptCount?: number;
|
||||||
}): Promise<WorkflowExecutorOutput> {
|
}): Promise<WorkflowExecutorOutput> {
|
||||||
if (currentStepIndex >= steps.length) {
|
if (currentStepIndex >= steps.length) {
|
||||||
@ -39,59 +40,55 @@ export class WorkflowExecutorWorkspaceService {
|
|||||||
|
|
||||||
const workflowAction = this.workflowActionFactory.get(step.type);
|
const workflowAction = this.workflowActionFactory.get(step.type);
|
||||||
|
|
||||||
const result = await workflowAction.execute({
|
const actionPayload = resolveInput(step.settings.input, context);
|
||||||
step,
|
|
||||||
payload,
|
|
||||||
});
|
|
||||||
|
|
||||||
const baseStepOutput = {
|
const result = await workflowAction.execute(actionPayload);
|
||||||
|
|
||||||
|
const stepOutput = output.steps[step.id];
|
||||||
|
|
||||||
|
const error =
|
||||||
|
result.error?.errorMessage ??
|
||||||
|
(result.result ? undefined : 'Execution result error, no data or error');
|
||||||
|
|
||||||
|
const updatedStepOutput = {
|
||||||
id: step.id,
|
id: step.id,
|
||||||
name: step.name,
|
name: step.name,
|
||||||
type: step.type,
|
type: step.type,
|
||||||
|
outputs: [
|
||||||
|
...(stepOutput?.outputs ?? []),
|
||||||
|
{
|
||||||
attemptCount,
|
attemptCount,
|
||||||
|
result: result.result,
|
||||||
|
error,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatedOutput = {
|
const updatedOutput = {
|
||||||
...output,
|
...output,
|
||||||
steps: [
|
steps: {
|
||||||
...output.steps,
|
...output.steps,
|
||||||
{
|
[step.id]: updatedStepOutput,
|
||||||
...baseStepOutput,
|
|
||||||
result: result.result,
|
|
||||||
error: result.error?.errorMessage,
|
|
||||||
},
|
},
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (result.result) {
|
if (result.result) {
|
||||||
return await this.execute({
|
return await this.execute({
|
||||||
currentStepIndex: currentStepIndex + 1,
|
currentStepIndex: currentStepIndex + 1,
|
||||||
steps,
|
steps,
|
||||||
payload: result.result,
|
context: {
|
||||||
|
...context,
|
||||||
|
[step.id]: result.result,
|
||||||
|
},
|
||||||
output: updatedOutput,
|
output: updatedOutput,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result.error) {
|
|
||||||
return {
|
|
||||||
...output,
|
|
||||||
steps: [
|
|
||||||
...output.steps,
|
|
||||||
{
|
|
||||||
...baseStepOutput,
|
|
||||||
result: undefined,
|
|
||||||
error: 'Execution result error, no data or error',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
status: WorkflowRunStatus.FAILED,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (step.settings.errorHandlingOptions.continueOnFailure.value) {
|
if (step.settings.errorHandlingOptions.continueOnFailure.value) {
|
||||||
return await this.execute({
|
return await this.execute({
|
||||||
currentStepIndex: currentStepIndex + 1,
|
currentStepIndex: currentStepIndex + 1,
|
||||||
steps,
|
steps,
|
||||||
payload,
|
context,
|
||||||
output: updatedOutput,
|
output: updatedOutput,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -103,7 +100,7 @@ export class WorkflowExecutorWorkspaceService {
|
|||||||
return await this.execute({
|
return await this.execute({
|
||||||
currentStepIndex,
|
currentStepIndex,
|
||||||
steps,
|
steps,
|
||||||
payload,
|
context,
|
||||||
output: updatedOutput,
|
output: updatedOutput,
|
||||||
attemptCount: attemptCount + 1,
|
attemptCount: attemptCount + 1,
|
||||||
});
|
});
|
||||||
|
@ -40,9 +40,11 @@ export class RunWorkflowJob {
|
|||||||
await this.workflowExecutorWorkspaceService.execute({
|
await this.workflowExecutorWorkspaceService.execute({
|
||||||
currentStepIndex: 0,
|
currentStepIndex: 0,
|
||||||
steps: workflowVersion.steps || [],
|
steps: workflowVersion.steps || [],
|
||||||
payload,
|
context: {
|
||||||
|
trigger: payload,
|
||||||
|
},
|
||||||
output: {
|
output: {
|
||||||
steps: [],
|
steps: {},
|
||||||
status: WorkflowRunStatus.RUNNING,
|
status: WorkflowRunStatus.RUNNING,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user