Add workflow runner (#6458)
- add workflow runner module
- add an endpoint to trigger a workflow via api
- improve error handling for serverless functions
## Testing
- create 2 serverless functions
- create a workflow
- create this workflow Version
```
{
"type": "MANUAL",
"input": {"b": "bb"},
"nextAction": {
"name": "step_1",
"displayName": "Code",
"type": "CODE",
"valid": true,
"settings": {
"serverlessFunctionId": "Serverless function 1 Id",
"errorHandlingOptions": {
"retryOnFailure": {
"value": false
},
"continueOnFailure": {
"value": false
}
}
},
"nextAction": {
"name": "step_1",
"displayName": "Code",
"type": "CODE",
"valid": true,
"settings": {
"serverlessFunctionId": "Serverless function 1 Id",
"errorHandlingOptions": {
"retryOnFailure": {
"value": false
},
"continueOnFailure": {
"value": false
}
}
},
"nextAction": {
"name": "step_1",
"displayName": "Code",
"type": "CODE",
"valid": true,
"settings": {
"serverlessFunctionId": "Serverless function 2 Id",
"errorHandlingOptions": {
"retryOnFailure": {
"value": false
},
"continueOnFailure": {
"value": false
}
}
}
}
}
}
}
`
``
- call
```
mutation Trigger {
triggerWorkflow(workflowVersionId: "WORKFLOW_VERSION_ID") {
result
}
}
```
- try when errors are injected in serverless function
This commit is contained in:
@ -36,7 +36,7 @@ const documents = {
|
|||||||
"\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n sourceCodeHash\n sourceCodeFullPath\n runtime\n syncStatus\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc,
|
"\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n sourceCodeHash\n sourceCodeFullPath\n runtime\n syncStatus\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc,
|
||||||
"\n \n mutation CreateOneServerlessFunctionItem(\n $input: CreateServerlessFunctionInput!\n ) {\n createOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.CreateOneServerlessFunctionItemDocument,
|
"\n \n mutation CreateOneServerlessFunctionItem(\n $input: CreateServerlessFunctionInput!\n ) {\n createOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.CreateOneServerlessFunctionItemDocument,
|
||||||
"\n \n mutation DeleteOneServerlessFunction($input: DeleteServerlessFunctionInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.DeleteOneServerlessFunctionDocument,
|
"\n \n mutation DeleteOneServerlessFunction($input: DeleteServerlessFunctionInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.DeleteOneServerlessFunctionDocument,
|
||||||
"\n mutation ExecuteOneServerlessFunction($id: UUID!, $payload: JSON!) {\n executeOneServerlessFunction(id: $id, payload: $payload) {\n result\n }\n }\n": types.ExecuteOneServerlessFunctionDocument,
|
"\n mutation ExecuteOneServerlessFunction($id: UUID!, $payload: JSON!) {\n executeOneServerlessFunction(id: $id, payload: $payload) {\n data\n duration\n status\n error\n }\n }\n": types.ExecuteOneServerlessFunctionDocument,
|
||||||
"\n \n mutation UpdateOneServerlessFunction($input: UpdateServerlessFunctionInput!) {\n updateOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.UpdateOneServerlessFunctionDocument,
|
"\n \n mutation UpdateOneServerlessFunction($input: UpdateServerlessFunctionInput!) {\n updateOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.UpdateOneServerlessFunctionDocument,
|
||||||
"\n \n query GetManyServerlessFunctions {\n serverlessFunctions(paging: { first: 100 }) {\n edges {\n node {\n ...ServerlessFunctionFields\n }\n }\n }\n }\n": types.GetManyServerlessFunctionsDocument,
|
"\n \n query GetManyServerlessFunctions {\n serverlessFunctions(paging: { first: 100 }) {\n edges {\n node {\n ...ServerlessFunctionFields\n }\n }\n }\n }\n": types.GetManyServerlessFunctionsDocument,
|
||||||
"\n \n query GetOneServerlessFunction($id: UUID!) {\n serverlessFunction(id: $id) {\n ...ServerlessFunctionFields\n }\n }\n": types.GetOneServerlessFunctionDocument,
|
"\n \n query GetOneServerlessFunction($id: UUID!) {\n serverlessFunction(id: $id) {\n ...ServerlessFunctionFields\n }\n }\n": types.GetOneServerlessFunctionDocument,
|
||||||
@ -151,7 +151,7 @@ export function graphql(source: "\n \n mutation DeleteOneServerlessFunction($i
|
|||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
export function graphql(source: "\n mutation ExecuteOneServerlessFunction($id: UUID!, $payload: JSON!) {\n executeOneServerlessFunction(id: $id, payload: $payload) {\n result\n }\n }\n"): (typeof documents)["\n mutation ExecuteOneServerlessFunction($id: UUID!, $payload: JSON!) {\n executeOneServerlessFunction(id: $id, payload: $payload) {\n result\n }\n }\n"];
|
export function graphql(source: "\n mutation ExecuteOneServerlessFunction($id: UUID!, $payload: JSON!) {\n executeOneServerlessFunction(id: $id, payload: $payload) {\n data\n duration\n status\n error\n }\n }\n"): (typeof documents)["\n mutation ExecuteOneServerlessFunction($id: UUID!, $payload: JSON!) {\n executeOneServerlessFunction(id: $id, payload: $payload) {\n data\n duration\n status\n error\n }\n }\n"];
|
||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,39 @@
|
|||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import {
|
||||||
|
DEFAULT_OUTPUT_VALUE,
|
||||||
|
settingsServerlessFunctionOutputState,
|
||||||
|
} from '@/settings/serverless-functions/states/settingsServerlessFunctionOutputState';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { IconSquareRoundedCheck } from 'twenty-ui';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import { ServerlessFunctionExecutionStatus } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
const StyledOutput = styled.div<{ status?: ServerlessFunctionExecutionStatus }>`
|
||||||
|
align-items: center;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
color: ${({ theme, status }) =>
|
||||||
|
status === ServerlessFunctionExecutionStatus.Success
|
||||||
|
? theme.color.turquoise
|
||||||
|
: theme.color.red};
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SettingsServerlessFunctionsOutputMetadataInfo = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const settingsServerlessFunctionOutput = useRecoilValue(
|
||||||
|
settingsServerlessFunctionOutputState,
|
||||||
|
);
|
||||||
|
return settingsServerlessFunctionOutput.data === DEFAULT_OUTPUT_VALUE ? (
|
||||||
|
'Output'
|
||||||
|
) : (
|
||||||
|
<StyledOutput status={settingsServerlessFunctionOutput.status}>
|
||||||
|
<IconSquareRoundedCheck size={theme.icon.size.md} />
|
||||||
|
{settingsServerlessFunctionOutput.status ===
|
||||||
|
ServerlessFunctionExecutionStatus.Success
|
||||||
|
? '200 OK'
|
||||||
|
: '500 Error'}
|
||||||
|
{' - '}
|
||||||
|
{settingsServerlessFunctionOutput.duration}ms
|
||||||
|
</StyledOutput>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -10,6 +10,7 @@ import { useRecoilState, useRecoilValue } from 'recoil';
|
|||||||
import { settingsServerlessFunctionOutputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionOutputState';
|
import { settingsServerlessFunctionOutputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionOutputState';
|
||||||
import { settingsServerlessFunctionInputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionInputState';
|
import { settingsServerlessFunctionInputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionInputState';
|
||||||
import { settingsServerlessFunctionCodeEditorOutputParamsState } from '@/settings/serverless-functions/states/settingsServerlessFunctionCodeEditorOutputParamsState';
|
import { settingsServerlessFunctionCodeEditorOutputParamsState } from '@/settings/serverless-functions/states/settingsServerlessFunctionCodeEditorOutputParamsState';
|
||||||
|
import { SettingsServerlessFunctionsOutputMetadataInfo } from '@/settings/serverless-functions/components/SettingsServerlessFunctionsOutputMetadataInfo';
|
||||||
|
|
||||||
const StyledInputsContainer = styled.div`
|
const StyledInputsContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -31,27 +32,32 @@ export const SettingsServerlessFunctionTestTab = ({
|
|||||||
const [settingsServerlessFunctionInput, setSettingsServerlessFunctionInput] =
|
const [settingsServerlessFunctionInput, setSettingsServerlessFunctionInput] =
|
||||||
useRecoilState(settingsServerlessFunctionInputState);
|
useRecoilState(settingsServerlessFunctionInputState);
|
||||||
|
|
||||||
const InputHeaderButton = (
|
const result =
|
||||||
<Button
|
settingsServerlessFunctionOutput.data ||
|
||||||
title="Run Function"
|
settingsServerlessFunctionOutput.error ||
|
||||||
variant="primary"
|
'';
|
||||||
accent="blue"
|
|
||||||
size="small"
|
const InputHeader = (
|
||||||
Icon={IconPlayerPlay}
|
<CoreEditorHeader
|
||||||
onClick={handleExecute}
|
title={'Input'}
|
||||||
|
rightNodes={[
|
||||||
|
<Button
|
||||||
|
title="Run Function"
|
||||||
|
variant="primary"
|
||||||
|
accent="blue"
|
||||||
|
size="small"
|
||||||
|
Icon={IconPlayerPlay}
|
||||||
|
onClick={handleExecute}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const InputHeader = (
|
|
||||||
<CoreEditorHeader title={'Input'} rightNodes={[InputHeaderButton]} />
|
|
||||||
);
|
|
||||||
|
|
||||||
const OutputHeaderButton = (
|
|
||||||
<LightCopyIconButton copyText={settingsServerlessFunctionOutput} />
|
|
||||||
);
|
|
||||||
|
|
||||||
const OutputHeader = (
|
const OutputHeader = (
|
||||||
<CoreEditorHeader title={'Output'} rightNodes={[OutputHeaderButton]} />
|
<CoreEditorHeader
|
||||||
|
leftNodes={[<SettingsServerlessFunctionsOutputMetadataInfo />]}
|
||||||
|
rightNodes={[<LightCopyIconButton copyText={result} />]}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -69,7 +75,7 @@ export const SettingsServerlessFunctionTestTab = ({
|
|||||||
header={InputHeader}
|
header={InputHeader}
|
||||||
/>
|
/>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={settingsServerlessFunctionOutput}
|
value={result}
|
||||||
height={settingsServerlessFunctionCodeEditorOutputParams.height}
|
height={settingsServerlessFunctionCodeEditorOutputParams.height}
|
||||||
language={settingsServerlessFunctionCodeEditorOutputParams.language}
|
language={settingsServerlessFunctionCodeEditorOutputParams.language}
|
||||||
options={{ readOnly: true, domReadOnly: true }}
|
options={{ readOnly: true, domReadOnly: true }}
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
import { settingsServerlessFunctionOutputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionOutputState';
|
import {
|
||||||
|
DEFAULT_OUTPUT_VALUE,
|
||||||
|
settingsServerlessFunctionOutputState,
|
||||||
|
} from '@/settings/serverless-functions/states/settingsServerlessFunctionOutputState';
|
||||||
import { settingsServerlessFunctionCodeEditorOutputParamsState } from '@/settings/serverless-functions/states/settingsServerlessFunctionCodeEditorOutputParamsState';
|
import { settingsServerlessFunctionCodeEditorOutputParamsState } from '@/settings/serverless-functions/states/settingsServerlessFunctionCodeEditorOutputParamsState';
|
||||||
|
|
||||||
export const SettingsServerlessFunctionTestTabEffect = () => {
|
export const SettingsServerlessFunctionTestTabEffect = () => {
|
||||||
@ -10,14 +13,11 @@ export const SettingsServerlessFunctionTestTabEffect = () => {
|
|||||||
const setSettingsServerlessFunctionCodeEditorOutputParams = useSetRecoilState(
|
const setSettingsServerlessFunctionCodeEditorOutputParams = useSetRecoilState(
|
||||||
settingsServerlessFunctionCodeEditorOutputParamsState,
|
settingsServerlessFunctionCodeEditorOutputParamsState,
|
||||||
);
|
);
|
||||||
try {
|
if (settingsServerlessFunctionOutput.data !== DEFAULT_OUTPUT_VALUE) {
|
||||||
JSON.parse(settingsServerlessFunctionOutput);
|
|
||||||
setSettingsServerlessFunctionCodeEditorOutputParams({
|
setSettingsServerlessFunctionCodeEditorOutputParams({
|
||||||
language: 'json',
|
language: 'json',
|
||||||
height: 300,
|
height: 300,
|
||||||
});
|
});
|
||||||
} catch {
|
|
||||||
return <></>;
|
|
||||||
}
|
}
|
||||||
return <></>;
|
return <></>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,7 +3,10 @@ import { gql } from '@apollo/client';
|
|||||||
export const EXECUTE_ONE_SERVERLESS_FUNCTION = gql`
|
export const EXECUTE_ONE_SERVERLESS_FUNCTION = gql`
|
||||||
mutation ExecuteOneServerlessFunction($id: UUID!, $payload: JSON!) {
|
mutation ExecuteOneServerlessFunction($id: UUID!, $payload: JSON!) {
|
||||||
executeOneServerlessFunction(id: $id, payload: $payload) {
|
executeOneServerlessFunction(id: $id, payload: $payload) {
|
||||||
result
|
data
|
||||||
|
duration
|
||||||
|
status
|
||||||
|
error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -1,6 +1,18 @@
|
|||||||
import { createState } from 'twenty-ui';
|
import { createState } from 'twenty-ui';
|
||||||
|
import { ServerlessFunctionExecutionStatus } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
export const settingsServerlessFunctionOutputState = createState<string>({
|
type settingsServerlessFunctionOutput = {
|
||||||
key: 'settingsServerlessFunctionOutputState',
|
data?: string;
|
||||||
defaultValue: 'Enter an input above then press "run Function"',
|
duration?: number;
|
||||||
});
|
status?: ServerlessFunctionExecutionStatus;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_OUTPUT_VALUE =
|
||||||
|
'Enter an input above then press "run Function"';
|
||||||
|
|
||||||
|
export const settingsServerlessFunctionOutputState =
|
||||||
|
createState<settingsServerlessFunctionOutput>({
|
||||||
|
key: 'settingsServerlessFunctionOutputState',
|
||||||
|
defaultValue: { data: DEFAULT_OUTPUT_VALUE },
|
||||||
|
});
|
||||||
|
|||||||
@ -78,13 +78,24 @@ export const SettingsServerlessFunctionDetail = () => {
|
|||||||
serverlessFunctionId,
|
serverlessFunctionId,
|
||||||
JSON.parse(settingsServerlessFunctionInput),
|
JSON.parse(settingsServerlessFunctionInput),
|
||||||
);
|
);
|
||||||
setSettingsServerlessFunctionOutput(
|
setSettingsServerlessFunctionOutput({
|
||||||
JSON.stringify(
|
data: result?.data?.executeOneServerlessFunction?.data
|
||||||
result?.data?.executeOneServerlessFunction?.result,
|
? JSON.stringify(
|
||||||
null,
|
result?.data?.executeOneServerlessFunction?.data,
|
||||||
4,
|
null,
|
||||||
),
|
4,
|
||||||
);
|
)
|
||||||
|
: undefined,
|
||||||
|
duration: result?.data?.executeOneServerlessFunction?.duration,
|
||||||
|
status: result?.data?.executeOneServerlessFunction?.status,
|
||||||
|
error: result?.data?.executeOneServerlessFunction?.error
|
||||||
|
? JSON.stringify(
|
||||||
|
result?.data?.executeOneServerlessFunction?.error,
|
||||||
|
null,
|
||||||
|
4,
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
enqueueSnackBar(
|
enqueueSnackBar(
|
||||||
(err as Error)?.message || 'An error occurred while executing function',
|
(err as Error)?.message || 'An error occurred while executing function',
|
||||||
@ -92,7 +103,6 @@ export const SettingsServerlessFunctionDetail = () => {
|
|||||||
variant: SnackBarVariant.Error,
|
variant: SnackBarVariant.Error,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
setSettingsServerlessFunctionOutput(JSON.stringify(err, null, 4));
|
|
||||||
}
|
}
|
||||||
setActiveTabId('test');
|
setActiveTabId('test');
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import { IsObject } from 'class-validator';
|
||||||
|
import graphqlTypeJson from 'graphql-type-json';
|
||||||
|
|
||||||
|
@ObjectType('WorkflowTriggerResult')
|
||||||
|
export class WorkflowTriggerResultDTO {
|
||||||
|
@IsObject()
|
||||||
|
@Field(() => graphqlTypeJson, {
|
||||||
|
description: 'Execution result in JSON format',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
result?: JSON;
|
||||||
|
}
|
||||||
@ -14,6 +14,7 @@ export const workflowTriggerGraphqlApiExceptionHandler = (error: Error) => {
|
|||||||
throw new UserInputError(error.message);
|
throw new UserInputError(error.message);
|
||||||
case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER:
|
case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER:
|
||||||
case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION:
|
case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION:
|
||||||
|
case WorkflowTriggerExceptionCode.INVALID_ACTION_TYPE:
|
||||||
default:
|
default:
|
||||||
throw new InternalServerError(error.message);
|
throw new InternalServerError(error.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { WorkflowTriggerResolver } from 'src/engine/core-modules/workflow/workflow-trigger.resolver';
|
import { WorkflowTriggerResolver } from 'src/engine/core-modules/workflow/workflow-trigger.resolver';
|
||||||
|
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
|
||||||
import { WorkflowTriggerService } from 'src/modules/workflow/workflow-trigger/workflow-trigger.service';
|
import { WorkflowTriggerService } from 'src/modules/workflow/workflow-trigger/workflow-trigger.service';
|
||||||
|
import { WorkflowRunnerModule } from 'src/modules/workflow/workflow-runner/workflow-runner.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [WorkflowCommonModule, WorkflowRunnerModule],
|
||||||
providers: [WorkflowTriggerService, WorkflowTriggerResolver],
|
providers: [WorkflowTriggerService, WorkflowTriggerResolver],
|
||||||
})
|
})
|
||||||
export class WorkflowTriggerModule {}
|
export class WorkflowTriggerModule {}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
|||||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||||
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
|
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
|
||||||
import { WorkflowTriggerService } from 'src/modules/workflow/workflow-trigger/workflow-trigger.service';
|
import { WorkflowTriggerService } from 'src/modules/workflow/workflow-trigger/workflow-trigger.service';
|
||||||
|
import { WorkflowTriggerResultDTO } from 'src/engine/core-modules/workflow/dtos/workflow-trigger-result.dto';
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Resolver()
|
@Resolver()
|
||||||
@ -28,4 +29,21 @@ export class WorkflowTriggerResolver {
|
|||||||
workflowTriggerGraphqlApiExceptionHandler(error);
|
workflowTriggerGraphqlApiExceptionHandler(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Mutation(() => WorkflowTriggerResultDTO)
|
||||||
|
async triggerWorkflow(
|
||||||
|
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||||
|
@Args('workflowVersionId') workflowVersionId: string,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
result: await this.workflowTriggerService.runWorkflow(
|
||||||
|
workspaceId,
|
||||||
|
workflowVersionId,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
workflowTriggerGraphqlApiExceptionHandler(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,4 +16,5 @@ export enum MessageQueue {
|
|||||||
recordPositionBackfillQueue = 'record-position-backfill-queue',
|
recordPositionBackfillQueue = 'record-position-backfill-queue',
|
||||||
entityEventsToDbQueue = 'entity-events-to-db-queue',
|
entityEventsToDbQueue = 'entity-events-to-db-queue',
|
||||||
testQueue = 'test-queue',
|
testQueue = 'test-queue',
|
||||||
|
workflowQueue = 'workflow-queue',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,18 @@
|
|||||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||||
|
import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
|
||||||
|
|
||||||
|
export type ServerlessExecuteError = {
|
||||||
|
errorType: string;
|
||||||
|
errorMessage: string;
|
||||||
|
stackTrace: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ServerlessExecuteResult = {
|
||||||
|
data: object | null;
|
||||||
|
duration: number;
|
||||||
|
status: ServerlessFunctionExecutionStatus;
|
||||||
|
error?: ServerlessExecuteError;
|
||||||
|
};
|
||||||
|
|
||||||
export interface ServerlessDriver {
|
export interface ServerlessDriver {
|
||||||
delete(serverlessFunction: ServerlessFunctionEntity): Promise<void>;
|
delete(serverlessFunction: ServerlessFunctionEntity): Promise<void>;
|
||||||
@ -6,5 +20,5 @@ export interface ServerlessDriver {
|
|||||||
execute(
|
execute(
|
||||||
serverlessFunction: ServerlessFunctionEntity,
|
serverlessFunction: ServerlessFunctionEntity,
|
||||||
payload: object | undefined,
|
payload: object | undefined,
|
||||||
): Promise<object>;
|
): Promise<ServerlessExecuteResult>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,13 +13,17 @@ import {
|
|||||||
import { CreateFunctionCommandInput } from '@aws-sdk/client-lambda/dist-types/commands/CreateFunctionCommand';
|
import { CreateFunctionCommandInput } from '@aws-sdk/client-lambda/dist-types/commands/CreateFunctionCommand';
|
||||||
import { UpdateFunctionCodeCommandInput } from '@aws-sdk/client-lambda/dist-types/commands/UpdateFunctionCodeCommand';
|
import { UpdateFunctionCodeCommandInput } from '@aws-sdk/client-lambda/dist-types/commands/UpdateFunctionCodeCommand';
|
||||||
|
|
||||||
import { ServerlessDriver } from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface';
|
import {
|
||||||
|
ServerlessDriver,
|
||||||
|
ServerlessExecuteResult,
|
||||||
|
} from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface';
|
||||||
|
|
||||||
import { createZipFile } from 'src/engine/integrations/serverless/drivers/utils/create-zip-file';
|
import { createZipFile } from 'src/engine/integrations/serverless/drivers/utils/create-zip-file';
|
||||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||||
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
|
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
|
||||||
import { BaseServerlessDriver } from 'src/engine/integrations/serverless/drivers/base-serverless.driver';
|
import { BaseServerlessDriver } from 'src/engine/integrations/serverless/drivers/base-serverless.driver';
|
||||||
import { BuildDirectoryManagerService } from 'src/engine/integrations/serverless/drivers/services/build-directory-manager.service';
|
import { BuildDirectoryManagerService } from 'src/engine/integrations/serverless/drivers/services/build-directory-manager.service';
|
||||||
|
import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
|
||||||
|
|
||||||
export interface LambdaDriverOptions extends LambdaClientConfig {
|
export interface LambdaDriverOptions extends LambdaClientConfig {
|
||||||
fileStorageService: FileStorageService;
|
fileStorageService: FileStorageService;
|
||||||
@ -134,7 +138,8 @@ export class LambdaDriver
|
|||||||
async execute(
|
async execute(
|
||||||
functionToExecute: ServerlessFunctionEntity,
|
functionToExecute: ServerlessFunctionEntity,
|
||||||
payload: object | undefined = undefined,
|
payload: object | undefined = undefined,
|
||||||
): Promise<object> {
|
): Promise<ServerlessExecuteResult> {
|
||||||
|
const startTime = Date.now();
|
||||||
const params = {
|
const params = {
|
||||||
FunctionName: functionToExecute.id,
|
FunctionName: functionToExecute.id,
|
||||||
Payload: JSON.stringify(payload),
|
Payload: JSON.stringify(payload),
|
||||||
@ -144,10 +149,25 @@ export class LambdaDriver
|
|||||||
|
|
||||||
const result = await this.lambdaClient.send(command);
|
const result = await this.lambdaClient.send(command);
|
||||||
|
|
||||||
if (!result.Payload) {
|
const parsedResult = result.Payload
|
||||||
return {};
|
? JSON.parse(result.Payload.transformToString())
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
if (result.FunctionError) {
|
||||||
|
return {
|
||||||
|
data: null,
|
||||||
|
duration,
|
||||||
|
status: ServerlessFunctionExecutionStatus.ERROR,
|
||||||
|
error: parsedResult,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return JSON.parse(result.Payload.transformToString());
|
return {
|
||||||
|
data: parsedResult,
|
||||||
|
duration,
|
||||||
|
status: ServerlessFunctionExecutionStatus.SUCCESS,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,13 +5,18 @@ import { fork } from 'child_process';
|
|||||||
|
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
import { ServerlessDriver } from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface';
|
import {
|
||||||
|
ServerlessDriver,
|
||||||
|
ServerlessExecuteError,
|
||||||
|
ServerlessExecuteResult,
|
||||||
|
} from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface';
|
||||||
|
|
||||||
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
|
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
|
||||||
import { readFileContent } from 'src/engine/integrations/file-storage/utils/read-file-content';
|
import { readFileContent } from 'src/engine/integrations/file-storage/utils/read-file-content';
|
||||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||||
import { BUILD_FILE_NAME } from 'src/engine/integrations/serverless/drivers/constants/build-file-name';
|
import { BUILD_FILE_NAME } from 'src/engine/integrations/serverless/drivers/constants/build-file-name';
|
||||||
import { BaseServerlessDriver } from 'src/engine/integrations/serverless/drivers/base-serverless.driver';
|
import { BaseServerlessDriver } from 'src/engine/integrations/serverless/drivers/base-serverless.driver';
|
||||||
|
import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
|
||||||
|
|
||||||
export interface LocalDriverOptions {
|
export interface LocalDriverOptions {
|
||||||
fileStorageService: FileStorageService;
|
fileStorageService: FileStorageService;
|
||||||
@ -51,7 +56,8 @@ export class LocalDriver
|
|||||||
async execute(
|
async execute(
|
||||||
serverlessFunction: ServerlessFunctionEntity,
|
serverlessFunction: ServerlessFunctionEntity,
|
||||||
payload: object | undefined = undefined,
|
payload: object | undefined = undefined,
|
||||||
): Promise<object> {
|
): Promise<ServerlessExecuteResult> {
|
||||||
|
const startTime = Date.now();
|
||||||
const fileStream = await this.fileStorageService.read({
|
const fileStream = await this.fileStorageService.read({
|
||||||
folderPath: this.getFolderPath(serverlessFunction),
|
folderPath: this.getFolderPath(serverlessFunction),
|
||||||
filename: BUILD_FILE_NAME,
|
filename: BUILD_FILE_NAME,
|
||||||
@ -83,8 +89,23 @@ export class LocalDriver
|
|||||||
return await new Promise((resolve, reject) => {
|
return await new Promise((resolve, reject) => {
|
||||||
const child = fork(tmpFilePath, { silent: true });
|
const child = fork(tmpFilePath, { silent: true });
|
||||||
|
|
||||||
child.on('message', (message: object) => {
|
child.on('message', (message: object | ServerlessExecuteError) => {
|
||||||
resolve(message);
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
if ('errorType' in message) {
|
||||||
|
resolve({
|
||||||
|
data: null,
|
||||||
|
duration,
|
||||||
|
error: message,
|
||||||
|
status: ServerlessFunctionExecutionStatus.ERROR,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve({
|
||||||
|
data: message,
|
||||||
|
duration,
|
||||||
|
status: ServerlessFunctionExecutionStatus.SUCCESS,
|
||||||
|
});
|
||||||
|
}
|
||||||
child.kill();
|
child.kill();
|
||||||
fs.unlink(tmpFilePath);
|
fs.unlink(tmpFilePath);
|
||||||
});
|
});
|
||||||
@ -93,8 +114,8 @@ export class LocalDriver
|
|||||||
const stackTrace = data
|
const stackTrace = data
|
||||||
.toString()
|
.toString()
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.filter((line) => line.trim() !== '');
|
.filter((line: string) => line.trim() !== '');
|
||||||
const errorTrace = stackTrace.filter((line) =>
|
const errorTrace = stackTrace.filter((line: string) =>
|
||||||
line.includes('Error: '),
|
line.includes('Error: '),
|
||||||
)?.[0];
|
)?.[0];
|
||||||
|
|
||||||
@ -105,11 +126,17 @@ export class LocalDriver
|
|||||||
errorType = errorTrace.split(':')[0];
|
errorType = errorTrace.split(':')[0];
|
||||||
errorMessage = errorTrace.split(': ')[1];
|
errorMessage = errorTrace.split(': ')[1];
|
||||||
}
|
}
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
errorType,
|
data: null,
|
||||||
errorMessage,
|
duration,
|
||||||
stackTrace: stackTrace,
|
status: ServerlessFunctionExecutionStatus.ERROR,
|
||||||
|
error: {
|
||||||
|
errorType,
|
||||||
|
errorMessage,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
child.kill();
|
child.kill();
|
||||||
fs.unlink(tmpFilePath);
|
fs.unlink(tmpFilePath);
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
import { ServerlessDriver } from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface';
|
import {
|
||||||
|
ServerlessDriver,
|
||||||
|
ServerlessExecuteResult,
|
||||||
|
} from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface';
|
||||||
|
|
||||||
import { SERVERLESS_DRIVER } from 'src/engine/integrations/serverless/serverless.constants';
|
import { SERVERLESS_DRIVER } from 'src/engine/integrations/serverless/serverless.constants';
|
||||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||||
@ -20,7 +23,7 @@ export class ServerlessService implements ServerlessDriver {
|
|||||||
async execute(
|
async execute(
|
||||||
serverlessFunction: ServerlessFunctionEntity,
|
serverlessFunction: ServerlessFunctionEntity,
|
||||||
payload: object | undefined = undefined,
|
payload: object | undefined = undefined,
|
||||||
) {
|
): Promise<ServerlessExecuteResult> {
|
||||||
return this.driver.execute(serverlessFunction, payload);
|
return this.driver.execute(serverlessFunction, payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,44 @@
|
|||||||
import { Field, ObjectType } from '@nestjs/graphql';
|
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||||
|
|
||||||
import { IsObject } from 'class-validator';
|
import { IsObject, IsOptional } from 'class-validator';
|
||||||
import graphqlTypeJson from 'graphql-type-json';
|
import graphqlTypeJson from 'graphql-type-json';
|
||||||
|
|
||||||
|
export enum ServerlessFunctionExecutionStatus {
|
||||||
|
SUCCESS = 'SUCCESS',
|
||||||
|
ERROR = 'ERROR',
|
||||||
|
}
|
||||||
|
|
||||||
|
registerEnumType(ServerlessFunctionExecutionStatus, {
|
||||||
|
name: 'ServerlessFunctionExecutionStatus',
|
||||||
|
description: 'Status of the serverless function execution',
|
||||||
|
});
|
||||||
|
|
||||||
@ObjectType('ServerlessFunctionExecutionResult')
|
@ObjectType('ServerlessFunctionExecutionResult')
|
||||||
export class ServerlessFunctionExecutionResultDto {
|
export class ServerlessFunctionExecutionResultDTO {
|
||||||
@IsObject()
|
@IsObject()
|
||||||
@Field(() => graphqlTypeJson, {
|
@Field(() => graphqlTypeJson, {
|
||||||
description: 'Execution result in JSON format',
|
description: 'Execution result in JSON format',
|
||||||
|
nullable: true,
|
||||||
})
|
})
|
||||||
result: JSON;
|
data?: JSON;
|
||||||
|
|
||||||
|
@Field({ description: 'Execution duration in milliseconds' })
|
||||||
|
duration: number;
|
||||||
|
|
||||||
|
@Field(() => ServerlessFunctionExecutionStatus, {
|
||||||
|
description: 'Execution status',
|
||||||
|
})
|
||||||
|
status: ServerlessFunctionExecutionStatus;
|
||||||
|
|
||||||
|
@IsObject()
|
||||||
|
@IsOptional()
|
||||||
|
@Field(() => graphqlTypeJson, {
|
||||||
|
description: 'Execution error in JSON format',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
error?: {
|
||||||
|
errorType: string;
|
||||||
|
errorMessage: string;
|
||||||
|
stackTrace: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,7 +36,7 @@ registerEnumType(ServerlessFunctionSyncStatus, {
|
|||||||
defaultResultSize: 10,
|
defaultResultSize: 10,
|
||||||
maxResultsSize: 1000,
|
maxResultsSize: 1000,
|
||||||
})
|
})
|
||||||
export class ServerlessFunctionDto {
|
export class ServerlessFunctionDTO {
|
||||||
@IsUUID()
|
@IsUUID()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IDField(() => UUIDScalarType)
|
@IDField(() => UUIDScalarType)
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { ServerlessModule } from 'src/engine/integrations/serverless/serverless.
|
|||||||
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 { ServerlessFunctionResolver } from 'src/engine/metadata-modules/serverless-function/serverless-function.resolver';
|
import { ServerlessFunctionResolver } from 'src/engine/metadata-modules/serverless-function/serverless-function.resolver';
|
||||||
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
|
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
|
||||||
import { ServerlessFunctionDto } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto';
|
import { ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto';
|
||||||
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
|
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
|
||||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-
|
|||||||
resolvers: [
|
resolvers: [
|
||||||
{
|
{
|
||||||
EntityClass: ServerlessFunctionEntity,
|
EntityClass: ServerlessFunctionEntity,
|
||||||
DTOClass: ServerlessFunctionDto,
|
DTOClass: ServerlessFunctionDTO,
|
||||||
ServiceClass: ServerlessFunctionService,
|
ServiceClass: ServerlessFunctionService,
|
||||||
pagingStrategy: PagingStrategies.CURSOR,
|
pagingStrategy: PagingStrategies.CURSOR,
|
||||||
read: {
|
read: {
|
||||||
|
|||||||
@ -14,8 +14,8 @@ import { CreateServerlessFunctionFromFileInput } from 'src/engine/metadata-modul
|
|||||||
import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input';
|
import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input';
|
||||||
import { DeleteServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/delete-serverless-function.input';
|
import { DeleteServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/delete-serverless-function.input';
|
||||||
import { ExecuteServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/execute-serverless-function.input';
|
import { ExecuteServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/execute-serverless-function.input';
|
||||||
import { ServerlessFunctionExecutionResultDto } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
|
import { ServerlessFunctionExecutionResultDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
|
||||||
import { ServerlessFunctionDto } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto';
|
import { ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto';
|
||||||
import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input';
|
import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input';
|
||||||
import {
|
import {
|
||||||
ServerlessFunctionException,
|
ServerlessFunctionException,
|
||||||
@ -49,7 +49,7 @@ export class ServerlessFunctionResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => ServerlessFunctionDto)
|
@Mutation(() => ServerlessFunctionDTO)
|
||||||
async deleteOneServerlessFunction(
|
async deleteOneServerlessFunction(
|
||||||
@Args('input') input: DeleteServerlessFunctionInput,
|
@Args('input') input: DeleteServerlessFunctionInput,
|
||||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||||
@ -66,7 +66,7 @@ export class ServerlessFunctionResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => ServerlessFunctionDto)
|
@Mutation(() => ServerlessFunctionDTO)
|
||||||
async updateOneServerlessFunction(
|
async updateOneServerlessFunction(
|
||||||
@Args('input')
|
@Args('input')
|
||||||
input: UpdateServerlessFunctionInput,
|
input: UpdateServerlessFunctionInput,
|
||||||
@ -84,7 +84,7 @@ export class ServerlessFunctionResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => ServerlessFunctionDto)
|
@Mutation(() => ServerlessFunctionDTO)
|
||||||
async createOneServerlessFunction(
|
async createOneServerlessFunction(
|
||||||
@Args('input')
|
@Args('input')
|
||||||
input: CreateServerlessFunctionInput,
|
input: CreateServerlessFunctionInput,
|
||||||
@ -106,7 +106,7 @@ export class ServerlessFunctionResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => ServerlessFunctionDto)
|
@Mutation(() => ServerlessFunctionDTO)
|
||||||
async createOneServerlessFunctionFromFile(
|
async createOneServerlessFunctionFromFile(
|
||||||
@Args({ name: 'file', type: () => GraphQLUpload })
|
@Args({ name: 'file', type: () => GraphQLUpload })
|
||||||
file: FileUpload,
|
file: FileUpload,
|
||||||
@ -127,7 +127,7 @@ export class ServerlessFunctionResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => ServerlessFunctionExecutionResultDto)
|
@Mutation(() => ServerlessFunctionExecutionResultDTO)
|
||||||
async executeOneServerlessFunction(
|
async executeOneServerlessFunction(
|
||||||
@Args() executeServerlessFunctionInput: ExecuteServerlessFunctionInput,
|
@Args() executeServerlessFunctionInput: ExecuteServerlessFunctionInput,
|
||||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||||
@ -136,13 +136,11 @@ export class ServerlessFunctionResolver {
|
|||||||
await this.checkFeatureFlag(workspaceId);
|
await this.checkFeatureFlag(workspaceId);
|
||||||
const { id, payload } = executeServerlessFunctionInput;
|
const { id, payload } = executeServerlessFunctionInput;
|
||||||
|
|
||||||
return {
|
return await this.serverlessFunctionService.executeOne(
|
||||||
result: await this.serverlessFunctionService.executeOne(
|
id,
|
||||||
id,
|
workspaceId,
|
||||||
workspaceId,
|
payload,
|
||||||
payload,
|
);
|
||||||
),
|
|
||||||
};
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
serverlessFunctionGraphQLApiExceptionHandler(error);
|
serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
|||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
||||||
|
import { ServerlessExecuteResult } from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface';
|
||||||
|
|
||||||
import { ServerlessService } from 'src/engine/integrations/serverless/serverless.service';
|
import { ServerlessService } from 'src/engine/integrations/serverless/serverless.service';
|
||||||
import {
|
import {
|
||||||
@ -41,7 +42,7 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
|
|||||||
id: string,
|
id: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
payload: object | undefined = undefined,
|
payload: object | undefined = undefined,
|
||||||
) {
|
): Promise<ServerlessExecuteResult> {
|
||||||
const functionToExecute = await this.serverlessFunctionRepository.findOne({
|
const functionToExecute = await this.serverlessFunctionRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
id,
|
id,
|
||||||
|
|||||||
@ -26,9 +26,9 @@ import { ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/vie
|
|||||||
import { ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity';
|
import { ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity';
|
||||||
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||||
import { WebhookWorkspaceEntity } from 'src/modules/webhook/standard-objects/webhook.workspace-entity';
|
import { WebhookWorkspaceEntity } from 'src/modules/webhook/standard-objects/webhook.workspace-entity';
|
||||||
import { WorkflowEventListenerWorkspaceEntity } from 'src/modules/workflow/standard-objects/workflow-event-listener.workspace-entity';
|
import { WorkflowEventListenerWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-event-listener.workspace-entity';
|
||||||
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/standard-objects/workflow-version.workspace-entity';
|
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
|
||||||
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/standard-objects/workflow.workspace-entity';
|
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
|
||||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||||
|
|
||||||
// TODO: Maybe we should automate this with the DiscoverService of Nest.JS
|
// TODO: Maybe we should automate this with the DiscoverService of Nest.JS
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import { ActivityWorkspaceEntity } from 'src/modules/activity/standard-objects/a
|
|||||||
import { CompanyWorkspaceEntity } from 'src/modules/company/standard-objects/company.workspace-entity';
|
import { CompanyWorkspaceEntity } from 'src/modules/company/standard-objects/company.workspace-entity';
|
||||||
import { OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity';
|
import { OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity';
|
||||||
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
||||||
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/standard-objects/workflow.workspace-entity';
|
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
|
||||||
|
|
||||||
@WorkspaceEntity({
|
@WorkspaceEntity({
|
||||||
standardId: STANDARD_OBJECT_IDS.activityTarget,
|
standardId: STANDARD_OBJECT_IDS.activityTarget,
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import { ActivityWorkspaceEntity } from 'src/modules/activity/standard-objects/a
|
|||||||
import { CompanyWorkspaceEntity } from 'src/modules/company/standard-objects/company.workspace-entity';
|
import { CompanyWorkspaceEntity } from 'src/modules/company/standard-objects/company.workspace-entity';
|
||||||
import { OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity';
|
import { OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity';
|
||||||
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
||||||
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/standard-objects/workflow.workspace-entity';
|
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
|
||||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||||
|
|
||||||
@WorkspaceEntity({
|
@WorkspaceEntity({
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync
|
|||||||
import { CompanyWorkspaceEntity } from 'src/modules/company/standard-objects/company.workspace-entity';
|
import { CompanyWorkspaceEntity } from 'src/modules/company/standard-objects/company.workspace-entity';
|
||||||
import { OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity';
|
import { OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity';
|
||||||
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
||||||
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/standard-objects/workflow.workspace-entity';
|
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
|
||||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||||
|
|
||||||
@WorkspaceEntity({
|
@WorkspaceEntity({
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { CalendarModule } from 'src/modules/calendar/calendar.module';
|
|||||||
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
||||||
import { MessagingModule } from 'src/modules/messaging/messaging.module';
|
import { MessagingModule } from 'src/modules/messaging/messaging.module';
|
||||||
import { ViewModule } from 'src/modules/view/view.module';
|
import { ViewModule } from 'src/modules/view/view.module';
|
||||||
|
import { WorkflowModule } from 'src/modules/workflow/workflow.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -11,6 +12,7 @@ import { ViewModule } from 'src/modules/view/view.module';
|
|||||||
CalendarModule,
|
CalendarModule,
|
||||||
ConnectedAccountModule,
|
ConnectedAccountModule,
|
||||||
ViewModule,
|
ViewModule,
|
||||||
|
WorkflowModule,
|
||||||
],
|
],
|
||||||
providers: [],
|
providers: [],
|
||||||
exports: [],
|
exports: [],
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync
|
|||||||
import { CompanyWorkspaceEntity } from 'src/modules/company/standard-objects/company.workspace-entity';
|
import { CompanyWorkspaceEntity } from 'src/modules/company/standard-objects/company.workspace-entity';
|
||||||
import { OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity';
|
import { OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity';
|
||||||
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
||||||
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/standard-objects/workflow.workspace-entity';
|
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
|
||||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||||
|
|
||||||
@WorkspaceEntity({
|
@WorkspaceEntity({
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-
|
|||||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||||
import { WORKFLOW_EVENT_LISTENER_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
import { WORKFLOW_EVENT_LISTENER_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||||
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/standard-objects/workflow.workspace-entity';
|
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
|
||||||
|
|
||||||
@WorkspaceEntity({
|
@WorkspaceEntity({
|
||||||
standardId: STANDARD_OBJECT_IDS.workflowEventListener,
|
standardId: STANDARD_OBJECT_IDS.workflowEventListener,
|
||||||
@ -13,21 +13,8 @@ import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-
|
|||||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||||
import { WORKFLOW_VERSION_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
import { WORKFLOW_VERSION_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||||
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/standard-objects/workflow.workspace-entity';
|
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
|
||||||
|
import { WorkflowTrigger } from 'src/modules/workflow/common/types/workflow-trigger.type';
|
||||||
export enum WorkflowTriggerType {
|
|
||||||
DATABASE_EVENT = 'DATABASE_EVENT',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type WorkflowDatabaseEventTrigger = {
|
|
||||||
type: WorkflowTriggerType.DATABASE_EVENT;
|
|
||||||
settings: {
|
|
||||||
eventName: string;
|
|
||||||
triggerName: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkflowTrigger = WorkflowDatabaseEventTrigger;
|
|
||||||
|
|
||||||
@WorkspaceEntity({
|
@WorkspaceEntity({
|
||||||
standardId: STANDARD_OBJECT_IDS.workflowVersion,
|
standardId: STANDARD_OBJECT_IDS.workflowVersion,
|
||||||
@ -60,7 +47,7 @@ export class WorkflowVersionWorkspaceEntity extends BaseWorkspaceEntity {
|
|||||||
icon: 'IconPlayerPlay',
|
icon: 'IconPlayerPlay',
|
||||||
})
|
})
|
||||||
@WorkspaceIsNullable()
|
@WorkspaceIsNullable()
|
||||||
trigger: JSON | null;
|
trigger: WorkflowTrigger | null;
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
@WorkspaceRelation({
|
@WorkspaceRelation({
|
||||||
@ -19,8 +19,8 @@ import { ActivityTargetWorkspaceEntity } from 'src/modules/activity/standard-obj
|
|||||||
import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
|
import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
|
||||||
import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
|
import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
|
||||||
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
||||||
import { WorkflowEventListenerWorkspaceEntity } from 'src/modules/workflow/standard-objects/workflow-event-listener.workspace-entity';
|
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
|
||||||
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/standard-objects/workflow-version.workspace-entity';
|
import { WorkflowEventListenerWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-event-listener.workspace-entity';
|
||||||
|
|
||||||
@WorkspaceEntity({
|
@WorkspaceEntity({
|
||||||
standardId: STANDARD_OBJECT_IDS.workflow,
|
standardId: STANDARD_OBJECT_IDS.workflow,
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { WorkflowSettingsType } from 'src/modules/workflow/common/types/workflow-settings.type';
|
||||||
|
|
||||||
|
export enum WorkflowActionType {
|
||||||
|
CODE = 'CODE',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WorkflowAction = {
|
||||||
|
name: string;
|
||||||
|
displayName: string;
|
||||||
|
type: WorkflowActionType;
|
||||||
|
valid: boolean;
|
||||||
|
settings: WorkflowSettingsType;
|
||||||
|
nextAction?: WorkflowAction;
|
||||||
|
};
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
export type WorkflowCodeSettingsType = {
|
||||||
|
serverlessFunctionId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkflowSettingsType = {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: {
|
||||||
|
value: boolean;
|
||||||
|
};
|
||||||
|
continueOnFailure: {
|
||||||
|
value: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
} & WorkflowCodeSettingsType;
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
import { WorkflowAction } from 'src/modules/workflow/common/types/workflow-action.type';
|
||||||
|
|
||||||
|
export enum WorkflowTriggerType {
|
||||||
|
DATABASE_EVENT = 'DATABASE_EVENT',
|
||||||
|
MANUAL = 'MANUAL',
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseTrigger = {
|
||||||
|
type: WorkflowTriggerType;
|
||||||
|
input?: object;
|
||||||
|
nextAction?: WorkflowAction;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkflowDatabaseEventTrigger = BaseTrigger & {
|
||||||
|
type: WorkflowTriggerType.DATABASE_EVENT;
|
||||||
|
settings: {
|
||||||
|
eventName: string;
|
||||||
|
triggerName: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type WorkflowManualTrigger = BaseTrigger & {
|
||||||
|
type: WorkflowTriggerType.MANUAL;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkflowTrigger =
|
||||||
|
| WorkflowDatabaseEventTrigger
|
||||||
|
| WorkflowManualTrigger;
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { WorkflowCommonService } from 'src/modules/workflow/common/workflow-common.services';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [WorkflowCommonService],
|
||||||
|
exports: [WorkflowCommonService],
|
||||||
|
})
|
||||||
|
export class WorkflowCommonModule {}
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
|
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
|
||||||
|
import {
|
||||||
|
WorkflowTriggerException,
|
||||||
|
WorkflowTriggerExceptionCode,
|
||||||
|
} from 'src/modules/workflow/workflow-trigger/workflow-trigger.exception';
|
||||||
|
import { WorkflowTrigger } from 'src/modules/workflow/common/types/workflow-trigger.type';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WorkflowCommonService {
|
||||||
|
constructor(
|
||||||
|
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getWorkflowVersion(
|
||||||
|
workspaceId: string,
|
||||||
|
workflowVersionId: string,
|
||||||
|
): Promise<
|
||||||
|
Omit<WorkflowVersionWorkspaceEntity, 'trigger'> & {
|
||||||
|
trigger: WorkflowTrigger;
|
||||||
|
}
|
||||||
|
> {
|
||||||
|
const workflowVersionRepository =
|
||||||
|
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
|
||||||
|
workspaceId,
|
||||||
|
'workflowVersion',
|
||||||
|
);
|
||||||
|
|
||||||
|
const workflowVersion = await workflowVersionRepository.findOne({
|
||||||
|
where: {
|
||||||
|
id: workflowVersionId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!workflowVersion) {
|
||||||
|
throw new WorkflowTriggerException(
|
||||||
|
'Workflow version not found',
|
||||||
|
WorkflowTriggerExceptionCode.INVALID_INPUT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!workflowVersion.trigger || !workflowVersion.trigger?.type) {
|
||||||
|
throw new WorkflowTriggerException(
|
||||||
|
'Workflow version does not contains trigger',
|
||||||
|
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...workflowVersion, trigger: workflowVersion.trigger };
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator';
|
||||||
|
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
|
||||||
|
import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator';
|
||||||
|
import { WorkflowRunnerService } from 'src/modules/workflow/workflow-runner/workflow-runner.service';
|
||||||
|
import { WorkflowCommonService } from 'src/modules/workflow/common/workflow-common.services';
|
||||||
|
|
||||||
|
type RunWorkflowJobData = { workspaceId: string; workflowVersionId: string };
|
||||||
|
|
||||||
|
@Processor(MessageQueue.workflowQueue)
|
||||||
|
export class WorkflowRunnerJob {
|
||||||
|
constructor(
|
||||||
|
private readonly workflowCommonService: WorkflowCommonService,
|
||||||
|
private readonly workflowRunnerService: WorkflowRunnerService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Process(WorkflowRunnerJob.name)
|
||||||
|
async handle({
|
||||||
|
workspaceId,
|
||||||
|
workflowVersionId,
|
||||||
|
}: RunWorkflowJobData): Promise<void> {
|
||||||
|
const workflowVersion = await this.workflowCommonService.getWorkflowVersion(
|
||||||
|
workspaceId,
|
||||||
|
workflowVersionId,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.workflowRunnerService.run({
|
||||||
|
action: workflowVersion.trigger.nextAction,
|
||||||
|
workspaceId,
|
||||||
|
payload: workflowVersion.trigger.input,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { WorkflowRunnerService } from 'src/modules/workflow/workflow-runner/workflow-runner.service';
|
||||||
|
import { WorkflowRunnerJob } from 'src/modules/workflow/workflow-runner/workflow-runner.job';
|
||||||
|
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
|
||||||
|
import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [WorkflowCommonModule, ServerlessFunctionModule],
|
||||||
|
providers: [WorkflowRunnerService, WorkflowRunnerJob],
|
||||||
|
exports: [WorkflowRunnerService],
|
||||||
|
})
|
||||||
|
export class WorkflowRunnerModule {}
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||||
|
import {
|
||||||
|
WorkflowTriggerException,
|
||||||
|
WorkflowTriggerExceptionCode,
|
||||||
|
} from 'src/modules/workflow/workflow-trigger/workflow-trigger.exception';
|
||||||
|
import {
|
||||||
|
WorkflowAction,
|
||||||
|
WorkflowActionType,
|
||||||
|
} from 'src/modules/workflow/common/types/workflow-action.type';
|
||||||
|
|
||||||
|
const MAX_RETRIES_ON_FAILURE = 3;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WorkflowRunnerService {
|
||||||
|
constructor(
|
||||||
|
private readonly serverlessFunctionService: ServerlessFunctionService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async run({
|
||||||
|
action,
|
||||||
|
workspaceId,
|
||||||
|
payload,
|
||||||
|
attemptCount = 1,
|
||||||
|
}: {
|
||||||
|
action?: WorkflowAction;
|
||||||
|
workspaceId: string;
|
||||||
|
payload?: object;
|
||||||
|
attemptCount?: number;
|
||||||
|
}) {
|
||||||
|
if (!action) {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: object | undefined = undefined;
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case WorkflowActionType.CODE: {
|
||||||
|
const executionResult = await this.serverlessFunctionService.executeOne(
|
||||||
|
action.settings.serverlessFunctionId,
|
||||||
|
workspaceId,
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (executionResult.data) {
|
||||||
|
result = executionResult.data;
|
||||||
|
}
|
||||||
|
if (!executionResult.error) {
|
||||||
|
throw new Error('Execution result error, no data or error');
|
||||||
|
}
|
||||||
|
if (action.settings.errorHandlingOptions.continueOnFailure.value) {
|
||||||
|
result = payload;
|
||||||
|
break;
|
||||||
|
} else if (
|
||||||
|
action.settings.errorHandlingOptions.retryOnFailure.value &&
|
||||||
|
attemptCount < MAX_RETRIES_ON_FAILURE
|
||||||
|
) {
|
||||||
|
return await this.run({
|
||||||
|
action,
|
||||||
|
workspaceId,
|
||||||
|
payload,
|
||||||
|
attemptCount: attemptCount + 1,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return executionResult.error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new WorkflowTriggerException(
|
||||||
|
`Unknown action type '${action.type}'`,
|
||||||
|
WorkflowTriggerExceptionCode.INVALID_ACTION_TYPE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.run({
|
||||||
|
action: action.nextAction,
|
||||||
|
workspaceId,
|
||||||
|
payload: result,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,4 +11,5 @@ export enum WorkflowTriggerExceptionCode {
|
|||||||
INVALID_INPUT = 'INVALID_INPUT',
|
INVALID_INPUT = 'INVALID_INPUT',
|
||||||
INVALID_WORKFLOW_TRIGGER = 'INVALID_WORKFLOW_TRIGGER',
|
INVALID_WORKFLOW_TRIGGER = 'INVALID_WORKFLOW_TRIGGER',
|
||||||
INVALID_WORKFLOW_VERSION = 'INVALID_WORKFLOW_VERSION',
|
INVALID_WORKFLOW_VERSION = 'INVALID_WORKFLOW_VERSION',
|
||||||
|
INVALID_ACTION_TYPE = 'INVALID_ACTION_TYPE',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,59 +1,51 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
import { WorkflowEventListenerWorkspaceEntity } from 'src/modules/workflow/standard-objects/workflow-event-listener.workspace-entity';
|
import { WorkflowEventListenerWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-event-listener.workspace-entity';
|
||||||
import {
|
|
||||||
WorkflowDatabaseEventTrigger,
|
|
||||||
WorkflowTrigger,
|
|
||||||
WorkflowTriggerType,
|
|
||||||
WorkflowVersionWorkspaceEntity,
|
|
||||||
} from 'src/modules/workflow/standard-objects/workflow-version.workspace-entity';
|
|
||||||
import {
|
import {
|
||||||
WorkflowTriggerException,
|
WorkflowTriggerException,
|
||||||
WorkflowTriggerExceptionCode,
|
WorkflowTriggerExceptionCode,
|
||||||
} from 'src/modules/workflow/workflow-trigger/workflow-trigger.exception';
|
} from 'src/modules/workflow/workflow-trigger/workflow-trigger.exception';
|
||||||
|
import {
|
||||||
|
WorkflowDatabaseEventTrigger,
|
||||||
|
WorkflowTriggerType,
|
||||||
|
} from 'src/modules/workflow/common/types/workflow-trigger.type';
|
||||||
|
import { WorkflowCommonService } from 'src/modules/workflow/common/workflow-common.services';
|
||||||
|
import { WorkflowRunnerService } from 'src/modules/workflow/workflow-runner/workflow-runner.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WorkflowTriggerService {
|
export class WorkflowTriggerService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||||
|
private readonly workflowCommonService: WorkflowCommonService,
|
||||||
|
private readonly workflowRunnerService: WorkflowRunnerService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async enableWorkflowTrigger(workspaceId: string, workflowVersionId: string) {
|
async runWorkflow(workspaceId: string, workflowVersionId: string) {
|
||||||
const workflowVersionRepository =
|
const workflowVersion = await this.workflowCommonService.getWorkflowVersion(
|
||||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
|
workspaceId,
|
||||||
workspaceId,
|
workflowVersionId,
|
||||||
'workflowVersion',
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const workflowVersion = await workflowVersionRepository.findOne({
|
return await this.workflowRunnerService.run({
|
||||||
where: {
|
action: workflowVersion.trigger.nextAction,
|
||||||
id: workflowVersionId,
|
workspaceId,
|
||||||
},
|
payload: workflowVersion.trigger.input,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!workflowVersion) {
|
async enableWorkflowTrigger(workspaceId: string, workflowVersionId: string) {
|
||||||
throw new WorkflowTriggerException(
|
const workflowVersion = await this.workflowCommonService.getWorkflowVersion(
|
||||||
'Workflow version not found',
|
workspaceId,
|
||||||
WorkflowTriggerExceptionCode.INVALID_INPUT,
|
workflowVersionId,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
const trigger = workflowVersion.trigger as unknown as WorkflowTrigger;
|
switch (workflowVersion.trigger.type) {
|
||||||
|
|
||||||
if (!trigger || !trigger?.type) {
|
|
||||||
throw new WorkflowTriggerException(
|
|
||||||
'Workflow version does not contains trigger',
|
|
||||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (trigger.type) {
|
|
||||||
case WorkflowTriggerType.DATABASE_EVENT:
|
case WorkflowTriggerType.DATABASE_EVENT:
|
||||||
await this.upsertWorkflowEventListener(
|
await this.upsertWorkflowEventListener(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
workflowVersion.workflowId,
|
workflowVersion.workflowId,
|
||||||
trigger,
|
workflowVersion.trigger,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -0,0 +1,8 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { WorkflowRunnerModule } from 'src/modules/workflow/workflow-runner/workflow-runner.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [WorkflowRunnerModule],
|
||||||
|
})
|
||||||
|
export class WorkflowModule {}
|
||||||
Reference in New Issue
Block a user