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:
@@ -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) {
|
||||
|
||||
@@ -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
-1
@@ -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
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const UPDATE_LOGIC_FUNCTION_SOURCE = gql`
|
||||
mutation UpdateLogicFunctionSource($input: UpdateLogicFunctionSourceInput!) {
|
||||
updateLogicFunctionSource(input: $input)
|
||||
}
|
||||
`;
|
||||
+7
@@ -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 };
|
||||
};
|
||||
+34
@@ -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 };
|
||||
};
|
||||
+26
-20
@@ -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 };
|
||||
};
|
||||
-11
@@ -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
|
||||
}
|
||||
}
|
||||
`;
|
||||
-9
@@ -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)
|
||||
}
|
||||
`;
|
||||
+1
-15
@@ -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 }),
|
||||
{
|
||||
|
||||
-27
@@ -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 };
|
||||
};
|
||||
-29
@@ -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 };
|
||||
};
|
||||
+2
-47
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
+28
-23
@@ -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}
|
||||
/>,
|
||||
]
|
||||
: []
|
||||
|
||||
+6
-5
@@ -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';
|
||||
|
||||
+17
-41
@@ -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,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"
|
||||
},
|
||||
{
|
||||
|
||||
+34
@@ -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,
|
||||
|
||||
-3
@@ -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) {
|
||||
|
||||
-9
@@ -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 {}
|
||||
-160
@@ -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;
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const NODE_LAYER_SUBFOLDER = 'nodejs';
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
export const SEED_PROJECT_INPUT_SCHEMA = {
|
||||
a: null,
|
||||
b: null,
|
||||
};
|
||||
+8
-10
@@ -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 {
|
||||
|
||||
+3
-7
@@ -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,
|
||||
|
||||
-2
@@ -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]),
|
||||
],
|
||||
|
||||
+29
-72
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -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 {}
|
||||
+375
@@ -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;
|
||||
}
|
||||
}
|
||||
+45
@@ -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);
|
||||
};
|
||||
+3
-3
@@ -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: [
|
||||
|
||||
+15
-8
@@ -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)[],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -38,6 +38,12 @@ export class CreateLogicFunctionInput {
|
||||
@HideField()
|
||||
universalIdentifier?: string;
|
||||
|
||||
@HideField()
|
||||
id?: string;
|
||||
|
||||
@HideField()
|
||||
checksum?: string;
|
||||
|
||||
@Field(() => graphqlTypeJson, { nullable: true })
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
|
||||
+16
-2
@@ -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;
|
||||
}
|
||||
|
||||
+25
@@ -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;
|
||||
}
|
||||
+3
-2
@@ -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 })
|
||||
|
||||
+3
@@ -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);
|
||||
}
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+117
-44
@@ -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 },
|
||||
|
||||
+88
-129
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+10
-6
@@ -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)
|
||||
|
||||
+8
-5
@@ -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({
|
||||
|
||||
+1
@@ -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);
|
||||
|
||||
+25
-113
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+14
-13
@@ -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> {}
|
||||
|
||||
+27
-110
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+26
-25
@@ -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();
|
||||
|
||||
+3
-1
@@ -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
-2
@@ -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';
|
||||
|
||||
+104
-4
@@ -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 () => {
|
||||
|
||||
+18
@@ -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 {}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export const CODE_STEP_DEFAULT_INPUT_SCHEMA = {
|
||||
a: null,
|
||||
b: null,
|
||||
};
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
var main = async (params) => {
|
||||
const { a, b } = params;
|
||||
const message = `Hello, input: ${a} and ${b}`;
|
||||
return { message };
|
||||
};
|
||||
export {
|
||||
main
|
||||
};
|
||||
+12
@@ -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 };
|
||||
};
|
||||
+222
@@ -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;
|
||||
}
|
||||
}
|
||||
+12
@@ -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}/`, '');
|
||||
};
|
||||
+15
-9
@@ -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);
|
||||
};
|
||||
+16
-10
@@ -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,
|
||||
|
||||
+5
@@ -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
@@ -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],
|
||||
|
||||
+11
-1
@@ -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,
|
||||
|
||||
+3
@@ -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,
|
||||
|
||||
+13
-28
@@ -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: {
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ export const executeLogicFunctionQueryFactory = ({
|
||||
gqlFields?: string;
|
||||
}) => ({
|
||||
query: gql`
|
||||
mutation ExecuteOneLogicFunction($input: ExecuteLogicFunctionInput!) {
|
||||
mutation ExecuteOneLogicFunction($input: ExecuteOneLogicFunctionInput!) {
|
||||
executeOneLogicFunction(input: $input) {
|
||||
${gqlFields}
|
||||
}
|
||||
|
||||
-36
@@ -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,
|
||||
},
|
||||
});
|
||||
-47
@@ -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 };
|
||||
};
|
||||
Reference in New Issue
Block a user