Refactor workflow-logic-function-interaction (#17699)

# Refactor workflow–logic function interaction

## Why

Workflow code steps and standalone logic functions shared the same build
layer and DB layer, which blurred two use cases: code steps belong to a
workflow version; standalone functions are deployable units. That made
workflow code steps harder to own and evolve.

## Goal

Treat code steps as **workflow-owned**: build and run them in workflow
context, and expose workflow-scoped APIs so the editor can load, test,
and save code step source without going through the generic
logic-function layer.
This commit is contained in:
Charles Bochet
2026-02-05 01:46:55 +01:00
committed by GitHub
parent 752359335e
commit 67a98f77e3
77 changed files with 1750 additions and 1336 deletions
@@ -18,6 +18,7 @@ module.exports = {
'./src/modules/billing/graphql/**/*.{ts,tsx}',
'./src/modules/settings/**/graphql/**/*.{ts,tsx}',
'./src/modules/logic-functions/graphql/**/*.{ts,tsx}',
'./src/modules/databases/graphql/**/*.{ts,tsx}',
'./src/modules/workflow/**/graphql/**/*.{ts,tsx}',
@@ -1447,7 +1447,9 @@ export type EventWithQueryIds = {
queryIds: Array<Scalars['String']>;
};
export type ExecuteLogicFunctionInput = {
export type ExecuteOneLogicFunctionInput = {
/** Force rebuild from source before executing */
forceRebuild?: InputMaybe<Scalars['Boolean']>;
/** Id of the logic function to execute */
id: Scalars['UUID'];
/** Payload in JSON format */
@@ -2221,11 +2223,11 @@ export type Mutation = {
updateDatabaseConfigVariable: Scalars['Boolean'];
updateFrontComponent: FrontComponent;
updateLabPublicFeatureFlag: FeatureFlagDto;
updateLogicFunctionSource: Scalars['Boolean'];
updateNavigationMenuItem: NavigationMenuItem;
updateOneAgent: Agent;
updateOneApplicationVariable: Scalars['Boolean'];
updateOneField: Field;
updateOneLogicFunction: LogicFunction;
updateOneObject: Object;
updateOneRole: Role;
updatePageLayout: PageLayout;
@@ -2732,7 +2734,7 @@ export type MutationEvaluateAgentTurnArgs = {
export type MutationExecuteOneLogicFunctionArgs = {
input: ExecuteLogicFunctionInput;
input: ExecuteOneLogicFunctionInput;
};
@@ -2981,6 +2983,11 @@ export type MutationUpdateLabPublicFeatureFlagArgs = {
};
export type MutationUpdateLogicFunctionSourceArgs = {
input: UpdateLogicFunctionSourceInput;
};
export type MutationUpdateNavigationMenuItemArgs = {
input: UpdateOneNavigationMenuItemInput;
};
@@ -3003,11 +3010,6 @@ export type MutationUpdateOneFieldArgs = {
};
export type MutationUpdateOneLogicFunctionArgs = {
input: UpdateLogicFunctionInput;
};
export type MutationUpdateOneObjectArgs = {
input: UpdateOneObjectInput;
};
@@ -4699,23 +4701,11 @@ export type UpdateLabPublicFeatureFlagInput = {
value: Scalars['Boolean'];
};
export type UpdateLogicFunctionInput = {
/** Id of the logic function to update */
id: Scalars['UUID'];
/** The logic function updates */
update: UpdateLogicFunctionInputUpdates;
};
export type UpdateLogicFunctionInputUpdates = {
builtHandlerPath?: InputMaybe<Scalars['String']>;
export type UpdateLogicFunctionSourceInput = {
/** The source code (Sources) to write. Only updates source files. */
code: Scalars['JSON'];
description?: InputMaybe<Scalars['String']>;
handlerName?: InputMaybe<Scalars['String']>;
isTool?: InputMaybe<Scalars['Boolean']>;
name: Scalars['String'];
sourceHandlerPath?: InputMaybe<Scalars['String']>;
timeoutSeconds?: InputMaybe<Scalars['Float']>;
toolInputSchema?: InputMaybe<Scalars['JSON']>;
/** The id of the logic function. */
id: Scalars['UUID'];
};
export type UpdateNavigationMenuItemInput = {
@@ -5994,6 +5984,27 @@ export type UploadFilesFieldFileMutationVariables = Exact<{
export type UploadFilesFieldFileMutation = { __typename?: 'Mutation', uploadFilesFieldFile: { __typename?: 'File', id: string, path: string, size: number, createdAt: string } };
export type ExecuteOneLogicFunctionMutationVariables = Exact<{
input: ExecuteOneLogicFunctionInput;
}>;
export type ExecuteOneLogicFunctionMutation = { __typename?: 'Mutation', executeOneLogicFunction: { __typename?: 'LogicFunctionExecutionResult', data?: any | null, logs: string, duration: number, status: LogicFunctionExecutionStatus, error?: any | null } };
export type UpdateLogicFunctionSourceMutationVariables = Exact<{
input: UpdateLogicFunctionSourceInput;
}>;
export type UpdateLogicFunctionSourceMutation = { __typename?: 'Mutation', updateLogicFunctionSource: boolean };
export type GetLogicFunctionSourceCodeQueryVariables = Exact<{
input: GetLogicFunctionSourceCodeInput;
}>;
export type GetLogicFunctionSourceCodeQuery = { __typename?: 'Query', getLogicFunctionSourceCode?: any | null };
export type NavigationMenuItemFieldsFragment = { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string };
export type NavigationMenuItemQueryFieldsFragment = { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string, targetRecordIdentifier?: { __typename?: 'RecordIdentifier', id: string, labelIdentifier: string, imageIdentifier?: string | null } | null };
@@ -6400,20 +6411,6 @@ export type DeleteOneLogicFunctionMutationVariables = Exact<{
export type DeleteOneLogicFunctionMutation = { __typename?: 'Mutation', deleteOneLogicFunction: { __typename?: 'LogicFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, sourceHandlerPath: string, builtHandlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string } };
export type ExecuteOneLogicFunctionMutationVariables = Exact<{
input: ExecuteLogicFunctionInput;
}>;
export type ExecuteOneLogicFunctionMutation = { __typename?: 'Mutation', executeOneLogicFunction: { __typename?: 'LogicFunctionExecutionResult', data?: any | null, logs: string, duration: number, status: LogicFunctionExecutionStatus, error?: any | null } };
export type UpdateOneLogicFunctionMutationVariables = Exact<{
input: UpdateLogicFunctionInput;
}>;
export type UpdateOneLogicFunctionMutation = { __typename?: 'Mutation', updateOneLogicFunction: { __typename?: 'LogicFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, sourceHandlerPath: string, builtHandlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string } };
export type FindManyAvailablePackagesQueryVariables = Exact<{
input: LogicFunctionIdInput;
}>;
@@ -6433,13 +6430,6 @@ export type GetOneLogicFunctionQueryVariables = Exact<{
export type GetOneLogicFunctionQuery = { __typename?: 'Query', findOneLogicFunction: { __typename?: 'LogicFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, sourceHandlerPath: string, builtHandlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string } };
export type FindOneLogicFunctionSourceCodeQueryVariables = Exact<{
input: GetLogicFunctionSourceCodeInput;
}>;
export type FindOneLogicFunctionSourceCodeQuery = { __typename?: 'Query', getLogicFunctionSourceCode?: any | null };
export type UploadWorkspaceMemberProfilePictureMutationVariables = Exact<{
file: Scalars['Upload'];
}>;
@@ -10371,6 +10361,107 @@ export function useUploadFilesFieldFileMutation(baseOptions?: Apollo.MutationHoo
export type UploadFilesFieldFileMutationHookResult = ReturnType<typeof useUploadFilesFieldFileMutation>;
export type UploadFilesFieldFileMutationResult = Apollo.MutationResult<UploadFilesFieldFileMutation>;
export type UploadFilesFieldFileMutationOptions = Apollo.BaseMutationOptions<UploadFilesFieldFileMutation, UploadFilesFieldFileMutationVariables>;
export const ExecuteOneLogicFunctionDocument = gql`
mutation ExecuteOneLogicFunction($input: ExecuteOneLogicFunctionInput!) {
executeOneLogicFunction(input: $input) {
data
logs
duration
status
error
}
}
`;
export type ExecuteOneLogicFunctionMutationFn = Apollo.MutationFunction<ExecuteOneLogicFunctionMutation, ExecuteOneLogicFunctionMutationVariables>;
/**
* __useExecuteOneLogicFunctionMutation__
*
* To run a mutation, you first call `useExecuteOneLogicFunctionMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useExecuteOneLogicFunctionMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [executeOneLogicFunctionMutation, { data, loading, error }] = useExecuteOneLogicFunctionMutation({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useExecuteOneLogicFunctionMutation(baseOptions?: Apollo.MutationHookOptions<ExecuteOneLogicFunctionMutation, ExecuteOneLogicFunctionMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<ExecuteOneLogicFunctionMutation, ExecuteOneLogicFunctionMutationVariables>(ExecuteOneLogicFunctionDocument, options);
}
export type ExecuteOneLogicFunctionMutationHookResult = ReturnType<typeof useExecuteOneLogicFunctionMutation>;
export type ExecuteOneLogicFunctionMutationResult = Apollo.MutationResult<ExecuteOneLogicFunctionMutation>;
export type ExecuteOneLogicFunctionMutationOptions = Apollo.BaseMutationOptions<ExecuteOneLogicFunctionMutation, ExecuteOneLogicFunctionMutationVariables>;
export const UpdateLogicFunctionSourceDocument = gql`
mutation UpdateLogicFunctionSource($input: UpdateLogicFunctionSourceInput!) {
updateLogicFunctionSource(input: $input)
}
`;
export type UpdateLogicFunctionSourceMutationFn = Apollo.MutationFunction<UpdateLogicFunctionSourceMutation, UpdateLogicFunctionSourceMutationVariables>;
/**
* __useUpdateLogicFunctionSourceMutation__
*
* To run a mutation, you first call `useUpdateLogicFunctionSourceMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateLogicFunctionSourceMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [updateLogicFunctionSourceMutation, { data, loading, error }] = useUpdateLogicFunctionSourceMutation({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useUpdateLogicFunctionSourceMutation(baseOptions?: Apollo.MutationHookOptions<UpdateLogicFunctionSourceMutation, UpdateLogicFunctionSourceMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UpdateLogicFunctionSourceMutation, UpdateLogicFunctionSourceMutationVariables>(UpdateLogicFunctionSourceDocument, options);
}
export type UpdateLogicFunctionSourceMutationHookResult = ReturnType<typeof useUpdateLogicFunctionSourceMutation>;
export type UpdateLogicFunctionSourceMutationResult = Apollo.MutationResult<UpdateLogicFunctionSourceMutation>;
export type UpdateLogicFunctionSourceMutationOptions = Apollo.BaseMutationOptions<UpdateLogicFunctionSourceMutation, UpdateLogicFunctionSourceMutationVariables>;
export const GetLogicFunctionSourceCodeDocument = gql`
query GetLogicFunctionSourceCode($input: GetLogicFunctionSourceCodeInput!) {
getLogicFunctionSourceCode(input: $input)
}
`;
/**
* __useGetLogicFunctionSourceCodeQuery__
*
* To run a query within a React component, call `useGetLogicFunctionSourceCodeQuery` and pass it any options that fit your needs.
* When your component renders, `useGetLogicFunctionSourceCodeQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetLogicFunctionSourceCodeQuery({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useGetLogicFunctionSourceCodeQuery(baseOptions: Apollo.QueryHookOptions<GetLogicFunctionSourceCodeQuery, GetLogicFunctionSourceCodeQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetLogicFunctionSourceCodeQuery, GetLogicFunctionSourceCodeQueryVariables>(GetLogicFunctionSourceCodeDocument, options);
}
export function useGetLogicFunctionSourceCodeLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetLogicFunctionSourceCodeQuery, GetLogicFunctionSourceCodeQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetLogicFunctionSourceCodeQuery, GetLogicFunctionSourceCodeQueryVariables>(GetLogicFunctionSourceCodeDocument, options);
}
export type GetLogicFunctionSourceCodeQueryHookResult = ReturnType<typeof useGetLogicFunctionSourceCodeQuery>;
export type GetLogicFunctionSourceCodeLazyQueryHookResult = ReturnType<typeof useGetLogicFunctionSourceCodeLazyQuery>;
export type GetLogicFunctionSourceCodeQueryResult = Apollo.QueryResult<GetLogicFunctionSourceCodeQuery, GetLogicFunctionSourceCodeQueryVariables>;
export const CreateNavigationMenuItemDocument = gql`
mutation CreateNavigationMenuItem($input: CreateNavigationMenuItemInput!) {
createNavigationMenuItem(input: $input) {
@@ -12617,76 +12708,6 @@ export function useDeleteOneLogicFunctionMutation(baseOptions?: Apollo.MutationH
export type DeleteOneLogicFunctionMutationHookResult = ReturnType<typeof useDeleteOneLogicFunctionMutation>;
export type DeleteOneLogicFunctionMutationResult = Apollo.MutationResult<DeleteOneLogicFunctionMutation>;
export type DeleteOneLogicFunctionMutationOptions = Apollo.BaseMutationOptions<DeleteOneLogicFunctionMutation, DeleteOneLogicFunctionMutationVariables>;
export const ExecuteOneLogicFunctionDocument = gql`
mutation ExecuteOneLogicFunction($input: ExecuteLogicFunctionInput!) {
executeOneLogicFunction(input: $input) {
data
logs
duration
status
error
}
}
`;
export type ExecuteOneLogicFunctionMutationFn = Apollo.MutationFunction<ExecuteOneLogicFunctionMutation, ExecuteOneLogicFunctionMutationVariables>;
/**
* __useExecuteOneLogicFunctionMutation__
*
* To run a mutation, you first call `useExecuteOneLogicFunctionMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useExecuteOneLogicFunctionMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [executeOneLogicFunctionMutation, { data, loading, error }] = useExecuteOneLogicFunctionMutation({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useExecuteOneLogicFunctionMutation(baseOptions?: Apollo.MutationHookOptions<ExecuteOneLogicFunctionMutation, ExecuteOneLogicFunctionMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<ExecuteOneLogicFunctionMutation, ExecuteOneLogicFunctionMutationVariables>(ExecuteOneLogicFunctionDocument, options);
}
export type ExecuteOneLogicFunctionMutationHookResult = ReturnType<typeof useExecuteOneLogicFunctionMutation>;
export type ExecuteOneLogicFunctionMutationResult = Apollo.MutationResult<ExecuteOneLogicFunctionMutation>;
export type ExecuteOneLogicFunctionMutationOptions = Apollo.BaseMutationOptions<ExecuteOneLogicFunctionMutation, ExecuteOneLogicFunctionMutationVariables>;
export const UpdateOneLogicFunctionDocument = gql`
mutation UpdateOneLogicFunction($input: UpdateLogicFunctionInput!) {
updateOneLogicFunction(input: $input) {
...LogicFunctionFields
}
}
${LogicFunctionFieldsFragmentDoc}`;
export type UpdateOneLogicFunctionMutationFn = Apollo.MutationFunction<UpdateOneLogicFunctionMutation, UpdateOneLogicFunctionMutationVariables>;
/**
* __useUpdateOneLogicFunctionMutation__
*
* To run a mutation, you first call `useUpdateOneLogicFunctionMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateOneLogicFunctionMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [updateOneLogicFunctionMutation, { data, loading, error }] = useUpdateOneLogicFunctionMutation({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useUpdateOneLogicFunctionMutation(baseOptions?: Apollo.MutationHookOptions<UpdateOneLogicFunctionMutation, UpdateOneLogicFunctionMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UpdateOneLogicFunctionMutation, UpdateOneLogicFunctionMutationVariables>(UpdateOneLogicFunctionDocument, options);
}
export type UpdateOneLogicFunctionMutationHookResult = ReturnType<typeof useUpdateOneLogicFunctionMutation>;
export type UpdateOneLogicFunctionMutationResult = Apollo.MutationResult<UpdateOneLogicFunctionMutation>;
export type UpdateOneLogicFunctionMutationOptions = Apollo.BaseMutationOptions<UpdateOneLogicFunctionMutation, UpdateOneLogicFunctionMutationVariables>;
export const FindManyAvailablePackagesDocument = gql`
query FindManyAvailablePackages($input: LogicFunctionIdInput!) {
getAvailablePackages(input: $input)
@@ -12789,39 +12810,6 @@ export function useGetOneLogicFunctionLazyQuery(baseOptions?: Apollo.LazyQueryHo
export type GetOneLogicFunctionQueryHookResult = ReturnType<typeof useGetOneLogicFunctionQuery>;
export type GetOneLogicFunctionLazyQueryHookResult = ReturnType<typeof useGetOneLogicFunctionLazyQuery>;
export type GetOneLogicFunctionQueryResult = Apollo.QueryResult<GetOneLogicFunctionQuery, GetOneLogicFunctionQueryVariables>;
export const FindOneLogicFunctionSourceCodeDocument = gql`
query FindOneLogicFunctionSourceCode($input: GetLogicFunctionSourceCodeInput!) {
getLogicFunctionSourceCode(input: $input)
}
`;
/**
* __useFindOneLogicFunctionSourceCodeQuery__
*
* To run a query within a React component, call `useFindOneLogicFunctionSourceCodeQuery` and pass it any options that fit your needs.
* When your component renders, `useFindOneLogicFunctionSourceCodeQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useFindOneLogicFunctionSourceCodeQuery({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useFindOneLogicFunctionSourceCodeQuery(baseOptions: Apollo.QueryHookOptions<FindOneLogicFunctionSourceCodeQuery, FindOneLogicFunctionSourceCodeQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<FindOneLogicFunctionSourceCodeQuery, FindOneLogicFunctionSourceCodeQueryVariables>(FindOneLogicFunctionSourceCodeDocument, options);
}
export function useFindOneLogicFunctionSourceCodeLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<FindOneLogicFunctionSourceCodeQuery, FindOneLogicFunctionSourceCodeQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<FindOneLogicFunctionSourceCodeQuery, FindOneLogicFunctionSourceCodeQueryVariables>(FindOneLogicFunctionSourceCodeDocument, options);
}
export type FindOneLogicFunctionSourceCodeQueryHookResult = ReturnType<typeof useFindOneLogicFunctionSourceCodeQuery>;
export type FindOneLogicFunctionSourceCodeLazyQueryHookResult = ReturnType<typeof useFindOneLogicFunctionSourceCodeLazyQuery>;
export type FindOneLogicFunctionSourceCodeQueryResult = Apollo.QueryResult<FindOneLogicFunctionSourceCodeQuery, FindOneLogicFunctionSourceCodeQueryVariables>;
export const UploadWorkspaceMemberProfilePictureDocument = gql`
mutation UploadWorkspaceMemberProfilePicture($file: Upload!) {
uploadWorkspaceMemberProfilePicture(file: $file) {
+14 -24
View File
@@ -1419,7 +1419,9 @@ export type EventWithQueryIds = {
queryIds: Array<Scalars['String']>;
};
export type ExecuteLogicFunctionInput = {
export type ExecuteOneLogicFunctionInput = {
/** Force rebuild from source before executing */
forceRebuild?: InputMaybe<Scalars['Boolean']>;
/** Id of the logic function to execute */
id: Scalars['UUID'];
/** Payload in JSON format */
@@ -2184,10 +2186,10 @@ export type Mutation = {
updateDatabaseConfigVariable: Scalars['Boolean'];
updateFrontComponent: FrontComponent;
updateLabPublicFeatureFlag: FeatureFlagDto;
updateLogicFunctionSource: Scalars['Boolean'];
updateOneAgent: Agent;
updateOneApplicationVariable: Scalars['Boolean'];
updateOneField: Field;
updateOneLogicFunction: LogicFunction;
updateOneObject: Object;
updateOneRole: Role;
updatePageLayout: PageLayout;
@@ -2653,7 +2655,7 @@ export type MutationEmailPasswordResetLinkArgs = {
export type MutationExecuteOneLogicFunctionArgs = {
input: ExecuteLogicFunctionInput;
input: ExecuteOneLogicFunctionInput;
};
@@ -2896,6 +2898,11 @@ export type MutationUpdateLabPublicFeatureFlagArgs = {
};
export type MutationUpdateLogicFunctionSourceArgs = {
input: UpdateLogicFunctionSourceInput;
};
export type MutationUpdateOneAgentArgs = {
input: UpdateAgentInput;
};
@@ -2913,11 +2920,6 @@ export type MutationUpdateOneFieldArgs = {
};
export type MutationUpdateOneLogicFunctionArgs = {
input: UpdateLogicFunctionInput;
};
export type MutationUpdateOneObjectArgs = {
input: UpdateOneObjectInput;
};
@@ -4538,23 +4540,11 @@ export type UpdateLabPublicFeatureFlagInput = {
value: Scalars['Boolean'];
};
export type UpdateLogicFunctionInput = {
/** Id of the logic function to update */
id: Scalars['UUID'];
/** The logic function updates */
update: UpdateLogicFunctionInputUpdates;
};
export type UpdateLogicFunctionInputUpdates = {
builtHandlerPath?: InputMaybe<Scalars['String']>;
export type UpdateLogicFunctionSourceInput = {
/** The source code (Sources) to write. Only updates source files. */
code: Scalars['JSON'];
description?: InputMaybe<Scalars['String']>;
handlerName?: InputMaybe<Scalars['String']>;
isTool?: InputMaybe<Scalars['Boolean']>;
name: Scalars['String'];
sourceHandlerPath?: InputMaybe<Scalars['String']>;
timeoutSeconds?: InputMaybe<Scalars['Float']>;
toolInputSchema?: InputMaybe<Scalars['JSON']>;
/** The id of the logic function. */
id: Scalars['UUID'];
};
export type UpdateObjectPayload = {
@@ -1,7 +1,7 @@
import { gql } from '@apollo/client';
export const EXECUTE_ONE_LOGIC_FUNCTION = gql`
mutation ExecuteOneLogicFunction($input: ExecuteLogicFunctionInput!) {
mutation ExecuteOneLogicFunction($input: ExecuteOneLogicFunctionInput!) {
executeOneLogicFunction(input: $input) {
data
logs
@@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const UPDATE_LOGIC_FUNCTION_SOURCE = gql`
mutation UpdateLogicFunctionSource($input: UpdateLogicFunctionSourceInput!) {
updateLogicFunctionSource(input: $input)
}
`;
@@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const GET_LOGIC_FUNCTION_SOURCE_CODE = gql`
query GetLogicFunctionSourceCode($input: GetLogicFunctionSourceCodeInput!) {
getLogicFunctionSourceCode(input: $input)
}
`;
@@ -0,0 +1,97 @@
import { EXECUTE_ONE_LOGIC_FUNCTION } from '@/logic-functions/graphql/mutations/executeOneLogicFunction';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { logicFunctionTestDataFamilyState } from '@/workflow/workflow-steps/workflow-actions/code-action/states/logicFunctionTestDataFamilyState';
import { useMutation } from '@apollo/client';
import { useState } from 'react';
import { useRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { LogicFunctionExecutionStatus } from '~/generated-metadata/graphql';
import { sleep } from '~/utils/sleep';
type ExecuteOneLogicFunctionInput = {
id: string;
payload: object;
forceRebuild?: boolean;
};
type ExecuteOneLogicFunctionResult = {
data?: object | null;
logs: string;
duration: number;
status: LogicFunctionExecutionStatus;
error?: {
errorType: string;
errorMessage: string;
stackTrace: string;
} | null;
};
export const useExecuteLogicFunction = ({
logicFunctionId,
callback,
}: {
logicFunctionId: string;
callback?: (result: object) => void;
}) => {
const [isExecuting, setIsExecuting] = useState(false);
const apolloMetadataClient = useApolloCoreClient();
const [executeOneLogicFunctionMutation] = useMutation<
{ executeOneLogicFunction: ExecuteOneLogicFunctionResult },
{ input: ExecuteOneLogicFunctionInput }
>(EXECUTE_ONE_LOGIC_FUNCTION, {
client: apolloMetadataClient,
});
const [logicFunctionTestData, setLogicFunctionTestData] = useRecoilState(
logicFunctionTestDataFamilyState(logicFunctionId),
);
const executeLogicFunction = async ({
forceRebuild = false,
}: { forceRebuild?: boolean } = {}) => {
try {
setIsExecuting(true);
await sleep(200); // Delay artificially to avoid flashing the UI
const result = await executeOneLogicFunctionMutation({
variables: {
input: {
id: logicFunctionId,
payload: logicFunctionTestData.input,
forceRebuild,
},
},
});
setIsExecuting(false);
const executionResult = result?.data?.executeOneLogicFunction;
if (isDefined(executionResult?.data)) {
callback?.(executionResult.data);
}
setLogicFunctionTestData((prev) => ({
...prev,
language: 'json',
height: 300,
output: {
data: executionResult?.data
? JSON.stringify(executionResult.data, null, 4)
: undefined,
logs: executionResult?.logs || '',
duration: executionResult?.duration,
status: (executionResult?.status ??
LogicFunctionExecutionStatus.IDLE) as LogicFunctionExecutionStatus,
error: executionResult?.error
? JSON.stringify(executionResult.error, null, 4)
: undefined,
},
}));
} catch (error) {
setIsExecuting(false);
throw error;
}
};
return { executeLogicFunction, isExecuting };
};
@@ -0,0 +1,34 @@
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { GET_LOGIC_FUNCTION_SOURCE_CODE } from '@/logic-functions/graphql/queries/getLogicFunctionSourceCode';
import { useQuery } from '@apollo/client';
import { type Sources } from 'twenty-shared/types';
import {
type GetLogicFunctionSourceCodeQuery,
type GetLogicFunctionSourceCodeQueryVariables,
} from '~/generated-metadata/graphql';
export const useGetLogicFunctionSourceCode = ({
logicFunctionId,
}: {
logicFunctionId: string;
}): { code: Sources | null; loading: boolean } => {
const apolloMetadataClient = useApolloCoreClient();
const { data, loading } = useQuery<
GetLogicFunctionSourceCodeQuery,
GetLogicFunctionSourceCodeQueryVariables
>(GET_LOGIC_FUNCTION_SOURCE_CODE, {
client: apolloMetadataClient ?? undefined,
variables: {
input: { id: logicFunctionId },
},
skip: !logicFunctionId,
});
const raw = data?.getLogicFunctionSourceCode;
const code =
raw != null && typeof raw === 'object' && !Array.isArray(raw)
? (raw as Sources)
: null;
return { code, loading };
};
@@ -1,27 +1,29 @@
import { useCallback } from 'react';
import { UPDATE_LOGIC_FUNCTION_SOURCE } from '@/logic-functions/graphql/mutations/updateLogicFunctionSource';
import { GET_LOGIC_FUNCTION_SOURCE_CODE } from '@/logic-functions/graphql/queries/getLogicFunctionSourceCode';
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type';
import { CREATE_ONE_LOGIC_FUNCTION } from '@/settings/logic-functions/graphql/mutations/createOneLogicFunction';
import { DELETE_ONE_LOGIC_FUNCTION } from '@/settings/logic-functions/graphql/mutations/deleteOneLogicFunction';
import { UPDATE_ONE_LOGIC_FUNCTION } from '@/settings/logic-functions/graphql/mutations/updateOneLogicFunction';
import { FIND_MANY_LOGIC_FUNCTIONS } from '@/settings/logic-functions/graphql/queries/findManyLogicFunctions';
import { FIND_ONE_LOGIC_FUNCTION_SOURCE_CODE } from '@/settings/logic-functions/graphql/queries/findOneLogicFunctionSourceCode';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError, useMutation } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import { t } from '@lingui/core/macro';
import { CrudOperationType } from 'twenty-shared/types';
import { type Sources, CrudOperationType } from 'twenty-shared/types';
import {
type CreateOneLogicFunctionItemMutation,
type CreateOneLogicFunctionItemMutationVariables,
type DeleteOneLogicFunctionMutation,
type DeleteOneLogicFunctionMutationVariables,
type UpdateOneLogicFunctionMutation,
type UpdateOneLogicFunctionMutationVariables,
} from '~/generated-metadata/graphql';
type UpdateLogicFunctionSourceMutationVariables = {
input: { id: string; code: Sources };
};
export const usePersistLogicFunction = () => {
const apolloMetadataClient = useApolloCoreClient();
const { handleMetadataError } = useMetadataErrorHandler();
@@ -34,13 +36,6 @@ export const usePersistLogicFunction = () => {
client: apolloMetadataClient,
});
const [updateLogicFunctionMutation] = useMutation<
UpdateOneLogicFunctionMutation,
UpdateOneLogicFunctionMutationVariables
>(UPDATE_ONE_LOGIC_FUNCTION, {
client: apolloMetadataClient,
});
const [deleteLogicFunctionMutation] = useMutation<
DeleteOneLogicFunctionMutation,
DeleteOneLogicFunctionMutationVariables
@@ -48,6 +43,13 @@ export const usePersistLogicFunction = () => {
client: apolloMetadataClient,
});
const [updateLogicFunctionSourceMutation] = useMutation<
{ updateLogicFunctionSource: boolean },
UpdateLogicFunctionSourceMutationVariables
>(UPDATE_LOGIC_FUNCTION_SOURCE, {
client: apolloMetadataClient,
});
const createLogicFunction = useCallback(
async (
variables: CreateOneLogicFunctionItemMutationVariables,
@@ -86,19 +88,19 @@ export const usePersistLogicFunction = () => {
[createLogicFunctionMutation, handleMetadataError, enqueueErrorSnackBar],
);
const updateLogicFunction = useCallback(
const updateLogicFunctionSource = useCallback(
async (
variables: UpdateOneLogicFunctionMutationVariables,
variables: UpdateLogicFunctionSourceMutationVariables,
): Promise<
MetadataRequestResult<
Awaited<ReturnType<typeof updateLogicFunctionMutation>>
Awaited<ReturnType<typeof updateLogicFunctionSourceMutation>>
>
> => {
try {
const result = await updateLogicFunctionMutation({
const result = await updateLogicFunctionSourceMutation({
variables,
refetchQueries: [
getOperationName(FIND_ONE_LOGIC_FUNCTION_SOURCE_CODE) ?? '',
getOperationName(GET_LOGIC_FUNCTION_SOURCE_CODE) ?? '',
],
});
@@ -122,7 +124,11 @@ export const usePersistLogicFunction = () => {
};
}
},
[updateLogicFunctionMutation, handleMetadataError, enqueueErrorSnackBar],
[
updateLogicFunctionSourceMutation,
handleMetadataError,
enqueueErrorSnackBar,
],
);
const deleteLogicFunction = useCallback(
@@ -138,7 +144,7 @@ export const usePersistLogicFunction = () => {
variables,
awaitRefetchQueries: true,
refetchQueries: [
getOperationName(FIND_ONE_LOGIC_FUNCTION_SOURCE_CODE) ?? '',
getOperationName(GET_LOGIC_FUNCTION_SOURCE_CODE) ?? '',
],
});
@@ -167,7 +173,7 @@ export const usePersistLogicFunction = () => {
return {
createLogicFunction,
updateLogicFunction,
updateLogicFunctionSource,
deleteLogicFunction,
};
};
@@ -1,67 +0,0 @@
import { useExecuteOneLogicFunction } from '@/settings/logic-functions/hooks/useExecuteOneLogicFunction';
import { logicFunctionTestDataFamilyState } from '@/workflow/workflow-steps/workflow-actions/code-action/states/logicFunctionTestDataFamilyState';
import { useState } from 'react';
import { useRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { sleep } from '~/utils/sleep';
export const useTestLogicFunction = ({
logicFunctionId,
callback,
}: {
logicFunctionId: string;
callback?: (testResult: object) => void;
}) => {
const [isTesting, setIsTesting] = useState(false);
const { executeOneLogicFunction } = useExecuteOneLogicFunction();
const [logicFunctionTestData, setLogicFunctionTestData] = useRecoilState(
logicFunctionTestDataFamilyState(logicFunctionId),
);
const testLogicFunction = async () => {
try {
setIsTesting(true);
await sleep(200); // Delay artificially to avoid flashing the UI
const result = await executeOneLogicFunction({
id: logicFunctionId,
payload: logicFunctionTestData.input,
});
setIsTesting(false);
if (isDefined(result?.data?.executeOneLogicFunction?.data)) {
callback?.(result?.data?.executeOneLogicFunction?.data);
}
setLogicFunctionTestData((prev) => ({
...prev,
language: 'json',
height: 300,
output: {
data: result?.data?.executeOneLogicFunction?.data
? JSON.stringify(
result?.data?.executeOneLogicFunction?.data,
null,
4,
)
: undefined,
logs: result?.data?.executeOneLogicFunction?.logs || '',
duration: result?.data?.executeOneLogicFunction?.duration,
status: result?.data?.executeOneLogicFunction?.status,
error: result?.data?.executeOneLogicFunction?.error
? JSON.stringify(
result?.data?.executeOneLogicFunction?.error,
null,
4,
)
: undefined,
},
}));
} catch (error) {
setIsTesting(false);
throw error;
}
};
return { testLogicFunction, isTesting };
};
@@ -1,11 +0,0 @@
import { gql } from '@apollo/client';
import { LOGIC_FUNCTION_FRAGMENT } from '@/settings/logic-functions/graphql/fragments/logicFunctionFragment';
export const UPDATE_ONE_LOGIC_FUNCTION = gql`
${LOGIC_FUNCTION_FRAGMENT}
mutation UpdateOneLogicFunction($input: UpdateLogicFunctionInput!) {
updateOneLogicFunction(input: $input) {
...LogicFunctionFields
}
}
`;
@@ -1,9 +0,0 @@
import { gql } from '@apollo/client';
export const FIND_ONE_LOGIC_FUNCTION_SOURCE_CODE = gql`
query FindOneLogicFunctionSourceCode(
$input: GetLogicFunctionSourceCodeInput!
) {
getLogicFunctionSourceCode(input: $input)
}
`;
@@ -6,13 +6,6 @@ jest.mock('@/settings/logic-functions/hooks/useGetOneLogicFunction', () => ({
useGetOneLogicFunction: jest.fn(),
}));
jest.mock(
'@/settings/logic-functions/hooks/useGetOneLogicFunctionSourceCode',
() => ({
useGetOneLogicFunctionSourceCode: jest.fn(),
}),
);
describe('useLogicFunctionUpdateFormState', () => {
test('should return a form', () => {
const logicFunctionId = 'logicFunctionId';
@@ -21,15 +14,8 @@ describe('useLogicFunctionUpdateFormState', () => {
);
useGetOneLogicFunctionMock.useGetOneLogicFunction.mockReturnValue({
logicFunction: { name: 'name' },
loading: false,
});
const useGetOneLogicFunctionSourceCodeMock = jest.requireMock(
'@/settings/logic-functions/hooks/useGetOneLogicFunctionSourceCode',
);
useGetOneLogicFunctionSourceCodeMock.useGetOneLogicFunctionSourceCode.mockReturnValue(
{
code: { src: { 'index.ts': 'export const handler = () => {}' } },
},
);
const { result } = renderHook(
() => useLogicFunctionUpdateFormState({ logicFunctionId }),
{
@@ -1,27 +0,0 @@
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { EXECUTE_ONE_LOGIC_FUNCTION } from '@/settings/logic-functions/graphql/mutations/executeOneLogicFunction';
import { useMutation } from '@apollo/client';
import {
type ExecuteOneLogicFunctionMutation,
type ExecuteOneLogicFunctionMutationVariables,
type ExecuteLogicFunctionInput,
} from '~/generated-metadata/graphql';
export const useExecuteOneLogicFunction = () => {
const apolloMetadataClient = useApolloCoreClient();
const [mutate] = useMutation<
ExecuteOneLogicFunctionMutation,
ExecuteOneLogicFunctionMutationVariables
>(EXECUTE_ONE_LOGIC_FUNCTION, {
client: apolloMetadataClient,
});
const executeOneLogicFunction = async (input: ExecuteLogicFunctionInput) => {
return await mutate({
variables: {
input,
},
});
};
return { executeOneLogicFunction };
};
@@ -1,29 +0,0 @@
import { useQuery } from '@apollo/client';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { FIND_ONE_LOGIC_FUNCTION_SOURCE_CODE } from '@/settings/logic-functions/graphql/queries/findOneLogicFunctionSourceCode';
import {
type FindOneLogicFunctionSourceCodeQuery,
type FindOneLogicFunctionSourceCodeQueryVariables,
} from '~/generated-metadata/graphql';
export const useGetOneLogicFunctionSourceCode = ({
id,
onCompleted,
}: {
id: string;
onCompleted?: (data: FindOneLogicFunctionSourceCodeQuery) => void;
}) => {
const apolloMetadataClient = useApolloCoreClient();
const { data, loading } = useQuery<
FindOneLogicFunctionSourceCodeQuery,
FindOneLogicFunctionSourceCodeQueryVariables
>(FIND_ONE_LOGIC_FUNCTION_SOURCE_CODE, {
client: apolloMetadataClient ?? undefined,
variables: {
input: { id },
},
onCompleted,
fetchPolicy: 'network-only',
});
return { code: data?.getLogicFunctionSourceCode, loading };
};
@@ -1,16 +1,8 @@
import { flattenSources } from '@/logic-functions/utils/flattenSources';
import { getFunctionInputFromSourceCode } from '@/logic-functions/utils/getFunctionInputFromSourceCode';
import { useGetOneLogicFunction } from '@/settings/logic-functions/hooks/useGetOneLogicFunction';
import { useGetOneLogicFunctionSourceCode } from '@/settings/logic-functions/hooks/useGetOneLogicFunctionSourceCode';
import { logicFunctionTestDataFamilyState } from '@/workflow/workflow-steps/workflow-actions/code-action/states/logicFunctionTestDataFamilyState';
import { type Dispatch, type SetStateAction, useState } from 'react';
import { useRecoilState } from 'recoil';
import { type Sources } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
type FindOneLogicFunctionSourceCodeQuery,
type GetOneLogicFunctionQuery,
} from '~/generated-metadata/graphql';
import { type GetOneLogicFunctionQuery } from '~/generated-metadata/graphql';
import { type LogicFunction } from '~/generated/graphql';
export type LogicFunctionNewFormValues = {
@@ -42,10 +34,6 @@ export const useLogicFunctionUpdateFormState = ({
code: { src: { 'index.ts': '' } },
});
const [logicFunctionTestData, setLogicFunctionTestData] = useRecoilState(
logicFunctionTestDataFamilyState(logicFunctionId),
);
const { logicFunction, loading: logicFunctionLoading } =
useGetOneLogicFunction({
id: logicFunctionId,
@@ -62,43 +50,10 @@ export const useLogicFunctionUpdateFormState = ({
},
});
const { loading: logicFunctionSourceCodeLoading } =
useGetOneLogicFunctionSourceCode({
id: logicFunctionId,
onCompleted: async (data: FindOneLogicFunctionSourceCodeQuery) => {
const code = data?.getLogicFunctionSourceCode;
setFormValues((prevState) => ({
...prevState,
code: code || prevState.code,
}));
if (logicFunctionTestData.shouldInitInput) {
const flattenedCode = flattenSources(code);
const sourceCode = flattenedCode.find(
(flatCode) => flatCode.path === logicFunction?.sourceHandlerPath,
);
if (isDefined(sourceCode)) {
const functionInput = await getFunctionInputFromSourceCode(
sourceCode.content,
);
setLogicFunctionTestData((prev) => ({
...prev,
input: functionInput,
shouldInitInput: false,
}));
}
}
},
});
return {
formValues,
setFormValues,
logicFunction,
loading: logicFunctionSourceCodeLoading || logicFunctionLoading,
loading: logicFunctionLoading,
};
};
@@ -1,8 +1,6 @@
import { useGetLogicFunctionSourceCode } from '@/logic-functions/hooks/useGetLogicFunctionSourceCode';
import { useGetAvailablePackages } from '@/settings/logic-functions/hooks/useGetAvailablePackages';
import {
type LogicFunctionFormValues,
useLogicFunctionUpdateFormState,
} from '@/settings/logic-functions/hooks/useLogicFunctionUpdateFormState';
import { type LogicFunctionFormValues } from '@/settings/logic-functions/hooks/useLogicFunctionUpdateFormState';
import { useFullScreenModal } from '@/ui/layout/fullscreen/hooks/useFullScreenModal';
import { type BreadcrumbProps } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow';
@@ -14,7 +12,6 @@ import { setNestedValue } from '@/workflow/workflow-steps/workflow-actions/code-
import { CmdEnterActionButton } from '@/action-menu/components/CmdEnterActionButton';
import { LogicFunctionExecutionResult } from '@/logic-functions/components/LogicFunctionExecutionResult';
import { INDEX_FILE_NAME } from '@/logic-functions/constants/IndexFileName';
import { useTestLogicFunction } from '@/logic-functions/hooks/useTestLogicFunction';
import { getFunctionInputFromSourceCode } from '@/logic-functions/utils/getFunctionInputFromSourceCode';
import { mergeDefaultFunctionInputAndFunctionInput } from '@/logic-functions/utils/mergeDefaultFunctionInputAndFunctionInput';
import { InputLabel } from '@/ui/input/components/InputLabel';
@@ -39,8 +36,9 @@ import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { SOURCE_FOLDER_NAME } from '@/logic-functions/constants/SourceFolderName';
import { useExecuteLogicFunction } from '@/logic-functions/hooks/useExecuteLogicFunction';
import { usePersistLogicFunction } from '@/logic-functions/hooks/usePersistLogicFunction';
import { computeNewSources } from '@/logic-functions/utils/computeNewSources';
import { usePersistLogicFunction } from '@/settings/logic-functions/hooks/usePersistLogicFunction';
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
import { CODE_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/CodeAction';
import { type Monaco } from '@monaco-editor/react';
@@ -106,7 +104,7 @@ export const WorkflowEditActionCode = ({
activeTabIdComponentState,
WORKFLOW_LOGIC_FUNCTION_TAB_LIST_COMPONENT_ID,
);
const { updateLogicFunction } = usePersistLogicFunction();
const { updateLogicFunctionSource } = usePersistLogicFunction();
const { getUpdatableWorkflowVersion } =
useGetUpdatableWorkflowVersionOrThrow();
@@ -128,10 +126,21 @@ export const WorkflowEditActionCode = ({
action.settings.input.logicFunctionInput,
);
const { formValues, setFormValues, loading } =
useLogicFunctionUpdateFormState({
logicFunctionId,
});
const { code: codeFromApi, loading } = useGetLogicFunctionSourceCode({
logicFunctionId,
});
const [formValues, setFormValues] = useState<LogicFunctionFormValues>({
name: '',
description: '',
code: { src: { 'index.ts': '' } },
});
useEffect(() => {
if (isDefined(codeFromApi)) {
setFormValues((prev) => ({ ...prev, code: codeFromApi }));
}
}, [codeFromApi]);
const updateOutputSchemaFromTestResult = async (testResult: object) => {
if (actionOptions.readonly === true) {
@@ -144,20 +153,16 @@ export const WorkflowEditActionCode = ({
});
};
const { testLogicFunction, isTesting } = useTestLogicFunction({
const { executeLogicFunction, isExecuting } = useExecuteLogicFunction({
logicFunctionId,
callback: updateOutputSchemaFromTestResult,
});
const handleSave = useDebouncedCallback(async () => {
await updateLogicFunction({
await updateLogicFunctionSource({
input: {
id: logicFunctionId,
update: {
name: formValues.name,
description: formValues.description,
code: formValues.code,
},
code: formValues.code,
},
});
}, 500);
@@ -267,8 +272,8 @@ export const WorkflowEditActionCode = ({
return;
}
if (!isTesting) {
await testLogicFunction();
if (!isExecuting) {
await executeLogicFunction({ forceRebuild: true });
}
};
@@ -461,7 +466,7 @@ export const WorkflowEditActionCode = ({
<InputLabel>{t`Result`}</InputLabel>
<LogicFunctionExecutionResult
logicFunctionTestData={logicFunctionTestData}
isTesting={isTesting}
isTesting={isExecuting}
/>
</StyledCodeEditorContainer>
{logicFunctionTestData.output.logs.length > 0 && (
@@ -469,7 +474,7 @@ export const WorkflowEditActionCode = ({
<InputLabel>{t`Logs`}</InputLabel>
<TextArea
textAreaId={testLogsTextAreaId}
value={isTesting ? '' : logicFunctionTestData.output.logs}
value={isExecuting ? '' : logicFunctionTestData.output.logs}
maxRows={20}
disabled
/>
@@ -487,7 +492,7 @@ export const WorkflowEditActionCode = ({
<CmdEnterActionButton
title={t`Test`}
onClick={handleRunFunction}
disabled={isTesting}
disabled={isExecuting}
/>,
]
: []
@@ -1,6 +1,6 @@
import { useGetAvailablePackages } from '@/settings/logic-functions/hooks/useGetAvailablePackages';
import { useLogicFunctionUpdateFormState } from '@/settings/logic-functions/hooks/useLogicFunctionUpdateFormState';
import { type WorkflowCodeAction } from '@/workflow/types/Workflow';
import { useGetLogicFunctionSourceCode } from '@/logic-functions/hooks/useGetLogicFunctionSourceCode';
import { INDEX_FILE_NAME } from '@/logic-functions/constants/IndexFileName';
import { SOURCE_FOLDER_NAME } from '@/logic-functions/constants/SourceFolderName';
@@ -31,7 +31,7 @@ export const WorkflowReadonlyActionCode = ({
id: logicFunctionId,
});
const { formValues, loading } = useLogicFunctionUpdateFormState({
const { code, loading } = useGetLogicFunctionSourceCode({
logicFunctionId,
});
@@ -53,9 +53,10 @@ export const WorkflowReadonlyActionCode = ({
}
const indexFileContent =
typeof formValues.code?.[SOURCE_FOLDER_NAME] !== 'string' &&
typeof formValues.code[SOURCE_FOLDER_NAME][INDEX_FILE_NAME] === 'string'
? formValues.code[SOURCE_FOLDER_NAME][INDEX_FILE_NAME]
code != null &&
typeof code?.[SOURCE_FOLDER_NAME] !== 'string' &&
typeof code[SOURCE_FOLDER_NAME]?.[INDEX_FILE_NAME] === 'string'
? code[SOURCE_FOLDER_NAME][INDEX_FILE_NAME]
: '';
return (
@@ -5,7 +5,7 @@ import Skeleton from 'react-loading-skeleton';
import { useRecoilValue } from 'recoil';
import { useGetToolIndex } from '@/ai/hooks/useGetToolIndex';
import { usePersistLogicFunction } from '@/settings/logic-functions/hooks/usePersistLogicFunction';
import { usePersistLogicFunction } from '@/logic-functions/hooks/usePersistLogicFunction';
import { logicFunctionsState } from '@/settings/logic-functions/states/logicFunctionsState';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
@@ -1,10 +1,9 @@
import { useTestLogicFunction } from '@/logic-functions/hooks/useTestLogicFunction';
import { useExecuteLogicFunction } from '@/logic-functions/hooks/useExecuteLogicFunction';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsLogicFunctionLabelContainer } from '@/settings/logic-functions/components/SettingsLogicFunctionLabelContainer';
import { SettingsLogicFunctionSettingsTab } from '@/settings/logic-functions/components/tabs/SettingsLogicFunctionSettingsTab';
import { SettingsLogicFunctionTestTab } from '@/settings/logic-functions/components/tabs/SettingsLogicFunctionTestTab';
import { SettingsLogicFunctionTriggersTab } from '@/settings/logic-functions/components/tabs/SettingsLogicFunctionTriggersTab';
import { usePersistLogicFunction } from '@/settings/logic-functions/hooks/usePersistLogicFunction';
import {
type LogicFunctionFormValues,
useLogicFunctionUpdateFormState,
@@ -14,11 +13,10 @@ import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { t } from '@lingui/core/macro';
import { useNavigate, useParams } from 'react-router-dom';
import { useParams } from 'react-router-dom';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import { IconBolt, IconSettings, IconTestPipe } from 'twenty-ui/display';
import { useDebouncedCallback } from 'use-debounce';
import { IconBolt, IconPlayerPlay, IconSettings } from 'twenty-ui/display';
import { useFindOneApplicationQuery } from '~/generated-metadata/graphql';
const LOGIC_FUNCTION_DETAIL_ID = 'logic-function-detail';
@@ -26,8 +24,6 @@ const LOGIC_FUNCTION_DETAIL_ID = 'logic-function-detail';
export const SettingsLogicFunctionDetail = () => {
const { logicFunctionId = '', applicationId = '' } = useParams();
const navigate = useNavigate();
const { data, loading: applicationLoading } = useFindOneApplicationQuery({
variables: { id: applicationId },
skip: !applicationId,
@@ -41,56 +37,36 @@ export const SettingsLogicFunctionDetail = () => {
activeTabIdComponentState,
instanceId,
);
const { updateLogicFunction } = usePersistLogicFunction();
const { formValues, setFormValues, logicFunction, loading } =
useLogicFunctionUpdateFormState({ logicFunctionId });
const { testLogicFunction, isTesting } = useTestLogicFunction({
const { executeLogicFunction, isExecuting } = useExecuteLogicFunction({
logicFunctionId,
});
const handleSave = useDebouncedCallback(
async (toolInputSchema?: object | null) => {
await updateLogicFunction({
input: {
id: logicFunctionId,
update: {
name: formValues.name,
description: formValues.description,
code: formValues.code,
...(toolInputSchema !== undefined && { toolInputSchema }),
},
},
});
},
500,
);
const handleExecute = async () => {
await executeLogicFunction({ forceRebuild: true });
};
const onChange = (key: string) => {
return async (value: string) => {
return (value: string) => {
setFormValues((prevState: LogicFunctionFormValues) => ({
...prevState,
[key]: value,
}));
await handleSave();
};
};
const handleTestFunction = async () => {
navigate('#test');
await testLogicFunction();
};
const tabs = [
{ id: 'settings', title: t`Settings`, Icon: IconSettings },
{ id: 'test', title: t`Test`, Icon: IconPlayerPlay },
{ id: 'triggers', title: t`Triggers`, Icon: IconBolt },
{ id: 'test', title: t`Test`, Icon: IconTestPipe },
];
const isTriggersTab = activeTabId === 'triggers';
const isTestTab = activeTabId === 'test';
const isSettingsTab = activeTabId === 'settings';
const isTestTab = activeTabId === 'test';
const breadcrumbLinks =
isDefined(applicationId) && applicationId !== ''
@@ -145,19 +121,19 @@ export const SettingsLogicFunctionDetail = () => {
{isTriggersTab && logicFunction && (
<SettingsLogicFunctionTriggersTab logicFunction={logicFunction} />
)}
{isTestTab && (
<SettingsLogicFunctionTestTab
logicFunctionId={logicFunctionId}
handleExecute={handleTestFunction}
isTesting={isTesting}
/>
)}
{isSettingsTab && (
<SettingsLogicFunctionSettingsTab
formValues={formValues}
onChange={onChange}
/>
)}
{isTestTab && (
<SettingsLogicFunctionTestTab
handleExecute={handleExecute}
logicFunctionId={logicFunctionId}
isTesting={isExecuting}
/>
)}
</SettingsPageContainer>
</SubMenuTopBarContainer>
)
+7 -2
View File
@@ -7,7 +7,8 @@
"**/node_modules/**",
"**/dist/**",
"**/.git/**",
"**/.nx/**"
"**/.nx/**",
"**/.local-storage/**"
]
},
"compilerOptions": {
@@ -16,7 +17,11 @@
"watchAssets": true,
"assets": [
{
"include": "engine/core-modules/logic-function/logic-function-drivers/constants/seed-project/**",
"include": "modules/workflow/workflow-builder/workflow-version-step/code-step/constants/seed-project/**",
"outDir": "dist/assets"
},
{
"include": "engine/core-modules/logic-function/logic-function-source-builder/constants/seed-project/**",
"outDir": "dist/assets"
},
{
@@ -0,0 +1,34 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
export class DropLogicFunctionLayerIdFromLogicFunction1770050300000
implements MigrationInterface
{
name = 'DropLogicFunctionLayerIdFromLogicFunction1770050300000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."logicFunction" DROP CONSTRAINT IF EXISTS "FK_87e3f7b8f23cd90709e127f60c5"`,
);
await queryRunner.query(
`ALTER TABLE "core"."logicFunction" DROP CONSTRAINT IF EXISTS "FK_4b9625a4babf7f4fa942fd26514"`,
);
await queryRunner.query(
`DROP INDEX IF EXISTS "core"."IDX_LOGIC_FUNCTION_LAYER_ID"`,
);
await queryRunner.query(
`ALTER TABLE "core"."logicFunction" DROP COLUMN IF EXISTS "logicFunctionLayerId"`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."logicFunction" ADD "logicFunctionLayerId" uuid`,
);
await queryRunner.query(
`CREATE INDEX "IDX_LOGIC_FUNCTION_LAYER_ID" ON "core"."logicFunction" ("logicFunctionLayerId")`,
);
await queryRunner.query(
`ALTER TABLE "core"."logicFunction" ADD CONSTRAINT "FK_4b9625a4babf7f4fa942fd26514" FOREIGN KEY ("logicFunctionLayerId") REFERENCES "core"."logicFunctionLayer"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
}
}
@@ -21,6 +21,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/workspace-migration-runner.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
import { CodeStepBuildModule } from 'src/modules/workflow/workflow-builder/workflow-version-step/code-step/code-step-build.module';
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
@Module({
@@ -39,6 +40,7 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
ObjectPermissionModule,
PermissionFlagModule,
WorkflowCommonModule,
CodeStepBuildModule,
FileStorageModule,
WorkspaceCacheModule,
WorkspaceMigrationRunnerModule,
@@ -923,9 +923,6 @@ export class ApplicationSyncService {
workspaceId,
ownerFlatApplication,
});
// Trigger settings are now embedded in the logic function entity
// They are handled through the update input
}
for (const logicFunctionToCreate of logicFunctionsToCreate) {
@@ -1,9 +0,0 @@
import { Module } from '@nestjs/common';
import { LogicFunctionBuildService } from 'src/engine/core-modules/logic-function/logic-function-build/services/logic-function-build.service';
@Module({
providers: [LogicFunctionBuildService],
exports: [LogicFunctionBuildService],
})
export class LogicFunctionBuildModule {}
@@ -1,160 +0,0 @@
import { Injectable } from '@nestjs/common';
import crypto from 'crypto';
import fs from 'fs/promises';
import { dirname, join } from 'path';
import { build } from 'esbuild';
import { FileFolder } from 'twenty-shared/types';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import {
getLogicFunctionBaseFolderPath,
getRelativePathFromBase,
} from 'src/engine/core-modules/logic-function/logic-function-build/utils/get-logic-function-base-folder-path.util';
import { LambdaBuildDirectoryManager } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/lambda-build-directory-manager';
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
export type FunctionBuildParams = {
flatLogicFunction: FlatLogicFunction;
applicationUniversalIdentifier: string;
};
@Injectable()
export class LogicFunctionBuildService {
constructor(private readonly fileStorageService: FileStorageService) {}
async hasLayerDependencies({
flatApplication,
applicationUniversalIdentifier,
}: {
flatApplication: FlatApplication;
applicationUniversalIdentifier: string;
}): Promise<boolean> {
const packageJsonExists = await this.fileStorageService.checkFileExists_v2({
workspaceId: flatApplication.workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Dependencies,
resourcePath: 'package.json',
});
const yarnLockExists = await this.fileStorageService.checkFileExists_v2({
workspaceId: flatApplication.workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Dependencies,
resourcePath: 'yarn.lock',
});
return packageJsonExists && yarnLockExists;
}
async uploadDependencies({
flatApplication: _flatApplication,
applicationUniversalIdentifier: _applicationUniversalIdentifier,
}: {
flatApplication: FlatApplication;
applicationUniversalIdentifier: string;
}) {
// Package files live in Dependencies; no copy needed drivers read from
// Dependencies when building the layer.
}
async isBuilt({
flatLogicFunction,
applicationUniversalIdentifier,
}: FunctionBuildParams): Promise<boolean> {
return await this.fileStorageService.checkFileExists_v2({
workspaceId: flatLogicFunction.workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.BuiltLogicFunction,
resourcePath: flatLogicFunction.builtHandlerPath,
});
}
async buildAndUpload({
flatLogicFunction,
applicationUniversalIdentifier,
}: FunctionBuildParams): Promise<{ checksum: string }> {
const lambdaBuildDirectoryManager = new LambdaBuildDirectoryManager();
try {
const { sourceTemporaryDir } = await lambdaBuildDirectoryManager.init();
const baseFolderPath = getLogicFunctionBaseFolderPath(
flatLogicFunction.sourceHandlerPath,
);
await this.fileStorageService.downloadFolder_v2({
workspaceId: flatLogicFunction.workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: baseFolderPath,
localPath: sourceTemporaryDir,
});
const relativeSourcePath = getRelativePathFromBase(
flatLogicFunction.sourceHandlerPath,
baseFolderPath,
);
const relativeBuiltPath = getRelativePathFromBase(
flatLogicFunction.builtHandlerPath,
baseFolderPath,
);
const builtBundleFilePath = await this.buildInMemory({
sourceTemporaryDir,
sourceHandlerPath: relativeSourcePath,
builtHandlerPath: relativeBuiltPath,
});
const builtFile = await fs.readFile(builtBundleFilePath, 'utf-8');
await this.fileStorageService.writeFile_v2({
workspaceId: flatLogicFunction.workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.BuiltLogicFunction,
resourcePath: flatLogicFunction.builtHandlerPath,
sourceFile: builtFile,
mimeType: 'application/javascript',
settings: {
isTemporaryFile: false,
toDelete: false,
},
});
return {
checksum: crypto.createHash('md5').update(builtFile).digest('hex'),
};
} finally {
await lambdaBuildDirectoryManager.clean();
}
}
private async buildInMemory({
sourceTemporaryDir,
sourceHandlerPath,
builtHandlerPath,
}: {
sourceTemporaryDir: string;
sourceHandlerPath: string;
builtHandlerPath: string;
}): Promise<string> {
const entryFilePath = join(sourceTemporaryDir, sourceHandlerPath);
const builtBundleFilePath = join(sourceTemporaryDir, builtHandlerPath);
await fs.mkdir(dirname(builtBundleFilePath), { recursive: true });
await build({
entryPoints: [entryFilePath],
outfile: builtBundleFilePath,
platform: 'node',
format: 'esm',
target: 'es2017',
bundle: true,
sourcemap: true,
packages: 'external',
});
return builtBundleFilePath;
}
}
@@ -0,0 +1 @@
export const NODE_LAYER_SUBFOLDER = 'nodejs';
@@ -1,4 +0,0 @@
export const SEED_PROJECT_INPUT_SCHEMA = {
a: null,
b: null,
};
@@ -30,13 +30,11 @@ import {
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { NODE_LAYER_SUBFOLDER } from 'src/engine/core-modules/logic-function/logic-function-drivers/constants/lambda-layer.constant';
import { copyExecutor } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/copy-executor';
import { copyYarnEngineAndBuildDependencies } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/copy-yarn-engine-and-build-dependencies';
import { createZipFile } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/create-zip-file';
import {
LambdaBuildDirectoryManager,
NODE_LAYER_SUBFOLDER,
} from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/lambda-build-directory-manager';
import { LambdaBuildDirectoryManager } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/lambda-build-directory-manager';
import { LogicFunctionExecutionStatus } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-execution-result.dto';
import { LogicFunctionRuntime } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import {
@@ -193,9 +191,9 @@ export class LambdaDriver implements LogicFunctionExecutorDriver {
return listLayerResult.LayerVersions[0].LayerVersionArn;
}
const lambdaBuildDirectoryManager = new LambdaBuildDirectoryManager();
const buildTemporaryDirectoryManager = new LambdaBuildDirectoryManager();
const { sourceTemporaryDir, lambdaZipPath } =
await lambdaBuildDirectoryManager.init();
await buildTemporaryDirectoryManager.init();
const nodeDependenciesFolder = join(
sourceTemporaryDir,
@@ -226,7 +224,7 @@ export class LambdaDriver implements LogicFunctionExecutorDriver {
const result = await (await this.getLambdaClient()).send(command);
await lambdaBuildDirectoryManager.clean();
await buildTemporaryDirectoryManager.clean();
if (!isDefined(result.LayerVersionArn)) {
throw new Error('new layer version arn if undefined');
@@ -308,10 +306,10 @@ export class LambdaDriver implements LogicFunctionExecutorDriver {
applicationUniversalIdentifier,
});
const lambdaBuildDirectoryManager = new LambdaBuildDirectoryManager();
const buildTemporaryDirectoryManager = new LambdaBuildDirectoryManager();
const { sourceTemporaryDir, lambdaZipPath } =
await lambdaBuildDirectoryManager.init();
await buildTemporaryDirectoryManager.init();
await copyExecutor(sourceTemporaryDir);
@@ -333,7 +331,7 @@ export class LambdaDriver implements LogicFunctionExecutorDriver {
await (await this.getLambdaClient()).send(command);
await lambdaBuildDirectoryManager.clean();
await buildTemporaryDirectoryManager.clean();
}
private extractLogs(logString: string): string {
@@ -1,6 +1,7 @@
import { promises as fs } from 'fs';
import { spawn } from 'node:child_process';
import { join } from 'path';
import { dirname } from 'node:path';
import { FileFolder } from 'twenty-shared/types';
@@ -12,15 +13,12 @@ import {
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import {
getLogicFunctionBaseFolderPath,
getRelativePathFromBase,
} from 'src/engine/core-modules/logic-function/logic-function-build/utils/get-logic-function-base-folder-path.util';
import { LOGIC_FUNCTION_EXECUTOR_TMPDIR_FOLDER } from 'src/engine/core-modules/logic-function/logic-function-drivers/constants/logic-function-executor-tmpdir-folder';
import { copyYarnEngineAndBuildDependencies } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/copy-yarn-engine-and-build-dependencies';
import { ConsoleListener } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/intercept-console';
import { LambdaBuildDirectoryManager } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/lambda-build-directory-manager';
import { LogicFunctionExecutionStatus } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-execution-result.dto';
import { getRelativePathFromBase } from 'src/modules/workflow/workflow-builder/workflow-version-step/code-step/utils/get-code-step-handler-path.util';
export interface LocalDriverOptions {
fileStorageService: FileStorageService;
@@ -122,9 +120,7 @@ export class LocalDriver implements LogicFunctionExecutorDriver {
try {
const { sourceTemporaryDir } = await lambdaBuildDirectoryManager.init();
const baseFolderPath = getLogicFunctionBaseFolderPath(
flatLogicFunction.builtHandlerPath,
);
const baseFolderPath = dirname(flatLogicFunction.builtHandlerPath);
await this.fileStorageService.downloadFolder_v2({
workspaceId: flatLogicFunction.workspaceId,
@@ -4,7 +4,6 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { LogicFunctionBuildModule } from 'src/engine/core-modules/logic-function/logic-function-build/logic-function-build.module';
import { AddPackagesCommand } from 'src/engine/core-modules/logic-function/logic-function-executor/commands/add-packages.command';
import { LogicFunctionExecutorService } from 'src/engine/core-modules/logic-function/logic-function-executor/services/logic-function-executor.service';
import { SecretEncryptionModule } from 'src/engine/core-modules/secret-encryption/secret-encryption.module';
@@ -21,7 +20,6 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
SecretEncryptionModule,
SubscriptionsModule,
WorkspaceCacheModule,
LogicFunctionBuildModule,
FileModule,
TypeOrmModule.forFeature([LogicFunctionEntity]),
],
@@ -14,14 +14,12 @@ import {
type LogicFunctionExecuteParams,
type LogicFunctionExecuteResult,
} from 'src/engine/core-modules/logic-function/logic-function-drivers/interfaces/logic-function-executor-driver.interface';
import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
import { LOGIC_FUNCTION_EXECUTED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/logic-function/logic-function-executed';
import { ApplicationTokenService } from 'src/engine/core-modules/auth/token/services/application-token.service';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { LogicFunctionBuildService } from 'src/engine/core-modules/logic-function/logic-function-build/services/logic-function-build.service';
import { getLogicFunctionBaseFolderPath } from 'src/engine/core-modules/logic-function/logic-function-build/utils/get-logic-function-base-folder-path.util';
import { buildEnvVar } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/build-env-var';
import { LOGIC_FUNCTION_EXECUTOR_DRIVER } from 'src/engine/core-modules/logic-function/logic-function-executor/constants/logic-function-executor.constants';
import { SecretEncryptionService } from 'src/engine/core-modules/secret-encryption/secret-encryption.service';
@@ -30,7 +28,6 @@ import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twent
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { findFlatLogicFunctionOrThrow } from 'src/engine/metadata-modules/logic-function/utils/find-flat-logic-function-or-throw.util';
import { SubscriptionChannel } from 'src/engine/subscriptions/enums/subscription-channel.enum';
import { SubscriptionService } from 'src/engine/subscriptions/subscription.service';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
@@ -65,7 +62,6 @@ export class LogicFunctionExecutorService
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly applicationTokenService: ApplicationTokenService,
private readonly secretEncryptionService: SecretEncryptionService,
private readonly functionBuildService: LogicFunctionBuildService,
private readonly subscriptionService: SubscriptionService,
private readonly auditService: AuditService,
private readonly fileStorageService: FileStorageService,
@@ -178,30 +174,17 @@ export class LogicFunctionExecutorService
}
if (
!(await this.functionBuildService.hasLayerDependencies({
!(await this.hasLayerDependencies({
flatApplication,
applicationUniversalIdentifier,
}))
) {
await this.functionBuildService.uploadDependencies({
flatApplication,
applicationUniversalIdentifier,
});
throw new LogicFunctionExecutionException(
'Logic function dependencies not found',
LogicFunctionExecutionExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
);
}
if (
!(await this.functionBuildService.isBuilt({
flatLogicFunction,
applicationUniversalIdentifier,
}))
) {
await this.functionBuildService.buildAndUpload({
flatLogicFunction,
applicationUniversalIdentifier,
});
}
// END TODO
const resultLogicFunction = await this.callWithTimeout({
callback: () =>
this.execute({
@@ -251,55 +234,6 @@ export class LogicFunctionExecutorService
return resultLogicFunction;
}
async getLogicFunctionSourceCode(workspaceId: string, id: string) {
try {
const { flatLogicFunctionMaps, flatApplicationMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatLogicFunctionMaps',
'flatApplicationMaps',
]);
const flatLogicFunction = findFlatLogicFunctionOrThrow({
id,
flatLogicFunctionMaps,
});
const applicationUniversalIdentifier = isDefined(
flatLogicFunction.applicationId,
)
? flatApplicationMaps.byId[flatLogicFunction.applicationId]
?.universalIdentifier
: undefined;
if (!isDefined(applicationUniversalIdentifier)) {
throw new LogicFunctionExecutionException(
`Application universal identifier not found for logic function ${id}`,
LogicFunctionExecutionExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
);
}
const baseFolderPath = getLogicFunctionBaseFolderPath(
flatLogicFunction.sourceHandlerPath,
);
return await this.fileStorageService.readFolder_v2({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: baseFolderPath,
});
} catch (error) {
if (
isDefined(error) &&
'code' in error &&
error.code === FileStorageExceptionCode.FILE_NOT_FOUND
) {
return;
}
throw error;
}
}
async getAvailablePackages(logicFunctionId: string) {
const logicFunction = await this.logicFunctionRepository.findOneOrFail({
where: { id: logicFunctionId },
@@ -339,4 +273,27 @@ export class LogicFunctionExecutorService
),
]);
}
private async hasLayerDependencies({
flatApplication,
applicationUniversalIdentifier,
}: {
flatApplication: FlatApplication;
applicationUniversalIdentifier: string;
}): Promise<boolean> {
const packageJsonExists = await this.fileStorageService.checkFileExists_v2({
workspaceId: flatApplication.workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Dependencies,
resourcePath: 'package.json',
});
const yarnLockExists = await this.fileStorageService.checkFileExists_v2({
workspaceId: flatApplication.workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Dependencies,
resourcePath: 'yarn.lock',
});
return packageJsonExists && yarnLockExists;
}
}
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { LogicFunctionSourceBuilderService } from './logic-function-source-builder.service';
@Module({
providers: [LogicFunctionSourceBuilderService],
exports: [LogicFunctionSourceBuilderService],
})
export class LogicFunctionSourceBuilderModule {}
@@ -0,0 +1,375 @@
import { Injectable } from '@nestjs/common';
import crypto from 'crypto';
import fs from 'fs/promises';
import { dirname, join } from 'path';
import { isObject } from '@sniptt/guards';
import { build } from 'esbuild';
import { FileFolder, Sources } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { LambdaBuildDirectoryManager } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/lambda-build-directory-manager';
import {
getLogicFunctionBaseFolderPath,
getRelativePathFromBase,
} from 'src/engine/core-modules/logic-function/logic-function-source-builder/utils/get-logic-function-handler-path.util';
import {
getLogicFunctionSeedProjectFiles,
LogicFunctionSeedProjectFile,
} from 'src/engine/core-modules/logic-function/logic-function-source-builder/utils/get-logic-function-seed-project-files.util';
import {
DEFAULT_BUILT_HANDLER_PATH,
DEFAULT_SOURCE_HANDLER_PATH,
} from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import {
LogicFunctionException,
LogicFunctionExceptionCode,
} from 'src/engine/metadata-modules/logic-function/logic-function.exception';
type SeedSourceFilesParams = {
logicFunctionId: string;
workspaceId: string;
applicationUniversalIdentifier: string;
code?: Sources;
};
type SeedSourceFilesResult = {
sourceHandlerPath: string;
builtHandlerPath: string;
checksum: string;
};
type UpdateSourceFilesParams = {
sourceHandlerPath: string;
workspaceId: string;
applicationUniversalIdentifier: string;
code: Sources;
};
type BuildFromSourceParams = {
sourceHandlerPath: string;
builtHandlerPath: string;
workspaceId: string;
applicationUniversalIdentifier: string;
};
type GetSourceCodeParams = {
sourceHandlerPath: string;
workspaceId: string;
applicationUniversalIdentifier: string;
};
type CopySourceAndBuiltParams = {
fromSourceHandlerPath: string;
fromBuiltHandlerPath: string;
toSourceHandlerPath: string;
toBuiltHandlerPath: string;
workspaceId: string;
applicationUniversalIdentifier: string;
};
@Injectable()
export class LogicFunctionSourceBuilderService {
constructor(private readonly fileStorageService: FileStorageService) {}
async seedSourceFiles({
logicFunctionId,
workspaceId,
applicationUniversalIdentifier,
code,
}: SeedSourceFilesParams): Promise<SeedSourceFilesResult> {
const sourceHandlerPath = `${logicFunctionId}/${DEFAULT_SOURCE_HANDLER_PATH}`;
const builtHandlerPath = `${logicFunctionId}/${DEFAULT_BUILT_HANDLER_PATH}`;
if (isDefined(code)) {
// Use provided code
await this.updateSourceFiles({
sourceHandlerPath,
workspaceId,
applicationUniversalIdentifier,
code,
});
const { checksum } = await this.buildFromSource({
sourceHandlerPath,
builtHandlerPath,
workspaceId,
applicationUniversalIdentifier,
});
return {
sourceHandlerPath,
builtHandlerPath,
checksum,
};
}
// Use seed project files
const seedProjectFiles = await getLogicFunctionSeedProjectFiles();
const sourceFiles = seedProjectFiles.filter(
(file: LogicFunctionSeedProjectFile) => file.name.endsWith('index.ts'),
);
const builtFiles = seedProjectFiles.filter(
(file: LogicFunctionSeedProjectFile) => file.name.endsWith('.mjs'),
);
if (sourceFiles.length !== 1 || builtFiles.length !== 1) {
throw new LogicFunctionException(
'Logic function seed project should have one index.ts file and one index.mjs file',
LogicFunctionExceptionCode.LOGIC_FUNCTION_INVALID_SEED_PROJECT,
);
}
const sourceFile = sourceFiles[0];
const builtFile = builtFiles[0];
await this.fileStorageService.writeFile_v2({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: sourceHandlerPath,
sourceFile: sourceFile.content,
mimeType: 'application/typescript',
settings: {
isTemporaryFile: false,
toDelete: false,
},
});
await this.fileStorageService.writeFile_v2({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.BuiltLogicFunction,
resourcePath: builtHandlerPath,
sourceFile: builtFile.content,
mimeType: 'application/javascript',
settings: {
isTemporaryFile: false,
toDelete: false,
},
});
const checksum = crypto
.createHash('md5')
.update(builtFile.content)
.digest('hex');
return {
sourceHandlerPath,
builtHandlerPath,
checksum,
};
}
async updateSourceFiles({
sourceHandlerPath,
workspaceId,
applicationUniversalIdentifier,
code,
}: UpdateSourceFilesParams): Promise<void> {
const lambdaBuildDirectoryManager = new LambdaBuildDirectoryManager();
try {
const { sourceTemporaryDir } = await lambdaBuildDirectoryManager.init();
await this.writeSourcesToLocalFolder(code, sourceTemporaryDir);
const baseFolderPath = getLogicFunctionBaseFolderPath(sourceHandlerPath);
await this.fileStorageService.uploadFolder_v2({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: baseFolderPath,
localPath: sourceTemporaryDir,
});
} finally {
await lambdaBuildDirectoryManager.clean();
}
}
async buildFromSource({
sourceHandlerPath,
builtHandlerPath,
workspaceId,
applicationUniversalIdentifier,
}: BuildFromSourceParams): Promise<{ checksum: string }> {
const lambdaBuildDirectoryManager = new LambdaBuildDirectoryManager();
try {
const { sourceTemporaryDir } = await lambdaBuildDirectoryManager.init();
const baseFolderPath = getLogicFunctionBaseFolderPath(sourceHandlerPath);
await this.fileStorageService.downloadFolder_v2({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: baseFolderPath,
localPath: sourceTemporaryDir,
});
const relativeSourcePath = getRelativePathFromBase(
sourceHandlerPath,
baseFolderPath,
);
const relativeBuiltPath = getRelativePathFromBase(
builtHandlerPath,
baseFolderPath,
);
const builtBundleFilePath = await this.buildInMemory({
sourceTemporaryDir,
sourceHandlerPath: relativeSourcePath,
builtHandlerPath: relativeBuiltPath,
});
const builtFile = await fs.readFile(builtBundleFilePath, 'utf-8');
await this.fileStorageService.writeFile_v2({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.BuiltLogicFunction,
resourcePath: builtHandlerPath,
sourceFile: builtFile,
mimeType: 'application/javascript',
settings: {
isTemporaryFile: false,
toDelete: false,
},
});
return {
checksum: crypto.createHash('md5').update(builtFile).digest('hex'),
};
} finally {
await lambdaBuildDirectoryManager.clean();
}
}
async getSourceCode({
sourceHandlerPath,
workspaceId,
applicationUniversalIdentifier,
}: GetSourceCodeParams): Promise<Sources | null> {
const baseFolderPath = getLogicFunctionBaseFolderPath(sourceHandlerPath);
try {
return await this.fileStorageService.readFolder_v2({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: baseFolderPath,
});
} catch (error) {
if (
isDefined(error) &&
typeof error === 'object' &&
'code' in error &&
error.code === FileStorageExceptionCode.FILE_NOT_FOUND
) {
return null;
}
throw error;
}
}
async copySourceAndBuilt({
fromSourceHandlerPath,
fromBuiltHandlerPath,
toSourceHandlerPath,
toBuiltHandlerPath,
workspaceId,
applicationUniversalIdentifier,
}: CopySourceAndBuiltParams): Promise<void> {
const fromSourceBaseFolderPath = getLogicFunctionBaseFolderPath(
fromSourceHandlerPath,
);
const toSourceBaseFolderPath =
getLogicFunctionBaseFolderPath(toSourceHandlerPath);
const fromBuiltBaseFolderPath =
getLogicFunctionBaseFolderPath(fromBuiltHandlerPath);
const toBuiltBaseFolderPath =
getLogicFunctionBaseFolderPath(toBuiltHandlerPath);
await this.fileStorageService.copy_v2({
from: {
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: fromSourceBaseFolderPath,
},
to: {
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: toSourceBaseFolderPath,
},
});
await this.fileStorageService.copy_v2({
from: {
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.BuiltLogicFunction,
resourcePath: fromBuiltBaseFolderPath,
},
to: {
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.BuiltLogicFunction,
resourcePath: toBuiltBaseFolderPath,
},
});
}
private async writeSourcesToLocalFolder(
sources: Sources,
localPath: string,
): Promise<void> {
for (const key of Object.keys(sources)) {
const filePath = join(localPath, key);
const value = sources[key];
if (isObject(value)) {
await this.writeSourcesToLocalFolder(value as Sources, filePath);
continue;
}
await fs.mkdir(dirname(filePath), { recursive: true });
await fs.writeFile(filePath, value);
}
}
private async buildInMemory({
sourceTemporaryDir,
sourceHandlerPath,
builtHandlerPath,
}: {
sourceTemporaryDir: string;
sourceHandlerPath: string;
builtHandlerPath: string;
}): Promise<string> {
const entryFilePath = join(sourceTemporaryDir, sourceHandlerPath);
const builtBundleFilePath = join(sourceTemporaryDir, builtHandlerPath);
await fs.mkdir(dirname(builtBundleFilePath), { recursive: true });
await build({
entryPoints: [entryFilePath],
outfile: builtBundleFilePath,
platform: 'node',
format: 'esm',
target: 'es2017',
bundle: true,
sourcemap: true,
packages: 'external',
});
return builtBundleFilePath;
}
}
@@ -0,0 +1,45 @@
import fs from 'fs/promises';
import path from 'path';
import { ASSET_PATH } from 'src/constants/assets-path';
export type LogicFunctionSeedProjectFile = {
name: string;
path: string;
content: Buffer;
};
const getAllFiles = async (
rootDir: string,
dir: string = rootDir,
files: LogicFunctionSeedProjectFile[] = [],
): Promise<LogicFunctionSeedProjectFile[]> => {
const dirEntries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of dirEntries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
await getAllFiles(rootDir, fullPath, files);
} else {
files.push({
path: path.relative(rootDir, dir),
name: entry.name,
content: await fs.readFile(fullPath),
});
}
}
return files;
};
export const getLogicFunctionSeedProjectFiles = async (): Promise<
LogicFunctionSeedProjectFile[]
> => {
const seedProjectPath = path.join(
ASSET_PATH,
'engine/core-modules/logic-function/logic-function-source-builder/constants/seed-project',
);
return getAllFiles(seedProjectPath);
};
@@ -2,9 +2,9 @@ import { type DynamicModule, Global, Module } from '@nestjs/common';
import { type LogicFunctionExecutorModuleAsyncOptions } from 'src/engine/core-modules/logic-function/logic-function-executor/interfaces/logic-function-executor.interface';
import { LogicFunctionBuildModule } from 'src/engine/core-modules/logic-function/logic-function-build/logic-function-build.module';
import { LogicFunctionDriversModule } from 'src/engine/core-modules/logic-function/logic-function-drivers/logic-function-drivers.module';
import { LogicFunctionExecutorModule } from 'src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.module';
import { LogicFunctionSourceBuilderModule } from 'src/engine/core-modules/logic-function/logic-function-source-builder/logic-function-source-builder.module';
import { LogicFunctionTriggerModule } from 'src/engine/core-modules/logic-function/logic-function-trigger/logic-function-trigger.module';
@Global()
@@ -18,13 +18,13 @@ export class LogicFunctionModule {
imports: [
LogicFunctionDriversModule.forRootAsync(options),
LogicFunctionExecutorModule,
LogicFunctionBuildModule,
LogicFunctionSourceBuilderModule,
LogicFunctionTriggerModule,
],
exports: [
LogicFunctionDriversModule,
LogicFunctionExecutorModule,
LogicFunctionBuildModule,
LogicFunctionSourceBuilderModule,
LogicFunctionTriggerModule,
],
};
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { LogicFunctionExecutorModule } from 'src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.module';
import { ToolModule } from 'src/engine/core-modules/tool/tool.module';
import { WorkflowTriggerController } from 'src/engine/core-modules/workflow/controllers/workflow-trigger.controller';
import { WorkflowBuilderResolver } from 'src/engine/core-modules/workflow/resolvers/workflow-builder.resolver';
@@ -10,9 +11,12 @@ import { WorkflowVersionEdgeResolver } from 'src/engine/core-modules/workflow/re
import { WorkflowVersionStepResolver } from 'src/engine/core-modules/workflow/resolvers/workflow-version-step.resolver';
import { WorkflowVersionResolver } from 'src/engine/core-modules/workflow/resolvers/workflow-version.resolver';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
import { WorkflowBuilderModule } from 'src/modules/workflow/workflow-builder/workflow-builder.module';
import { CodeStepBuildModule } from 'src/modules/workflow/workflow-builder/workflow-version-step/code-step/code-step-build.module';
import { WorkflowVersionModule } from 'src/modules/workflow/workflow-builder/workflow-version/workflow-version.module';
import { WorkflowRunModule } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.module';
import { WorkflowRunnerModule } from 'src/modules/workflow/workflow-runner/workflow-runner.module';
@@ -30,6 +34,10 @@ import { WorkflowTriggerModule } from 'src/modules/workflow/workflow-trigger/wor
WorkflowRunnerModule,
PermissionsModule,
ToolModule,
LogicFunctionExecutorModule,
LogicFunctionModule,
CodeStepBuildModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
],
controllers: [WorkflowTriggerController],
providers: [
@@ -3,29 +3,34 @@ import { Injectable } from '@nestjs/common';
import { ALL_FLAT_ENTITY_MAPS_PROPERTIES } from 'src/engine/metadata-modules/flat-entity/constant/all-flat-entity-maps-properties.constant';
import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { type WorkspaceCacheDataMap } from 'src/engine/workspace-cache/types/workspace-cache-key.type';
export type FlatEntityMapsCacheKeyName =
| keyof AllFlatEntityMaps
| 'flatApplicationMaps';
@Injectable()
export class WorkspaceManyOrAllFlatEntityMapsCacheService {
constructor(private readonly workspaceCacheService: WorkspaceCacheService) {}
public async getOrRecomputeManyOrAllFlatEntityMaps<
T extends (keyof AllFlatEntityMaps)[] = (keyof AllFlatEntityMaps)[],
TWithCustomMapsProperties extends boolean = true,
T extends FlatEntityMapsCacheKeyName[] = (keyof AllFlatEntityMaps)[],
>({
flatMapsKeys,
workspaceId,
}: {
workspaceId: string;
flatMapsKeys?: T;
}): Promise<Pick<AllFlatEntityMaps<TWithCustomMapsProperties>, T[number]>> {
}): Promise<Pick<WorkspaceCacheDataMap, T[number]>> {
return await this.workspaceCacheService.getOrRecompute(
workspaceId,
flatMapsKeys ?? ALL_FLAT_ENTITY_MAPS_PROPERTIES,
(flatMapsKeys ??
ALL_FLAT_ENTITY_MAPS_PROPERTIES) as (keyof WorkspaceCacheDataMap)[],
);
}
public async invalidateFlatEntityMaps<
T extends (keyof AllFlatEntityMaps)[] = (keyof AllFlatEntityMaps)[],
T extends FlatEntityMapsCacheKeyName[] = (keyof AllFlatEntityMaps)[],
>({
flatMapsKeys,
workspaceId,
@@ -35,12 +40,13 @@ export class WorkspaceManyOrAllFlatEntityMapsCacheService {
}): Promise<void> {
await this.workspaceCacheService.invalidateAndRecompute(
workspaceId,
flatMapsKeys ?? ALL_FLAT_ENTITY_MAPS_PROPERTIES,
(flatMapsKeys ??
ALL_FLAT_ENTITY_MAPS_PROPERTIES) as (keyof WorkspaceCacheDataMap)[],
);
}
public async flushFlatEntityMaps<
T extends (keyof AllFlatEntityMaps)[] = (keyof AllFlatEntityMaps)[],
T extends FlatEntityMapsCacheKeyName[] = (keyof AllFlatEntityMaps)[],
>({
flatMapsKeys,
workspaceId,
@@ -50,7 +56,8 @@ export class WorkspaceManyOrAllFlatEntityMapsCacheService {
}): Promise<void> {
await this.workspaceCacheService.flush(
workspaceId,
flatMapsKeys ?? ALL_FLAT_ENTITY_MAPS_PROPERTIES,
(flatMapsKeys ??
ALL_FLAT_ENTITY_MAPS_PROPERTIES) as (keyof WorkspaceCacheDataMap)[],
);
}
}
@@ -38,6 +38,12 @@ export class CreateLogicFunctionInput {
@HideField()
universalIdentifier?: string;
@HideField()
id?: string;
@HideField()
checksum?: string;
@Field(() => graphqlTypeJson, { nullable: true })
@IsObject()
@IsOptional()
@@ -1,12 +1,18 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsNotEmpty, IsObject, IsUUID } from 'class-validator';
import {
IsBoolean,
IsNotEmpty,
IsObject,
IsOptional,
IsUUID,
} from 'class-validator';
import graphqlTypeJson from 'graphql-type-json';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@InputType()
export class ExecuteLogicFunctionInput {
export class ExecuteOneLogicFunctionInput {
@Field(() => UUIDScalarType, {
description: 'Id of the logic function to execute',
})
@@ -19,4 +25,12 @@ export class ExecuteLogicFunctionInput {
})
@IsObject()
payload: JSON;
@Field(() => Boolean, {
description: 'Force rebuild from source before executing',
nullable: true,
})
@IsOptional()
@IsBoolean()
forceRebuild?: boolean;
}
@@ -0,0 +1,25 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsNotEmpty, IsObject, IsUUID } from 'class-validator';
import graphqlTypeJson from 'graphql-type-json';
import type { Sources } from 'twenty-shared/types';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@InputType()
export class UpdateLogicFunctionSourceInput {
@Field(() => UUIDScalarType, {
description: 'The id of the logic function.',
})
@IsNotEmpty()
@IsUUID()
id!: string;
@Field(() => graphqlTypeJson, {
description:
'The source code (Sources) to write. Only updates source files.',
})
@IsObject()
code!: Sources;
}
@@ -38,9 +38,10 @@ class UpdateLogicFunctionInputUpdates {
@IsOptional()
timeoutSeconds?: number;
@Field(() => graphqlTypeJson)
@Field(() => graphqlTypeJson, { nullable: true })
@IsObject()
code: Sources;
@IsOptional()
code?: Sources;
@IsString()
@Field({ nullable: true })
@@ -14,6 +14,7 @@ export enum LogicFunctionExceptionCode {
LOGIC_FUNCTION_CREATE_FAILED = 'LOGIC_FUNCTION_CREATE_FAILED',
LOGIC_FUNCTION_EXECUTION_TIMEOUT = 'LOGIC_FUNCTION_EXECUTION_TIMEOUT',
LOGIC_FUNCTION_DISABLED = 'LOGIC_FUNCTION_DISABLED',
LOGIC_FUNCTION_INVALID_SEED_PROJECT = 'LOGIC_FUNCTION_INVALID_SEED_PROJECT',
}
const getLogicFunctionExceptionUserFriendlyMessage = (
@@ -38,6 +39,8 @@ const getLogicFunctionExceptionUserFriendlyMessage = (
return msg`Function execution timed out.`;
case LogicFunctionExceptionCode.LOGIC_FUNCTION_DISABLED:
return msg`Logic function execution is disabled.`;
case LogicFunctionExceptionCode.LOGIC_FUNCTION_INVALID_SEED_PROJECT:
return msg`Invalid seed project configuration.`;
default:
assertUnreachable(code);
}
@@ -12,6 +12,7 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { LogicFunctionExecutorModule } from 'src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.module';
import { LogicFunctionSourceBuilderModule } from 'src/engine/core-modules/logic-function/logic-function-source-builder/logic-function-source-builder.module';
import { SecretEncryptionModule } from 'src/engine/core-modules/secret-encryption/secret-encryption.module';
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
@@ -40,6 +41,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
WorkspaceMigrationModule,
LogicFunctionLayerModule,
LogicFunctionExecutorModule,
LogicFunctionSourceBuilderModule,
SubscriptionsModule,
WorkspaceCacheModule,
TokenModule,
@@ -5,9 +5,11 @@ import graphqlTypeJson from 'graphql-type-json';
import { PermissionFlagType } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { LogicFunctionExecutorService } from 'src/engine/core-modules/logic-function/logic-function-executor/services/logic-function-executor.service';
import { LogicFunctionSourceBuilderService } from 'src/engine/core-modules/logic-function/logic-function-source-builder/logic-function-source-builder.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { FeatureFlagGuard } from 'src/engine/guards/feature-flag.guard';
@@ -15,14 +17,14 @@ import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.g
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { CreateLogicFunctionInput } from 'src/engine/metadata-modules/logic-function/dtos/create-logic-function.input';
import { ExecuteLogicFunctionInput } from 'src/engine/metadata-modules/logic-function/dtos/execute-logic-function.input';
import { ExecuteOneLogicFunctionInput } from 'src/engine/metadata-modules/logic-function/dtos/execute-logic-function.input';
import { GetLogicFunctionSourceCodeInput } from 'src/engine/metadata-modules/logic-function/dtos/get-logic-function-source-code.input';
import { LogicFunctionExecutionResultDTO } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-execution-result.dto';
import { LogicFunctionIdInput } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-id.input';
import { LogicFunctionLogsDTO } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-logs.dto';
import { LogicFunctionLogsInput } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-logs.input';
import { LogicFunctionDTO } from 'src/engine/metadata-modules/logic-function/dtos/logic-function.dto';
import { UpdateLogicFunctionInput } from 'src/engine/metadata-modules/logic-function/dtos/update-logic-function.input';
import { UpdateLogicFunctionSourceInput } from 'src/engine/metadata-modules/logic-function/dtos/update-logic-function-source.input';
import { LogicFunctionService } from 'src/engine/metadata-modules/logic-function/services/logic-function.service';
import { FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { findFlatLogicFunctionOrThrow } from 'src/engine/metadata-modules/logic-function/utils/find-flat-logic-function-or-throw.util';
@@ -43,10 +45,42 @@ export class LogicFunctionResolver {
constructor(
private readonly logicFunctionService: LogicFunctionService,
private readonly logicFunctionExecutorService: LogicFunctionExecutorService,
private readonly logicFunctionSourceBuilderService: LogicFunctionSourceBuilderService,
private readonly applicationService: ApplicationService,
private readonly subscriptionService: SubscriptionService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
private async getLogicFunctionContext({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}) {
const [{ flatLogicFunctionMaps }, { workspaceCustomFlatApplication }] =
await Promise.all([
this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps({
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
}),
this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
),
]);
const flatLogicFunction = findFlatLogicFunctionOrThrow({
id,
flatLogicFunctionMaps,
});
return {
flatLogicFunction,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
};
}
@Query(() => LogicFunctionDTO)
async findOneLogicFunction(
@Args('input') { id }: LogicFunctionIdInput,
@@ -112,21 +146,6 @@ export class LogicFunctionResolver {
}
}
@Query(() => graphqlTypeJson, { nullable: true })
async getLogicFunctionSourceCode(
@Args('input') input: GetLogicFunctionSourceCodeInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
try {
return await this.logicFunctionExecutorService.getLogicFunctionSourceCode(
workspaceId,
input.id,
);
} catch (error) {
return logicFunctionGraphQLApiExceptionHandler(error);
}
}
@Mutation(() => LogicFunctionDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.WORKFLOWS))
async deleteOneLogicFunction(
@@ -147,28 +166,6 @@ export class LogicFunctionResolver {
}
}
@Mutation(() => LogicFunctionDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.WORKFLOWS))
async updateOneLogicFunction(
@Args('input')
input: UpdateLogicFunctionInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<LogicFunctionDTO> {
try {
const flatLogicFunction = await this.logicFunctionService.updateOne({
id: input.id,
update: input.update,
workspaceId,
});
return fromFlatLogicFunctionToLogicFunctionDto({
flatLogicFunction,
});
} catch (error) {
return logicFunctionGraphQLApiExceptionHandler(error);
}
}
@Mutation(() => LogicFunctionDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.WORKFLOWS))
async createOneLogicFunction(
@@ -193,22 +190,98 @@ export class LogicFunctionResolver {
@Mutation(() => LogicFunctionExecutionResultDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.WORKFLOWS))
async executeOneLogicFunction(
@Args('input') input: ExecuteLogicFunctionInput,
@Args('input') { id, payload, forceRebuild }: ExecuteOneLogicFunctionInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<LogicFunctionExecutionResultDTO> {
try {
if (forceRebuild) {
const { flatLogicFunction, applicationUniversalIdentifier } =
await this.getLogicFunctionContext({ id, workspaceId });
const { checksum } =
await this.logicFunctionSourceBuilderService.buildFromSource({
workspaceId,
applicationUniversalIdentifier,
sourceHandlerPath: flatLogicFunction.sourceHandlerPath,
builtHandlerPath: flatLogicFunction.builtHandlerPath,
});
await this.logicFunctionService.updateChecksum({
id,
checksum,
workspaceId,
});
}
const result =
await this.logicFunctionExecutorService.executeOneLogicFunction({
id,
workspaceId,
payload,
});
return {
data: result.data as LogicFunctionExecutionResultDTO['data'],
logs: result.logs,
duration: result.duration,
status: result.status,
error: result.error
? {
errorType: result.error.errorType,
errorMessage: result.error.errorMessage,
stackTrace: Array.isArray(result.error.stackTrace)
? result.error.stackTrace.join('\n')
: result.error.stackTrace,
}
: undefined,
};
} catch (error) {
return logicFunctionGraphQLApiExceptionHandler(error);
}
}
@Query(() => graphqlTypeJson, { nullable: true })
async getLogicFunctionSourceCode(
@Args('input') { id }: GetLogicFunctionSourceCodeInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
try {
const { id, payload } = input;
const { flatLogicFunction, applicationUniversalIdentifier } =
await this.getLogicFunctionContext({ id, workspaceId });
return await this.logicFunctionExecutorService.executeOneLogicFunction({
id,
return this.logicFunctionSourceBuilderService.getSourceCode({
workspaceId,
payload,
applicationUniversalIdentifier,
sourceHandlerPath: flatLogicFunction.sourceHandlerPath,
});
} catch (error) {
return logicFunctionGraphQLApiExceptionHandler(error);
}
}
@Mutation(() => Boolean)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.WORKFLOWS))
async updateLogicFunctionSource(
@Args('input') { id, code }: UpdateLogicFunctionSourceInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<boolean> {
try {
const { flatLogicFunction, applicationUniversalIdentifier } =
await this.getLogicFunctionContext({ id, workspaceId });
await this.logicFunctionSourceBuilderService.updateSourceFiles({
workspaceId,
applicationUniversalIdentifier,
sourceHandlerPath: flatLogicFunction.sourceHandlerPath,
code,
});
return true;
} catch (error) {
return logicFunctionGraphQLApiExceptionHandler(error);
}
}
@Subscription(() => LogicFunctionLogsDTO, {
filter: (
payload: { logicFunctionLogs: LogicFunctionLogsDTO },
@@ -1,12 +1,11 @@
import { Injectable } from '@nestjs/common';
import { FileFolder } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { getLogicFunctionBaseFolderPath } from 'src/engine/core-modules/logic-function/logic-function-build/utils/get-logic-function-base-folder-path.util';
import { LogicFunctionSourceBuilderService } from 'src/engine/core-modules/logic-function/logic-function-source-builder/logic-function-source-builder.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
@@ -32,6 +31,7 @@ export class LogicFunctionService {
private readonly applicationService: ApplicationService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly fileStorageService: FileStorageService,
private readonly logicFunctionSourceBuilderService: LogicFunctionSourceBuilderService,
) {}
async createOne({
@@ -52,6 +52,7 @@ export class LogicFunctionService {
)
).workspaceCustomFlatApplication;
// Generate flat logic function to get the ID
const flatLogicFunctionToCreate =
fromCreateLogicFunctionInputToFlatLogicFunction({
createLogicFunctionInput: input,
@@ -59,6 +60,21 @@ export class LogicFunctionService {
ownerFlatApplication: resolvedOwnerFlatApplication,
});
// Seed the source files (uses provided code or default seed project)
const { sourceHandlerPath, builtHandlerPath, checksum } =
await this.logicFunctionSourceBuilderService.seedSourceFiles({
logicFunctionId: flatLogicFunctionToCreate.id,
workspaceId,
applicationUniversalIdentifier:
resolvedOwnerFlatApplication.universalIdentifier,
code: input.code,
});
// Update paths and checksum with actual values from seeding
flatLogicFunctionToCreate.sourceHandlerPath = sourceHandlerPath;
flatLogicFunctionToCreate.builtHandlerPath = builtHandlerPath;
flatLogicFunctionToCreate.checksum = checksum;
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
@@ -83,18 +99,7 @@ export class LogicFunctionService {
);
}
const { flatLogicFunctionMaps: recomputedExistingFlatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatLogicFunctionToCreate.id,
flatEntityMaps: recomputedExistingFlatLogicFunctionMaps,
});
return flatLogicFunctionToCreate;
}
async updateOne({
@@ -154,6 +159,74 @@ export class LogicFunctionService {
);
}
return this.getFlatLogicFunctionAfterUpdate(
optimisticallyUpdatedFlatLogicFunction.id,
workspaceId,
);
}
async updateChecksum({
id,
checksum,
workspaceId,
}: {
id: string;
checksum: string;
workspaceId: string;
}) {
const resolvedOwnerFlatApplication = (
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
)
).workspaceCustomFlatApplication;
const { flatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
const flatLogicFunction = findFlatLogicFunctionOrThrow({
id,
flatLogicFunctionMaps,
});
const optimisticallyUpdatedFlatLogicFunction = {
...flatLogicFunction,
checksum,
};
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
logicFunction: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [optimisticallyUpdatedFlatLogicFunction],
},
},
workspaceId,
isSystemBuild: false,
applicationUniversalIdentifier:
resolvedOwnerFlatApplication.universalIdentifier,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating logic function checksum',
);
}
}
private async getFlatLogicFunctionAfterUpdate(
id: string,
workspaceId: string,
) {
const { flatLogicFunctionMaps: recomputedExistingFlatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
@@ -163,7 +236,7 @@ export class LogicFunctionService {
);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: optimisticallyUpdatedFlatLogicFunction.id,
flatEntityId: id,
flatEntityMaps: recomputedExistingFlatLogicFunctionMaps,
});
}
@@ -235,118 +308,4 @@ export class LogicFunctionService {
return existingFlatLogicFunction;
}
async duplicateLogicFunction({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}): Promise<FlatLogicFunction> {
return this.createLogicFunctionFromExistingLogicFunctionById({
id,
workspaceId,
});
}
async createLogicFunctionFromExistingLogicFunctionById({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}): Promise<FlatLogicFunction> {
const { flatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
const existingLogicFunction = findFlatLogicFunctionOrThrow({
id,
flatLogicFunctionMaps,
});
return this.createLogicFunctionFromExistingLogicFunction({
existingLogicFunction,
workspaceId,
});
}
async createLogicFunctionFromExistingLogicFunction({
existingLogicFunction,
workspaceId,
}: {
existingLogicFunction: FlatLogicFunction;
workspaceId: string;
}): Promise<FlatLogicFunction> {
const { flatApplicationMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatApplicationMaps',
]);
const existingApplicationUniversalIdentifier = isDefined(
existingLogicFunction.applicationId,
)
? flatApplicationMaps.byId[existingLogicFunction.applicationId]
?.universalIdentifier
: undefined;
if (!isDefined(existingApplicationUniversalIdentifier)) {
throw new LogicFunctionException(
`Application universal identifier not found for logic function ${existingLogicFunction.id}`,
LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
);
}
const newFlatLogicFunction = await this.createOne({
input: {
name: existingLogicFunction.name,
description: existingLogicFunction.description ?? undefined,
timeoutSeconds: existingLogicFunction.timeoutSeconds,
},
workspaceId,
applicationId: existingLogicFunction.applicationId ?? undefined,
});
const newApplicationUniversalIdentifier = isDefined(
newFlatLogicFunction.applicationId,
)
? flatApplicationMaps.byId[newFlatLogicFunction.applicationId]
?.universalIdentifier
: undefined;
if (!isDefined(newApplicationUniversalIdentifier)) {
throw new LogicFunctionException(
`Application universal identifier not found for logic function ${newFlatLogicFunction.id}`,
LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
);
}
const fromBaseFolderPath = getLogicFunctionBaseFolderPath(
existingLogicFunction.sourceHandlerPath,
);
const toBaseFolderPath = getLogicFunctionBaseFolderPath(
newFlatLogicFunction.sourceHandlerPath,
);
await this.fileStorageService.copy_v2({
from: {
workspaceId,
applicationUniversalIdentifier: existingApplicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: fromBaseFolderPath,
},
to: {
workspaceId,
applicationUniversalIdentifier: newApplicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: toBaseFolderPath,
},
});
return newFlatLogicFunction;
}
}
@@ -26,7 +26,7 @@ export const fromCreateLogicFunctionInputToFlatLogicFunction = ({
workspaceId,
ownerFlatApplication,
}: FromCreateLogicFunctionInputToFlatLogicFunctionArgs): FlatLogicFunction => {
const id = v4();
const id = rawCreateLogicFunctionInput.id ?? v4();
const currentDate = new Date();
// Build full paths including the base folder
@@ -41,6 +41,14 @@ export const fromCreateLogicFunctionInputToFlatLogicFunction = ({
const universalIdentifier =
rawCreateLogicFunctionInput.universalIdentifier ?? v4();
const checksum = isDefined(rawCreateLogicFunctionInput.checksum)
? rawCreateLogicFunctionInput.checksum
: rawCreateLogicFunctionInput?.code
? logicFunctionCreateHash(
JSON.stringify(rawCreateLogicFunctionInput.code),
)
: null;
return {
id,
cronTriggerSettings: null,
@@ -61,11 +69,7 @@ export const fromCreateLogicFunctionInputToFlatLogicFunction = ({
timeoutSeconds: rawCreateLogicFunctionInput.timeoutSeconds ?? 300,
workspaceId,
code: rawCreateLogicFunctionInput?.code,
checksum: rawCreateLogicFunctionInput?.code
? logicFunctionCreateHash(
JSON.stringify(rawCreateLogicFunctionInput.code),
)
: null,
checksum,
// If no schema provided and no code provided, use default schema
// (because the default template will be used)
toolInputSchema: isDefined(rawCreateLogicFunctionInput?.toolInputSchema)
@@ -1,5 +1,6 @@
import {
extractAndSanitizeObjectStringFields,
isDefined,
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties,
} from 'twenty-shared/utils';
@@ -28,17 +29,19 @@ export const fromUpdateLogicFunctionInputToFlatLogicFunctionToUpdateOrThrow = ({
id: logicFunctionToUpdateId,
flatLogicFunctionMaps,
});
const update = rawUpdateLogicFunctionInput.update;
const updatedEditableFieldProperties = {
...extractAndSanitizeObjectStringFields(
{
...rawUpdateLogicFunctionInput.update,
checksum: logicFunctionCreateHash(
JSON.stringify(rawUpdateLogicFunctionInput.update.code),
),
...update,
...(isDefined(update.code) && {
checksum: logicFunctionCreateHash(JSON.stringify(update.code)),
}),
},
FLAT_LOGIC_FUNCTION_EDITABLE_PROPERTIES,
),
code: rawUpdateLogicFunctionInput.update.code,
...(isDefined(update.code) && { code: update.code }),
};
return mergeUpdateInExistingRecord({
@@ -27,6 +27,7 @@ export const logicFunctionGraphQLApiExceptionHandler = (error: any) => {
throw new TimeoutError(error);
case LogicFunctionExceptionCode.LOGIC_FUNCTION_CODE_UNCHANGED:
case LogicFunctionExceptionCode.LOGIC_FUNCTION_CREATE_FAILED:
case LogicFunctionExceptionCode.LOGIC_FUNCTION_INVALID_SEED_PROJECT:
throw error;
case LogicFunctionExceptionCode.LOGIC_FUNCTION_DISABLED:
throw new ForbiddenError(error);
@@ -1,40 +1,27 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import crypto from 'crypto';
import { Repository } from 'typeorm';
import { FileFolder } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { getSeedProjectFiles } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/get-seed-project-files';
import { getLogicFunctionBaseFolderPath } from 'src/engine/core-modules/logic-function/logic-function-build/utils/get-logic-function-base-folder-path.util';
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import {
LogicFunctionException,
LogicFunctionExceptionCode,
} from 'src/engine/metadata-modules/logic-function/logic-function.exception';
import { FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { FlatCreateLogicFunctionAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/logic-function/types/workspace-migration-logic-function-action.type';
import {
WorkspaceMigrationActionRunnerArgs,
WorkspaceMigrationActionRunnerContext,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
import { FlatCreateLogicFunctionAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/logic-function/types/workspace-migration-logic-function-action.type';
@Injectable()
export class CreateLogicFunctionActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'create',
'logicFunction',
) {
constructor(
private readonly fileStorageService: FileStorageService,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
) {
constructor(private readonly fileStorageService: FileStorageService) {
super();
}
@@ -47,18 +34,30 @@ export class CreateLogicFunctionActionHandlerService extends WorkspaceMigrationR
async executeForMetadata(
context: WorkspaceMigrationActionRunnerContext<FlatCreateLogicFunctionAction>,
): Promise<void> {
const { flatAction, queryRunner, workspaceId } = context;
const { flatAction, queryRunner, workspaceId, flatApplication } = context;
const { flatEntity: logicFunction } = flatAction;
const applicationUniversalIdentifier =
await this.getApplicationUniversalIdentifier(logicFunction.applicationId);
const applicationUniversalIdentifier = flatApplication.universalIdentifier;
let seedChecksum: string | undefined;
if (!isDefined(logicFunction.checksum)) {
seedChecksum = await this.seedLogicFunctionFiles(
logicFunction,
const [sourceExists, builtExists] = await Promise.all([
this.fileStorageService.checkFileExists_v2({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: logicFunction.sourceHandlerPath,
}),
this.fileStorageService.checkFileExists_v2({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.BuiltLogicFunction,
resourcePath: logicFunction.builtHandlerPath,
}),
]);
if (!sourceExists || !builtExists) {
throw new LogicFunctionException(
`Logic function source or built file missing before create (source: ${sourceExists}, built: ${builtExists})`,
LogicFunctionExceptionCode.LOGIC_FUNCTION_CREATE_FAILED,
);
}
@@ -70,103 +69,16 @@ export class CreateLogicFunctionActionHandlerService extends WorkspaceMigrationR
await logicFunctionRepository.insert({
...logicFunction,
workspaceId,
checksum: seedChecksum ?? logicFunction.checksum,
});
}
private async getApplicationUniversalIdentifier(
applicationId: string,
): Promise<string> {
const application = await this.applicationRepository.findOne({
where: { id: applicationId },
select: ['universalIdentifier'],
});
if (!isDefined(application)) {
throw new LogicFunctionException(
`Application with id ${applicationId} not found`,
LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
);
}
return application.universalIdentifier;
}
private async seedLogicFunctionFiles(
logicFunction: FlatLogicFunction,
applicationUniversalIdentifier: string,
): Promise<string> {
const seedProjectFiles = await getSeedProjectFiles;
const sourceFiles = seedProjectFiles.filter((file) =>
file.name.endsWith('index.ts'),
);
const builtFiles = seedProjectFiles.filter((file) =>
file.name.endsWith('.mjs'),
);
if (sourceFiles.length !== 1 || builtFiles.length !== 1) {
throw new LogicFunctionException(
'Seed project should have one index.ts file and one index.mjs file',
LogicFunctionExceptionCode.LOGIC_FUNCTION_CREATE_FAILED,
);
}
const sourceFile = sourceFiles[0];
const builtFile = builtFiles[0];
await this.fileStorageService.writeFile_v2({
workspaceId: logicFunction.workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: logicFunction.sourceHandlerPath,
sourceFile: sourceFile.content,
mimeType: 'application/typescript',
settings: {
isTemporaryFile: false,
toDelete: false,
},
});
await this.fileStorageService.writeFile_v2({
workspaceId: logicFunction.workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.BuiltLogicFunction,
resourcePath: logicFunction.builtHandlerPath,
sourceFile: builtFile.content,
mimeType: 'application/javascript',
settings: {
isTemporaryFile: false,
toDelete: false,
},
});
return crypto.createHash('md5').update(builtFile.content).digest('hex');
}
async rollbackForMetadata(
context: Omit<
_context: Omit<
WorkspaceMigrationActionRunnerArgs<FlatCreateLogicFunctionAction>,
'queryRunner'
>,
): Promise<void> {
const { action } = context;
const applicationUniversalIdentifier =
await this.getApplicationUniversalIdentifier(
action.flatEntity.applicationId,
);
const baseFolderPath = getLogicFunctionBaseFolderPath(
action.flatEntity.sourceHandlerPath,
);
await this.fileStorageService.delete_v2({
workspaceId: action.flatEntity.workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: baseFolderPath,
});
// Nothing to rollback for now
return;
}
}
@@ -7,7 +7,6 @@ import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-mana
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import { getLogicFunctionBaseFolderPath } from 'src/engine/core-modules/logic-function/logic-function-build/utils/get-logic-function-base-folder-path.util';
import {
FlatDeleteLogicFunctionAction,
UniversalDeleteLogicFunctionAction,
@@ -35,7 +34,13 @@ export class DeleteLogicFunctionActionHandlerService extends WorkspaceMigrationR
async executeForMetadata(
context: WorkspaceMigrationActionRunnerContext<FlatDeleteLogicFunctionAction>,
): Promise<void> {
const { flatAction, queryRunner, workspaceId, allFlatEntityMaps } = context;
const {
flatAction,
queryRunner,
workspaceId,
allFlatEntityMaps,
flatApplication,
} = context;
const flatLogicFunction = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: allFlatEntityMaps.flatLogicFunctionMaps,
@@ -52,18 +57,14 @@ export class DeleteLogicFunctionActionHandlerService extends WorkspaceMigrationR
workspaceId,
});
const sourceBaseFolderPath = getLogicFunctionBaseFolderPath(
flatLogicFunction.sourceHandlerPath,
);
const builtBaseFolderPath = getLogicFunctionBaseFolderPath(
flatLogicFunction.builtHandlerPath,
);
const applicationUniversalIdentifier = flatApplication.universalIdentifier;
const sourceFolderPath = `workspace-${workspaceId}/${FileFolder.Source}/${sourceBaseFolderPath}`;
const builtFolderPath = `workspace-${workspaceId}/${FileFolder.BuiltLogicFunction}/${builtBaseFolderPath}`;
await this.fileStorageService.delete({ folderPath: sourceFolderPath });
await this.fileStorageService.delete({ folderPath: builtFolderPath });
await this.fileStorageService.delete_v2({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.BuiltLogicFunction,
resourcePath: flatLogicFunction.builtHandlerPath,
});
}
async rollbackForMetadata(): Promise<void> {}
@@ -1,21 +1,10 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { promises as fs } from 'fs';
import { dirname, join } from 'path';
import { isObject } from '@sniptt/guards';
import { FileFolder, Sources } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { FileFolder } from 'twenty-shared/types';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { LogicFunctionBuildService } from 'src/engine/core-modules/logic-function/logic-function-build/services/logic-function-build.service';
import { getLogicFunctionBaseFolderPath } from 'src/engine/core-modules/logic-function/logic-function-build/utils/get-logic-function-base-folder-path.util';
import { LambdaBuildDirectoryManager } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/lambda-build-directory-manager';
import { LogicFunctionExecutorService } from 'src/engine/core-modules/logic-function/logic-function-executor/services/logic-function-executor.service';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
@@ -37,9 +26,6 @@ export class UpdateLogicFunctionActionHandlerService extends WorkspaceMigrationR
) {
constructor(
private readonly logicFunctionExecutorService: LogicFunctionExecutorService,
private readonly functionBuildService: LogicFunctionBuildService,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
private readonly fileStorageService: FileStorageService,
) {
super();
@@ -54,8 +40,8 @@ export class UpdateLogicFunctionActionHandlerService extends WorkspaceMigrationR
async executeForMetadata(
context: WorkspaceMigrationActionRunnerContext<FlatUpdateLogicFunctionAction>,
): Promise<void> {
const { flatAction, queryRunner, workspaceId } = context;
const { entityId, code, update } = flatAction;
const { flatAction, queryRunner, flatApplication, workspaceId } = context;
const { entityId, update } = flatAction;
const logicFunctionRepository =
queryRunner.manager.getRepository<LogicFunctionEntity>(
@@ -69,111 +55,42 @@ export class UpdateLogicFunctionActionHandlerService extends WorkspaceMigrationR
flatEntityMaps: context.allFlatEntityMaps.flatLogicFunctionMaps,
});
if (isDefined(update.checksum) && isDefined(code)) {
await this.handleChecksumUpdate({
flatLogicFunction,
code,
});
}
if (update.deletedAt !== undefined) {
await this.handleDeletedAtUpdate({
const applicationUniversalIdentifier = flatApplication.universalIdentifier;
if (update.checksum) {
await this.verifySourceAndBuiltFilesExist({
flatLogicFunction,
applicationUniversalIdentifier,
});
}
}
async handleDeletedAtUpdate({
private async verifySourceAndBuiltFilesExist({
flatLogicFunction,
applicationUniversalIdentifier,
}: {
flatLogicFunction: FlatLogicFunction;
}) {
await this.logicFunctionExecutorService.delete(flatLogicFunction);
}
private async getApplicationUniversalIdentifier(
applicationId: string,
): Promise<string> {
const application = await this.applicationRepository.findOne({
where: { id: applicationId },
select: ['universalIdentifier'],
});
if (!isDefined(application)) {
throw new LogicFunctionException(
`Application with id ${applicationId} not found`,
LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
);
}
return application.universalIdentifier;
}
private async writeSourcesToLocalFolder(
sources: Sources,
localPath: string,
): Promise<void> {
for (const key of Object.keys(sources)) {
const filePath = join(localPath, key);
const value = sources[key];
if (isObject(value)) {
await this.writeSourcesToLocalFolder(value as Sources, filePath);
continue;
}
await fs.mkdir(dirname(filePath), { recursive: true });
await fs.writeFile(filePath, value);
}
}
async handleChecksumUpdate({
flatLogicFunction,
code,
}: {
flatLogicFunction: FlatLogicFunction;
code: Sources;
}) {
const applicationUniversalIdentifier =
await this.getApplicationUniversalIdentifier(
flatLogicFunction.applicationId,
);
const lambdaBuildDirectoryManager = new LambdaBuildDirectoryManager();
try {
const { sourceTemporaryDir } = await lambdaBuildDirectoryManager.init();
await this.writeSourcesToLocalFolder(code, sourceTemporaryDir);
const baseFolderPath = getLogicFunctionBaseFolderPath(
flatLogicFunction.sourceHandlerPath,
);
await this.fileStorageService.uploadFolder_v2({
applicationUniversalIdentifier: string;
}): Promise<void> {
const [sourceExists, builtExists] = await Promise.all([
this.fileStorageService.checkFileExists_v2({
workspaceId: flatLogicFunction.workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: baseFolderPath,
localPath: sourceTemporaryDir,
});
} catch (error) {
this.logger.log(
'workspace-migration-runner',
`Error updating logic function ${flatLogicFunction.id}: ${error.message}`,
);
} finally {
await lambdaBuildDirectoryManager.clean();
}
try {
await this.functionBuildService.buildAndUpload({
flatLogicFunction,
resourcePath: flatLogicFunction.sourceHandlerPath,
}),
this.fileStorageService.checkFileExists_v2({
workspaceId: flatLogicFunction.workspaceId,
applicationUniversalIdentifier,
});
} catch (error) {
// TODO: logic should be improved
this.logger.log(
'workspace-migration-runner',
`Error building and uploading logic function ${flatLogicFunction.id}: ${error.message}`,
fileFolder: FileFolder.BuiltLogicFunction,
resourcePath: flatLogicFunction.builtHandlerPath,
}),
]);
if (!sourceExists || !builtExists) {
throw new LogicFunctionException(
`Logic function source or built file missing before update (source: ${sourceExists}, built: ${builtExists})`,
LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_READY,
);
}
}
@@ -155,6 +155,30 @@ export class WorkspaceMigrationRunnerService {
this.logger.time('Runner', 'Total execution');
this.logger.time('Runner', 'Initial cache retrieval');
const queryRunner = this.coreDataSource.createQueryRunner();
const actionMetadataNames = [
...new Set(actions.flatMap((action) => action.metadataName)),
];
const actionsMetadataAndRelatedMetadataNames: AllMetadataName[] = [
...new Set([
...actionMetadataNames,
...actionMetadataNames.flatMap(getMetadataRelatedMetadataNames),
]),
];
const allFlatEntityMapsKeys = actionsMetadataAndRelatedMetadataNames.map(
getMetadataFlatEntityMapsKey,
);
let allFlatEntityMaps =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps<
typeof allFlatEntityMapsKeys
>({
workspaceId,
flatMapsKeys: allFlatEntityMapsKeys,
});
this.logger.timeEnd('Runner', 'Initial cache retrieval');
const { flatApplicationMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatApplicationMaps',
@@ -175,35 +199,12 @@ export class WorkspaceMigrationRunnerService {
});
}
const queryRunner = this.coreDataSource.createQueryRunner();
const actionMetadataNames = [
...new Set(actions.flatMap((action) => action.metadataName)),
];
const actionsMetadataAndRelatedMetadataNames: AllMetadataName[] = [
...new Set([
...actionMetadataNames,
...actionMetadataNames.flatMap(getMetadataRelatedMetadataNames),
]),
];
const allFlatEntityMapsKeys = actionsMetadataAndRelatedMetadataNames.map(
getMetadataFlatEntityMapsKey,
);
let allFlatEntityMaps =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps<
typeof allFlatEntityMapsKeys,
false
>({
workspaceId,
flatMapsKeys: allFlatEntityMapsKeys,
});
this.logger.timeEnd('Runner', 'Initial cache retrieval');
this.logger.time('Runner', 'Transaction execution');
try {
await queryRunner.connect();
await queryRunner.startTransaction();
for (const action of actions) {
const result =
await this.workspaceMigrationRunnerActionHandlerRegistry.executeActionHandler(
@@ -222,7 +223,7 @@ export class WorkspaceMigrationRunnerService {
allFlatEntityMaps = {
...allFlatEntityMaps,
...result,
};
} as typeof allFlatEntityMaps;
}
await queryRunner.commitTransaction();
@@ -4,9 +4,10 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { RecordPositionModule } from 'src/engine/core-modules/record-position/record-position.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
import { CodeStepBuildModule } from 'src/modules/workflow/workflow-builder/workflow-version-step/code-step/code-step-build.module';
import { WorkflowCreateManyPostQueryHook } from 'src/modules/workflow/common/query-hooks/workflow-create-many.post-query.hook';
import { WorkflowCreateManyPreQueryHook } from 'src/modules/workflow/common/query-hooks/workflow-create-many.pre-query.hook';
import { WorkflowCreateOnePostQueryHook } from 'src/modules/workflow/common/query-hooks/workflow-create-one.post-query.hook';
@@ -41,6 +42,7 @@ import { WorkflowVersionValidationWorkspaceService } from 'src/modules/workflow/
RecordPositionModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
ObjectMetadataModule,
CodeStepBuildModule,
],
providers: [
WorkflowCreateOnePreQueryHook,
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
import { CodeStepBuildModule } from 'src/modules/workflow/workflow-builder/workflow-version-step/code-step/code-step-build.module';
import { WorkflowQueryHookModule } from 'src/modules/workflow/common/query-hooks/workflow-query-hook.module';
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
@@ -10,6 +11,7 @@ import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/work
WorkflowQueryHookModule,
LogicFunctionModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
CodeStepBuildModule,
],
providers: [WorkflowCommonWorkspaceService],
exports: [WorkflowCommonWorkspaceService],
@@ -2,12 +2,12 @@ import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { LogicFunctionService } from 'src/engine/metadata-modules/logic-function/services/logic-function.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
@@ -1,6 +1,7 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
import { AiAgentRoleService } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.service';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { createEmptyAllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-all-flat-entity-maps.constant';
@@ -14,6 +15,7 @@ import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
import { CodeStepBuildService } from 'src/modules/workflow/workflow-builder/workflow-version-step/code-step/services/code-step-build.service';
import { WorkflowVersionStepOperationsWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service';
import {
type WorkflowAction,
@@ -26,6 +28,8 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
let service: WorkflowVersionStepOperationsWorkspaceService;
let globalWorkspaceOrmManager: jest.Mocked<GlobalWorkspaceOrmManager>;
let logicFunctionService: jest.Mocked<LogicFunctionService>;
let applicationService: jest.Mocked<ApplicationService>;
let codeStepBuildService: jest.Mocked<CodeStepBuildService>;
let agentRepository: jest.Mocked<any>;
let roleTargetRepository: jest.Mocked<any>;
let roleRepository: jest.Mocked<any>;
@@ -33,12 +37,53 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
let workflowCommonWorkspaceService: jest.Mocked<WorkflowCommonWorkspaceService>;
let aiAgentRoleService: jest.Mocked<AiAgentRoleService>;
let workspaceCacheService: jest.Mocked<WorkspaceCacheService>;
let flatEntityMapsCacheService: jest.Mocked<WorkspaceManyOrAllFlatEntityMapsCacheService>;
beforeEach(async () => {
applicationService = {
findWorkspaceTwentyStandardAndCustomApplicationOrThrow: jest
.fn()
.mockResolvedValue({
workspaceCustomFlatApplication: {
universalIdentifier: 'app-universal-id',
},
}),
} as unknown as jest.Mocked<ApplicationService>;
codeStepBuildService = {
seedCodeStepFiles: jest.fn().mockResolvedValue({
sourceHandlerPath: 'workflow/logic-fn-id/src/index.ts',
builtHandlerPath: 'workflow/logic-fn-id/src/index.mjs',
checksum: 'seed-checksum',
}),
copySourceAndBuiltForNewCodeStep: jest.fn().mockResolvedValue(undefined),
duplicateCodeStepLogicFunction: jest.fn().mockResolvedValue({
id: 'new-function-id',
name: 'Test Function',
description: 'Test Description',
workspaceId: mockWorkspaceId,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
runtime: LogicFunctionRuntime.NODE22,
timeoutSeconds: 30,
sourceHandlerPath: 'src/index.ts',
builtHandlerPath: 'index.mjs',
handlerName: 'main',
checksum: null,
toolInputSchema: null,
isTool: false,
universalIdentifier: 'universal-id',
applicationId: 'application-id',
cronTriggerSettings: null,
databaseEventTriggerSettings: null,
httpRouteTriggerSettings: null,
}),
} as unknown as jest.Mocked<CodeStepBuildService>;
logicFunctionService = {
createOne: jest.fn(),
destroyOne: jest.fn(),
duplicateLogicFunction: jest.fn(),
} as unknown as jest.Mocked<LogicFunctionService>;
agentRepository = {
@@ -86,6 +131,14 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
provide: LogicFunctionService,
useValue: logicFunctionService,
},
{
provide: ApplicationService,
useValue: applicationService,
},
{
provide: CodeStepBuildService,
useValue: codeStepBuildService,
},
{
provide: getRepositoryToken(AgentEntity),
useValue: agentRepository,
@@ -116,12 +169,12 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
},
{
provide: WorkspaceManyOrAllFlatEntityMapsCacheService,
useValue: {
useValue: (flatEntityMapsCacheService = {
flushFlatEntityMaps: jest.fn(),
getOrRecomputeManyOrAllFlatEntityMaps: jest
.fn()
.mockResolvedValue(createEmptyAllFlatEntityMaps()),
},
} as unknown as jest.Mocked<WorkspaceManyOrAllFlatEntityMapsCacheService>),
},
],
}).compile();
@@ -310,6 +363,29 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
nextStepIds: ['next-step'],
} as unknown as WorkflowAction;
const mockExistingFlatLogicFunction: FlatLogicFunction = {
id: 'function-id',
name: 'Existing Function',
description: 'Existing Description',
workspaceId: mockWorkspaceId,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
runtime: LogicFunctionRuntime.NODE22,
timeoutSeconds: 30,
sourceHandlerPath: 'workflow/function-id/src/index.ts',
builtHandlerPath: 'workflow/function-id/src/index.mjs',
handlerName: 'main',
checksum: 'existing-checksum',
toolInputSchema: null,
isTool: false,
universalIdentifier: 'existing-universal-id',
applicationId: 'application-id',
cronTriggerSettings: null,
databaseEventTriggerSettings: null,
httpRouteTriggerSettings: null,
};
const mockNewFlatLogicFunction: FlatLogicFunction = {
id: 'new-function-id',
name: 'Test Function',
@@ -333,7 +409,25 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
httpRouteTriggerSettings: null,
};
logicFunctionService.duplicateLogicFunction.mockResolvedValue(
const emptyMaps = createEmptyAllFlatEntityMaps();
const flatLogicFunctionMapsKey = 'flatLogicFunctionMaps' as const;
flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValueOnce(
{
...emptyMaps,
[flatLogicFunctionMapsKey]: {
byUniversalIdentifier: {
'existing-universal-id': mockExistingFlatLogicFunction,
},
universalIdentifierById: {
'function-id': 'existing-universal-id',
},
universalIdentifiersByApplicationId: {},
},
},
);
logicFunctionService.createOne.mockResolvedValue(
mockNewFlatLogicFunction,
);
@@ -358,6 +452,12 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
expect(codeResult.settings.input.logicFunctionId).toBe('new-function-id');
expect(duplicateStep.nextStepIds).toEqual([]);
expect(
codeStepBuildService.duplicateCodeStepLogicFunction,
).toHaveBeenCalledWith({
existingLogicFunctionId: 'function-id',
workspaceId: mockWorkspaceId,
});
});
it('should duplicate non-code step', async () => {
@@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
import { CodeStepBuildService } from './services/code-step-build.service';
@Module({
imports: [
ApplicationModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
LogicFunctionModule,
],
providers: [CodeStepBuildService],
exports: [CodeStepBuildService],
})
export class CodeStepBuildModule {}
@@ -0,0 +1,4 @@
export const CODE_STEP_DEFAULT_INPUT_SCHEMA = {
a: null,
b: null,
};
@@ -0,0 +1,8 @@
var main = async (params) => {
const { a, b } = params;
const message = `Hello, input: ${a} and ${b}`;
return { message };
};
export {
main
};
@@ -0,0 +1,12 @@
export const main = async (params: {
a: string;
b: number;
}): Promise<object> => {
const { a, b } = params;
// Rename the parameters and code below with your own logic
// This is just an example
const message = `Hello, input: ${a} and ${b}`;
return { message };
};
@@ -0,0 +1,222 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
import { LogicFunctionSourceBuilderService } from 'src/engine/core-modules/logic-function/logic-function-source-builder/logic-function-source-builder.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { LogicFunctionService } from 'src/engine/metadata-modules/logic-function/services/logic-function.service';
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { findFlatLogicFunctionOrThrow } from 'src/engine/metadata-modules/logic-function/utils/find-flat-logic-function-or-throw.util';
import { fromCreateLogicFunctionInputToFlatLogicFunction } from 'src/engine/metadata-modules/logic-function/utils/from-create-logic-function-input-to-flat-logic-function.util';
import {
WorkflowActionType,
type WorkflowAction,
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
const WORKFLOW_BASE_FOLDER_PREFIX = 'workflow';
@Injectable()
export class CodeStepBuildService {
constructor(
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly logicFunctionService: LogicFunctionService,
private readonly applicationService: ApplicationService,
private readonly logicFunctionSourceBuilderService: LogicFunctionSourceBuilderService,
) {}
async duplicateCodeStepLogicFunction({
existingLogicFunctionId,
workspaceId,
}: {
existingLogicFunctionId: string;
workspaceId: string;
}): Promise<FlatLogicFunction> {
const { flatLogicFunctionMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
const existingLogicFunction = findFlatLogicFunctionOrThrow({
id: existingLogicFunctionId,
flatLogicFunctionMaps,
});
const resolvedOwnerFlatApplication = (
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
)
).workspaceCustomFlatApplication;
const applicationUniversalIdentifier =
resolvedOwnerFlatApplication.universalIdentifier;
const newId = v4();
const newFlatLogicFunction =
fromCreateLogicFunctionInputToFlatLogicFunction({
createLogicFunctionInput: {
name: existingLogicFunction.name,
description: existingLogicFunction.description ?? undefined,
timeoutSeconds: existingLogicFunction.timeoutSeconds,
id: newId,
},
workspaceId,
ownerFlatApplication: resolvedOwnerFlatApplication,
});
await this.logicFunctionSourceBuilderService.copySourceAndBuilt({
fromSourceHandlerPath: existingLogicFunction.sourceHandlerPath,
fromBuiltHandlerPath: existingLogicFunction.builtHandlerPath,
toSourceHandlerPath: newFlatLogicFunction.sourceHandlerPath,
toBuiltHandlerPath: newFlatLogicFunction.builtHandlerPath,
workspaceId,
applicationUniversalIdentifier,
});
const created = await this.logicFunctionService.createOne({
input: {
name: existingLogicFunction.name,
description: existingLogicFunction.description ?? undefined,
timeoutSeconds: existingLogicFunction.timeoutSeconds,
id: newFlatLogicFunction.id,
sourceHandlerPath: newFlatLogicFunction.sourceHandlerPath,
builtHandlerPath: newFlatLogicFunction.builtHandlerPath,
checksum: existingLogicFunction.checksum ?? undefined,
},
workspaceId,
ownerFlatApplication: resolvedOwnerFlatApplication,
});
if (!isDefined(created)) {
throw new Error(
'Failed to create logic function when duplicating code step',
);
}
return created;
}
async buildCodeStepsFromSourceForSteps({
workspaceId,
steps,
}: {
workspaceId: string;
steps: WorkflowAction[];
}): Promise<void> {
const codeSteps = steps.filter(
(
step,
): step is WorkflowAction & {
type: typeof WorkflowActionType.CODE;
settings: { input: { logicFunctionId: string } };
} =>
step.type === WorkflowActionType.CODE &&
isDefined(
(step.settings?.input as { logicFunctionId?: string })
?.logicFunctionId,
),
);
if (codeSteps.length === 0) {
return;
}
const { flatLogicFunctionMaps, flatApplicationMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps', 'flatApplicationMaps'],
},
);
for (const step of codeSteps) {
const logicFunctionId = step.settings.input.logicFunctionId;
const flatLogicFunction = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: logicFunctionId,
flatEntityMaps: flatLogicFunctionMaps,
});
if (
!isDefined(flatLogicFunction) ||
flatLogicFunction.deletedAt ||
!this.isWorkflowCodeStepLogicFunction(flatLogicFunction)
) {
continue;
}
const applicationUniversalIdentifier = isDefined(
flatLogicFunction.applicationId,
)
? flatApplicationMaps.byId[flatLogicFunction.applicationId]
?.universalIdentifier
: undefined;
if (!isDefined(applicationUniversalIdentifier)) {
continue;
}
const { checksum } =
await this.logicFunctionSourceBuilderService.buildFromSource({
sourceHandlerPath: flatLogicFunction.sourceHandlerPath,
builtHandlerPath: flatLogicFunction.builtHandlerPath,
workspaceId,
applicationUniversalIdentifier,
});
await this.logicFunctionService.updateChecksum({
id: flatLogicFunction.id,
checksum,
workspaceId,
});
}
}
isWorkflowCodeStepLogicFunction(
flatLogicFunction: FlatLogicFunction,
): boolean {
return (
flatLogicFunction.sourceHandlerPath.startsWith(
`${WORKFLOW_BASE_FOLDER_PREFIX}/`,
) ||
flatLogicFunction.builtHandlerPath.startsWith(
`${WORKFLOW_BASE_FOLDER_PREFIX}/`,
)
);
}
async getFlatLogicFunctionForCodeStepOrNull({
logicFunctionId,
workspaceId,
}: {
logicFunctionId: string;
workspaceId: string;
}): Promise<FlatLogicFunction | null> {
const { flatLogicFunctionMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
const flatLogicFunction = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: logicFunctionId,
flatEntityMaps: flatLogicFunctionMaps,
});
if (
!isDefined(flatLogicFunction) ||
flatLogicFunction.deletedAt ||
!this.isWorkflowCodeStepLogicFunction(flatLogicFunction)
) {
return null;
}
return flatLogicFunction;
}
}
@@ -0,0 +1,12 @@
import { dirname } from 'path';
export const getLogicFunctionBaseFolderPath = (handlerPath: string): string => {
return dirname(dirname(handlerPath));
};
export const getRelativePathFromBase = (
handlerPath: string,
baseFolderPath: string,
): string => {
return handlerPath.replace(`${baseFolderPath}/`, '');
};
@@ -1,15 +1,19 @@
import fs from 'fs/promises';
import path, { join } from 'path';
import path from 'path';
import { ASSET_PATH } from 'src/constants/assets-path';
type File = { name: string; path: string; content: Buffer };
export type CodeStepSeedProjectFile = {
name: string;
path: string;
content: Buffer;
};
const getAllFiles = async (
rootDir: string,
dir: string = rootDir,
files: File[] = [],
): Promise<File[]> => {
files: CodeStepSeedProjectFile[] = [],
): Promise<CodeStepSeedProjectFile[]> => {
const dirEntries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of dirEntries) {
@@ -29,11 +33,13 @@ const getAllFiles = async (
return files;
};
export const getSeedProjectFiles = (async () => {
const seedProjectPath = join(
export const getCodeStepSeedProjectFiles = async (): Promise<
CodeStepSeedProjectFile[]
> => {
const seedProjectPath = path.join(
ASSET_PATH,
`engine/core-modules/logic-function/logic-function-drivers/constants/seed-project`,
'modules/workflow/workflow-builder/workflow-version-step/code-step/constants/seed-project',
);
return await getAllFiles(seedProjectPath);
})();
return getAllFiles(seedProjectPath);
};
@@ -15,7 +15,7 @@ import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
import { SEED_PROJECT_INPUT_SCHEMA } from 'src/engine/core-modules/logic-function/logic-function-drivers/constants/seed-project-input-schema';
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
import { type WorkflowStepPositionInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-step-position-input.dto';
import { AiAgentRoleService } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.service';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
@@ -28,6 +28,8 @@ import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-t
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { CODE_STEP_DEFAULT_INPUT_SCHEMA } from 'src/modules/workflow/workflow-builder/workflow-version-step/code-step/constants/seed-project/code-step-default-input-schema';
import { CodeStepBuildService } from 'src/modules/workflow/workflow-builder/workflow-version-step/code-step/services/code-step-build.service';
import {
WorkflowVersionStepException,
WorkflowVersionStepExceptionCode,
@@ -65,6 +67,8 @@ export class WorkflowVersionStepOperationsWorkspaceService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly logicFunctionService: LogicFunctionService,
private readonly applicationService: ApplicationService,
private readonly codeStepBuildService: CodeStepBuildService,
@InjectRepository(AgentEntity)
private readonly agentRepository: Repository<AgentEntity>,
@InjectRepository(RoleTargetEntity)
@@ -151,8 +155,12 @@ export class WorkflowVersionStepOperationsWorkspaceService {
switch (type) {
case WorkflowActionType.CODE: {
const logicFunctionId = id ?? v4();
// createOne handles seeding source files with default seed project
const newLogicFunction = await this.logicFunctionService.createOne({
input: {
id: logicFunctionId,
name: 'A Logic Function Code Workflow Step',
description: '',
},
@@ -184,7 +192,7 @@ export class WorkflowVersionStepOperationsWorkspaceService {
},
input: {
logicFunctionId: newLogicFunction.id,
logicFunctionInput: SEED_PROJECT_INPUT_SCHEMA,
logicFunctionInput: CODE_STEP_DEFAULT_INPUT_SCHEMA,
},
},
},
@@ -655,8 +663,8 @@ export class WorkflowVersionStepOperationsWorkspaceService {
switch (step.type) {
case WorkflowActionType.CODE: {
const newLogicFunction =
await this.logicFunctionService.duplicateLogicFunction({
id: step.settings.input.logicFunctionId,
await this.codeStepBuildService.duplicateCodeStepLogicFunction({
existingLogicFunctionId: step.settings.input.logicFunctionId,
workspaceId,
});
@@ -928,12 +936,10 @@ export class WorkflowVersionStepOperationsWorkspaceService {
switch (step.type) {
case WorkflowActionType.CODE: {
const newLogicFunction =
await this.logicFunctionService.createLogicFunctionFromExistingLogicFunctionById(
{
id: step.settings.input.logicFunctionId,
workspaceId,
},
);
await this.codeStepBuildService.duplicateCodeStepLogicFunction({
existingLogicFunctionId: step.settings.input.logicFunctionId,
workspaceId,
});
return {
...step,
@@ -2,10 +2,12 @@ import { Module } from '@nestjs/common';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { AiAgentRoleModule } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.module';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
import { CodeStepBuildModule } from 'src/modules/workflow/workflow-builder/workflow-version-step/code-step/code-step-build.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
@@ -24,6 +26,8 @@ import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workfl
WorkflowSchemaModule,
LogicFunctionModule,
WorkflowCommonModule,
ApplicationModule,
CodeStepBuildModule,
AiAgentRoleModule,
WorkspaceCacheModule,
NestjsQueryTypeOrmModule.forFeature([
@@ -46,6 +50,7 @@ import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workfl
WorkflowVersionStepWorkspaceService,
WorkflowVersionStepOperationsWorkspaceService,
WorkflowVersionStepHelpersWorkspaceService,
CodeStepBuildModule,
],
})
export class WorkflowVersionStepModule {}
@@ -11,6 +11,7 @@ import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspac
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
import { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
import { CodeStepBuildService } from 'src/modules/workflow/workflow-builder/workflow-version-step/code-step/services/code-step-build.service';
import { WorkflowExecutorWorkspaceService } from 'src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service';
import { RUN_WORKFLOW_JOB_NAME } from 'src/modules/workflow/workflow-runner/constants/run-workflow-job-name';
import {
@@ -25,6 +26,7 @@ import { WorkflowTriggerType } from 'src/modules/workflow/workflow-trigger/types
export class RunWorkflowJob {
constructor(
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
private readonly codeStepBuildService: CodeStepBuildService,
private readonly workflowExecutorWorkspaceService: WorkflowExecutorWorkspaceService,
private readonly workflowRunWorkspaceService: WorkflowRunWorkspaceService,
private readonly metricsService: MetricsService,
@@ -97,6 +99,11 @@ export class RunWorkflowJob {
);
}
await this.codeStepBuildService.buildCodeStepsFromSourceForSteps({
workspaceId,
steps: workflowVersion.steps,
});
await this.workflowRunWorkspaceService.startWorkflowRun({
workflowRunId,
workspaceId,
@@ -4,8 +4,10 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { CommandMenuItemModule } from 'src/engine/metadata-modules/command-menu-item/command-menu-item.module';
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
import { CodeStepBuildModule } from 'src/modules/workflow/workflow-builder/workflow-version-step/code-step/code-step-build.module';
import { WorkflowRunnerModule } from 'src/modules/workflow/workflow-runner/workflow-runner.module';
import { AutomatedTriggerModule } from 'src/modules/workflow/workflow-trigger/automated-trigger/automated-trigger.module';
import { WorkflowTriggerJob } from 'src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job';
@@ -14,11 +16,13 @@ import { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-t
@Module({
imports: [
WorkflowCommonModule,
CodeStepBuildModule,
WorkflowRunnerModule,
AutomatedTriggerModule,
CommandMenuItemModule,
FeatureFlagModule,
NestjsQueryTypeOrmModule.forFeature([ObjectMetadataEntity]),
LogicFunctionModule,
],
providers: [WorkflowTriggerWorkspaceService, WorkflowTriggerJob],
exports: [WorkflowTriggerWorkspaceService],
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import { msg } from '@lingui/core/macro';
import { type ActorMetadata } from 'twenty-shared/types';
import { isNonEmptyString } from '@sniptt/guards';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
@@ -19,6 +20,7 @@ import {
import { type WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
import { assertWorkflowVersionTriggerIsDefined } from 'src/modules/workflow/common/utils/assert-workflow-version-trigger-is-defined.util';
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
import { CodeStepBuildService } from 'src/modules/workflow/workflow-builder/workflow-version-step/code-step/services/code-step-build.service';
import { WorkflowRunnerWorkspaceService } from 'src/modules/workflow/workflow-runner/workspace-services/workflow-runner.workspace-service';
import { WORKFLOW_VERSION_STATUS_UPDATED } from 'src/modules/workflow/workflow-status/constants/workflow-version-status-updated.constants';
import { type WorkflowVersionStatusUpdate } from 'src/modules/workflow/workflow-status/jobs/workflow-statuses-update.job';
@@ -41,6 +43,7 @@ export class WorkflowTriggerWorkspaceService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
private readonly codeStepBuildService: CodeStepBuildService,
private readonly workflowRunnerWorkspaceService: WorkflowRunnerWorkspaceService,
private readonly automatedTriggerWorkspaceService: AutomatedTriggerWorkspaceService,
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
@@ -121,6 +124,11 @@ export class WorkflowTriggerWorkspaceService {
assertVersionCanBeActivated(workflowVersion, workflow);
await this.codeStepBuildService.buildCodeStepsFromSourceForSteps({
workspaceId,
steps: workflowVersion.steps ?? [],
});
await this.performActivationSteps(
workflow,
workflowVersion,
@@ -376,7 +384,9 @@ export class WorkflowTriggerWorkspaceService {
await this.commandMenuItemService.create(
{
workflowVersionId: workflowVersion.id,
label: workflow.name ?? 'Manual Trigger',
label: isNonEmptyString(workflow.name)
? workflow.name
: 'Manual Trigger',
icon: trigger.settings.icon,
availabilityType,
availabilityObjectMetadataId,
@@ -194,6 +194,9 @@ describe('Successful user and workspace creation', () => {
input: {
name: 'test-function-for-deletion',
description: 'A test logic function for workspace deletion test',
code: {
'src/index.ts': 'export const main = async () => { return {}; };',
},
},
token: newWorkspaceAccessToken,
expectToFail: false,
@@ -1,10 +1,16 @@
import { createOneLogicFunction } from 'test/integration/metadata/suites/logic-function/utils/create-one-logic-function.util';
import { deleteLogicFunction } from 'test/integration/metadata/suites/logic-function/utils/delete-logic-function.util';
import { executeLogicFunction } from 'test/integration/metadata/suites/logic-function/utils/execute-logic-function.util';
import { updateLogicFunction } from 'test/integration/metadata/suites/logic-function/utils/update-logic-function.util';
import { LogicFunctionExecutionStatus } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-execution-result.dto';
// Default template function code that matches the expected behavior
const DEFAULT_TEMPLATE_FUNCTION_CODE = {
'src/index.ts': `export const main = async (params: { a: string; b: number }): Promise<object> => {
return { message: \`Hello, input: \${params.a} and \${params.b}\` };
};`,
};
// Test function using external packages from default layer (lodash.groupby)
const EXTERNAL_PACKAGES_FUNCTION_CODE = {
'src/index.ts': `import groupBy from 'lodash.groupby';
@@ -46,10 +52,11 @@ describe('Logic Function Execution', () => {
});
it('should execute the default logic function template', async () => {
// Create the function (uses default template)
// Create the function with default template code
const { data: createData } = await createOneLogicFunction({
input: {
name: 'Test Default Function',
code: DEFAULT_TEMPLATE_FUNCTION_CODE,
},
expectToFail: false,
});
@@ -82,10 +89,11 @@ describe('Logic Function Execution', () => {
});
it('should execute a function with external packages (lodash.groupby)', async () => {
// Create the function (uses default template initially)
// Create the function with the external packages code
const { data: createData } = await createOneLogicFunction({
input: {
name: 'External Packages Test',
code: EXTERNAL_PACKAGES_FUNCTION_CODE,
},
expectToFail: false,
});
@@ -95,18 +103,6 @@ describe('Logic Function Execution', () => {
expect(functionId).toBeDefined();
createdFunctionIds.push(functionId);
// Update with custom code using external packages
await updateLogicFunction({
input: {
id: functionId,
update: {
name: 'External Packages Test',
code: EXTERNAL_PACKAGES_FUNCTION_CODE,
},
},
expectToFail: false,
});
// Execute the function with items to group
const { data: executeData } = await executeLogicFunction({
input: {
@@ -148,10 +144,11 @@ describe('Logic Function Execution', () => {
});
it('should handle errors thrown by logic functions', async () => {
// Create the function
// Create the function with error-throwing code
const { data: createData } = await createOneLogicFunction({
input: {
name: 'Error Test Function',
code: ERROR_FUNCTION_CODE,
},
expectToFail: false,
});
@@ -161,18 +158,6 @@ describe('Logic Function Execution', () => {
expect(functionId).toBeDefined();
createdFunctionIds.push(functionId);
// Update with error-throwing code
await updateLogicFunction({
input: {
id: functionId,
update: {
name: 'Error Test Function',
code: ERROR_FUNCTION_CODE,
},
},
expectToFail: false,
});
// Execute with shouldFail = false (should succeed)
const { data: successData } = await executeLogicFunction({
input: {
@@ -21,7 +21,7 @@ export const executeLogicFunctionQueryFactory = ({
gqlFields?: string;
}) => ({
query: gql`
mutation ExecuteOneLogicFunction($input: ExecuteLogicFunctionInput!) {
mutation ExecuteOneLogicFunction($input: ExecuteOneLogicFunctionInput!) {
executeOneLogicFunction(input: $input) {
${gqlFields}
}
@@ -1,36 +0,0 @@
import gql from 'graphql-tag';
import { type Sources } from 'twenty-shared/types';
export type UpdateLogicFunctionFactoryInput = {
id: string;
update: {
name: string;
description?: string;
code: Sources;
};
};
const DEFAULT_LOGIC_FUNCTION_GQL_FIELDS = `
id
name
description
`;
export const updateLogicFunctionQueryFactory = ({
input,
gqlFields = DEFAULT_LOGIC_FUNCTION_GQL_FIELDS,
}: {
input: UpdateLogicFunctionFactoryInput;
gqlFields?: string;
}) => ({
query: gql`
mutation UpdateOneLogicFunction($input: UpdateLogicFunctionInput!) {
updateOneLogicFunction(input: $input) {
${gqlFields}
}
}
`,
variables: {
input,
},
});
@@ -1,47 +0,0 @@
import {
type UpdateLogicFunctionFactoryInput,
updateLogicFunctionQueryFactory,
} from 'test/integration/metadata/suites/logic-function/utils/update-logic-function-query-factory.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type LogicFunctionDTO } from 'src/engine/metadata-modules/logic-function/dtos/logic-function.dto';
export const updateLogicFunction = async ({
input,
gqlFields,
expectToFail = false,
token,
}: {
input: UpdateLogicFunctionFactoryInput;
gqlFields?: string;
expectToFail?: boolean;
token?: string;
}): CommonResponseBody<{
updateOneLogicFunction: LogicFunctionDTO;
}> => {
const graphqlOperation = updateLogicFunctionQueryFactory({
input,
gqlFields,
});
const response = await makeMetadataAPIRequest(graphqlOperation, token);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Logic Function update should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'Logic Function update has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};