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:
@ -73,7 +73,7 @@
|
||||
"bcrypt": "^5.1.1",
|
||||
"better-sqlite3": "^9.2.2",
|
||||
"body-parser": "^1.20.2",
|
||||
"bullmq": "^4.15.0",
|
||||
"bullmq": "^5.40.0",
|
||||
"bytes": "^3.1.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"clsx": "^2.1.1",
|
||||
|
||||
@ -19,14 +19,14 @@ export class StartDataSeedDemoWorkspaceCronCommand extends CommandRunner {
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
await this.messageQueueService.addCron<undefined>(
|
||||
DataSeedDemoWorkspaceJob.name,
|
||||
undefined,
|
||||
{
|
||||
await this.messageQueueService.addCron<undefined>({
|
||||
jobName: DataSeedDemoWorkspaceJob.name,
|
||||
data: undefined,
|
||||
options: {
|
||||
repeat: {
|
||||
pattern: dataSeedDemoWorkspaceCronPattern,
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { Command, CommandRunner } from 'nest-commander';
|
||||
|
||||
import { dataSeedDemoWorkspaceCronPattern } from 'src/database/commands/data-seed-demo-workspace/crons/data-seed-demo-workspace-cron-pattern';
|
||||
import { DataSeedDemoWorkspaceJob } from 'src/database/commands/data-seed-demo-workspace/jobs/data-seed-demo-workspace.job';
|
||||
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';
|
||||
@ -19,9 +18,8 @@ export class StopDataSeedDemoWorkspaceCronCommand extends CommandRunner {
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
await this.messageQueueService.removeCron(
|
||||
DataSeedDemoWorkspaceJob.name,
|
||||
dataSeedDemoWorkspaceCronPattern,
|
||||
);
|
||||
await this.messageQueueService.removeCron({
|
||||
jobName: DataSeedDemoWorkspaceJob.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { OnModuleDestroy } from '@nestjs/common';
|
||||
|
||||
import { JobsOptions, Queue, QueueOptions, Worker } from 'bullmq';
|
||||
import omitBy from 'lodash.omitby';
|
||||
import { v4 } from 'uuid';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
import {
|
||||
QueueCronJobOptions,
|
||||
@ -13,6 +13,7 @@ import { MessageQueueJob } from 'src/engine/core-modules/message-queue/interface
|
||||
import { MessageQueueWorkerOptions } from 'src/engine/core-modules/message-queue/interfaces/message-queue-worker-options.interface';
|
||||
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { getJobKey } from 'src/engine/core-modules/message-queue/utils/get-job-key.util';
|
||||
|
||||
export type BullMQDriverOptions = QueueOptions;
|
||||
|
||||
@ -49,54 +50,72 @@ export class BullMQDriver implements MessageQueueDriver, OnModuleDestroy {
|
||||
handler: (job: MessageQueueJob<T>) => Promise<void>,
|
||||
options?: MessageQueueWorkerOptions,
|
||||
) {
|
||||
const worker = new Worker(
|
||||
const workerOptions = isDefined(options?.concurrency)
|
||||
? {
|
||||
...this.options,
|
||||
concurrency: options.concurrency,
|
||||
}
|
||||
: this.options;
|
||||
|
||||
this.workerMap[queueName] = new Worker(
|
||||
queueName,
|
||||
async (job) => {
|
||||
// TODO: Correctly support for job.id
|
||||
await handler({ data: job.data, id: job.id ?? '', name: job.name });
|
||||
},
|
||||
omitBy(
|
||||
{
|
||||
...this.options,
|
||||
concurrency: options?.concurrency,
|
||||
},
|
||||
(value) => value === undefined,
|
||||
),
|
||||
workerOptions,
|
||||
);
|
||||
|
||||
this.workerMap[queueName] = worker;
|
||||
}
|
||||
|
||||
async addCron<T>(
|
||||
queueName: MessageQueue,
|
||||
jobName: string,
|
||||
data: T,
|
||||
options?: QueueCronJobOptions,
|
||||
): Promise<void> {
|
||||
async addCron<T>({
|
||||
queueName,
|
||||
jobName,
|
||||
data,
|
||||
options,
|
||||
jobId,
|
||||
}: {
|
||||
queueName: MessageQueue;
|
||||
jobName: string;
|
||||
data: T;
|
||||
options: QueueCronJobOptions;
|
||||
jobId?: string;
|
||||
}): Promise<void> {
|
||||
if (!this.queueMap[queueName]) {
|
||||
throw new Error(
|
||||
`Queue ${queueName} is not registered, make sure you have added it as a queue provider`,
|
||||
);
|
||||
}
|
||||
|
||||
const queueOptions: JobsOptions = {
|
||||
jobId: options?.id,
|
||||
priority: options?.priority,
|
||||
repeat: options?.repeat,
|
||||
removeOnComplete: true,
|
||||
removeOnFail: 100,
|
||||
};
|
||||
|
||||
await this.queueMap[queueName].add(jobName, data, queueOptions);
|
||||
await this.queueMap[queueName].upsertJobScheduler(
|
||||
getJobKey({ jobName, jobId }),
|
||||
options?.repeat,
|
||||
{
|
||||
name: jobName,
|
||||
data,
|
||||
opts: queueOptions,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async removeCron(
|
||||
queueName: MessageQueue,
|
||||
jobName: string,
|
||||
pattern: string,
|
||||
): Promise<void> {
|
||||
await this.queueMap[queueName].removeRepeatable(jobName, {
|
||||
pattern,
|
||||
});
|
||||
async removeCron({
|
||||
queueName,
|
||||
jobName,
|
||||
jobId,
|
||||
}: {
|
||||
queueName: MessageQueue;
|
||||
jobName: string;
|
||||
jobId?: string;
|
||||
}): Promise<void> {
|
||||
await this.queueMap[queueName].removeJobScheduler(
|
||||
getJobKey({ jobName, jobId }),
|
||||
);
|
||||
}
|
||||
|
||||
async add<T>(
|
||||
|
||||
@ -5,7 +5,7 @@ export interface QueueJobOptions {
|
||||
}
|
||||
|
||||
export interface QueueCronJobOptions extends QueueJobOptions {
|
||||
repeat?: {
|
||||
repeat: {
|
||||
every?: number;
|
||||
pattern?: string;
|
||||
limit?: number;
|
||||
|
||||
@ -19,12 +19,27 @@ export interface MessageQueueDriver {
|
||||
handler: ({ data, id }: { data: T; id: string }) => Promise<void> | void,
|
||||
options?: MessageQueueWorkerOptions,
|
||||
);
|
||||
addCron<T extends MessageQueueJobData | undefined>(
|
||||
queueName: MessageQueue,
|
||||
jobName: string,
|
||||
data: T,
|
||||
options?: QueueCronJobOptions,
|
||||
);
|
||||
removeCron(queueName: MessageQueue, jobName: string, pattern?: string);
|
||||
addCron<T extends MessageQueueJobData | undefined>({
|
||||
queueName,
|
||||
jobName,
|
||||
data,
|
||||
options,
|
||||
jobId,
|
||||
}: {
|
||||
queueName: MessageQueue;
|
||||
jobName: string;
|
||||
data: T;
|
||||
options: QueueCronJobOptions;
|
||||
jobId?: string;
|
||||
});
|
||||
removeCron({
|
||||
queueName,
|
||||
jobName,
|
||||
jobId,
|
||||
}: {
|
||||
queueName: MessageQueue;
|
||||
jobName: string;
|
||||
jobId?: string;
|
||||
});
|
||||
register?(queueName: MessageQueue): void;
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import { MessageQueueWorkerOptions } from 'src/engine/core-modules/message-queue
|
||||
import { MessageQueueDriver } from 'src/engine/core-modules/message-queue/drivers/interfaces/message-queue-driver.interface';
|
||||
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { getJobKey } from 'src/engine/core-modules/message-queue/utils/get-job-key.util';
|
||||
|
||||
export type PgBossDriverOptions = PgBoss.ConstructorOptions;
|
||||
|
||||
@ -62,27 +63,40 @@ export class PgBossDriver
|
||||
);
|
||||
}
|
||||
|
||||
async addCron<T>(
|
||||
queueName: MessageQueue,
|
||||
jobName: string,
|
||||
data: T,
|
||||
options?: QueueCronJobOptions,
|
||||
): Promise<void> {
|
||||
async addCron<T>({
|
||||
queueName,
|
||||
jobName,
|
||||
data,
|
||||
options,
|
||||
jobId,
|
||||
}: {
|
||||
queueName: MessageQueue;
|
||||
jobName: string;
|
||||
data: T;
|
||||
options: QueueCronJobOptions;
|
||||
jobId?: string;
|
||||
}): Promise<void> {
|
||||
const name = `${queueName}.${getJobKey({ jobName, jobId })}`;
|
||||
|
||||
await this.pgBoss.schedule(
|
||||
`${queueName}.${jobName}`,
|
||||
options?.repeat?.pattern ??
|
||||
DEFAULT_PG_BOSS_CRON_PATTERN_WHEN_NOT_PROVIDED,
|
||||
name,
|
||||
options.repeat.pattern ?? DEFAULT_PG_BOSS_CRON_PATTERN_WHEN_NOT_PROVIDED,
|
||||
data as object,
|
||||
options
|
||||
? {
|
||||
singletonKey: options?.id,
|
||||
}
|
||||
: {},
|
||||
);
|
||||
}
|
||||
|
||||
async removeCron(queueName: MessageQueue, jobName: string): Promise<void> {
|
||||
await this.pgBoss.unschedule(`${queueName}.${jobName}`);
|
||||
async removeCron({
|
||||
queueName,
|
||||
jobName,
|
||||
jobId,
|
||||
}: {
|
||||
queueName: MessageQueue;
|
||||
jobName: string;
|
||||
jobId?: string;
|
||||
}): Promise<void> {
|
||||
const name = `${queueName}.${getJobKey({ jobName, jobId })}`;
|
||||
|
||||
await this.pgBoss.unschedule(name);
|
||||
}
|
||||
|
||||
async add<T>(
|
||||
|
||||
@ -24,11 +24,15 @@ export class SyncDriver implements MessageQueueDriver {
|
||||
await this.processJob(queueName, { id: '', name: jobName, data });
|
||||
}
|
||||
|
||||
async addCron<T extends MessageQueueJobData | undefined>(
|
||||
queueName: MessageQueue,
|
||||
jobName: string,
|
||||
data: T,
|
||||
): Promise<void> {
|
||||
async addCron<T extends MessageQueueJobData | undefined>({
|
||||
queueName,
|
||||
jobName,
|
||||
data,
|
||||
}: {
|
||||
queueName: MessageQueue;
|
||||
jobName: string;
|
||||
data: T;
|
||||
}): Promise<void> {
|
||||
this.logger.log(`Running cron job with SyncDriver`);
|
||||
await this.processJob(queueName, {
|
||||
id: '',
|
||||
@ -38,7 +42,7 @@ export class SyncDriver implements MessageQueueDriver {
|
||||
});
|
||||
}
|
||||
|
||||
async removeCron(queueName: MessageQueue) {
|
||||
async removeCron({ queueName }: { queueName: MessageQueue }) {
|
||||
this.logger.log(`Removing '${queueName}' cron job with SyncDriver`);
|
||||
}
|
||||
|
||||
|
||||
@ -35,16 +35,38 @@ export class MessageQueueService {
|
||||
return this.driver.add(this.queueName, jobName, data, options);
|
||||
}
|
||||
|
||||
addCron<T extends MessageQueueJobData | undefined>(
|
||||
jobName: string,
|
||||
data: T,
|
||||
options?: QueueCronJobOptions,
|
||||
): Promise<void> {
|
||||
return this.driver.addCron(this.queueName, jobName, data, options);
|
||||
addCron<T extends MessageQueueJobData | undefined>({
|
||||
jobName,
|
||||
data,
|
||||
options,
|
||||
jobId,
|
||||
}: {
|
||||
jobName: string;
|
||||
data: T;
|
||||
options: QueueCronJobOptions;
|
||||
jobId?: string;
|
||||
}): Promise<void> {
|
||||
return this.driver.addCron({
|
||||
queueName: this.queueName,
|
||||
jobName,
|
||||
data,
|
||||
options,
|
||||
jobId,
|
||||
});
|
||||
}
|
||||
|
||||
removeCron(jobName: string, pattern: string): Promise<void> {
|
||||
return this.driver.removeCron(this.queueName, jobName, pattern);
|
||||
removeCron({
|
||||
jobName,
|
||||
jobId,
|
||||
}: {
|
||||
jobName: string;
|
||||
jobId?: string;
|
||||
}): Promise<void> {
|
||||
return this.driver.removeCron({
|
||||
queueName: this.queueName,
|
||||
jobName,
|
||||
jobId,
|
||||
});
|
||||
}
|
||||
|
||||
work<T extends MessageQueueJobData>(
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
export const getJobKey = ({
|
||||
jobName,
|
||||
jobId,
|
||||
}: {
|
||||
jobName: string;
|
||||
jobId?: string;
|
||||
}) => {
|
||||
return `${jobName}${jobId ? `.${jobId}` : ''}`;
|
||||
};
|
||||
@ -19,12 +19,12 @@ export class CleanSuspendedWorkspacesCronCommand extends CommandRunner {
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
await this.messageQueueService.addCron<undefined>(
|
||||
CleanSuspendedWorkspacesJob.name,
|
||||
undefined,
|
||||
{
|
||||
await this.messageQueueService.addCron<undefined>({
|
||||
jobName: CleanSuspendedWorkspacesJob.name,
|
||||
data: undefined,
|
||||
options: {
|
||||
repeat: { pattern: cleanSuspendedWorkspaceCronPattern },
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,14 +20,14 @@ export class CalendarEventListFetchCronCommand extends CommandRunner {
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
await this.messageQueueService.addCron<undefined>(
|
||||
CalendarEventListFetchCronJob.name,
|
||||
undefined,
|
||||
{
|
||||
await this.messageQueueService.addCron<undefined>({
|
||||
jobName: CalendarEventListFetchCronJob.name,
|
||||
data: undefined,
|
||||
options: {
|
||||
repeat: {
|
||||
pattern: CALENDAR_EVENTS_LIST_CRON_PATTERN,
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,12 +21,12 @@ export class CalendarEventsImportCronCommand extends CommandRunner {
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
await this.messageQueueService.addCron<undefined>(
|
||||
CalendarEventsImportCronJob.name,
|
||||
undefined,
|
||||
{
|
||||
await this.messageQueueService.addCron<undefined>({
|
||||
jobName: CalendarEventsImportCronJob.name,
|
||||
data: undefined,
|
||||
options: {
|
||||
repeat: { pattern: CALENDAR_EVENTS_IMPORT_CRON_PATTERN },
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,12 +22,12 @@ export class CalendarOngoingStaleCronCommand extends CommandRunner {
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
await this.messageQueueService.addCron<undefined>(
|
||||
CalendarOngoingStaleCronJob.name,
|
||||
undefined,
|
||||
{
|
||||
await this.messageQueueService.addCron<undefined>({
|
||||
jobName: CalendarOngoingStaleCronJob.name,
|
||||
data: undefined,
|
||||
options: {
|
||||
repeat: { pattern: CALENDAR_ONGOING_STALE_CRON_PATTERN },
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,12 +22,12 @@ export class MessagingMessageListFetchCronCommand extends CommandRunner {
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
await this.messageQueueService.addCron<undefined>(
|
||||
MessagingMessageListFetchCronJob.name,
|
||||
undefined,
|
||||
{
|
||||
await this.messageQueueService.addCron<undefined>({
|
||||
jobName: MessagingMessageListFetchCronJob.name,
|
||||
data: undefined,
|
||||
options: {
|
||||
repeat: { pattern: MESSAGING_MESSAGE_LIST_FETCH_CRON_PATTERN },
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,14 +21,14 @@ export class MessagingMessagesImportCronCommand extends CommandRunner {
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
await this.messageQueueService.addCron<undefined>(
|
||||
MessagingMessagesImportCronJob.name,
|
||||
undefined,
|
||||
{
|
||||
await this.messageQueueService.addCron<undefined>({
|
||||
jobName: MessagingMessagesImportCronJob.name,
|
||||
data: undefined,
|
||||
options: {
|
||||
repeat: {
|
||||
pattern: MESSAGING_MESSAGES_IMPORT_CRON_PATTERN,
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,12 +22,12 @@ export class MessagingOngoingStaleCronCommand extends CommandRunner {
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
await this.messageQueueService.addCron<undefined>(
|
||||
MessagingOngoingStaleCronJob.name,
|
||||
undefined,
|
||||
{
|
||||
await this.messageQueueService.addCron<undefined>({
|
||||
jobName: MessagingOngoingStaleCronJob.name,
|
||||
data: undefined,
|
||||
options: {
|
||||
repeat: { pattern: MESSAGING_ONGOING_STALE_CRON_PATTERN },
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,15 +22,15 @@ export class MessagingMessageChannelSyncStatusMonitoringCronCommand extends Comm
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
await this.messageQueueService.addCron<undefined>(
|
||||
MessagingMessageChannelSyncStatusMonitoringCronJob.name,
|
||||
undefined,
|
||||
{
|
||||
await this.messageQueueService.addCron<undefined>({
|
||||
jobName: MessagingMessageChannelSyncStatusMonitoringCronJob.name,
|
||||
data: undefined,
|
||||
options: {
|
||||
repeat: {
|
||||
pattern:
|
||||
MESSAGING_MESSAGE_CHANNEL_SYNC_STATUS_MONITORING_CRON_PATTERN,
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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],
|
||||
})
|
||||
|
||||
@ -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);
|
||||
|
||||
5
packages/twenty-shared/src/constants/index.ts
Normal file
5
packages/twenty-shared/src/constants/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from './FieldForTotalCountAggregateOperation';
|
||||
export * from './Locales';
|
||||
export * from './TwentyCompaniesBaseUrl';
|
||||
export * from './TwentyIconsBaseUrl';
|
||||
export * from './SettingsFeatures';
|
||||
@ -1,13 +1,4 @@
|
||||
export * from './constants/FieldForTotalCountAggregateOperation';
|
||||
export * from './constants/Locales';
|
||||
export * from './constants/SettingsFeatures';
|
||||
export * from './constants/TwentyCompaniesBaseUrl';
|
||||
export * from './constants/TwentyIconsBaseUrl';
|
||||
export * from './types/ConnectedAccountProvider';
|
||||
export * from './types/FieldMetadataType';
|
||||
export * from './utils/fieldMetadata/isFieldMetadataDateKind';
|
||||
export * from './utils/image/getImageAbsoluteURI';
|
||||
export * from './utils/isDefined';
|
||||
export * from './utils/isValidLocale';
|
||||
export * from './utils/strings';
|
||||
export * from './constants';
|
||||
export * from './types';
|
||||
export * from './utils';
|
||||
export * from './workspace';
|
||||
|
||||
2
packages/twenty-shared/src/types/index.ts
Normal file
2
packages/twenty-shared/src/types/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './ConnectedAccountProvider';
|
||||
export * from './FieldMetadataType';
|
||||
1
packages/twenty-shared/src/utils/fieldMetadata/index.ts
Normal file
1
packages/twenty-shared/src/utils/fieldMetadata/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './isFieldMetadataDateKind';
|
||||
1
packages/twenty-shared/src/utils/image/index.ts
Normal file
1
packages/twenty-shared/src/utils/image/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './getImageAbsoluteURI';
|
||||
5
packages/twenty-shared/src/utils/index.ts
Normal file
5
packages/twenty-shared/src/utils/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from './fieldMetadata';
|
||||
export * from './image';
|
||||
export * from './strings';
|
||||
export * from './validation';
|
||||
export * from './validation';
|
||||
@ -1,4 +1,4 @@
|
||||
import { capitalize } from '../capitalize.util';
|
||||
import { capitalize } from '../capitalize';
|
||||
describe('capitalize', () => {
|
||||
it('should capitalize a string', () => {
|
||||
expect(capitalize('test')).toBe('Test');
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
import { isValidUuid } from '../isValidUuid.util';
|
||||
|
||||
describe('isValidUuid', () => {
|
||||
it('should return true for a valid UUID', () => {
|
||||
expect(isValidUuid('123e4567-e89b-12d3-a456-426614174000')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for an invalid UUID', () => {
|
||||
expect(isValidUuid('123e4567-e89b-12d3-a456-426614174000')).toBe(false);
|
||||
});
|
||||
});
|
||||
@ -1,2 +1 @@
|
||||
export * from './capitalize.util';
|
||||
export * from './isValidUuid.util';
|
||||
export * from './capitalize';
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
export const isValidUuid = (value: string) => {
|
||||
return /^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i.test(
|
||||
value,
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,19 @@
|
||||
import { isValidUuid } from '../isValidUuid';
|
||||
|
||||
describe('isValidUuid', () => {
|
||||
it('should return true for a valid UUID', () => {
|
||||
expect(isValidUuid('123e4567-e89b-12d3-a456-426614174000')).toBe(true);
|
||||
expect(isValidUuid('550e8400-e29b-41d4-a716-446655440000')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for an invalid UUID', () => {
|
||||
expect(isValidUuid('invalid-uuid')).toBe(false);
|
||||
expect(isValidUuid('12345')).toBe(false);
|
||||
expect(isValidUuid('550e8400e29b41d4a716446655440000')).toBe(false);
|
||||
expect(isValidUuid('')).toBe(false);
|
||||
expect(isValidUuid('123e4567-e89b-12d3-a456-42661417400-')).toBe(false);
|
||||
expect(isValidUuid('123e4567-e89b-12d3-a456-42661417400')).toBe(false);
|
||||
expect(isValidUuid('123e4567-e89b-12d3-a456-42661417400)')).toBe(false);
|
||||
expect(isValidUuid('123e4567-e89b-12d3-a456-4266141740001')).toBe(false);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,15 @@
|
||||
import { isValidLocale } from '../isValidLocale';
|
||||
import { APP_LOCALES } from 'src/constants/Locales';
|
||||
|
||||
describe('isValidLocale', () => {
|
||||
it('should return true for valid locales', () => {
|
||||
Object.keys(APP_LOCALES).forEach((locale) => {
|
||||
expect(isValidLocale(locale)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false for invalid locales', () => {
|
||||
expect(isValidLocale('invalidLocale')).toBe(false);
|
||||
expect(isValidLocale(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
3
packages/twenty-shared/src/utils/validation/index.ts
Normal file
3
packages/twenty-shared/src/utils/validation/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './isValidUuid';
|
||||
export * from './isDefined';
|
||||
export * from './isValidLocale';
|
||||
@ -0,0 +1,5 @@
|
||||
export const isValidUuid = (value: string): boolean => {
|
||||
const uuidRegex =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
return uuidRegex.test(value);
|
||||
};
|
||||
@ -1,2 +1,2 @@
|
||||
export * from './types/WorkspaceActivationStatus';
|
||||
export * from './utils/isWorkspaceActiveOrSuspended';
|
||||
export * from './types';
|
||||
export * from './utils';
|
||||
|
||||
1
packages/twenty-shared/src/workspace/types/index.ts
Normal file
1
packages/twenty-shared/src/workspace/types/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './WorkspaceActivationStatus';
|
||||
1
packages/twenty-shared/src/workspace/utils/index.ts
Normal file
1
packages/twenty-shared/src/workspace/utils/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './isWorkspaceActiveOrSuspended';
|
||||
38
yarn.lock
38
yarn.lock
@ -22018,20 +22018,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bullmq@npm:^4.15.0":
|
||||
version: 4.18.2
|
||||
resolution: "bullmq@npm:4.18.2"
|
||||
"bullmq@npm:^5.40.0":
|
||||
version: 5.40.0
|
||||
resolution: "bullmq@npm:5.40.0"
|
||||
dependencies:
|
||||
cron-parser: "npm:^4.6.0"
|
||||
glob: "npm:^8.0.3"
|
||||
ioredis: "npm:^5.3.2"
|
||||
lodash: "npm:^4.17.21"
|
||||
msgpackr: "npm:^1.6.2"
|
||||
cron-parser: "npm:^4.9.0"
|
||||
ioredis: "npm:^5.4.1"
|
||||
msgpackr: "npm:^1.11.2"
|
||||
node-abort-controller: "npm:^3.1.1"
|
||||
semver: "npm:^7.5.4"
|
||||
tslib: "npm:^2.0.0"
|
||||
uuid: "npm:^9.0.0"
|
||||
checksum: 10c0/09371a6d53377e556a37e3e046576bb20056a14ac26d29528bd0a4054c33f458f1764ae696ae6a39fda688e8215ef34f1ba94fe56ba89db5b9ad2c9aa0082f2f
|
||||
checksum: 10c0/00ca72939af44815cfd33e366d2bf650232ac14bc2e3afe32c3aa4bbd74e6b23bec6622f1ecf4a0da684c548ac5553738457a9780bba9bfe23bb831efb766eb8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -24025,7 +24023,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cron-parser@npm:^4.0.0, cron-parser@npm:^4.6.0":
|
||||
"cron-parser@npm:^4.0.0, cron-parser@npm:^4.9.0":
|
||||
version: 4.9.0
|
||||
resolution: "cron-parser@npm:4.9.0"
|
||||
dependencies:
|
||||
@ -29005,7 +29003,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"glob@npm:^8.0.1, glob@npm:^8.0.3, glob@npm:^8.1.0":
|
||||
"glob@npm:^8.0.1, glob@npm:^8.1.0":
|
||||
version: 8.1.0
|
||||
resolution: "glob@npm:8.1.0"
|
||||
dependencies:
|
||||
@ -31081,9 +31079,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ioredis@npm:^5.3.2":
|
||||
version: 5.4.1
|
||||
resolution: "ioredis@npm:5.4.1"
|
||||
"ioredis@npm:^5.4.1":
|
||||
version: 5.4.2
|
||||
resolution: "ioredis@npm:5.4.2"
|
||||
dependencies:
|
||||
"@ioredis/commands": "npm:^1.1.1"
|
||||
cluster-key-slot: "npm:^1.1.0"
|
||||
@ -31094,7 +31092,7 @@ __metadata:
|
||||
redis-errors: "npm:^1.2.0"
|
||||
redis-parser: "npm:^3.0.0"
|
||||
standard-as-callback: "npm:^2.1.0"
|
||||
checksum: 10c0/5d28b7c89a3cab5b76d75923d7d4ce79172b3a1ca9be690133f6e8e393a7a4b4ffd55513e618bbb5504fed80d9e1395c9d9531a7c5c5c84aa4c4e765cca75456
|
||||
checksum: 10c0/e59d2cceb43ed74b487d7b50fa91b93246e734e5d4835c7e62f64e44da072f12ab43b044248012e6f8b76c61a7c091a2388caad50e8ad69a8ce5515a730b23b8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -36774,15 +36772,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"msgpackr@npm:^1.6.2":
|
||||
version: 1.11.0
|
||||
resolution: "msgpackr@npm:1.11.0"
|
||||
"msgpackr@npm:^1.11.2":
|
||||
version: 1.11.2
|
||||
resolution: "msgpackr@npm:1.11.2"
|
||||
dependencies:
|
||||
msgpackr-extract: "npm:^3.0.2"
|
||||
dependenciesMeta:
|
||||
msgpackr-extract:
|
||||
optional: true
|
||||
checksum: 10c0/a7edc36754ec9f8469bc14c896f0f36e0e3de595c0bb5ac7b2ab8c2a72a2e188c12f1345d71a127f8537d9bbc880407a7073ac1d29c27822178bc0b81ae7370e
|
||||
checksum: 10c0/7d2e81ca82c397b2352d470d6bc8f4a967fe4fe14f8fc1fc9906b23009fdfb543999b1ad29c700b8861581e0b6bf903d6f0fefb69a09375cbca6d4d802e6c906
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -46188,7 +46186,7 @@ __metadata:
|
||||
bcrypt: "npm:^5.1.1"
|
||||
better-sqlite3: "npm:^9.2.2"
|
||||
body-parser: "npm:^1.20.2"
|
||||
bullmq: "npm:^4.15.0"
|
||||
bullmq: "npm:^5.40.0"
|
||||
bytes: "npm:^3.1.2"
|
||||
chromatic: "npm:^6.18.0"
|
||||
class-transformer: "npm:^0.5.1"
|
||||
|
||||
Reference in New Issue
Block a user