360 workflow implement workflow cron triggers frontend 2 (#10051)

as title, closes https://github.com/twentyhq/core-team-issues/issues/360

## Cron Setting behavior

https://github.com/user-attachments/assets/0de3a8b9-d899-4455-a945-20c7541c3053

## Cron running behavior


https://github.com/user-attachments/assets/4c33f167-857c-4fcb-9dbe-0f9b661c9e61
This commit is contained in:
martmull
2025-02-07 17:15:03 +01:00
committed by GitHub
parent 988ab9697c
commit ead626c2ec
36 changed files with 826 additions and 19 deletions

View File

@ -59,6 +59,9 @@ export class WorkflowBuilderWorkspaceService {
objectMetadataRepository: this.objectMetadataRepository,
});
}
case WorkflowTriggerType.CRON: {
return {};
}
case WorkflowActionType.SEND_EMAIL: {
return this.computeSendEmailActionOutputSchema();
}

View File

@ -38,9 +38,20 @@ export type WorkflowManualTrigger = BaseTrigger & {
export type WorkflowCronTrigger = BaseTrigger & {
type: WorkflowTriggerType.CRON;
settings: {
pattern: string;
};
settings: (
| {
type: 'HOURS';
schedule: { hour: number; minute: number };
}
| {
type: 'MINUTES';
schedule: { minute: number };
}
| {
type: 'CUSTOM';
pattern: string;
}
) & { outputSchema: object };
};
export type WorkflowManualTriggerSettings = WorkflowManualTrigger['settings'];

View File

@ -0,0 +1,88 @@
import {
WorkflowCronTrigger,
WorkflowTriggerType,
} from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
import { WorkflowTriggerException } from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception';
import { computeCronPatternFromSchedule } from 'src/modules/workflow/workflow-trigger/utils/compute-cron-pattern-from-schedule';
describe('computeCronPatternFromSchedule', () => {
it('should return the pattern for CUSTOM type', () => {
const trigger: WorkflowCronTrigger = {
name: '',
type: WorkflowTriggerType.CRON,
settings: {
type: 'CUSTOM',
pattern: '12 * * * *',
outputSchema: {},
},
};
expect(computeCronPatternFromSchedule(trigger)).toBe('12 * * * *');
});
it('should throw an exception for unsupported pattern for CUSTOM type', () => {
const trigger: WorkflowCronTrigger = {
name: '',
type: WorkflowTriggerType.CRON,
settings: {
type: 'CUSTOM',
pattern: '0 12 * * * *',
outputSchema: {},
},
};
expect(() => computeCronPatternFromSchedule(trigger)).toThrow(
WorkflowTriggerException,
);
expect(() => computeCronPatternFromSchedule(trigger)).toThrow(
"Cron pattern '0 12 * * * *' is invalid",
);
});
it('should return the correct cron pattern for HOURS type', () => {
const trigger: WorkflowCronTrigger = {
name: '',
type: WorkflowTriggerType.CRON,
settings: {
type: 'HOURS',
schedule: { hour: 10, minute: 30 },
outputSchema: {},
},
};
expect(computeCronPatternFromSchedule(trigger)).toBe('30 */10 * * *');
});
it('should return the correct cron pattern for MINUTES type', () => {
const trigger: WorkflowCronTrigger = {
name: '',
type: WorkflowTriggerType.CRON,
settings: {
type: 'MINUTES',
schedule: { minute: 15 },
outputSchema: {},
},
};
expect(computeCronPatternFromSchedule(trigger)).toBe('*/15 * * * *');
});
it('should throw an exception for unsupported schedule type', () => {
const trigger: WorkflowCronTrigger = {
name: '',
type: WorkflowTriggerType.CRON,
settings: {
type: 'INVALID_TYPE' as any,
pattern: '',
outputSchema: {},
},
};
expect(() => computeCronPatternFromSchedule(trigger)).toThrow(
WorkflowTriggerException,
);
expect(() => computeCronPatternFromSchedule(trigger)).toThrow(
'Unsupported cron schedule type',
);
});
});

View File

@ -70,6 +70,9 @@ function assertTriggerSettingsAreValid(
break;
case WorkflowTriggerType.MANUAL:
break;
case WorkflowTriggerType.CRON:
assertCronTriggerSettingsAreValid(settings);
break;
default:
throw new WorkflowTriggerException(
'Invalid trigger type for enabling workflow trigger',
@ -78,6 +81,50 @@ function assertTriggerSettingsAreValid(
}
}
function assertCronTriggerSettingsAreValid(settings: any) {
if (!settings?.type) {
throw new WorkflowTriggerException(
'No setting type provided in cron trigger',
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
);
}
if (settings.type === 'CUSTOM' && !settings.pattern) {
throw new WorkflowTriggerException(
'No pattern provided in CUSTOM cron trigger',
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
);
}
if (!settings.schedule) {
throw new WorkflowTriggerException(
'No schedule provided in cron trigger',
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
);
}
if (settings.type === 'HOURS' && settings.schedule.hour <= 0) {
throw new WorkflowTriggerException(
'Invalid hour value. Should be integer greater than 1',
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
);
}
if (
settings.type === 'HOURS' &&
(settings.schedule.minute < 0 || settings.schedule.minute > 59)
) {
throw new WorkflowTriggerException(
'Invalid minute value. Should be integer between 0 and 59',
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
);
}
if (settings.type === 'MINUTES' && settings.schedule.minute <= 0) {
throw new WorkflowTriggerException(
'Invalid minute value. Should be integer greater than 1',
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
);
}
}
function assertDatabaseEventTriggerSettingsAreValid(settings: any) {
if (!settings?.eventName) {
throw new WorkflowTriggerException(

View File

@ -0,0 +1,49 @@
import cron from 'cron-validate';
import { WorkflowCronTrigger } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
import {
WorkflowTriggerException,
WorkflowTriggerExceptionCode,
} from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception';
const validatePattern = (pattern: string) => {
const cronValidator = cron(pattern);
if (cronValidator.isError()) {
throw new WorkflowTriggerException(
`Cron pattern '${pattern}' is invalid`,
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
);
}
};
export const computeCronPatternFromSchedule = (
trigger: WorkflowCronTrigger,
) => {
switch (trigger.settings.type) {
case 'CUSTOM': {
validatePattern(trigger.settings.pattern);
return trigger.settings.pattern;
}
case 'HOURS': {
const pattern = `${trigger.settings.schedule.minute} */${trigger.settings.schedule.hour} * * *`;
validatePattern(pattern);
return pattern;
}
case 'MINUTES': {
const pattern = `*/${trigger.settings.schedule.minute} * * * *`;
validatePattern(pattern);
return pattern;
}
default:
throw new WorkflowTriggerException(
'Unsupported cron schedule type',
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
);
}
};

View File

@ -36,6 +36,7 @@ import {
WorkflowTriggerJob,
WorkflowTriggerJobData,
} from 'src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job';
import { computeCronPatternFromSchedule } from 'src/modules/workflow/workflow-trigger/utils/compute-cron-pattern-from-schedule';
@Injectable()
export class WorkflowTriggerWorkspaceService {
@ -339,7 +340,9 @@ export class WorkflowTriggerWorkspaceService {
return;
case WorkflowTriggerType.MANUAL:
return;
case WorkflowTriggerType.CRON:
case WorkflowTriggerType.CRON: {
const pattern = computeCronPatternFromSchedule(workflowVersion.trigger);
await this.messageQueueService.addCron<WorkflowTriggerJobData>({
jobName: WorkflowTriggerJob.name,
jobId: workflowVersion.workflowId,
@ -350,12 +353,13 @@ export class WorkflowTriggerWorkspaceService {
},
options: {
repeat: {
pattern: workflowVersion.trigger.settings.pattern,
pattern,
},
},
});
return;
}
default: {
assertNever(workflowVersion.trigger);
}