diff --git a/packages/twenty-front/src/modules/workflow/hooks/useCreateDraftFromWorkflowVersion.ts b/packages/twenty-front/src/modules/workflow/hooks/useCreateDraftFromWorkflowVersion.ts index 360e0162b7..07cb99be55 100644 --- a/packages/twenty-front/src/modules/workflow/hooks/useCreateDraftFromWorkflowVersion.ts +++ b/packages/twenty-front/src/modules/workflow/hooks/useCreateDraftFromWorkflowVersion.ts @@ -21,7 +21,12 @@ export const useCreateDraftFromWorkflowVersion = () => { name: true, statuses: true, lastPublishedVersionId: true, - versions: true, + versions: { + id: true, + status: true, + name: true, + createdAt: true, + }, }, }); diff --git a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts index dd4c18a519..02364003d1 100644 --- a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts +++ b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts @@ -2,6 +2,37 @@ import deepEqual from 'deep-equal'; import { FieldMetadataType, type ObjectRecord } from 'twenty-shared/types'; import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; + +const isWorkflowVersionStepsOrTrigger = ( + objectMetadataItem: ObjectMetadataItemWithFieldMaps, + key: string, +) => { + return ( + objectMetadataItem.standardId === STANDARD_OBJECT_IDS.workflowVersion && + (key === 'steps' || key === 'trigger') + ); +}; + +const isWorkflowRunState = ( + objectMetadataItem: ObjectMetadataItemWithFieldMaps, + key: string, +) => { + return ( + objectMetadataItem.standardId === STANDARD_OBJECT_IDS.workflowRun && + key === 'state' + ); +}; + +const isWorkflowAutomatedTriggerSettings = ( + objectMetadataItem: ObjectMetadataItemWithFieldMaps, + key: string, +) => { + return ( + objectMetadataItem.standardId === + STANDARD_OBJECT_IDS.workflowAutomatedTrigger && key === 'settings' + ); +}; export const objectRecordChangedValues = ( oldRecord: Partial, @@ -16,6 +47,15 @@ export const objectRecordChangedValues = ( const oldRecordValue = oldRecord[key]; const newRecordValue = newRecord[key]; + // Temporary ignore workflow json fields changes + if ( + isWorkflowAutomatedTriggerSettings(objectMetadataItem, key) || + isWorkflowVersionStepsOrTrigger(objectMetadataItem, key) || + isWorkflowRunState(objectMetadataItem, key) + ) { + return acc; + } + if ( key === 'updatedAt' || key === 'searchVector' || diff --git a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver.ts b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver.ts index 05aa762ead..884966e54e 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver.ts @@ -45,9 +45,9 @@ export class WorkflowTriggerResolver { @Args('workflowVersionId', { type: () => UUIDScalarType }) workflowVersionId: string, ) { - return this.workflowTriggerWorkspaceService.activateWorkflowVersion( + return this.workflowTriggerWorkspaceService.activateWorkflowVersion({ workflowVersionId, - ); + }); } @Mutation(() => Boolean) diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts index 3375d8ba78..e416fc8b49 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts @@ -159,6 +159,7 @@ export class WorkspaceUpdateQueryBuilder< ); const result = await super.execute(); + const after = await eventSelectQueryBuilder.getMany(); const formattedAfter = formatResult( diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util.ts index 3a122882f9..aace270044 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util.ts @@ -3,17 +3,17 @@ import { isDefined } from 'twenty-shared/utils'; import type { ObjectLiteral } from 'typeorm'; import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; -import type { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import type { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; -import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; -import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; +import { ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event'; +import type { ObjectRecordDiff } from 'src/engine/core-modules/event-emitter/types/object-record-diff'; +import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; import { ObjectRecordUpsertEvent } from 'src/engine/core-modules/event-emitter/types/object-record-upsert.event'; import { objectRecordChangedValues } from 'src/engine/core-modules/event-emitter/utils/object-record-changed-values'; -import type { ObjectRecordDiff } from 'src/engine/core-modules/event-emitter/types/object-record-diff'; -import { ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event'; +import type { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { type DatabaseBatchEventInput } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; +import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; export const formatTwentyOrmEventToDatabaseBatchEvent = < T extends ObjectLiteral, @@ -143,7 +143,6 @@ export const formatTwentyOrmEventToDatabaseBatchEvent = < after, objectMetadataItem, ) as Partial>; - updatedFields = Object.keys(diff); event.properties = { diff --git a/packages/twenty-server/src/engine/workspace-event-emitter/workspace-event-emitter.ts b/packages/twenty-server/src/engine/workspace-event-emitter/workspace-event-emitter.ts index 8d99d318af..a6fca9ba13 100644 --- a/packages/twenty-server/src/engine/workspace-event-emitter/workspace-event-emitter.ts +++ b/packages/twenty-server/src/engine/workspace-event-emitter/workspace-event-emitter.ts @@ -10,11 +10,11 @@ import { ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/ import { type ObjectRecordRestoreEvent } from 'src/engine/core-modules/event-emitter/types/object-record-restore.event'; import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; import { ObjectRecordUpsertEvent } from 'src/engine/core-modules/event-emitter/types/object-record-upsert.event'; -import { type CustomEventName } from 'src/engine/workspace-event-emitter/types/custom-event-name.type'; -import { computeEventName } from 'src/engine/workspace-event-emitter/utils/compute-event-name'; import type { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type'; +import { type CustomEventName } from 'src/engine/workspace-event-emitter/types/custom-event-name.type'; import { CustomWorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/custom-workspace-batch-event.type'; +import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type'; +import { computeEventName } from 'src/engine/workspace-event-emitter/utils/compute-event-name'; type ActionEventMap = { [DatabaseEventAction.CREATED]: ObjectRecordCreateEvent; diff --git a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts index 767f3d4f27..da2c6bec25 100644 --- a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts @@ -67,28 +67,14 @@ export class WorkflowCommonWorkspaceService { }, }); - return this.getValidWorkflowVersionOrFail(workflowVersion); - } - - async getValidWorkflowVersionOrFail( - workflowVersion: WorkflowVersionWorkspaceEntity | null, - ): Promise { if (!workflowVersion) { throw new WorkflowTriggerException( 'Workflow version not found', - WorkflowTriggerExceptionCode.INVALID_INPUT, + WorkflowTriggerExceptionCode.NOT_FOUND, ); } - // FIXME: For now we will make the trigger optional. Later, we'll have to ensure the trigger is defined when publishing the flow. - // if (!workflowVersion.trigger) { - // throw new WorkflowTriggerException( - // 'Workflow version does not contains trigger', - // WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION, - // ); - // } - - return { ...workflowVersion, trigger: workflowVersion.trigger }; + return workflowVersion; } async getObjectMetadataMaps( diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts index 307b201dbd..c9c4d3284f 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts @@ -128,9 +128,9 @@ This is the most efficient way for AI to create workflows as it handles all the } if (parameters.activate) { - await this.workflowTriggerService.activateWorkflowVersion( + await this.workflowTriggerService.activateWorkflowVersion({ workflowVersionId, - ); + }); await this.updateWorkflowStatus({ workspaceId, @@ -340,11 +340,14 @@ This is the most efficient way for AI to create workflows as it handles all the description: 'Activate a workflow version. This makes the workflow version active and available for execution.', inputSchema: activateWorkflowVersionSchema, - execute: async (parameters: { workflowVersionId: string }) => { + execute: async (parameters: { + workflowVersionId: string; + workflowId: string; + }) => { try { - return await this.workflowTriggerService.activateWorkflowVersion( - parameters.workflowVersionId, - ); + return await this.workflowTriggerService.activateWorkflowVersion({ + workflowVersionId: parameters.workflowVersionId, + }); } catch (error) { return { success: false, diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts index 9645e401ea..85870a3137 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts @@ -4,7 +4,6 @@ import { WorkflowVersionStatus, type WorkflowVersionWorkspaceEntity, } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; -import { type WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity'; import { type WorkflowAction, WorkflowActionType, @@ -18,25 +17,20 @@ import { assertFormStepIsValid } from 'src/modules/workflow/workflow-trigger/uti export function assertVersionCanBeActivated( workflowVersion: WorkflowVersionWorkspaceEntity, - workflow: WorkflowWorkspaceEntity, ) { assertVersionIsValid(workflowVersion); - const isLastPublishedVersion = - workflow.lastPublishedVersionId === workflowVersion.id; - const isDraft = workflowVersion.status === WorkflowVersionStatus.DRAFT; - const isLastPublishedVersionDeactivated = - workflowVersion.status === WorkflowVersionStatus.DEACTIVATED && - isLastPublishedVersion; + const isDeactivated = + workflowVersion.status === WorkflowVersionStatus.DEACTIVATED; - if (!isDraft && !isLastPublishedVersionDeactivated) { + if (!isDraft && !isDeactivated) { throw new WorkflowTriggerException( - 'Cannot activate non-draft or non-last-published version', + 'Cannot activate non-draft or non-deactivated version', WorkflowTriggerExceptionCode.INVALID_INPUT, { - userFriendlyMessage: msg`Cannot activate non-draft or non-last-published version`, + userFriendlyMessage: msg`Cannot activate non-draft or non-deactivated version`, }, ); } diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts index 1ce8241e52..b34db13921 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { msg } from '@lingui/core/macro'; +import { Not } from 'typeorm'; import { type ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; @@ -78,24 +79,19 @@ export class WorkflowTriggerWorkspaceService { }); } - async activateWorkflowVersion(workflowVersionId: string) { + async activateWorkflowVersion({ + workflowVersionId, + }: { + workflowVersionId: string; + }) { const workspaceId = this.getWorkspaceId(); const workflowVersionRepository = await this.twentyORMGlobalManager.getRepositoryForWorkspace( - this.getWorkspaceId(), + workspaceId, 'workflowVersion', { shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level ); - const workflowVersionNullable = await workflowVersionRepository.findOne({ - where: { id: workflowVersionId }, - }); - - const workflowVersion = - await this.workflowCommonWorkspaceService.getValidWorkflowVersionOrFail( - workflowVersionNullable, - ); - const workflowRepository = await this.twentyORMGlobalManager.getRepositoryForWorkspace( workspaceId, @@ -103,25 +99,61 @@ export class WorkflowTriggerWorkspaceService { { shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level ); - const workflow = await workflowRepository.findOne({ - where: { id: workflowVersion.workflowId }, + const workflowVersionToActivate = await workflowVersionRepository.findOne({ + where: { id: workflowVersionId }, + select: { + id: true, + status: true, + workflowId: true, + steps: { + id: true, + type: true, + }, + trigger: { + type: true, + }, + }, }); - if (!workflow) { + if (!workflowVersionToActivate) { throw new WorkflowTriggerException( - 'No workflow found', - WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION, + 'Workflow version not found', + WorkflowTriggerExceptionCode.NOT_FOUND, ); } - assertVersionCanBeActivated(workflowVersion, workflow); + assertVersionCanBeActivated(workflowVersionToActivate); - await this.performActivationSteps( - workflow, - workflowVersion, + const workflowId = workflowVersionToActivate.workflowId; + + const allWorkflowVersions = await workflowVersionRepository.find({ + where: { + workflowId: workflowVersionToActivate.workflowId, + status: Not(WorkflowVersionStatus.ARCHIVED), + }, + select: { id: true, status: true, trigger: { type: true } }, + }); + + const currentPublishedVersions = allWorkflowVersions.filter( + (version) => + version.status === WorkflowVersionStatus.ACTIVE || + version.status === WorkflowVersionStatus.DEACTIVATED, + ); + + if (currentPublishedVersions.length > 1) { + throw new WorkflowTriggerException( + 'Multiple published versions found', + WorkflowTriggerExceptionCode.INTERNAL_ERROR, + ); + } + + await this.performActivationSteps({ + workflowId, + workflowVersionToActivate, + currentPublishedVersion: currentPublishedVersions?.[0], workflowRepository, workflowVersionRepository, - ); + }); return true; } @@ -149,49 +181,57 @@ export class WorkflowTriggerWorkspaceService { ); } - private async performActivationSteps( - workflow: WorkflowWorkspaceEntity, - workflowVersion: WorkflowVersionWorkspaceEntity, - workflowRepository: WorkspaceRepository, - workflowVersionRepository: WorkspaceRepository, - ) { + private async performActivationSteps({ + workflowId, + workflowVersionToActivate, + currentPublishedVersion, + workflowRepository, + workflowVersionRepository, + }: { + workflowId: string; + workflowVersionToActivate: WorkflowVersionWorkspaceEntity; + currentPublishedVersion: WorkflowVersionWorkspaceEntity | undefined; + workflowRepository: WorkspaceRepository; + workflowVersionRepository: WorkspaceRepository; + }) { if ( - workflow.lastPublishedVersionId && - workflowVersion.id !== workflow.lastPublishedVersionId + currentPublishedVersion && + currentPublishedVersion.id !== workflowVersionToActivate.id ) { - await this.performDeactivationSteps( - workflow.lastPublishedVersionId, - workflowVersionRepository, + await this.disableTrigger(currentPublishedVersion); + await workflowVersionRepository.update( + { id: currentPublishedVersion.id }, + { status: WorkflowVersionStatus.ARCHIVED }, ); } - await this.upgradeWorkflowVersion( - workflow, - workflowVersion.id, - workflowRepository, - workflowVersionRepository, - ); - await this.setActiveVersionStatus( - workflowVersion, + workflowVersionToActivate, workflowVersionRepository, ); - await this.enableTrigger(workflowVersion); + await this.enableTrigger(workflowVersionToActivate); + + await workflowRepository.update( + { id: workflowId }, + { lastPublishedVersionId: workflowVersionToActivate.id }, + ); } private async performDeactivationSteps( workflowVersionId: string, workflowVersionRepository: WorkspaceRepository, ) { - const workflowVersionNullable = await workflowVersionRepository.findOne({ + const workflowVersion = await workflowVersionRepository.findOne({ where: { id: workflowVersionId }, }); - const workflowVersion = - await this.workflowCommonWorkspaceService.getValidWorkflowVersionOrFail( - workflowVersionNullable, + if (!workflowVersion) { + throw new WorkflowTriggerException( + 'Workflow version not found', + WorkflowTriggerExceptionCode.NOT_FOUND, ); + } if (workflowVersion.status !== WorkflowVersionStatus.ACTIVE) { return; @@ -209,23 +249,6 @@ export class WorkflowTriggerWorkspaceService { workflowVersion: WorkflowVersionWorkspaceEntity, workflowVersionRepository: WorkspaceRepository, ) { - const activeWorkflowVersions = await workflowVersionRepository.find({ - where: { - workflowId: workflowVersion.workflowId, - status: WorkflowVersionStatus.ACTIVE, - }, - }); - - if (activeWorkflowVersions.length > 0) { - throw new WorkflowTriggerException( - 'Cannot have more than one active workflow version', - WorkflowTriggerExceptionCode.FORBIDDEN, - { - userFriendlyMessage: msg`Cannot have more than one active workflow version`, - }, - ); - } - await workflowVersionRepository.update( { id: workflowVersion.id }, { status: WorkflowVersionStatus.ACTIVE }, @@ -264,29 +287,6 @@ export class WorkflowTriggerWorkspaceService { ); } - private async upgradeWorkflowVersion( - workflow: WorkflowWorkspaceEntity, - newPublishedVersionId: string, - workflowRepository: WorkspaceRepository, - workflowVersionRepository: WorkspaceRepository, - ) { - if (workflow.lastPublishedVersionId === newPublishedVersionId) { - return; - } - - if (workflow.lastPublishedVersionId) { - await workflowVersionRepository.update( - { id: workflow.lastPublishedVersionId }, - { status: WorkflowVersionStatus.ARCHIVED }, - ); - } - - await workflowRepository.update( - { id: workflow.id }, - { lastPublishedVersionId: newPublishedVersionId }, - ); - } - private async enableTrigger(workflowVersion: WorkflowVersionWorkspaceEntity) { assertWorkflowVersionTriggerIsDefined(workflowVersion);