335 workflow implement workflow cron triggers backend (#9988)

[Backend side] Add cron triggers to workflow
Closes https://github.com/twentyhq/core-team-issues/issues/335
This commit is contained in:
martmull
2025-02-05 12:02:49 +01:00
committed by GitHub
parent 074cc113ac
commit 736b845c98
46 changed files with 419 additions and 253 deletions

View File

@ -13,9 +13,9 @@ import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
import { WorkflowEventListenerWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-event-listener.workspace-entity';
import {
WorkflowEventTriggerJob,
WorkflowEventTriggerJobData,
} from 'src/modules/workflow/workflow-trigger/jobs/workflow-event-trigger.job';
WorkflowTriggerJob,
WorkflowTriggerJobData,
} from 'src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job';
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
@ -103,8 +103,8 @@ export class DatabaseEventTriggerListener {
for (const eventListener of eventListeners) {
for (const eventPayload of payload.events) {
this.messageQueueService.add<WorkflowEventTriggerJobData>(
WorkflowEventTriggerJob.name,
this.messageQueueService.add<WorkflowTriggerJobData>(
WorkflowTriggerJob.name,
{
workspaceId,
workflowId: eventListener.workflowId,

View File

@ -1,76 +0,0 @@
import { Scope } from '@nestjs/common';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import {
WorkflowVersionStatus,
WorkflowVersionWorkspaceEntity,
} from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
import { WorkflowRunnerWorkspaceService } from 'src/modules/workflow/workflow-runner/workspace-services/workflow-runner.workspace-service';
import {
WorkflowTriggerException,
WorkflowTriggerExceptionCode,
} from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception';
export type WorkflowEventTriggerJobData = {
workspaceId: string;
workflowId: string;
payload: object;
};
@Processor({ queueName: MessageQueue.workflowQueue, scope: Scope.REQUEST })
export class WorkflowEventTriggerJob {
constructor(
private readonly twentyORMManager: TwentyORMManager,
private readonly workflowRunnerWorkspaceService: WorkflowRunnerWorkspaceService,
) {}
@Process(WorkflowEventTriggerJob.name)
async handle(data: WorkflowEventTriggerJobData): Promise<void> {
const workflowRepository =
await this.twentyORMManager.getRepository<WorkflowWorkspaceEntity>(
'workflow',
);
const workflow = await workflowRepository.findOneByOrFail({
id: data.workflowId,
});
if (!workflow.lastPublishedVersionId) {
throw new WorkflowTriggerException(
'Workflow has no published version',
WorkflowTriggerExceptionCode.INTERNAL_ERROR,
);
}
const workflowVersionRepository =
await this.twentyORMManager.getRepository<WorkflowVersionWorkspaceEntity>(
'workflowVersion',
);
const workflowVersion = await workflowVersionRepository.findOneByOrFail({
id: workflow.lastPublishedVersionId,
});
if (workflowVersion.status !== WorkflowVersionStatus.ACTIVE) {
throw new WorkflowTriggerException(
'Workflow version is not active',
WorkflowTriggerExceptionCode.INTERNAL_ERROR,
);
}
await this.workflowRunnerWorkspaceService.run(
data.workspaceId,
workflow.lastPublishedVersionId,
data.payload,
{
source: FieldActorSource.WORKFLOW,
name: workflow.name,
},
);
}
}

View File

@ -0,0 +1,89 @@
import { Scope } from '@nestjs/common';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import {
WorkflowVersionStatus,
WorkflowVersionWorkspaceEntity,
} from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
import { WorkflowRunnerWorkspaceService } from 'src/modules/workflow/workflow-runner/workspace-services/workflow-runner.workspace-service';
import {
WorkflowTriggerException,
WorkflowTriggerExceptionCode,
} from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
export type WorkflowTriggerJobData = {
workspaceId: string;
workflowId: string;
payload: object;
};
@Processor({ queueName: MessageQueue.workflowQueue, scope: Scope.REQUEST })
export class WorkflowTriggerJob {
constructor(
private readonly twentyORMManager: TwentyORMManager,
private readonly workflowRunnerWorkspaceService: WorkflowRunnerWorkspaceService,
@InjectMessageQueue(MessageQueue.workflowQueue)
private readonly messageQueueService: MessageQueueService,
) {}
@Process(WorkflowTriggerJob.name)
async handle(data: WorkflowTriggerJobData): Promise<void> {
try {
const workflowRepository =
await this.twentyORMManager.getRepository<WorkflowWorkspaceEntity>(
'workflow',
);
const workflow = await workflowRepository.findOneByOrFail({
id: data.workflowId,
});
if (!workflow.lastPublishedVersionId) {
throw new WorkflowTriggerException(
'Workflow has no published version',
WorkflowTriggerExceptionCode.INTERNAL_ERROR,
);
}
const workflowVersionRepository =
await this.twentyORMManager.getRepository<WorkflowVersionWorkspaceEntity>(
'workflowVersion',
);
const workflowVersion = await workflowVersionRepository.findOneByOrFail({
id: workflow.lastPublishedVersionId,
});
if (workflowVersion.status !== WorkflowVersionStatus.ACTIVE) {
throw new WorkflowTriggerException(
'Workflow version is not active',
WorkflowTriggerExceptionCode.INTERNAL_ERROR,
);
}
await this.workflowRunnerWorkspaceService.run(
data.workspaceId,
workflow.lastPublishedVersionId,
data.payload,
{
source: FieldActorSource.WORKFLOW,
name: workflow.name,
},
);
} catch (e) {
// We remove cron if it exists when no valid workflowVersion exists
await this.messageQueueService.removeCron({
jobName: WorkflowTriggerJob.name,
jobId: data.workflowId,
});
throw e;
}
}
}

View File

@ -3,6 +3,7 @@ import { OutputSchema } from 'src/modules/workflow/workflow-builder/types/output
export enum WorkflowTriggerType {
DATABASE_EVENT = 'DATABASE_EVENT',
MANUAL = 'MANUAL',
CRON = 'CRON',
}
type BaseWorkflowTriggerSettings = {
@ -35,8 +36,16 @@ export type WorkflowManualTrigger = BaseTrigger & {
};
};
export type WorkflowCronTrigger = BaseTrigger & {
type: WorkflowTriggerType.CRON;
settings: {
pattern: string;
};
};
export type WorkflowManualTriggerSettings = WorkflowManualTrigger['settings'];
export type WorkflowTrigger =
| WorkflowDatabaseEventTrigger
| WorkflowManualTrigger;
| WorkflowManualTrigger
| WorkflowCronTrigger;

View File

@ -6,7 +6,7 @@ import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/s
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
import { WorkflowRunnerModule } from 'src/modules/workflow/workflow-runner/workflow-runner.module';
import { DatabaseEventTriggerModule } from 'src/modules/workflow/workflow-trigger/database-event-trigger/database-event-trigger.module';
import { WorkflowEventTriggerJob } from 'src/modules/workflow/workflow-trigger/jobs/workflow-event-trigger.job';
import { WorkflowTriggerJob } from 'src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job';
import { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@ -20,7 +20,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
providers: [
WorkflowTriggerWorkspaceService,
ScopedWorkspaceContextFactory,
WorkflowEventTriggerJob,
WorkflowTriggerJob,
],
exports: [WorkflowTriggerWorkspaceService],
})

View File

@ -29,6 +29,13 @@ import {
import { WorkflowTriggerType } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
import { assertVersionCanBeActivated } from 'src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util';
import { assertNever } from 'src/utils/assert';
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 {
WorkflowTriggerJob,
WorkflowTriggerJobData,
} from 'src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job';
@Injectable()
export class WorkflowTriggerWorkspaceService {
@ -41,6 +48,8 @@ export class WorkflowTriggerWorkspaceService {
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectMessageQueue(MessageQueue.workflowQueue)
private readonly messageQueueService: MessageQueueService,
) {}
private getWorkspaceId() {
@ -329,6 +338,23 @@ export class WorkflowTriggerWorkspaceService {
return;
case WorkflowTriggerType.MANUAL:
return;
case WorkflowTriggerType.CRON:
await this.messageQueueService.addCron<WorkflowTriggerJobData>({
jobName: WorkflowTriggerJob.name,
jobId: workflowVersion.workflowId,
data: {
workspaceId: this.getWorkspaceId(),
workflowId: workflowVersion.workflowId,
payload: {},
},
options: {
repeat: {
pattern: workflowVersion.trigger.settings.pattern,
},
},
});
return;
default: {
assertNever(workflowVersion.trigger);
@ -351,6 +377,13 @@ export class WorkflowTriggerWorkspaceService {
return;
case WorkflowTriggerType.MANUAL:
return;
case WorkflowTriggerType.CRON:
await this.messageQueueService.removeCron({
jobName: WorkflowTriggerJob.name,
jobId: workflowVersion.workflowId,
});
return;
default:
assertNever(workflowVersion.trigger);