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; + } + } + } }