From ee14f25996fd7d8fcfdd68137d5de5a81977e652 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Tue, 30 Jul 2024 14:00:37 +0200 Subject: [PATCH] Add enable workflow trigger endpoint (#6443) Basic endpoint that only returns a boolean currently and overrides the previous listener. --- .../workspace-query-runner.exception.ts | 2 +- .../engine/core-modules/core-engine.module.ts | 18 ++-- ...gger-graphql-api-exception-handler.util.ts | 23 +++++ .../workflow/workflow-trigger.module.ts | 9 ++ .../workflow/workflow-trigger.resolver.ts | 31 ++++++ .../constants/standard-field-ids.ts | 6 ++ .../constants/standard-object-ids.ts | 1 + .../standard-objects/index.ts | 2 + ...orkflow-event-listener.workspace-entity.ts | 56 +++++++++++ .../workflow-trigger.exception.ts | 14 +++ .../workflow-trigger.service.ts | 99 +++++++++++++++++++ .../workflow-version.workspace-entity.ts | 14 +++ .../workflow.workspace-entity.ts | 13 +++ 13 files changed, 279 insertions(+), 9 deletions(-) create mode 100644 packages/twenty-server/src/engine/core-modules/workflow/utils/workflow-trigger-graphql-api-exception-handler.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/workflow/workflow-trigger.module.ts create mode 100644 packages/twenty-server/src/engine/core-modules/workflow/workflow-trigger.resolver.ts create mode 100644 packages/twenty-server/src/modules/workflow/standard-objects/workflow-event-listener.workspace-entity.ts create mode 100644 packages/twenty-server/src/modules/workflow/standard-objects/workflow-trigger/workflow-trigger.exception.ts create mode 100644 packages/twenty-server/src/modules/workflow/standard-objects/workflow-trigger/workflow-trigger.service.ts diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception.ts index e750eec1f4a8..f734e667d1d9 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception.ts @@ -8,7 +8,7 @@ export class WorkspaceQueryRunnerException extends CustomException { } export enum WorkspaceQueryRunnerExceptionCode { - INVALID_QUERY_INPUT = 'INVALID_FIELD_INPUT', + INVALID_QUERY_INPUT = 'INVALID_QUERY_INPUT', DATA_NOT_FOUND = 'DATA_NOT_FOUND', QUERY_TIMEOUT = 'QUERY_TIMEOUT', QUERY_VIOLATES_UNIQUE_CONSTRAINT = 'QUERY_VIOLATES_UNIQUE_CONSTRAINT', diff --git a/packages/twenty-server/src/engine/core-modules/core-engine.module.ts b/packages/twenty-server/src/engine/core-modules/core-engine.module.ts index a8c98201c189..91d7c3e20f30 100644 --- a/packages/twenty-server/src/engine/core-modules/core-engine.module.ts +++ b/packages/twenty-server/src/engine/core-modules/core-engine.module.ts @@ -1,21 +1,22 @@ import { Module } from '@nestjs/common'; -import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module'; -import { UserModule } from 'src/engine/core-modules/user/user.module'; +import { AISQLQueryModule } from 'src/engine/core-modules/ai-sql-query/ai-sql-query.module'; import { AppTokenModule } from 'src/engine/core-modules/app-token/app-token.module'; import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; -import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; -import { OpenApiModule } from 'src/engine/core-modules/open-api/open-api.module'; -import { TimelineMessagingModule } from 'src/engine/core-modules/messaging/timeline-messaging.module'; -import { TimelineCalendarEventModule } from 'src/engine/core-modules/calendar/timeline-calendar-event.module'; import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; +import { TimelineCalendarEventModule } from 'src/engine/core-modules/calendar/timeline-calendar-event.module'; +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { HealthModule } from 'src/engine/core-modules/health/health.module'; -import { AISQLQueryModule } from 'src/engine/core-modules/ai-sql-query/ai-sql-query.module'; +import { TimelineMessagingModule } from 'src/engine/core-modules/messaging/timeline-messaging.module'; +import { OpenApiModule } from 'src/engine/core-modules/open-api/open-api.module'; import { PostgresCredentialsModule } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.module'; +import { UserModule } from 'src/engine/core-modules/user/user.module'; +import { WorkflowTriggerModule } from 'src/engine/core-modules/workflow/workflow-trigger.module'; +import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module'; +import { AnalyticsModule } from './analytics/analytics.module'; import { ClientConfigModule } from './client-config/client-config.module'; import { FileModule } from './file/file.module'; -import { AnalyticsModule } from './analytics/analytics.module'; @Module({ imports: [ @@ -34,6 +35,7 @@ import { AnalyticsModule } from './analytics/analytics.module'; WorkspaceModule, AISQLQueryModule, PostgresCredentialsModule, + WorkflowTriggerModule, ], exports: [ AnalyticsModule, diff --git a/packages/twenty-server/src/engine/core-modules/workflow/utils/workflow-trigger-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/core-modules/workflow/utils/workflow-trigger-graphql-api-exception-handler.util.ts new file mode 100644 index 000000000000..be71dca48505 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/workflow/utils/workflow-trigger-graphql-api-exception-handler.util.ts @@ -0,0 +1,23 @@ +import { + InternalServerError, + UserInputError, +} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +import { + WorkflowTriggerException, + WorkflowTriggerExceptionCode, +} from 'src/modules/workflow/standard-objects/workflow-trigger/workflow-trigger.exception'; + +export const workflowTriggerGraphqlApiExceptionHandler = (error: Error) => { + if (error instanceof WorkflowTriggerException) { + switch (error.code) { + case WorkflowTriggerExceptionCode.INVALID_INPUT: + throw new UserInputError(error.message); + case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER: + case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION: + default: + throw new InternalServerError(error.message); + } + } + + throw error; +}; diff --git a/packages/twenty-server/src/engine/core-modules/workflow/workflow-trigger.module.ts b/packages/twenty-server/src/engine/core-modules/workflow/workflow-trigger.module.ts new file mode 100644 index 000000000000..a71459f9e1bc --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/workflow/workflow-trigger.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; + +import { WorkflowTriggerResolver } from 'src/engine/core-modules/workflow/workflow-trigger.resolver'; +import { WorkflowTriggerService } from 'src/modules/workflow/standard-objects/workflow-trigger/workflow-trigger.service'; + +@Module({ + providers: [WorkflowTriggerService, WorkflowTriggerResolver], +}) +export class WorkflowTriggerModule {} diff --git a/packages/twenty-server/src/engine/core-modules/workflow/workflow-trigger.resolver.ts b/packages/twenty-server/src/engine/core-modules/workflow/workflow-trigger.resolver.ts new file mode 100644 index 000000000000..b589bb1aec45 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/workflow/workflow-trigger.resolver.ts @@ -0,0 +1,31 @@ +import { UseGuards } from '@nestjs/common'; +import { Args, Mutation, Resolver } from '@nestjs/graphql'; + +import { workflowTriggerGraphqlApiExceptionHandler } from 'src/engine/core-modules/workflow/utils/workflow-trigger-graphql-api-exception-handler.util'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; +import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; +import { WorkflowTriggerService } from 'src/modules/workflow/standard-objects/workflow-trigger/workflow-trigger.service'; + +@UseGuards(JwtAuthGuard) +@Resolver() +export class WorkflowTriggerResolver { + constructor( + private readonly workflowTriggerService: WorkflowTriggerService, + ) {} + + @Mutation(() => Boolean) + async enableWorkflowTrigger( + @AuthWorkspace() { id: workspaceId }: Workspace, + @Args('workflowVersionId') workflowVersionId: string, + ) { + try { + return await this.workflowTriggerService.enableWorkflowTrigger( + workspaceId, + workflowVersionId, + ); + } catch (error) { + workflowTriggerGraphqlApiExceptionHandler(error); + } + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts index af6e7dd975c4..907f8da7a948 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts @@ -327,10 +327,16 @@ export const WEBHOOK_STANDARD_FIELD_IDS = { operation: '20202020-15b7-458e-bf30-74770a54410c', }; +export const WORKFLOW_EVENT_LISTENER_STANDARD_FIELD_IDS = { + eventName: '20202020-7318-4cf8-a6ac-2de75e3fd97d', + workflow: '20202020-4082-4641-8569-dc08d5365002', +}; + export const WORKFLOW_STANDARD_FIELD_IDS = { name: '20202020-b3d3-478f-acc0-5d901e725b20', publishedVersionId: '20202020-326a-4fba-8639-3456c0a169e8', versions: '20202020-9432-416e-8f3c-27ee3153d099', + eventListeners: '20202020-0229-4c66-832e-035c67579a38', position: '20202020-39b0-4d8c-8c5f-33c2326deb5f', favorites: '20202020-c554-4c41-be7a-cf9cd4b0d512', activityTargets: '20202020-9d65-445a-899d-1c6f1cf3a9ab', diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids.ts index 100f4aa3b3f3..4e927c3cd5ed 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids.ts @@ -35,6 +35,7 @@ export const STANDARD_OBJECT_IDS = { view: '20202020-722e-4739-8e2c-0c372d661f49', webhook: '20202020-be4d-4e08-811d-0fffcd13ffd4', workflow: '20202020-62be-406c-b9ca-8caa50d51392', + workflowEventListener: '20202020-92aa-462f-965c-a785b00e9989', workflowVersion: '20202020-d65d-4ab9-9344-d77bfb376a3d', workspaceMember: '20202020-2632-4659-9540-567498166593', }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts index f8e4d410a347..87d8813b2caa 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts @@ -26,6 +26,7 @@ import { ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/vie import { ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity'; import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity'; import { WebhookWorkspaceEntity } from 'src/modules/webhook/standard-objects/webhook.workspace-entity'; +import { WorkflowEventListenerWorkspaceEntity } from 'src/modules/workflow/standard-objects/workflow-event-listener.workspace-entity'; import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/standard-objects/workflow-version.workspace-entity'; import { WorkflowWorkspaceEntity } from 'src/modules/workflow/standard-objects/workflow.workspace-entity'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @@ -56,6 +57,7 @@ export const standardObjectMetadataDefinitions = [ ViewWorkspaceEntity, WebhookWorkspaceEntity, WorkflowWorkspaceEntity, + WorkflowEventListenerWorkspaceEntity, WorkflowVersionWorkspaceEntity, WorkspaceMemberWorkspaceEntity, MessageThreadWorkspaceEntity, diff --git a/packages/twenty-server/src/modules/workflow/standard-objects/workflow-event-listener.workspace-entity.ts b/packages/twenty-server/src/modules/workflow/standard-objects/workflow-event-listener.workspace-entity.ts new file mode 100644 index 000000000000..2cf07c5f354c --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/standard-objects/workflow-event-listener.workspace-entity.ts @@ -0,0 +1,56 @@ +import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; + +import { FeatureFlagKeys } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; +import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; +import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; +import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; +import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; +import { WorkspaceGate } from 'src/engine/twenty-orm/decorators/workspace-gate.decorator'; +import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator'; +import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator'; +import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator'; +import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator'; +import { WORKFLOW_EVENT_LISTENER_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; +import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; +import { WorkflowWorkspaceEntity } from 'src/modules/workflow/standard-objects/workflow.workspace-entity'; + +@WorkspaceEntity({ + standardId: STANDARD_OBJECT_IDS.workflowEventListener, + namePlural: 'workflowEventListeners', + labelSingular: 'WorkflowEventListener', + labelPlural: 'WorkflowEventListeners', + description: 'A workflow event listener', + labelIdentifierStandardId: + WORKFLOW_EVENT_LISTENER_STANDARD_FIELD_IDS.eventName, +}) +@WorkspaceGate({ + featureFlag: FeatureFlagKeys.IsWorkflowEnabled, +}) +@WorkspaceIsSystem() +export class WorkflowEventListenerWorkspaceEntity extends BaseWorkspaceEntity { + @WorkspaceField({ + standardId: WORKFLOW_EVENT_LISTENER_STANDARD_FIELD_IDS.eventName, + type: FieldMetadataType.TEXT, + label: 'Name', + description: 'The workflow event listener name', + icon: 'IconPhoneCheck', + }) + eventName: string; + + // Relations + @WorkspaceRelation({ + standardId: WORKFLOW_EVENT_LISTENER_STANDARD_FIELD_IDS.workflow, + type: RelationMetadataType.MANY_TO_ONE, + label: 'Workflow', + description: 'WorkflowEventListener workflow', + icon: 'IconSettingsAutomation', + inverseSideTarget: () => WorkflowWorkspaceEntity, + inverseSideFieldKey: 'eventListeners', + }) + @WorkspaceIsNullable() + workflow: Relation; + + @WorkspaceJoinColumn('workflow') + workflowId: string; +} diff --git a/packages/twenty-server/src/modules/workflow/standard-objects/workflow-trigger/workflow-trigger.exception.ts b/packages/twenty-server/src/modules/workflow/standard-objects/workflow-trigger/workflow-trigger.exception.ts new file mode 100644 index 000000000000..d153e827d00a --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/standard-objects/workflow-trigger/workflow-trigger.exception.ts @@ -0,0 +1,14 @@ +import { CustomException } from 'src/utils/custom-exception'; + +export class WorkflowTriggerException extends CustomException { + code: WorkflowTriggerExceptionCode; + constructor(message: string, code: WorkflowTriggerExceptionCode) { + super(message, code); + } +} + +export enum WorkflowTriggerExceptionCode { + INVALID_INPUT = 'INVALID_INPUT', + INVALID_WORKFLOW_TRIGGER = 'INVALID_WORKFLOW_TRIGGER', + INVALID_WORKFLOW_VERSION = 'INVALID_WORKFLOW_VERSION', +} diff --git a/packages/twenty-server/src/modules/workflow/standard-objects/workflow-trigger/workflow-trigger.service.ts b/packages/twenty-server/src/modules/workflow/standard-objects/workflow-trigger/workflow-trigger.service.ts new file mode 100644 index 000000000000..7ce804088f40 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/standard-objects/workflow-trigger/workflow-trigger.service.ts @@ -0,0 +1,99 @@ +import { Injectable } from '@nestjs/common'; + +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { WorkflowEventListenerWorkspaceEntity } from 'src/modules/workflow/standard-objects/workflow-event-listener.workspace-entity'; +import { + WorkflowTriggerException, + WorkflowTriggerExceptionCode, +} from 'src/modules/workflow/standard-objects/workflow-trigger/workflow-trigger.exception'; +import { + WorkflowDatabaseEventTrigger, + WorkflowTrigger, + WorkflowTriggerType, + WorkflowVersionWorkspaceEntity, +} from 'src/modules/workflow/standard-objects/workflow-version.workspace-entity'; + +@Injectable() +export class WorkflowTriggerService { + constructor( + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + ) {} + + async enableWorkflowTrigger(workspaceId: string, workflowVersionId: string) { + const workflowVersionRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'workflowVersion', + ); + + const workflowVersion = await workflowVersionRepository.findOne({ + where: { + id: workflowVersionId, + }, + }); + + if (!workflowVersion) { + throw new WorkflowTriggerException( + 'Workflow version not found', + WorkflowTriggerExceptionCode.INVALID_INPUT, + ); + } + + const trigger = workflowVersion.trigger as unknown as WorkflowTrigger; + + if (!trigger || !trigger?.type) { + throw new WorkflowTriggerException( + 'Workflow version does not contains trigger', + WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION, + ); + } + + switch (trigger.type) { + case WorkflowTriggerType.DATABASE_EVENT: + await this.upsertWorkflowEventListener( + workspaceId, + workflowVersion.workflowId, + trigger, + ); + break; + default: + break; + } + + return true; + } + + private async upsertWorkflowEventListener( + workspaceId: string, + workflowId: string, + trigger: WorkflowDatabaseEventTrigger, + ) { + const eventName = trigger?.settings?.eventName; + + if (!eventName) { + throw new WorkflowTriggerException( + 'No event name provided in database event trigger', + WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER, + ); + } + + const workflowEventListenerRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'workflowEventListener', + ); + + // TODO: Use upsert when available for workspace entities + await workflowEventListenerRepository.delete({ + workflowId, + eventName, + }); + + const workflowEventListener = await workflowEventListenerRepository.create({ + workflowId, + eventName, + }); + + await workflowEventListenerRepository.save(workflowEventListener); + } +} diff --git a/packages/twenty-server/src/modules/workflow/standard-objects/workflow-version.workspace-entity.ts b/packages/twenty-server/src/modules/workflow/standard-objects/workflow-version.workspace-entity.ts index 7c09304f3bd2..27191682432f 100644 --- a/packages/twenty-server/src/modules/workflow/standard-objects/workflow-version.workspace-entity.ts +++ b/packages/twenty-server/src/modules/workflow/standard-objects/workflow-version.workspace-entity.ts @@ -15,6 +15,20 @@ import { WORKFLOW_VERSION_STANDARD_FIELD_IDS } from 'src/engine/workspace-manage import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; import { WorkflowWorkspaceEntity } from 'src/modules/workflow/standard-objects/workflow.workspace-entity'; +export enum WorkflowTriggerType { + DATABASE_EVENT = 'DATABASE_EVENT', +} + +export type WorkflowDatabaseEventTrigger = { + type: WorkflowTriggerType.DATABASE_EVENT; + settings: { + eventName: string; + triggerName: string; + }; +}; + +export type WorkflowTrigger = WorkflowDatabaseEventTrigger; + @WorkspaceEntity({ standardId: STANDARD_OBJECT_IDS.workflowVersion, namePlural: 'workflowVersions', diff --git a/packages/twenty-server/src/modules/workflow/standard-objects/workflow.workspace-entity.ts b/packages/twenty-server/src/modules/workflow/standard-objects/workflow.workspace-entity.ts index 37f35a15fd5b..d09cdd1f3e0a 100644 --- a/packages/twenty-server/src/modules/workflow/standard-objects/workflow.workspace-entity.ts +++ b/packages/twenty-server/src/modules/workflow/standard-objects/workflow.workspace-entity.ts @@ -19,6 +19,7 @@ import { ActivityTargetWorkspaceEntity } from 'src/modules/activity/standard-obj import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity'; import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity'; import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity'; +import { WorkflowEventListenerWorkspaceEntity } from 'src/modules/workflow/standard-objects/workflow-event-listener.workspace-entity'; import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/standard-objects/workflow-version.workspace-entity'; @WorkspaceEntity({ @@ -77,6 +78,18 @@ export class WorkflowWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceIsNullable() versions: Relation; + @WorkspaceRelation({ + standardId: WORKFLOW_STANDARD_FIELD_IDS.eventListeners, + type: RelationMetadataType.ONE_TO_MANY, + label: 'Event Listeners', + description: 'Workflow event listeners linked to the workflow.', + icon: 'IconVersions', + inverseSideTarget: () => WorkflowEventListenerWorkspaceEntity, + onDelete: RelationOnDeleteAction.SET_NULL, + }) + @WorkspaceIsNullable() + eventListeners: Relation; + @WorkspaceRelation({ standardId: WORKFLOW_STANDARD_FIELD_IDS.activityTargets, type: RelationMetadataType.ONE_TO_MANY,