Infer function input in workflow step (#8308)
- add `inputSchema` column in serverless function. This is an array of parameters, with their name and type - on serverless function id update, get the `inputSchema` + store empty settings in step - from step settings, build the form TODO in next PR: - use field type to decide what kind of form should be printed - have a strategy to handle object as input https://github.com/user-attachments/assets/ed96f919-24b5-4baf-a051-31f76f45e575
This commit is contained in:
@ -33,7 +33,7 @@ const documents = {
|
|||||||
"\n mutation DeleteOneFieldMetadataItem($idToDelete: UUID!) {\n deleteOneField(input: { id: $idToDelete }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n }\n }\n": types.DeleteOneFieldMetadataItemDocument,
|
"\n mutation DeleteOneFieldMetadataItem($idToDelete: UUID!) {\n deleteOneField(input: { id: $idToDelete }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n }\n }\n": types.DeleteOneFieldMetadataItemDocument,
|
||||||
"\n mutation DeleteOneRelationMetadataItem($idToDelete: UUID!) {\n deleteOneRelation(input: { id: $idToDelete }) {\n id\n }\n }\n": types.DeleteOneRelationMetadataItemDocument,
|
"\n mutation DeleteOneRelationMetadataItem($idToDelete: UUID!) {\n deleteOneRelation(input: { id: $idToDelete }) {\n id\n }\n }\n": types.DeleteOneRelationMetadataItemDocument,
|
||||||
"\n query ObjectMetadataItems(\n $objectFilter: objectFilter\n $fieldFilter: fieldFilter\n ) {\n objects(paging: { first: 1000 }, filter: $objectFilter) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fields(paging: { first: 1000 }, filter: $fieldFilter) {\n edges {\n node {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n": types.ObjectMetadataItemsDocument,
|
"\n query ObjectMetadataItems(\n $objectFilter: objectFilter\n $fieldFilter: fieldFilter\n ) {\n objects(paging: { first: 1000 }, filter: $objectFilter) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fields(paging: { first: 1000 }, filter: $fieldFilter) {\n edges {\n node {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n": types.ObjectMetadataItemsDocument,
|
||||||
"\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n syncStatus\n latestVersion\n publishedVersions\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc,
|
"\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n syncStatus\n latestVersion\n latestVersionInputSchema {\n name\n type\n }\n publishedVersions\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: ServerlessFunctionIdInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.DeleteOneServerlessFunctionDocument,
|
"\n \n mutation DeleteOneServerlessFunction($input: ServerlessFunctionIdInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.DeleteOneServerlessFunctionDocument,
|
||||||
"\n mutation ExecuteOneServerlessFunction(\n $input: ExecuteServerlessFunctionInput!\n ) {\n executeOneServerlessFunction(input: $input) {\n data\n duration\n status\n error\n }\n }\n": types.ExecuteOneServerlessFunctionDocument,
|
"\n mutation ExecuteOneServerlessFunction(\n $input: ExecuteServerlessFunctionInput!\n ) {\n executeOneServerlessFunction(input: $input) {\n data\n duration\n status\n error\n }\n }\n": types.ExecuteOneServerlessFunctionDocument,
|
||||||
@ -142,7 +142,7 @@ export function graphql(source: "\n query ObjectMetadataItems(\n $objectFilt
|
|||||||
/**
|
/**
|
||||||
* 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 fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n syncStatus\n latestVersion\n publishedVersions\n createdAt\n updatedAt\n }\n"): (typeof documents)["\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n syncStatus\n latestVersion\n publishedVersions\n createdAt\n updatedAt\n }\n"];
|
export function graphql(source: "\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n syncStatus\n latestVersion\n latestVersionInputSchema {\n name\n type\n }\n publishedVersions\n createdAt\n updatedAt\n }\n"): (typeof documents)["\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n syncStatus\n latestVersion\n latestVersionInputSchema {\n name\n type\n }\n publishedVersions\n createdAt\n updatedAt\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
@ -321,6 +321,12 @@ export type FullName = {
|
|||||||
lastName: Scalars['String'];
|
lastName: Scalars['String'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type FunctionParameter = {
|
||||||
|
__typename?: 'FunctionParameter';
|
||||||
|
name: Scalars['String'];
|
||||||
|
type: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
export type GenerateJwt = GenerateJwtOutputWithAuthTokens | GenerateJwtOutputWithSsoauth;
|
export type GenerateJwt = GenerateJwtOutputWithAuthTokens | GenerateJwtOutputWithSsoauth;
|
||||||
|
|
||||||
export type GenerateJwtOutputWithAuthTokens = {
|
export type GenerateJwtOutputWithAuthTokens = {
|
||||||
@ -967,6 +973,7 @@ export type ServerlessFunction = {
|
|||||||
description?: Maybe<Scalars['String']>;
|
description?: Maybe<Scalars['String']>;
|
||||||
id: Scalars['UUID'];
|
id: Scalars['UUID'];
|
||||||
latestVersion?: Maybe<Scalars['String']>;
|
latestVersion?: Maybe<Scalars['String']>;
|
||||||
|
latestVersionInputSchema?: Maybe<Array<FunctionParameter>>;
|
||||||
name: Scalars['String'];
|
name: Scalars['String'];
|
||||||
publishedVersions: Array<Scalars['String']>;
|
publishedVersions: Array<Scalars['String']>;
|
||||||
runtime: Scalars['String'];
|
runtime: Scalars['String'];
|
||||||
|
|||||||
@ -8,6 +8,10 @@ export const SERVERLESS_FUNCTION_FRAGMENT = gql`
|
|||||||
runtime
|
runtime
|
||||||
syncStatus
|
syncStatus
|
||||||
latestVersion
|
latestVersion
|
||||||
|
latestVersionInputSchema {
|
||||||
|
name
|
||||||
|
type
|
||||||
|
}
|
||||||
publishedVersions
|
publishedVersions
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
import { useGetManyServerlessFunctions } from '@/settings/serverless-functions/hooks/useGetManyServerlessFunctions';
|
import { useGetManyServerlessFunctions } from '@/settings/serverless-functions/hooks/useGetManyServerlessFunctions';
|
||||||
import { Select, SelectOption } from '@/ui/input/components/Select';
|
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||||
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
|
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
|
||||||
|
import VariableTagInput from '@/workflow/search-variables/components/VariableTagInput';
|
||||||
import { WorkflowCodeStep } from '@/workflow/types/Workflow';
|
import { WorkflowCodeStep } from '@/workflow/types/Workflow';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
|
import { useState } from 'react';
|
||||||
import { IconCode, isDefined } from 'twenty-ui';
|
import { IconCode, isDefined } from 'twenty-ui';
|
||||||
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
type WorkflowEditActionFormServerlessFunctionProps =
|
type WorkflowEditActionFormServerlessFunctionProps =
|
||||||
| {
|
| {
|
||||||
@ -23,6 +27,48 @@ export const WorkflowEditActionFormServerlessFunction = (
|
|||||||
|
|
||||||
const { serverlessFunctions } = useGetManyServerlessFunctions();
|
const { serverlessFunctions } = useGetManyServerlessFunctions();
|
||||||
|
|
||||||
|
const defaultFunctionInput =
|
||||||
|
props.action.settings.input.serverlessFunctionInput;
|
||||||
|
|
||||||
|
const [functionInput, setFunctionInput] =
|
||||||
|
useState<Record<string, any>>(defaultFunctionInput);
|
||||||
|
|
||||||
|
const [serverlessFunctionId, setServerlessFunctionId] = useState<string>(
|
||||||
|
props.action.settings.input.serverlessFunctionId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateFunctionInput = useDebouncedCallback(
|
||||||
|
async (newFunctionInput: object) => {
|
||||||
|
if (props.readonly === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
props.onActionUpdate({
|
||||||
|
...props.action,
|
||||||
|
settings: {
|
||||||
|
...props.action.settings,
|
||||||
|
input: {
|
||||||
|
serverlessFunctionId:
|
||||||
|
props.action.settings.input.serverlessFunctionId,
|
||||||
|
serverlessFunctionVersion:
|
||||||
|
props.action.settings.input.serverlessFunctionVersion,
|
||||||
|
serverlessFunctionInput: {
|
||||||
|
...props.action.settings.input.serverlessFunctionInput,
|
||||||
|
...newFunctionInput,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
1_000,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleInputChange = (key: string, value: any) => {
|
||||||
|
const newFunctionInput = { ...functionInput, [key]: value };
|
||||||
|
setFunctionInput(newFunctionInput);
|
||||||
|
updateFunctionInput(newFunctionInput);
|
||||||
|
};
|
||||||
|
|
||||||
const availableFunctions: Array<SelectOption<string>> = [
|
const availableFunctions: Array<SelectOption<string>> = [
|
||||||
{ label: 'None', value: '' },
|
{ label: 'None', value: '' },
|
||||||
...serverlessFunctions
|
...serverlessFunctions
|
||||||
@ -32,9 +78,43 @@ export const WorkflowEditActionFormServerlessFunction = (
|
|||||||
.map((serverlessFunction) => ({
|
.map((serverlessFunction) => ({
|
||||||
label: serverlessFunction.name,
|
label: serverlessFunction.name,
|
||||||
value: serverlessFunction.id,
|
value: serverlessFunction.id,
|
||||||
|
latestVersionInputSchema: serverlessFunction.latestVersionInputSchema,
|
||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const handleFunctionChange = (newServerlessFunctionId: string) => {
|
||||||
|
setServerlessFunctionId(newServerlessFunctionId);
|
||||||
|
|
||||||
|
const serverlessFunction = serverlessFunctions.find(
|
||||||
|
(f) => f.id === newServerlessFunctionId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const serverlessFunctionVersion =
|
||||||
|
serverlessFunction?.latestVersion || 'latest';
|
||||||
|
|
||||||
|
const defaultFunctionInput = serverlessFunction?.latestVersionInputSchema
|
||||||
|
? serverlessFunction.latestVersionInputSchema
|
||||||
|
.map((parameter) => parameter.name)
|
||||||
|
.reduce((acc, name) => ({ ...acc, [name]: null }), {})
|
||||||
|
: {};
|
||||||
|
|
||||||
|
if (!props.readonly) {
|
||||||
|
props.onActionUpdate({
|
||||||
|
...props.action,
|
||||||
|
settings: {
|
||||||
|
...props.action.settings,
|
||||||
|
input: {
|
||||||
|
serverlessFunctionId: newServerlessFunctionId,
|
||||||
|
serverlessFunctionVersion,
|
||||||
|
serverlessFunctionInput: defaultFunctionInput,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setFunctionInput(defaultFunctionInput);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkflowEditGenericFormBase
|
<WorkflowEditGenericFormBase
|
||||||
HeaderIcon={<IconCode color={theme.color.orange} />}
|
HeaderIcon={<IconCode color={theme.color.orange} />}
|
||||||
@ -42,31 +122,24 @@ export const WorkflowEditActionFormServerlessFunction = (
|
|||||||
headerType="Code"
|
headerType="Code"
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
dropdownId="workflow-edit-action-function"
|
dropdownId="select-serverless-function-id"
|
||||||
label="Function"
|
label="Function"
|
||||||
fullWidth
|
fullWidth
|
||||||
value={props.action.settings.input.serverlessFunctionId}
|
value={serverlessFunctionId}
|
||||||
options={availableFunctions}
|
options={availableFunctions}
|
||||||
disabled={props.readonly}
|
disabled={props.readonly}
|
||||||
onChange={(serverlessFunctionId) => {
|
onChange={handleFunctionChange}
|
||||||
if (props.readonly === true) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
props.onActionUpdate({
|
|
||||||
...props.action,
|
|
||||||
settings: {
|
|
||||||
...props.action.settings,
|
|
||||||
input: {
|
|
||||||
serverlessFunctionId,
|
|
||||||
serverlessFunctionVersion:
|
|
||||||
serverlessFunctions.find((f) => f.id === serverlessFunctionId)
|
|
||||||
?.latestVersion || 'latest',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
{functionInput &&
|
||||||
|
Object.entries(functionInput).map(([inputKey, inputValue]) => (
|
||||||
|
<VariableTagInput
|
||||||
|
inputId={`input-${inputKey}`}
|
||||||
|
label={capitalize(inputKey)}
|
||||||
|
placeholder="Enter value (use {{variable}} for dynamic content)"
|
||||||
|
value={inputValue ?? ''}
|
||||||
|
onChange={(value) => handleInputChange(inputKey, value)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</WorkflowEditGenericFormBase>
|
</WorkflowEditGenericFormBase>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,6 +15,9 @@ export type WorkflowCodeStepSettings = BaseWorkflowStepSettings & {
|
|||||||
input: {
|
input: {
|
||||||
serverlessFunctionId: string;
|
serverlessFunctionId: string;
|
||||||
serverlessFunctionVersion: string;
|
serverlessFunctionVersion: string;
|
||||||
|
serverlessFunctionInput: {
|
||||||
|
[hello: string]: any;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -25,6 +25,7 @@ describe('addCreateStepNodes', () => {
|
|||||||
input: {
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
serverlessFunctionVersion: '1',
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
},
|
},
|
||||||
outputSchema: {},
|
outputSchema: {},
|
||||||
},
|
},
|
||||||
@ -42,6 +43,7 @@ describe('addCreateStepNodes', () => {
|
|||||||
input: {
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
serverlessFunctionVersion: '1',
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
},
|
},
|
||||||
outputSchema: {},
|
outputSchema: {},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -47,6 +47,7 @@ describe('generateWorkflowDiagram', () => {
|
|||||||
input: {
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
serverlessFunctionVersion: '1',
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
},
|
},
|
||||||
outputSchema: {},
|
outputSchema: {},
|
||||||
},
|
},
|
||||||
@ -64,6 +65,7 @@ describe('generateWorkflowDiagram', () => {
|
|||||||
input: {
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
serverlessFunctionVersion: '1',
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
},
|
},
|
||||||
outputSchema: {},
|
outputSchema: {},
|
||||||
},
|
},
|
||||||
@ -110,6 +112,7 @@ describe('generateWorkflowDiagram', () => {
|
|||||||
input: {
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
serverlessFunctionVersion: '1',
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
},
|
},
|
||||||
outputSchema: {},
|
outputSchema: {},
|
||||||
},
|
},
|
||||||
@ -127,6 +130,7 @@ describe('generateWorkflowDiagram', () => {
|
|||||||
input: {
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
serverlessFunctionVersion: '1',
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
},
|
},
|
||||||
outputSchema: {},
|
outputSchema: {},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -84,6 +84,7 @@ describe('getWorkflowVersionDiagram', () => {
|
|||||||
input: {
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
serverlessFunctionVersion: '1',
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
},
|
},
|
||||||
outputSchema: {},
|
outputSchema: {},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -28,6 +28,7 @@ describe('insertStep', () => {
|
|||||||
input: {
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
serverlessFunctionVersion: '1',
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
},
|
},
|
||||||
outputSchema: {},
|
outputSchema: {},
|
||||||
},
|
},
|
||||||
@ -70,6 +71,7 @@ describe('insertStep', () => {
|
|||||||
input: {
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
serverlessFunctionVersion: '1',
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
},
|
},
|
||||||
outputSchema: {},
|
outputSchema: {},
|
||||||
},
|
},
|
||||||
@ -106,6 +108,7 @@ describe('insertStep', () => {
|
|||||||
input: {
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
serverlessFunctionVersion: '1',
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
},
|
},
|
||||||
outputSchema: {},
|
outputSchema: {},
|
||||||
},
|
},
|
||||||
@ -123,6 +126,7 @@ describe('insertStep', () => {
|
|||||||
input: {
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
serverlessFunctionVersion: '1',
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
},
|
},
|
||||||
outputSchema: {},
|
outputSchema: {},
|
||||||
},
|
},
|
||||||
@ -148,6 +152,7 @@ describe('insertStep', () => {
|
|||||||
input: {
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
serverlessFunctionVersion: '1',
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
},
|
},
|
||||||
outputSchema: {},
|
outputSchema: {},
|
||||||
},
|
},
|
||||||
@ -188,6 +193,7 @@ describe('insertStep', () => {
|
|||||||
input: {
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
serverlessFunctionVersion: '1',
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
},
|
},
|
||||||
outputSchema: {},
|
outputSchema: {},
|
||||||
},
|
},
|
||||||
@ -205,6 +211,7 @@ describe('insertStep', () => {
|
|||||||
input: {
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
serverlessFunctionVersion: '1',
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
},
|
},
|
||||||
outputSchema: {},
|
outputSchema: {},
|
||||||
},
|
},
|
||||||
@ -230,6 +237,7 @@ describe('insertStep', () => {
|
|||||||
input: {
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
serverlessFunctionVersion: '1',
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
},
|
},
|
||||||
outputSchema: {},
|
outputSchema: {},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -13,6 +13,7 @@ it('returns a deep copy of the provided steps array instead of mutating it', ()
|
|||||||
input: {
|
input: {
|
||||||
serverlessFunctionId: 'first',
|
serverlessFunctionId: 'first',
|
||||||
serverlessFunctionVersion: '1',
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
},
|
},
|
||||||
outputSchema: {},
|
outputSchema: {},
|
||||||
},
|
},
|
||||||
@ -54,6 +55,7 @@ it('removes a step in a non-empty steps array', () => {
|
|||||||
input: {
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
serverlessFunctionVersion: '1',
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
},
|
},
|
||||||
outputSchema: {},
|
outputSchema: {},
|
||||||
},
|
},
|
||||||
@ -78,6 +80,7 @@ it('removes a step in a non-empty steps array', () => {
|
|||||||
input: {
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
serverlessFunctionVersion: '1',
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
},
|
},
|
||||||
outputSchema: {},
|
outputSchema: {},
|
||||||
},
|
},
|
||||||
@ -96,6 +99,7 @@ it('removes a step in a non-empty steps array', () => {
|
|||||||
input: {
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
serverlessFunctionVersion: '1',
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
},
|
},
|
||||||
outputSchema: {},
|
outputSchema: {},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -14,6 +14,7 @@ describe('replaceStep', () => {
|
|||||||
input: {
|
input: {
|
||||||
serverlessFunctionId: 'first',
|
serverlessFunctionId: 'first',
|
||||||
serverlessFunctionVersion: '1',
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
},
|
},
|
||||||
outputSchema: {},
|
outputSchema: {},
|
||||||
},
|
},
|
||||||
@ -46,6 +47,7 @@ describe('replaceStep', () => {
|
|||||||
input: {
|
input: {
|
||||||
serverlessFunctionId: 'second',
|
serverlessFunctionId: 'second',
|
||||||
serverlessFunctionVersion: '1',
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
},
|
},
|
||||||
outputSchema: {},
|
outputSchema: {},
|
||||||
},
|
},
|
||||||
@ -68,6 +70,7 @@ describe('replaceStep', () => {
|
|||||||
input: {
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
serverlessFunctionVersion: '1',
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
},
|
},
|
||||||
outputSchema: {},
|
outputSchema: {},
|
||||||
},
|
},
|
||||||
@ -92,6 +95,7 @@ describe('replaceStep', () => {
|
|||||||
input: {
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
serverlessFunctionVersion: '1',
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
},
|
},
|
||||||
outputSchema: {},
|
outputSchema: {},
|
||||||
},
|
},
|
||||||
@ -110,6 +114,7 @@ describe('replaceStep', () => {
|
|||||||
input: {
|
input: {
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
serverlessFunctionVersion: '1',
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
},
|
},
|
||||||
outputSchema: {},
|
outputSchema: {},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export const getStepDefaultDefinition = (
|
|||||||
input: {
|
input: {
|
||||||
serverlessFunctionId: '',
|
serverlessFunctionId: '',
|
||||||
serverlessFunctionVersion: '',
|
serverlessFunctionVersion: '',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
},
|
},
|
||||||
outputSchema: {},
|
outputSchema: {},
|
||||||
errorHandlingOptions: {
|
errorHandlingOptions: {
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddInputSchemaToFunction1730803174864
|
||||||
|
implements MigrationInterface
|
||||||
|
{
|
||||||
|
name = 'AddInputSchemaToFunction1730803174864';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."serverlessFunction" ADD "latestVersionInputSchema" jsonb`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."serverlessFunction" DROP COLUMN "latestVersionInputSchema"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,7 +9,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 { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
||||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||||
import { WorkflowBuilderService } from 'src/modules/workflow/workflow-builder/workflow-builder.service';
|
import { WorkflowBuilderWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-builder.workspace-service';
|
||||||
import { OutputSchema } from 'src/modules/workflow/workflow-executor/types/workflow-step-settings.type';
|
import { OutputSchema } from 'src/modules/workflow/workflow-executor/types/workflow-step-settings.type';
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
@ -17,7 +17,7 @@ import { OutputSchema } from 'src/modules/workflow/workflow-executor/types/workf
|
|||||||
@UseFilters(WorkflowTriggerGraphqlApiExceptionFilter)
|
@UseFilters(WorkflowTriggerGraphqlApiExceptionFilter)
|
||||||
export class WorkflowBuilderResolver {
|
export class WorkflowBuilderResolver {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly workflowBuilderService: WorkflowBuilderService,
|
private readonly workflowBuilderWorkspaceService: WorkflowBuilderWorkspaceService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Mutation(() => graphqlTypeJson)
|
@Mutation(() => graphqlTypeJson)
|
||||||
@ -25,7 +25,7 @@ export class WorkflowBuilderResolver {
|
|||||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||||
@Args('input') { step }: ComputeStepOutputSchemaInput,
|
@Args('input') { step }: ComputeStepOutputSchemaInput,
|
||||||
): Promise<OutputSchema> {
|
): Promise<OutputSchema> {
|
||||||
return this.workflowBuilderService.computeStepOutputSchema({
|
return this.workflowBuilderWorkspaceService.computeStepOutputSchema({
|
||||||
step,
|
step,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export const SERVERLESS_FUNCTION_PUBLISHED = 'serverlessFunction.published';
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import { IsString } from 'class-validator';
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class FunctionParameter {
|
||||||
|
@IsString()
|
||||||
|
@Field(() => String)
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Field(() => String)
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
@ -20,6 +20,7 @@ import {
|
|||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
|
||||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||||
|
import { FunctionParameter } from 'src/engine/metadata-modules/serverless-function/dtos/function-parameter.dto';
|
||||||
import { ServerlessFunctionSyncStatus } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
import { ServerlessFunctionSyncStatus } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||||
|
|
||||||
registerEnumType(ServerlessFunctionSyncStatus, {
|
registerEnumType(ServerlessFunctionSyncStatus, {
|
||||||
@ -64,6 +65,10 @@ export class ServerlessFunctionDTO {
|
|||||||
@Field(() => [String], { nullable: false })
|
@Field(() => [String], { nullable: false })
|
||||||
publishedVersions: string[];
|
publishedVersions: string[];
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@Field(() => [FunctionParameter], { nullable: true })
|
||||||
|
latestVersionInputSchema: FunctionParameter[] | null;
|
||||||
|
|
||||||
@IsEnum(ServerlessFunctionSyncStatus)
|
@IsEnum(ServerlessFunctionSyncStatus)
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@Field(() => ServerlessFunctionSyncStatus)
|
@Field(() => ServerlessFunctionSyncStatus)
|
||||||
|
|||||||
@ -0,0 +1,59 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { INDEX_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/index-file-name';
|
||||||
|
import { SERVERLESS_FUNCTION_PUBLISHED } from 'src/engine/metadata-modules/serverless-function/constants/serverless-function-published';
|
||||||
|
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||||
|
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||||
|
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/workspace-event.type';
|
||||||
|
import { CodeIntrospectionService } from 'src/modules/code-introspection/code-introspection.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ServerlessFunctionPublicationListener {
|
||||||
|
constructor(
|
||||||
|
private readonly serverlessFunctionService: ServerlessFunctionService,
|
||||||
|
private readonly codeIntrospectionService: CodeIntrospectionService,
|
||||||
|
@InjectRepository(ServerlessFunctionEntity, 'metadata')
|
||||||
|
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@OnEvent(SERVERLESS_FUNCTION_PUBLISHED)
|
||||||
|
async handle(
|
||||||
|
payload: WorkspaceEventBatch<{
|
||||||
|
serverlessFunctionId: string;
|
||||||
|
serverlessFunctionVersion: string;
|
||||||
|
}>,
|
||||||
|
): Promise<void> {
|
||||||
|
payload.events.forEach(async (event) => {
|
||||||
|
const sourceCode =
|
||||||
|
await this.serverlessFunctionService.getServerlessFunctionSourceCode(
|
||||||
|
payload.workspaceId,
|
||||||
|
event.serverlessFunctionId,
|
||||||
|
event.serverlessFunctionVersion,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!sourceCode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexCode = sourceCode[join('src', INDEX_FILE_NAME)];
|
||||||
|
|
||||||
|
if (!indexCode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestVersionInputSchema =
|
||||||
|
await this.codeIntrospectionService.getFunctionInputSchema(indexCode);
|
||||||
|
|
||||||
|
await this.serverlessFunctionRepository.update(
|
||||||
|
{ id: event.serverlessFunctionId },
|
||||||
|
{ latestVersionInputSchema },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,6 +6,8 @@ import {
|
|||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
|
import { FunctionParameter } from 'src/engine/metadata-modules/serverless-function/dtos/function-parameter.dto';
|
||||||
|
|
||||||
export enum ServerlessFunctionSyncStatus {
|
export enum ServerlessFunctionSyncStatus {
|
||||||
NOT_READY = 'NOT_READY',
|
NOT_READY = 'NOT_READY',
|
||||||
READY = 'READY',
|
READY = 'READY',
|
||||||
@ -32,6 +34,9 @@ export class ServerlessFunctionEntity {
|
|||||||
@Column({ nullable: false, type: 'jsonb', default: [] })
|
@Column({ nullable: false, type: 'jsonb', default: [] })
|
||||||
publishedVersions: string[];
|
publishedVersions: string[];
|
||||||
|
|
||||||
|
@Column({ nullable: true, type: 'jsonb' })
|
||||||
|
latestVersionInputSchema: FunctionParameter[];
|
||||||
|
|
||||||
@Column({ nullable: false, default: ServerlessFunctionRuntime.NODE18 })
|
@Column({ nullable: false, default: ServerlessFunctionRuntime.NODE18 })
|
||||||
runtime: ServerlessFunctionRuntime;
|
runtime: ServerlessFunctionRuntime;
|
||||||
|
|
||||||
|
|||||||
@ -8,9 +8,11 @@ import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-
|
|||||||
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 { FileModule } from 'src/engine/core-modules/file/file.module';
|
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||||
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
|
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
|
||||||
|
import { ServerlessFunctionPublicationListener } from 'src/engine/metadata-modules/serverless-function/listeners/serverless-function-publication.listener';
|
||||||
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 { ServerlessFunctionResolver } from 'src/engine/metadata-modules/serverless-function/serverless-function.resolver';
|
import { ServerlessFunctionResolver } from 'src/engine/metadata-modules/serverless-function/serverless-function.resolver';
|
||||||
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 { CodeIntrospectionModule } from 'src/modules/code-introspection/code-introspection.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -20,8 +22,13 @@ import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverles
|
|||||||
FileModule,
|
FileModule,
|
||||||
ThrottlerModule,
|
ThrottlerModule,
|
||||||
AnalyticsModule,
|
AnalyticsModule,
|
||||||
|
CodeIntrospectionModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
ServerlessFunctionService,
|
||||||
|
ServerlessFunctionResolver,
|
||||||
|
ServerlessFunctionPublicationListener,
|
||||||
],
|
],
|
||||||
providers: [ServerlessFunctionService, ServerlessFunctionResolver],
|
|
||||||
exports: [ServerlessFunctionService],
|
exports: [ServerlessFunctionService],
|
||||||
})
|
})
|
||||||
export class ServerlessFunctionModule {}
|
export class ServerlessFunctionModule {}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import { getLastLayerDependencies } from 'src/engine/core-modules/serverless/dri
|
|||||||
import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service';
|
import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service';
|
||||||
import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils';
|
import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils';
|
||||||
import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service';
|
import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service';
|
||||||
|
import { SERVERLESS_FUNCTION_PUBLISHED } from 'src/engine/metadata-modules/serverless-function/constants/serverless-function-published';
|
||||||
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 { 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 {
|
||||||
@ -31,6 +32,7 @@ import {
|
|||||||
ServerlessFunctionException,
|
ServerlessFunctionException,
|
||||||
ServerlessFunctionExceptionCode,
|
ServerlessFunctionExceptionCode,
|
||||||
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
||||||
|
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
||||||
import { isDefined } from 'src/utils/is-defined';
|
import { isDefined } from 'src/utils/is-defined';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -43,6 +45,7 @@ export class ServerlessFunctionService {
|
|||||||
private readonly throttlerService: ThrottlerService,
|
private readonly throttlerService: ThrottlerService,
|
||||||
private readonly environmentService: EnvironmentService,
|
private readonly environmentService: EnvironmentService,
|
||||||
private readonly analyticsService: AnalyticsService,
|
private readonly analyticsService: AnalyticsService,
|
||||||
|
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async findManyServerlessFunctions(where) {
|
async findManyServerlessFunctions(where) {
|
||||||
@ -191,6 +194,17 @@ export class ServerlessFunctionService {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.workspaceEventEmitter.emit(
|
||||||
|
SERVERLESS_FUNCTION_PUBLISHED,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
serverlessFunctionId: existingServerlessFunction.id,
|
||||||
|
serverlessFunctionVersion: newVersion,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
return this.serverlessFunctionRepository.findOneBy({
|
return this.serverlessFunctionRepository.findOneBy({
|
||||||
id: existingServerlessFunction.id,
|
id: existingServerlessFunction.id,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -18,7 +18,7 @@ describe('CodeIntrospectionService', () => {
|
|||||||
expect(service).toBeDefined();
|
expect(service).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('analyze', () => {
|
describe('getFunctionInputSchema', () => {
|
||||||
it('should analyze a function declaration correctly', () => {
|
it('should analyze a function declaration correctly', () => {
|
||||||
const fileContent = `
|
const fileContent = `
|
||||||
function testFunction(param1: string, param2: number): void {
|
function testFunction(param1: string, param2: number): void {
|
||||||
@ -26,7 +26,7 @@ describe('CodeIntrospectionService', () => {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = service.analyze(fileContent);
|
const result = service.getFunctionInputSchema(fileContent);
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{ name: 'param1', type: 'string' },
|
{ name: 'param1', type: 'string' },
|
||||||
@ -41,7 +41,7 @@ describe('CodeIntrospectionService', () => {
|
|||||||
};
|
};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = service.analyze(fileContent);
|
const result = service.getFunctionInputSchema(fileContent);
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{ name: 'param1', type: 'string' },
|
{ name: 'param1', type: 'string' },
|
||||||
@ -55,7 +55,7 @@ describe('CodeIntrospectionService', () => {
|
|||||||
console.log(x);
|
console.log(x);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = service.analyze(fileContent);
|
const result = service.getFunctionInputSchema(fileContent);
|
||||||
|
|
||||||
expect(result).toEqual([]);
|
expect(result).toEqual([]);
|
||||||
});
|
});
|
||||||
@ -66,10 +66,10 @@ describe('CodeIntrospectionService', () => {
|
|||||||
function func2(param2: number) {}
|
function func2(param2: number) {}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
expect(() => service.analyze(fileContent)).toThrow(
|
expect(() => service.getFunctionInputSchema(fileContent)).toThrow(
|
||||||
CodeIntrospectionException,
|
CodeIntrospectionException,
|
||||||
);
|
);
|
||||||
expect(() => service.analyze(fileContent)).toThrow(
|
expect(() => service.getFunctionInputSchema(fileContent)).toThrow(
|
||||||
'Only one function is allowed',
|
'Only one function is allowed',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -80,10 +80,10 @@ describe('CodeIntrospectionService', () => {
|
|||||||
const func2 = (param2: number) => {};
|
const func2 = (param2: number) => {};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
expect(() => service.analyze(fileContent)).toThrow(
|
expect(() => service.getFunctionInputSchema(fileContent)).toThrow(
|
||||||
CodeIntrospectionException,
|
CodeIntrospectionException,
|
||||||
);
|
);
|
||||||
expect(() => service.analyze(fileContent)).toThrow(
|
expect(() => service.getFunctionInputSchema(fileContent)).toThrow(
|
||||||
'Only one arrow function is allowed',
|
'Only one arrow function is allowed',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -95,7 +95,7 @@ describe('CodeIntrospectionService', () => {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = service.analyze(fileContent);
|
const result = service.getFunctionInputSchema(fileContent);
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{ name: 'param1', type: 'string[]' },
|
{ name: 'param1', type: 'string[]' },
|
||||||
|
|||||||
@ -8,16 +8,12 @@ import {
|
|||||||
SyntaxKind,
|
SyntaxKind,
|
||||||
} from 'ts-morph';
|
} from 'ts-morph';
|
||||||
|
|
||||||
|
import { FunctionParameter } from 'src/engine/metadata-modules/serverless-function/dtos/function-parameter.dto';
|
||||||
|
import { generateFakeValue } from 'src/engine/utils/generate-fake-value';
|
||||||
import {
|
import {
|
||||||
CodeIntrospectionException,
|
CodeIntrospectionException,
|
||||||
CodeIntrospectionExceptionCode,
|
CodeIntrospectionExceptionCode,
|
||||||
} from 'src/modules/code-introspection/code-introspection.exception';
|
} from 'src/modules/code-introspection/code-introspection.exception';
|
||||||
import { generateFakeValue } from 'src/engine/utils/generate-fake-value';
|
|
||||||
|
|
||||||
type FunctionParameter = {
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CodeIntrospectionService {
|
export class CodeIntrospectionService {
|
||||||
@ -27,7 +23,13 @@ export class CodeIntrospectionService {
|
|||||||
this.project = new Project();
|
this.project = new Project();
|
||||||
}
|
}
|
||||||
|
|
||||||
public analyze(
|
public generateInputData(fileContent: string, fileName = 'temp.ts') {
|
||||||
|
const parameters = this.getFunctionInputSchema(fileContent, fileName);
|
||||||
|
|
||||||
|
return this.generateFakeDataFromParams(parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getFunctionInputSchema(
|
||||||
fileContent: string,
|
fileContent: string,
|
||||||
fileName = 'temp.ts',
|
fileName = 'temp.ts',
|
||||||
): FunctionParameter[] {
|
): FunctionParameter[] {
|
||||||
@ -38,7 +40,7 @@ export class CodeIntrospectionService {
|
|||||||
const functionDeclarations = sourceFile.getFunctions();
|
const functionDeclarations = sourceFile.getFunctions();
|
||||||
|
|
||||||
if (functionDeclarations.length > 0) {
|
if (functionDeclarations.length > 0) {
|
||||||
return this.analyzeFunctions(functionDeclarations);
|
return this.getFunctionParameters(functionDeclarations);
|
||||||
}
|
}
|
||||||
|
|
||||||
const arrowFunctions = sourceFile.getDescendantsOfKind(
|
const arrowFunctions = sourceFile.getDescendantsOfKind(
|
||||||
@ -46,13 +48,13 @@ export class CodeIntrospectionService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (arrowFunctions.length > 0) {
|
if (arrowFunctions.length > 0) {
|
||||||
return this.analyzeArrowFunctions(arrowFunctions);
|
return this.getArrowFunctionParameters(arrowFunctions);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private analyzeFunctions(
|
private getFunctionParameters(
|
||||||
functionDeclarations: FunctionDeclaration[],
|
functionDeclarations: FunctionDeclaration[],
|
||||||
): FunctionParameter[] {
|
): FunctionParameter[] {
|
||||||
if (functionDeclarations.length > 1) {
|
if (functionDeclarations.length > 1) {
|
||||||
@ -67,7 +69,7 @@ export class CodeIntrospectionService {
|
|||||||
return functionDeclaration.getParameters().map(this.buildFunctionParameter);
|
return functionDeclaration.getParameters().map(this.buildFunctionParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
private analyzeArrowFunctions(
|
private getArrowFunctionParameters(
|
||||||
arrowFunctions: ArrowFunction[],
|
arrowFunctions: ArrowFunction[],
|
||||||
): FunctionParameter[] {
|
): FunctionParameter[] {
|
||||||
if (arrowFunctions.length > 1) {
|
if (arrowFunctions.length > 1) {
|
||||||
@ -91,23 +93,13 @@ export class CodeIntrospectionService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public generateInputData(fileContent: string, fileName = 'temp.ts') {
|
|
||||||
const parameters = this.analyze(fileContent, fileName);
|
|
||||||
|
|
||||||
return this.generateFakeDataFromParams(parameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateFakeDataFromParams(
|
private generateFakeDataFromParams(
|
||||||
params: FunctionParameter[],
|
params: FunctionParameter[],
|
||||||
): Record<string, any> {
|
): Record<string, any> {
|
||||||
const data: Record<string, any> = {};
|
return params.reduce((acc, param) => {
|
||||||
|
acc[param.name] = generateFakeValue(param.type);
|
||||||
|
|
||||||
params.forEach((param) => {
|
return acc;
|
||||||
const type = param.type;
|
}, {});
|
||||||
|
|
||||||
data[param.name] = generateFakeValue(type);
|
|
||||||
});
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,17 +30,21 @@ export class CodeWorkflowAction implements WorkflowAction {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result =
|
try {
|
||||||
await this.serverlessFunctionService.executeOneServerlessFunction(
|
const result =
|
||||||
workflowStepInput.serverlessFunctionId,
|
await this.serverlessFunctionService.executeOneServerlessFunction(
|
||||||
workspaceId,
|
workflowStepInput.serverlessFunctionId,
|
||||||
{}, // TODO: input will be dynamically calculated from function input
|
workspaceId,
|
||||||
);
|
workflowStepInput.serverlessFunctionInput,
|
||||||
|
);
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
return { error: result.error };
|
return { error: result.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { result: result.data || {} };
|
||||||
|
} catch (error) {
|
||||||
|
return { error: error.message };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { result: result.data || {} };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { WorkflowBuilderService } from 'src/modules/workflow/workflow-builder/workflow-builder.service';
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
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 { CodeIntrospectionModule } from 'src/modules/code-introspection/code-introspection.module';
|
import { CodeIntrospectionModule } from 'src/modules/code-introspection/code-introspection.module';
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
import { WorkflowBuilderWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-builder.workspace-service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -12,7 +12,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
|
|||||||
ServerlessFunctionModule,
|
ServerlessFunctionModule,
|
||||||
CodeIntrospectionModule,
|
CodeIntrospectionModule,
|
||||||
],
|
],
|
||||||
providers: [WorkflowBuilderService],
|
providers: [WorkflowBuilderWorkspaceService],
|
||||||
exports: [WorkflowBuilderService],
|
exports: [WorkflowBuilderWorkspaceService],
|
||||||
})
|
})
|
||||||
export class WorkflowBuilderModule {}
|
export class WorkflowBuilderModule {}
|
||||||
|
|||||||
@ -25,7 +25,7 @@ import { generateFakeObjectRecordEvent } from 'src/modules/workflow/workflow-bui
|
|||||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WorkflowBuilderService {
|
export class WorkflowBuilderWorkspaceService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly serverlessFunctionService: ServerlessFunctionService,
|
private readonly serverlessFunctionService: ServerlessFunctionService,
|
||||||
private readonly codeIntrospectionService: CodeIntrospectionService,
|
private readonly codeIntrospectionService: CodeIntrospectionService,
|
||||||
@ -1,4 +1,5 @@
|
|||||||
export type OutputSchema = object;
|
export type OutputSchema = object;
|
||||||
|
export type InputSchema = object;
|
||||||
|
|
||||||
type BaseWorkflowStepSettings = {
|
type BaseWorkflowStepSettings = {
|
||||||
input: object;
|
input: object;
|
||||||
@ -16,7 +17,9 @@ type BaseWorkflowStepSettings = {
|
|||||||
export type WorkflowCodeStepInput = {
|
export type WorkflowCodeStepInput = {
|
||||||
serverlessFunctionId: string;
|
serverlessFunctionId: string;
|
||||||
serverlessFunctionVersion: string;
|
serverlessFunctionVersion: string;
|
||||||
payloadInput: object;
|
serverlessFunctionInput: {
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkflowCodeStepSettings = BaseWorkflowStepSettings & {
|
export type WorkflowCodeStepSettings = BaseWorkflowStepSettings & {
|
||||||
|
|||||||
@ -33,6 +33,7 @@ export class WorkflowRunWorkspaceService {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
await workflowRunRepository.save({
|
await workflowRunRepository.save({
|
||||||
|
name: `Execution of ${workflowVersion.name}`,
|
||||||
workflowVersionId,
|
workflowVersionId,
|
||||||
createdBy,
|
createdBy,
|
||||||
workflowId: workflowVersion.workflowId,
|
workflowId: workflowVersion.workflowId,
|
||||||
|
|||||||
Reference in New Issue
Block a user