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

@ -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,
},
},
);
});
}
}

View File

@ -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,
});
}
}

View File

@ -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>(

View File

@ -5,7 +5,7 @@ export interface QueueJobOptions {
}
export interface QueueCronJobOptions extends QueueJobOptions {
repeat?: {
repeat: {
every?: number;
pattern?: string;
limit?: number;

View File

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

View File

@ -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>(

View File

@ -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`);
}

View File

@ -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>(

View File

@ -0,0 +1,9 @@
export const getJobKey = ({
jobName,
jobId,
}: {
jobName: string;
jobId?: string;
}) => {
return `${jobName}${jobId ? `.${jobId}` : ''}`;
};

View File

@ -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 },
},
);
});
}
}

View File

@ -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,
},
},
);
});
}
}

View File

@ -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 },
},
);
});
}
}

View File

@ -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 },
},
);
});
}
}

View File

@ -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 },
},
);
});
}
}

View File

@ -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,
},
},
);
});
}
}

View File

@ -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 },
},
);
});
}
}

View File

@ -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,
},
},
);
});
}
}

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);

View File

@ -0,0 +1,5 @@
export * from './FieldForTotalCountAggregateOperation';
export * from './Locales';
export * from './TwentyCompaniesBaseUrl';
export * from './TwentyIconsBaseUrl';
export * from './SettingsFeatures';

View File

@ -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';

View File

@ -0,0 +1,2 @@
export * from './ConnectedAccountProvider';
export * from './FieldMetadataType';

View File

@ -0,0 +1 @@
export * from './isFieldMetadataDateKind';

View File

@ -0,0 +1 @@
export * from './getImageAbsoluteURI';

View File

@ -0,0 +1,5 @@
export * from './fieldMetadata';
export * from './image';
export * from './strings';
export * from './validation';
export * from './validation';

View File

@ -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');

View File

@ -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);
});
});

View File

@ -1,2 +1 @@
export * from './capitalize.util';
export * from './isValidUuid.util';
export * from './capitalize';

View File

@ -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,
);
};

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -0,0 +1,3 @@
export * from './isValidUuid';
export * from './isDefined';
export * from './isValidLocale';

View File

@ -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);
};

View File

@ -1,2 +1,2 @@
export * from './types/WorkspaceActivationStatus';
export * from './utils/isWorkspaceActiveOrSuspended';
export * from './types';
export * from './utils';

View File

@ -0,0 +1 @@
export * from './WorkspaceActivationStatus';

View File

@ -0,0 +1 @@
export * from './isWorkspaceActiveOrSuspended';