From 17bf2b6173b6a9205540cfc49050cd50dd0c8f13 Mon Sep 17 00:00:00 2001 From: Baptiste Devessier Date: Mon, 6 Jan 2025 14:56:09 +0100 Subject: [PATCH] Move the workflow draft version overriding to the backend (#9328) - In the `formatFieldMetadataValue` function, allow people to call TypeORM's `save()` method with unserialized JSON data. - Create an `overrideWorkflowDraftVersion` mutation that takes a workflow id and the id of the workflow version to use as the new draft - If no draft exists yet, create one - If a draft already exists, deactivate its serverless functions - Duplicate every step. For serverless function steps, it includes duplicating the functions - Save the data of the step in DB - Call the `overrideWorkflowDraftVersion` mutation in the old workflow header and in the new Cmd+K actions - I chose to not update the Apollo cache manually as the information of the new draft are going to be automatically fetched once the user lands on the workflow's show page. Note that we redirect the user to this page after overriding the draft version. --- .../twenty-front/src/generated/graphql.tsx | 51 ++++++ ...DraftWorkflowVersionSingleRecordAction.tsx | 21 +-- ...OverrideWorkflowDraftConfirmationModal.tsx | 21 +-- .../RecordShowPageWorkflowVersionHeader.tsx | 19 +- .../mutations/overrideWorkflowDraftVersion.ts | 9 + .../useCreateDraftFromWorkflowVersion.ts | 23 +++ .../utils/serverless-get-folder.utils.ts | 2 +- ...reate-draft-from-workflow-version-input.ts | 16 ++ .../workflow-version-step.resolver.ts | 33 +++- .../serverless-function.service.ts | 78 ++++++++- .../twenty-orm/utils/format-data.util.ts | 5 +- .../assert-workflow-version-has-steps.ts | 19 ++ ...workflow-version-step.workspace-service.ts | 165 +++++++++++++++++- 13 files changed, 399 insertions(+), 63 deletions(-) create mode 100644 packages/twenty-front/src/modules/workflow/graphql/mutations/overrideWorkflowDraftVersion.ts create mode 100644 packages/twenty-front/src/modules/workflow/hooks/useCreateDraftFromWorkflowVersion.ts create mode 100644 packages/twenty-server/src/engine/core-modules/workflow/dtos/create-draft-from-workflow-version-input.ts create mode 100644 packages/twenty-server/src/modules/workflow/common/utils/assert-workflow-version-has-steps.ts diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 0d3840221..7b237d0f2 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -188,6 +188,13 @@ export type ComputeStepOutputSchemaInput = { step: Scalars['JSON']; }; +export type CreateDraftFromWorkflowVersionInput = { + /** Workflow ID */ + workflowId: Scalars['String']; + /** Workflow version ID */ + workflowVersionIdToCopy: Scalars['String']; +}; + export type CreateFieldInput = { defaultValue?: InputMaybe; description?: InputMaybe; @@ -501,6 +508,7 @@ export type Mutation = { challenge: LoginToken; checkoutSession: SessionEntity; computeStepOutputSchema: Scalars['JSON']; + createDraftFromWorkflowVersion: Scalars['Boolean']; createOIDCIdentityProvider: SetupSsoOutput; createOneAppToken: AppToken; createOneField: Field; @@ -599,6 +607,11 @@ export type MutationComputeStepOutputSchemaArgs = { }; +export type MutationCreateDraftFromWorkflowVersionArgs = { + input: CreateDraftFromWorkflowVersionInput; +}; + + export type MutationCreateOidcIdentityProviderArgs = { input: SetupOidcSsoInput; }; @@ -2177,6 +2190,13 @@ export type DeleteWorkflowVersionStepMutationVariables = Exact<{ export type DeleteWorkflowVersionStepMutation = { __typename?: 'Mutation', deleteWorkflowVersionStep: { __typename?: 'WorkflowAction', id: any, name: string, type: string, settings: any, valid: boolean } }; +export type CreateDraftFromWorkflowVersionMutationVariables = Exact<{ + input: CreateDraftFromWorkflowVersionInput; +}>; + + +export type CreateDraftFromWorkflowVersionMutation = { __typename?: 'Mutation', createDraftFromWorkflowVersion: boolean }; + export type RunWorkflowVersionMutationVariables = Exact<{ input: RunWorkflowVersionInput; }>; @@ -4093,6 +4113,37 @@ export function useDeleteWorkflowVersionStepMutation(baseOptions?: Apollo.Mutati export type DeleteWorkflowVersionStepMutationHookResult = ReturnType; export type DeleteWorkflowVersionStepMutationResult = Apollo.MutationResult; export type DeleteWorkflowVersionStepMutationOptions = Apollo.BaseMutationOptions; +export const CreateDraftFromWorkflowVersionDocument = gql` + mutation CreateDraftFromWorkflowVersion($input: CreateDraftFromWorkflowVersionInput!) { + createDraftFromWorkflowVersion(input: $input) +} + `; +export type CreateDraftFromWorkflowVersionMutationFn = Apollo.MutationFunction; + +/** + * __useCreateDraftFromWorkflowVersionMutation__ + * + * To run a mutation, you first call `useCreateDraftFromWorkflowVersionMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useCreateDraftFromWorkflowVersionMutation` 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 [createDraftFromWorkflowVersionMutation, { data, loading, error }] = useCreateDraftFromWorkflowVersionMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useCreateDraftFromWorkflowVersionMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(CreateDraftFromWorkflowVersionDocument, options); + } +export type CreateDraftFromWorkflowVersionMutationHookResult = ReturnType; +export type CreateDraftFromWorkflowVersionMutationResult = Apollo.MutationResult; +export type CreateDraftFromWorkflowVersionMutationOptions = Apollo.BaseMutationOptions; export const RunWorkflowVersionDocument = gql` mutation RunWorkflowVersion($input: RunWorkflowVersionInput!) { runWorkflowVersion(input: $input) { diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useUseAsDraftWorkflowVersionSingleRecordAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useUseAsDraftWorkflowVersionSingleRecordAction.tsx index 437d16e12..1ea564255 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useUseAsDraftWorkflowVersionSingleRecordAction.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useUseAsDraftWorkflowVersionSingleRecordAction.tsx @@ -3,7 +3,7 @@ import { ActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { buildShowPageURL } from '@/object-record/record-show/utils/buildShowPageURL'; import { OverrideWorkflowDraftConfirmationModal } from '@/workflow/components/OverrideWorkflowDraftConfirmationModal'; -import { useCreateNewWorkflowVersion } from '@/workflow/hooks/useCreateNewWorkflowVersion'; +import { useCreateDraftFromWorkflowVersion } from '@/workflow/hooks/useCreateDraftFromWorkflowVersion'; import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion'; import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; import { openOverrideWorkflowDraftConfirmationModalState } from '@/workflow/states/openOverrideWorkflowDraftConfirmationModalState'; @@ -21,7 +21,8 @@ export const useUseAsDraftWorkflowVersionSingleRecordAction: ActionHookWithoutOb workflowVersion?.workflow?.id ?? '', ); - const { createNewWorkflowVersion } = useCreateNewWorkflowVersion(); + const { createDraftFromWorkflowVersion } = + useCreateDraftFromWorkflowVersion(); const setOpenOverrideWorkflowDraftConfirmationModal = useSetRecoilState( openOverrideWorkflowDraftConfirmationModalState, @@ -45,13 +46,11 @@ export const useUseAsDraftWorkflowVersionSingleRecordAction: ActionHookWithoutOb if (hasAlreadyDraftVersion) { setOpenOverrideWorkflowDraftConfirmationModal(true); } else { - await createNewWorkflowVersion({ + await createDraftFromWorkflowVersion({ workflowId: workflowVersion.workflow.id, - name: `v${workflow.versions.length + 1}`, - status: 'DRAFT', - trigger: workflowVersion.trigger, - steps: workflowVersion.steps, + workflowVersionIdToCopy: workflowVersion.id, }); + navigate( buildShowPageURL( CoreObjectNameSingular.Workflow, @@ -63,12 +62,8 @@ export const useUseAsDraftWorkflowVersionSingleRecordAction: ActionHookWithoutOb const ConfirmationModal = shouldBeRegistered ? ( ) : undefined; diff --git a/packages/twenty-front/src/modules/workflow/components/OverrideWorkflowDraftConfirmationModal.tsx b/packages/twenty-front/src/modules/workflow/components/OverrideWorkflowDraftConfirmationModal.tsx index d5b18fcbe..24a2f7198 100644 --- a/packages/twenty-front/src/modules/workflow/components/OverrideWorkflowDraftConfirmationModal.tsx +++ b/packages/twenty-front/src/modules/workflow/components/OverrideWorkflowDraftConfirmationModal.tsx @@ -1,40 +1,35 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { buildShowPageURL } from '@/object-record/record-show/utils/buildShowPageURL'; import { ConfirmationModal, StyledCenteredButton, } from '@/ui/layout/modal/components/ConfirmationModal'; +import { useCreateDraftFromWorkflowVersion } from '@/workflow/hooks/useCreateDraftFromWorkflowVersion'; import { openOverrideWorkflowDraftConfirmationModalState } from '@/workflow/states/openOverrideWorkflowDraftConfirmationModalState'; -import { WorkflowVersion } from '@/workflow/types/Workflow'; import { useNavigate } from 'react-router-dom'; import { useRecoilState } from 'recoil'; export const OverrideWorkflowDraftConfirmationModal = ({ - draftWorkflowVersionId, - workflowVersionUpdateInput, workflowId, + workflowVersionIdToCopy, }: { - draftWorkflowVersionId: string; - workflowVersionUpdateInput: Pick; workflowId: string; + workflowVersionIdToCopy: string; }) => { const [ openOverrideWorkflowDraftConfirmationModal, setOpenOverrideWorkflowDraftConfirmationModal, ] = useRecoilState(openOverrideWorkflowDraftConfirmationModalState); - const { updateOneRecord: updateOneWorkflowVersion } = - useUpdateOneRecord({ - objectNameSingular: CoreObjectNameSingular.WorkflowVersion, - }); + const { createDraftFromWorkflowVersion } = + useCreateDraftFromWorkflowVersion(); const navigate = useNavigate(); const handleOverrideDraft = async () => { - await updateOneWorkflowVersion({ - idToUpdate: draftWorkflowVersionId, - updateOneRecordInput: workflowVersionUpdateInput, + await createDraftFromWorkflowVersion({ + workflowId, + workflowVersionIdToCopy, }); navigate(buildShowPageURL(CoreObjectNameSingular.Workflow, workflowId)); diff --git a/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowVersionHeader.tsx b/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowVersionHeader.tsx index 77f09463d..e8ab90fc1 100644 --- a/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowVersionHeader.tsx +++ b/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowVersionHeader.tsx @@ -4,7 +4,7 @@ import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; import { buildShowPageURL } from '@/object-record/record-show/utils/buildShowPageURL'; import { OverrideWorkflowDraftConfirmationModal } from '@/workflow/components/OverrideWorkflowDraftConfirmationModal'; import { useActivateWorkflowVersion } from '@/workflow/hooks/useActivateWorkflowVersion'; -import { useCreateNewWorkflowVersion } from '@/workflow/hooks/useCreateNewWorkflowVersion'; +import { useCreateDraftFromWorkflowVersion } from '@/workflow/hooks/useCreateDraftFromWorkflowVersion'; import { useDeactivateWorkflowVersion } from '@/workflow/hooks/useDeactivateWorkflowVersion'; import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion'; import { openOverrideWorkflowDraftConfirmationModalState } from '@/workflow/states/openOverrideWorkflowDraftConfirmationModalState'; @@ -74,7 +74,8 @@ export const RecordShowPageWorkflowVersionHeader = ({ const { activateWorkflowVersion } = useActivateWorkflowVersion(); const { deactivateWorkflowVersion } = useDeactivateWorkflowVersion(); - const { createNewWorkflowVersion } = useCreateNewWorkflowVersion(); + const { createDraftFromWorkflowVersion } = + useCreateDraftFromWorkflowVersion(); const setOpenOverrideWorkflowDraftConfirmationModal = useSetRecoilState( openOverrideWorkflowDraftConfirmationModalState, @@ -94,13 +95,11 @@ export const RecordShowPageWorkflowVersionHeader = ({ if (hasAlreadyDraftVersion) { setOpenOverrideWorkflowDraftConfirmationModal(true); } else { - await createNewWorkflowVersion({ + await createDraftFromWorkflowVersion({ workflowId: workflowVersion.workflow.id, - name: `v${workflowVersion.workflow.versions.length + 1}`, - status: 'DRAFT', - trigger: workflowVersion.trigger, - steps: workflowVersion.steps, + workflowVersionIdToCopy: workflowVersion.id, }); + navigate( buildShowPageURL( CoreObjectNameSingular.Workflow, @@ -140,12 +139,8 @@ export const RecordShowPageWorkflowVersionHeader = ({ {isDefined(workflowVersion) && isDefined(draftWorkflowVersion) ? ( ) : null} diff --git a/packages/twenty-front/src/modules/workflow/graphql/mutations/overrideWorkflowDraftVersion.ts b/packages/twenty-front/src/modules/workflow/graphql/mutations/overrideWorkflowDraftVersion.ts new file mode 100644 index 000000000..10f5e4464 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/graphql/mutations/overrideWorkflowDraftVersion.ts @@ -0,0 +1,9 @@ +import { gql } from '@apollo/client'; + +export const OVERRIDE_WORKFLOW_DRAFT_VERSION = gql` + mutation CreateDraftFromWorkflowVersion( + $input: CreateDraftFromWorkflowVersionInput! + ) { + createDraftFromWorkflowVersion(input: $input) + } +`; diff --git a/packages/twenty-front/src/modules/workflow/hooks/useCreateDraftFromWorkflowVersion.ts b/packages/twenty-front/src/modules/workflow/hooks/useCreateDraftFromWorkflowVersion.ts new file mode 100644 index 000000000..96d480e11 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/hooks/useCreateDraftFromWorkflowVersion.ts @@ -0,0 +1,23 @@ +import { useApolloClient } from '@apollo/client'; +import { + CreateDraftFromWorkflowVersionInput, + useCreateDraftFromWorkflowVersionMutation, +} from '~/generated/graphql'; + +export const useCreateDraftFromWorkflowVersion = () => { + const apolloClient = useApolloClient(); + + const [mutate] = useCreateDraftFromWorkflowVersionMutation({ + client: apolloClient, + }); + + const createDraftFromWorkflowVersion = async ( + input: CreateDraftFromWorkflowVersionInput, + ) => { + await mutate({ variables: { input } }); + }; + + return { + createDraftFromWorkflowVersion, + }; +}; diff --git a/packages/twenty-server/src/engine/core-modules/serverless/utils/serverless-get-folder.utils.ts b/packages/twenty-server/src/engine/core-modules/serverless/utils/serverless-get-folder.utils.ts index 8dc4a97bf..01f24af83 100644 --- a/packages/twenty-server/src/engine/core-modules/serverless/utils/serverless-get-folder.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/serverless/utils/serverless-get-folder.utils.ts @@ -9,7 +9,7 @@ export const getServerlessFolder = ({ version, }: { serverlessFunction: ServerlessFunctionEntity; - version?: string; + version?: 'draft' | 'latest' | (string & NonNullable); }) => { const computedVersion = version === 'latest' ? serverlessFunction.latestVersion : version; diff --git a/packages/twenty-server/src/engine/core-modules/workflow/dtos/create-draft-from-workflow-version-input.ts b/packages/twenty-server/src/engine/core-modules/workflow/dtos/create-draft-from-workflow-version-input.ts new file mode 100644 index 000000000..6b38e22dc --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/workflow/dtos/create-draft-from-workflow-version-input.ts @@ -0,0 +1,16 @@ +import { Field, InputType } from '@nestjs/graphql'; + +@InputType() +export class CreateDraftFromWorkflowVersionInput { + @Field(() => String, { + description: 'Workflow ID', + nullable: false, + }) + workflowId: string; + + @Field(() => String, { + description: 'Workflow version ID', + nullable: false, + }) + workflowVersionIdToCopy: string; +} diff --git a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-version-step.resolver.ts b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-version-step.resolver.ts index d5c254949..2a2257e7c 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-version-step.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-version-step.resolver.ts @@ -1,16 +1,17 @@ import { UseFilters, UseGuards } from '@nestjs/common'; import { Args, Mutation, Resolver } from '@nestjs/graphql'; +import { CreateDraftFromWorkflowVersionInput } from 'src/engine/core-modules/workflow/dtos/create-draft-from-workflow-version-input'; +import { CreateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/create-workflow-version-step-input.dto'; +import { DeleteWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/delete-workflow-version-step-input.dto'; +import { UpdateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-version-step-input.dto'; +import { WorkflowActionDTO } from 'src/engine/core-modules/workflow/dtos/workflow-step.dto'; import { WorkflowTriggerGraphqlApiExceptionFilter } from 'src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; -import { CreateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/create-workflow-version-step-input.dto'; -import { UpdateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-version-step-input.dto'; -import { DeleteWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/delete-workflow-version-step-input.dto'; import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-version-step.workspace-service'; -import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { WorkflowActionDTO } from 'src/engine/core-modules/workflow/dtos/workflow-step.dto'; @Resolver() @UseGuards(WorkspaceAuthGuard, UserAuthGuard) @@ -58,4 +59,24 @@ export class WorkflowVersionStepResolver { stepId, }); } + + @Mutation(() => Boolean) + async createDraftFromWorkflowVersion( + @AuthWorkspace() { id: workspaceId }: Workspace, + @Args('input') + { + workflowId, + workflowVersionIdToCopy, + }: CreateDraftFromWorkflowVersionInput, + ) { + await this.workflowVersionStepWorkspaceService.createDraftFromWorkflowVersion( + { + workspaceId, + workflowId, + workflowVersionIdToCopy, + }, + ); + + return true; + } } diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts index 47167b5ca..7b8034fcf 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts @@ -13,15 +13,23 @@ import { AnalyticsService } from 'src/engine/core-modules/analytics/analytics.se import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; import { readFileContent } from 'src/engine/core-modules/file-storage/utils/read-file-content'; +import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; +import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; +import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; import { ENV_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/env-file-name'; import { INDEX_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/index-file-name'; import { LAST_LAYER_VERSION } from 'src/engine/core-modules/serverless/drivers/layers/last-layer-version'; import { getBaseTypescriptProjectFiles } from 'src/engine/core-modules/serverless/drivers/utils/get-base-typescript-project-files'; +import { getLayerDependencies } from 'src/engine/core-modules/serverless/drivers/utils/get-last-layer-dependencies'; import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service'; import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service'; import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input'; import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input'; +import { + BuildServerlessFunctionBatchEvent, + BuildServerlessFunctionJob, +} from 'src/engine/metadata-modules/serverless-function/jobs/build-serverless-function.job'; import { ServerlessFunctionEntity, ServerlessFunctionSyncStatus, @@ -31,14 +39,6 @@ import { ServerlessFunctionExceptionCode, } from 'src/engine/metadata-modules/serverless-function/serverless-function.exception'; import { isDefined } from 'src/utils/is-defined'; -import { getLayerDependencies } from 'src/engine/core-modules/serverless/drivers/utils/get-last-layer-dependencies'; -import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; -import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; -import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; -import { - BuildServerlessFunctionBatchEvent, - BuildServerlessFunctionJob, -} from 'src/engine/metadata-modules/serverless-function/jobs/build-serverless-function.job'; @Injectable() export class ServerlessFunctionService { @@ -348,6 +348,68 @@ export class ServerlessFunctionService { }); } + async copyOneServerlessFunction({ + serverlessFunctionToCopyId, + serverlessFunctionToCopyVersion, + workspaceId, + }: { + serverlessFunctionToCopyId: string; + serverlessFunctionToCopyVersion: string; + workspaceId: string; + }) { + const serverlessFunctionToCopy = + await this.serverlessFunctionRepository.findOneBy({ + workspaceId, + id: serverlessFunctionToCopyId, + latestVersion: serverlessFunctionToCopyVersion, + }); + + if (!serverlessFunctionToCopy) { + throw new ServerlessFunctionException( + 'Function does not exist', + ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND, + ); + } + + const serverlessFunctionToCreate = this.serverlessFunctionRepository.create( + { + name: serverlessFunctionToCopy?.name, + description: serverlessFunctionToCopy?.description, + workspaceId, + layerVersion: LAST_LAYER_VERSION, + }, + ); + + const copiedServerlessFunction = + await this.serverlessFunctionRepository.save(serverlessFunctionToCreate); + + const serverlessFunctionToCopyFileFolder = getServerlessFolder({ + serverlessFunction: serverlessFunctionToCopy, + version: 'latest', + }); + const copiedServerlessFunctionFileFolder = getServerlessFolder({ + serverlessFunction: copiedServerlessFunction, + version: 'draft', + }); + + await this.fileStorageService.copy({ + from: { + folderPath: serverlessFunctionToCopyFileFolder, + }, + to: { + folderPath: copiedServerlessFunctionFileFolder, + }, + }); + + await this.buildServerlessFunction({ + serverlessFunctionId: copiedServerlessFunction.id, + serverlessFunctionVersion: 'draft', + workspaceId, + }); + + return copiedServerlessFunction; + } + private async throttleExecution(workspaceId: string) { try { await this.throttlerService.throttle( diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts index d856a47b9..9e97811e3 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts @@ -82,7 +82,10 @@ function formatFieldMetadataValue( value: any, fieldMetadata: FieldMetadataInterface, ) { - if (fieldMetadata.type === FieldMetadataType.RAW_JSON) { + if ( + fieldMetadata.type === FieldMetadataType.RAW_JSON && + typeof value === 'string' + ) { return JSON.parse(value as string); } diff --git a/packages/twenty-server/src/modules/workflow/common/utils/assert-workflow-version-has-steps.ts b/packages/twenty-server/src/modules/workflow/common/utils/assert-workflow-version-has-steps.ts new file mode 100644 index 000000000..e7fe6b4c1 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/common/utils/assert-workflow-version-has-steps.ts @@ -0,0 +1,19 @@ +import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; +import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; +import { + WorkflowTriggerException, + WorkflowTriggerExceptionCode, +} from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception'; + +export function assertWorkflowVersionHasSteps( + workflowVersion: WorkflowVersionWorkspaceEntity, +): asserts workflowVersion is WorkflowVersionWorkspaceEntity & { + steps: WorkflowAction[]; +} { + if (workflowVersion.steps === null || workflowVersion.steps.length < 1) { + throw new WorkflowTriggerException( + 'Workflow version does not contain at least one step', + WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION, + ); + } +} diff --git a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-version-step.workspace-service.ts b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-version-step.workspace-service.ts index 467807b47..91d463f4b 100644 --- a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-version-step.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-version-step.workspace-service.ts @@ -13,7 +13,13 @@ import { WorkflowVersionStepException, WorkflowVersionStepExceptionCode, } from 'src/modules/workflow/common/exceptions/workflow-version-step.exception'; -import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; +import { + WorkflowVersionStatus, + WorkflowVersionWorkspaceEntity, +} from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; +import { assertWorkflowVersionHasSteps } from 'src/modules/workflow/common/utils/assert-workflow-version-has-steps'; +import { assertWorkflowVersionIsDraft } from 'src/modules/workflow/common/utils/assert-workflow-version-is-draft.util'; +import { assertWorkflowVersionTriggerIsDefined } from 'src/modules/workflow/common/utils/assert-workflow-version-trigger-is-defined.util'; import { WorkflowBuilderWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-builder.workspace-service'; import { BaseWorkflowActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type'; import { @@ -56,7 +62,7 @@ export class WorkflowVersionStepWorkspaceService { }): Promise { const newStepId = v4(); - switch (`${type}`) { + switch (type) { case WorkflowActionType.CODE: { const newServerlessFunction = await this.serverlessFunctionService.createOneServerlessFunction( @@ -185,6 +191,48 @@ export class WorkflowVersionStepWorkspaceService { } } + private async duplicateStep({ + step, + workspaceId, + }: { + step: WorkflowAction; + workspaceId: string; + }): Promise { + const newStepId = v4(); + + switch (step.type) { + case WorkflowActionType.CODE: { + const copiedServerlessFunction = + await this.serverlessFunctionService.copyOneServerlessFunction({ + serverlessFunctionToCopyId: + step.settings.input.serverlessFunctionId, + serverlessFunctionToCopyVersion: + step.settings.input.serverlessFunctionVersion, + workspaceId, + }); + + return { + ...step, + id: newStepId, + settings: { + ...step.settings, + input: { + ...step.settings.input, + serverlessFunctionId: copiedServerlessFunction.id, + serverlessFunctionVersion: copiedServerlessFunction.latestVersion, + }, + }, + }; + } + default: { + return { + ...step, + id: newStepId, + }; + } + } + } + private async enrichOutputSchema({ step, workspaceId, @@ -363,14 +411,113 @@ export class WorkflowVersionStepWorkspaceService { workflowVersionUpdates, ); - switch (stepToDelete.type) { - case WorkflowActionType.CODE: - await this.serverlessFunctionService.deleteOneServerlessFunction( - stepToDelete.settings.input.serverlessFunctionId, - workspaceId, - ); - } + await this.runWorkflowVersionStepDeletionSideEffects({ + step: stepToDelete, + workspaceId, + }); return stepToDelete; } + + async createDraftFromWorkflowVersion({ + workspaceId, + workflowId, + workflowVersionIdToCopy, + }: { + workspaceId: string; + workflowId: string; + workflowVersionIdToCopy: string; + }) { + const workflowVersionRepository = + await this.twentyORMManager.getRepository( + 'workflowVersion', + ); + + const workflowVersionToCopy = await workflowVersionRepository.findOne({ + where: { + id: workflowVersionIdToCopy, + workflowId, + }, + }); + + if (!isDefined(workflowVersionToCopy)) { + throw new WorkflowVersionStepException( + 'WorkflowVersion to copy not found', + WorkflowVersionStepExceptionCode.NOT_FOUND, + ); + } + + assertWorkflowVersionTriggerIsDefined(workflowVersionToCopy); + assertWorkflowVersionHasSteps(workflowVersionToCopy); + + let draftWorkflowVersion = await workflowVersionRepository.findOne({ + where: { + workflowId, + status: WorkflowVersionStatus.DRAFT, + }, + }); + + if (!isDefined(draftWorkflowVersion)) { + const workflowVersionsCount = await workflowVersionRepository.count({ + where: { + workflowId, + }, + }); + + draftWorkflowVersion = await workflowVersionRepository.save({ + workflowId, + name: `v${workflowVersionsCount + 1}`, + status: WorkflowVersionStatus.DRAFT, + }); + } + + assertWorkflowVersionIsDraft(draftWorkflowVersion); + + if (Array.isArray(draftWorkflowVersion.steps)) { + await Promise.all( + draftWorkflowVersion.steps.map((step) => + this.runWorkflowVersionStepDeletionSideEffects({ + step, + workspaceId, + }), + ), + ); + } + + const newWorkflowVersionTrigger = workflowVersionToCopy.trigger; + const newWorkflowVersionSteps: WorkflowAction[] = []; + + for (const step of workflowVersionToCopy.steps) { + const duplicatedStep = await this.duplicateStep({ + step, + workspaceId, + }); + + newWorkflowVersionSteps.push(duplicatedStep); + } + + await workflowVersionRepository.update(draftWorkflowVersion.id, { + steps: newWorkflowVersionSteps, + trigger: newWorkflowVersionTrigger, + }); + } + + private async runWorkflowVersionStepDeletionSideEffects({ + step, + workspaceId, + }: { + step: WorkflowAction; + workspaceId: string; + }) { + switch (step.type) { + case WorkflowActionType.CODE: { + await this.serverlessFunctionService.deleteOneServerlessFunction( + step.settings.input.serverlessFunctionId, + workspaceId, + ); + + break; + } + } + } }