Skip to content

Commit

Permalink
Infer function input in workflow step (#8308)
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
thomtrp authored Nov 5, 2024
1 parent d1531aa commit be8141c
Show file tree
Hide file tree
Showing 29 changed files with 335 additions and 91 deletions.
4 changes: 2 additions & 2 deletions packages/twenty-front/src/generated-metadata/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 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 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 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,
Expand Down Expand Up @@ -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.
*/
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.
*/
Expand Down
36 changes: 22 additions & 14 deletions packages/twenty-front/src/generated-metadata/graphql.ts

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions packages/twenty-front/src/generated/graphql.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,12 @@ export type FullName = {
lastName: Scalars['String'];
};

export type FunctionParameter = {
__typename?: 'FunctionParameter';
name: Scalars['String'];
type: Scalars['String'];
};

export type GenerateJwt = GenerateJwtOutputWithAuthTokens | GenerateJwtOutputWithSsoauth;

export type GenerateJwtOutputWithAuthTokens = {
Expand Down Expand Up @@ -967,6 +973,7 @@ export type ServerlessFunction = {
description?: Maybe<Scalars['String']>;
id: Scalars['UUID'];
latestVersion?: Maybe<Scalars['String']>;
latestVersionInputSchema?: Maybe<Array<FunctionParameter>>;
name: Scalars['String'];
publishedVersions: Array<Scalars['String']>;
runtime: Scalars['String'];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export const SERVERLESS_FUNCTION_FRAGMENT = gql`
runtime
syncStatus
latestVersion
latestVersionInputSchema {
name
type
}
publishedVersions
createdAt
updatedAt
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { useGetManyServerlessFunctions } from '@/settings/serverless-functions/hooks/useGetManyServerlessFunctions';
import { Select, SelectOption } from '@/ui/input/components/Select';
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
import VariableTagInput from '@/workflow/search-variables/components/VariableTagInput';
import { WorkflowCodeStep } from '@/workflow/types/Workflow';
import { useTheme } from '@emotion/react';
import { useState } from 'react';
import { IconCode, isDefined } from 'twenty-ui';
import { useDebouncedCallback } from 'use-debounce';
import { capitalize } from '~/utils/string/capitalize';

type WorkflowEditActionFormServerlessFunctionProps =
| {
Expand All @@ -23,6 +27,48 @@ export const WorkflowEditActionFormServerlessFunction = (

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>> = [
{ label: 'None', value: '' },
...serverlessFunctions
Expand All @@ -32,41 +78,68 @@ export const WorkflowEditActionFormServerlessFunction = (
.map((serverlessFunction) => ({
label: serverlessFunction.name,
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 (
<WorkflowEditGenericFormBase
HeaderIcon={<IconCode color={theme.color.orange} />}
headerTitle="Code - Serverless Function"
headerType="Code"
>
<Select
dropdownId="workflow-edit-action-function"
dropdownId="select-serverless-function-id"
label="Function"
fullWidth
value={props.action.settings.input.serverlessFunctionId}
value={serverlessFunctionId}
options={availableFunctions}
disabled={props.readonly}
onChange={(serverlessFunctionId) => {
if (props.readonly === true) {
return;
}

props.onActionUpdate({
...props.action,
settings: {
...props.action.settings,
input: {
serverlessFunctionId,
serverlessFunctionVersion:
serverlessFunctions.find((f) => f.id === serverlessFunctionId)
?.latestVersion || 'latest',
},
},
});
}}
onChange={handleFunctionChange}
/>
{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>
);
};
3 changes: 3 additions & 0 deletions packages/twenty-front/src/modules/workflow/types/Workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ export type WorkflowCodeStepSettings = BaseWorkflowStepSettings & {
input: {
serverlessFunctionId: string;
serverlessFunctionVersion: string;
serverlessFunctionInput: {
[hello: string]: any;
};
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe('addCreateStepNodes', () => {
input: {
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
serverlessFunctionVersion: '1',
serverlessFunctionInput: {},
},
outputSchema: {},
},
Expand All @@ -42,6 +43,7 @@ describe('addCreateStepNodes', () => {
input: {
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
serverlessFunctionVersion: '1',
serverlessFunctionInput: {},
},
outputSchema: {},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ describe('generateWorkflowDiagram', () => {
input: {
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
serverlessFunctionVersion: '1',
serverlessFunctionInput: {},
},
outputSchema: {},
},
Expand All @@ -64,6 +65,7 @@ describe('generateWorkflowDiagram', () => {
input: {
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
serverlessFunctionVersion: '1',
serverlessFunctionInput: {},
},
outputSchema: {},
},
Expand Down Expand Up @@ -110,6 +112,7 @@ describe('generateWorkflowDiagram', () => {
input: {
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
serverlessFunctionVersion: '1',
serverlessFunctionInput: {},
},
outputSchema: {},
},
Expand All @@ -127,6 +130,7 @@ describe('generateWorkflowDiagram', () => {
input: {
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
serverlessFunctionVersion: '1',
serverlessFunctionInput: {},
},
outputSchema: {},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ describe('getWorkflowVersionDiagram', () => {
input: {
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
serverlessFunctionVersion: '1',
serverlessFunctionInput: {},
},
outputSchema: {},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ describe('insertStep', () => {
input: {
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
serverlessFunctionVersion: '1',
serverlessFunctionInput: {},
},
outputSchema: {},
},
Expand Down Expand Up @@ -70,6 +71,7 @@ describe('insertStep', () => {
input: {
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
serverlessFunctionVersion: '1',
serverlessFunctionInput: {},
},
outputSchema: {},
},
Expand Down Expand Up @@ -106,6 +108,7 @@ describe('insertStep', () => {
input: {
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
serverlessFunctionVersion: '1',
serverlessFunctionInput: {},
},
outputSchema: {},
},
Expand All @@ -123,6 +126,7 @@ describe('insertStep', () => {
input: {
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
serverlessFunctionVersion: '1',
serverlessFunctionInput: {},
},
outputSchema: {},
},
Expand All @@ -148,6 +152,7 @@ describe('insertStep', () => {
input: {
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
serverlessFunctionVersion: '1',
serverlessFunctionInput: {},
},
outputSchema: {},
},
Expand Down Expand Up @@ -188,6 +193,7 @@ describe('insertStep', () => {
input: {
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
serverlessFunctionVersion: '1',
serverlessFunctionInput: {},
},
outputSchema: {},
},
Expand All @@ -205,6 +211,7 @@ describe('insertStep', () => {
input: {
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
serverlessFunctionVersion: '1',
serverlessFunctionInput: {},
},
outputSchema: {},
},
Expand All @@ -230,6 +237,7 @@ describe('insertStep', () => {
input: {
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
serverlessFunctionVersion: '1',
serverlessFunctionInput: {},
},
outputSchema: {},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ it('returns a deep copy of the provided steps array instead of mutating it', ()
input: {
serverlessFunctionId: 'first',
serverlessFunctionVersion: '1',
serverlessFunctionInput: {},
},
outputSchema: {},
},
Expand Down Expand Up @@ -54,6 +55,7 @@ it('removes a step in a non-empty steps array', () => {
input: {
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
serverlessFunctionVersion: '1',
serverlessFunctionInput: {},
},
outputSchema: {},
},
Expand All @@ -78,6 +80,7 @@ it('removes a step in a non-empty steps array', () => {
input: {
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
serverlessFunctionVersion: '1',
serverlessFunctionInput: {},
},
outputSchema: {},
},
Expand All @@ -96,6 +99,7 @@ it('removes a step in a non-empty steps array', () => {
input: {
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
serverlessFunctionVersion: '1',
serverlessFunctionInput: {},
},
outputSchema: {},
},
Expand Down
Loading

0 comments on commit be8141c

Please sign in to comment.