refactor(core): Centralize CronJob management (#10033)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2024-07-16 20:42:48 +02:00
committed by GitHub
parent 36b314d031
commit 09f2cf9eaf
18 changed files with 730 additions and 429 deletions

View File

@@ -1,93 +1,128 @@
import type { IDataObject } from 'n8n-workflow';
import moment from 'moment-timezone';
import type { IRecurencyRule } from './SchedulerInterface';
import { type CronExpression, randomInt } from 'n8n-workflow';
import type { IRecurrenceRule, ScheduleInterval } from './SchedulerInterface';
export function recurencyCheck(
recurrency: IRecurencyRule,
recurrencyRules: number[],
export function recurrenceCheck(
recurrence: IRecurrenceRule,
recurrenceRules: number[],
timezone: string,
): boolean {
const recurrencyRuleIndex = recurrency.index;
const intervalSize = recurrency.intervalSize;
const typeInterval = recurrency.typeInterval;
if (!recurrence.activated) return true;
const lastExecution =
recurrencyRuleIndex !== undefined ? recurrencyRules[recurrencyRuleIndex] : undefined;
const intervalSize = recurrence.intervalSize;
if (!intervalSize) return false;
if (
intervalSize &&
recurrencyRuleIndex !== undefined &&
(typeInterval === 'weeks' || typeInterval === 'undefined')
) {
const index = recurrence.index;
const typeInterval = recurrence.typeInterval;
const lastExecution = recurrenceRules[index];
const momentTz = moment.tz(timezone);
if (typeInterval === 'hours') {
const hour = momentTz.hour();
if (lastExecution === undefined || hour === (intervalSize + lastExecution) % 24) {
recurrenceRules[index] = hour;
return true;
}
} else if (typeInterval === 'days') {
const dayOfYear = momentTz.dayOfYear();
if (lastExecution === undefined || dayOfYear === (intervalSize + lastExecution) % 365) {
recurrenceRules[index] = dayOfYear;
return true;
}
} else if (typeInterval === 'weeks') {
const week = momentTz.week();
if (
lastExecution === undefined || // First time executing this rule
moment.tz(timezone).week() === (intervalSize + lastExecution) % 52 || // not first time, but minimum interval has passed
moment.tz(timezone).week() === lastExecution // Trigger on multiple days in the same week
week === (intervalSize + lastExecution) % 52 || // not first time, but minimum interval has passed
week === lastExecution // Trigger on multiple days in the same week
) {
recurrencyRules[recurrencyRuleIndex] = moment.tz(timezone).week();
recurrenceRules[index] = week;
return true;
}
} else if (intervalSize && recurrencyRuleIndex !== undefined && typeInterval === 'days') {
if (
lastExecution === undefined ||
moment.tz(timezone).dayOfYear() === (intervalSize + lastExecution) % 365
) {
recurrencyRules[recurrencyRuleIndex] = moment.tz(timezone).dayOfYear();
} else if (typeInterval === 'months') {
const month = momentTz.month();
if (lastExecution === undefined || month === (intervalSize + lastExecution) % 12) {
recurrenceRules[index] = month;
return true;
}
} else if (intervalSize && recurrencyRuleIndex !== undefined && typeInterval === 'hours') {
if (
lastExecution === undefined ||
moment.tz(timezone).hour() === (intervalSize + lastExecution) % 24
) {
recurrencyRules[recurrencyRuleIndex] = moment.tz(timezone).hour();
return true;
}
} else if (intervalSize && recurrencyRuleIndex !== undefined && typeInterval === 'months') {
if (
lastExecution === undefined ||
moment.tz(timezone).month() === (intervalSize + lastExecution) % 12
) {
recurrencyRules[recurrencyRuleIndex] = moment.tz(timezone).month();
return true;
}
} else {
return true;
}
return false;
}
export function convertMonthToUnix(expression: string): string {
if (!isNaN(parseInt(expression)) || expression.includes('-') || expression.includes(',')) {
let matches = expression.match(/([0-9])+/g) as string[];
if (matches) {
matches = matches.map((match) =>
parseInt(match) !== 0 ? String(parseInt(match) - 1) : match,
);
}
expression = matches?.join(expression.includes('-') ? '-' : ',') || '';
}
return expression;
}
export const toCronExpression = (interval: ScheduleInterval): CronExpression => {
if (interval.field === 'cronExpression') return interval.expression;
if (interval.field === 'seconds') return `*/${interval.secondsInterval} * * * * *`;
export function convertToUnixFormat(interval: IDataObject) {
const expression = (interval.expression as string).split(' ');
if (expression.length === 5) {
expression[3] = convertMonthToUnix(expression[3]);
expression[4] = expression[4].replace('7', '0');
} else if (expression.length === 6) {
expression[4] = convertMonthToUnix(expression[4]);
expression[5] = expression[5].replace('7', '0');
}
interval.expression = expression.join(' ');
}
const randomSecond = randomInt(0, 60);
if (interval.field === 'minutes') return `${randomSecond} */${interval.minutesInterval} * * * *`;
export const addFallbackValue = <T>(enabled: boolean, fallback: T) => {
if (enabled) {
return (value: T) => {
if (!value) return fallback;
return value;
};
const minute = interval.triggerAtMinute ?? randomInt(0, 60);
if (interval.field === 'hours')
return `${randomSecond} ${minute} */${interval.hoursInterval} * * *`;
// Since Cron does not support `*/` for days or weeks, all following expressions trigger more often, but are then filtered by `recurrenceCheck`
const hour = interval.triggerAtHour ?? randomInt(0, 24);
if (interval.field === 'days') return `${randomSecond} ${minute} ${hour} * * *`;
if (interval.field === 'weeks') {
const days = interval.triggerAtDay;
const daysOfWeek = days.length === 0 ? '*' : days.join(',');
return `${randomSecond} ${minute} ${hour} * * ${daysOfWeek}` as CronExpression;
}
return (value: T) => value;
const dayOfMonth = interval.triggerAtDayOfMonth ?? randomInt(0, 31);
return `${randomSecond} ${minute} ${hour} ${dayOfMonth} */${interval.monthsInterval} *`;
};
export function intervalToRecurrence(interval: ScheduleInterval, index: number) {
let recurrence: IRecurrenceRule = { activated: false };
if (interval.field === 'hours') {
const { hoursInterval } = interval;
if (hoursInterval !== 1) {
recurrence = {
activated: true,
index,
intervalSize: hoursInterval,
typeInterval: 'hours',
};
}
}
if (interval.field === 'days') {
const { daysInterval } = interval;
if (daysInterval !== 1) {
recurrence = {
activated: true,
index,
intervalSize: daysInterval,
typeInterval: 'days',
};
}
}
if (interval.field === 'weeks') {
const { weeksInterval } = interval;
if (weeksInterval !== 1) {
recurrence = {
activated: true,
index,
intervalSize: weeksInterval,
typeInterval: 'weeks',
};
}
}
if (interval.field === 'months') {
const { monthsInterval } = interval;
if (monthsInterval !== 1) {
recurrence = {
activated: true,
index,
intervalSize: monthsInterval,
typeInterval: 'months',
};
}
}
return recurrence;
}

View File

@@ -1,16 +1,15 @@
import type {
ITriggerFunctions,
IDataObject,
INodeType,
INodeTypeDescription,
ITriggerResponse,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { CronJob } from 'cron';
import moment from 'moment-timezone';
import type { IRecurencyRule } from './SchedulerInterface';
import { addFallbackValue, convertToUnixFormat, recurencyCheck } from './GenericFunctions';
import { sendAt } from 'cron';
import type { IRecurrenceRule, Rule } from './SchedulerInterface';
import { intervalToRecurrence, recurrenceCheck, toCronExpression } from './GenericFunctions';
export class ScheduleTrigger implements INodeType {
description: INodeTypeDescription = {
@@ -402,7 +401,7 @@ export class ScheduleTrigger implements INodeType {
field: ['cronExpression'],
},
},
hint: 'Format: [Minute] [Hour] [Day of Month] [Month] [Day of Week]',
hint: 'Format: [Second] [Minute] [Hour] [Day of Month] [Month] [Day of Week]',
},
],
},
@@ -412,239 +411,74 @@ export class ScheduleTrigger implements INodeType {
};
async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> {
const rule = this.getNodeParameter('rule', []) as IDataObject;
const interval = rule.interval as IDataObject[];
const { interval: intervals } = this.getNodeParameter('rule', []) as Rule;
const timezone = this.getTimezone();
const nodeVersion = this.getNode().typeVersion;
const cronJobs: CronJob[] = [];
const intervalArr: NodeJS.Timeout[] = [];
const staticData = this.getWorkflowStaticData('node') as {
recurrencyRules: number[];
recurrenceRules: number[];
};
if (!staticData.recurrencyRules) {
staticData.recurrencyRules = [];
if (!staticData.recurrenceRules) {
staticData.recurrenceRules = [];
}
const fallbackToZero = addFallbackValue(nodeVersion >= 1.2, '0');
const executeTrigger = async (recurency: IRecurencyRule) => {
const executeTrigger = (recurrence: IRecurrenceRule) => {
const shouldTrigger = recurrenceCheck(recurrence, staticData.recurrenceRules, timezone);
if (!shouldTrigger) return;
const momentTz = moment.tz(timezone);
const resultData = {
timestamp: moment.tz(timezone).toISOString(true),
'Readable date': moment.tz(timezone).format('MMMM Do YYYY, h:mm:ss a'),
'Readable time': moment.tz(timezone).format('h:mm:ss a'),
'Day of week': moment.tz(timezone).format('dddd'),
Year: moment.tz(timezone).format('YYYY'),
Month: moment.tz(timezone).format('MMMM'),
'Day of month': moment.tz(timezone).format('DD'),
Hour: moment.tz(timezone).format('HH'),
Minute: moment.tz(timezone).format('mm'),
Second: moment.tz(timezone).format('ss'),
Timezone: moment.tz(timezone).format('z Z'),
timestamp: momentTz.toISOString(true),
'Readable date': momentTz.format('MMMM Do YYYY, h:mm:ss a'),
'Readable time': momentTz.format('h:mm:ss a'),
'Day of week': momentTz.format('dddd'),
Year: momentTz.format('YYYY'),
Month: momentTz.format('MMMM'),
'Day of month': momentTz.format('DD'),
Hour: momentTz.format('HH'),
Minute: momentTz.format('mm'),
Second: momentTz.format('ss'),
Timezone: `${timezone} (UTC${momentTz.format('Z')})`,
};
if (!recurency.activated) {
this.emit([this.helpers.returnJsonArray([resultData])]);
} else {
if (recurencyCheck(recurency, staticData.recurrencyRules, timezone)) {
this.emit([this.helpers.returnJsonArray([resultData])]);
}
}
this.emit([this.helpers.returnJsonArray([resultData])]);
};
for (let i = 0; i < interval.length; i++) {
let intervalValue = 1000;
if (interval[i].field === 'cronExpression') {
if (nodeVersion > 1) {
// ! Remove this part if we use a cron library that follows unix cron expression
convertToUnixFormat(interval[i]);
}
const cronExpression = interval[i].expression as string;
const rules = intervals.map((interval, i) => ({
interval,
cronExpression: toCronExpression(interval),
recurrence: intervalToRecurrence(interval, i),
}));
if (this.getMode() !== 'manual') {
for (const { interval, cronExpression, recurrence } of rules) {
try {
const cronJob = new CronJob(
cronExpression,
async () => await executeTrigger({ activated: false } as IRecurencyRule),
undefined,
true,
timezone,
);
cronJobs.push(cronJob);
this.helpers.registerCron(cronExpression, () => executeTrigger(recurrence));
} catch (error) {
throw new NodeOperationError(this.getNode(), 'Invalid cron expression', {
description: 'More information on how to build them at https://crontab.guru/',
});
if (interval.field === 'cronExpression') {
throw new NodeOperationError(this.getNode(), 'Invalid cron expression', {
description: 'More information on how to build them at https://crontab.guru/',
});
} else {
throw error;
}
}
}
if (interval[i].field === 'seconds') {
const seconds = interval[i].secondsInterval as number;
intervalValue *= seconds;
const intervalObj = setInterval(
async () => await executeTrigger({ activated: false } as IRecurencyRule),
intervalValue,
) as NodeJS.Timeout;
intervalArr.push(intervalObj);
}
if (interval[i].field === 'minutes') {
const minutes = interval[i].minutesInterval as number;
intervalValue *= 60 * minutes;
const intervalObj = setInterval(
async () => await executeTrigger({ activated: false } as IRecurencyRule),
intervalValue,
) as NodeJS.Timeout;
intervalArr.push(intervalObj);
}
if (interval[i].field === 'hours') {
const hour = interval[i].hoursInterval as number;
const minute = fallbackToZero(interval[i].triggerAtMinute?.toString() as string);
const cronTimes: string[] = [minute, '*', '*', '*', '*'];
const cronExpression: string = cronTimes.join(' ');
if (hour === 1) {
const cronJob = new CronJob(
cronExpression,
async () => await executeTrigger({ activated: false } as IRecurencyRule),
undefined,
true,
timezone,
);
cronJobs.push(cronJob);
} else {
const cronJob = new CronJob(
cronExpression,
async () =>
await executeTrigger({
activated: true,
index: i,
intervalSize: hour,
typeInterval: 'hours',
} as IRecurencyRule),
undefined,
true,
timezone,
);
cronJobs.push(cronJob);
return {};
} else {
const manualTriggerFunction = async () => {
const { interval, cronExpression, recurrence } = rules[0];
if (interval.field === 'cronExpression') {
try {
sendAt(cronExpression);
} catch (error) {
throw new NodeOperationError(this.getNode(), 'Invalid cron expression', {
description: 'More information on how to build them at https://crontab.guru/',
});
}
}
}
executeTrigger(recurrence);
};
if (interval[i].field === 'days') {
const day = interval[i].daysInterval as number;
const hour = interval[i].triggerAtHour?.toString() as string;
const minute = fallbackToZero(interval[i].triggerAtMinute?.toString() as string);
const cronTimes: string[] = [minute, hour, '*', '*', '*'];
const cronExpression: string = cronTimes.join(' ');
if (day === 1) {
const cronJob = new CronJob(
cronExpression,
async () => await executeTrigger({ activated: false } as IRecurencyRule),
undefined,
true,
timezone,
);
cronJobs.push(cronJob);
} else {
const cronJob = new CronJob(
cronExpression,
async () =>
await executeTrigger({
activated: true,
index: i,
intervalSize: day,
typeInterval: 'days',
} as IRecurencyRule),
undefined,
true,
timezone,
);
cronJobs.push(cronJob);
}
}
if (interval[i].field === 'weeks') {
const hour = interval[i].triggerAtHour?.toString() as string;
const minute = fallbackToZero(interval[i].triggerAtMinute?.toString() as string);
const week = interval[i].weeksInterval as number;
const days = interval[i].triggerAtDay as IDataObject[];
const day = days.length === 0 ? '*' : days.join(',');
const cronTimes: string[] = [minute, hour, '*', '*', day];
const cronExpression = cronTimes.join(' ');
if (week === 1) {
const cronJob = new CronJob(
cronExpression,
async () => await executeTrigger({ activated: false } as IRecurencyRule),
undefined,
true,
timezone,
);
cronJobs.push(cronJob);
} else {
const cronJob = new CronJob(
cronExpression,
async () =>
await executeTrigger({
activated: true,
index: i,
intervalSize: week,
typeInterval: 'weeks',
} as IRecurencyRule),
undefined,
true,
timezone,
);
cronJobs.push(cronJob);
}
}
if (interval[i].field === 'months') {
const month = interval[i].monthsInterval;
const day = interval[i].triggerAtDayOfMonth?.toString() as string;
const hour = interval[i].triggerAtHour?.toString() as string;
const minute = fallbackToZero(interval[i].triggerAtMinute?.toString() as string);
const cronTimes: string[] = [minute, hour, day, '*', '*'];
const cronExpression: string = cronTimes.join(' ');
if (month === 1) {
const cronJob = new CronJob(
cronExpression,
async () => await executeTrigger({ activated: false } as IRecurencyRule),
undefined,
true,
timezone,
);
cronJobs.push(cronJob);
} else {
const cronJob = new CronJob(
cronExpression,
async () =>
await executeTrigger({
activated: true,
index: i,
intervalSize: month,
typeInterval: 'months',
} as IRecurencyRule),
undefined,
true,
timezone,
);
cronJobs.push(cronJob);
}
}
return { manualTriggerFunction };
}
async function closeFunction() {
for (const cronJob of cronJobs) {
cronJob.stop();
}
for (const entry of intervalArr) {
clearInterval(entry);
}
}
async function manualTriggerFunction() {
void executeTrigger({ activated: false } as IRecurencyRule);
}
return {
closeFunction,
manualTriggerFunction,
};
}
}

View File

@@ -1,6 +1,53 @@
export interface IRecurencyRule {
activated: boolean;
index?: number;
intervalSize?: number;
typeInterval?: string;
import type { CronExpression } from 'n8n-workflow';
export type IRecurrenceRule =
| { activated: false }
| {
activated: true;
index: number;
intervalSize: number;
typeInterval: 'hours' | 'days' | 'weeks' | 'months';
};
export type ScheduleInterval =
| {
field: 'cronExpression';
expression: CronExpression;
}
| {
field: 'seconds';
secondsInterval: number;
}
| {
field: 'minutes';
minutesInterval: number;
}
| {
field: 'hours';
hoursInterval: number;
triggerAtMinute?: number;
}
| {
field: 'days';
daysInterval: number;
triggerAtHour?: number;
triggerAtMinute?: number;
}
| {
field: 'weeks';
weeksInterval: number;
triggerAtDay: number[];
triggerAtHour?: number;
triggerAtMinute?: number;
}
| {
field: 'months';
monthsInterval: number;
triggerAtDayOfMonth?: number;
triggerAtHour?: number;
triggerAtMinute?: number;
};
export interface Rule {
interval: ScheduleInterval[];
}

View File

@@ -0,0 +1,257 @@
import * as n8nWorkflow from 'n8n-workflow';
import { intervalToRecurrence, recurrenceCheck, toCronExpression } from '../GenericFunctions';
import type { IRecurrenceRule } from '../SchedulerInterface';
describe('toCronExpression', () => {
Object.defineProperty(n8nWorkflow, 'randomInt', {
value: (min: number, max: number) => Math.floor((min + max) / 2),
});
it('should return cron expression for cronExpression field', () => {
const result = toCronExpression({
field: 'cronExpression',
expression: '1 2 3 * * *',
});
expect(result).toEqual('1 2 3 * * *');
});
it('should return cron expression for seconds interval', () => {
const result = toCronExpression({
field: 'seconds',
secondsInterval: 10,
});
expect(result).toEqual('*/10 * * * * *');
});
it('should return cron expression for minutes interval', () => {
const result = toCronExpression({
field: 'minutes',
minutesInterval: 30,
});
expect(result).toEqual('30 */30 * * * *');
});
it('should return cron expression for hours interval', () => {
const result = toCronExpression({
field: 'hours',
hoursInterval: 3,
triggerAtMinute: 22,
});
expect(result).toEqual('30 22 */3 * * *');
const result1 = toCronExpression({
field: 'hours',
hoursInterval: 3,
});
expect(result1).toEqual('30 30 */3 * * *');
});
it('should return cron expression for days interval', () => {
const result = toCronExpression({
field: 'days',
daysInterval: 4,
triggerAtMinute: 30,
triggerAtHour: 10,
});
expect(result).toEqual('30 30 10 * * *');
const result1 = toCronExpression({
field: 'days',
daysInterval: 4,
});
expect(result1).toEqual('30 30 12 * * *');
});
it('should return cron expression for weeks interval', () => {
const result = toCronExpression({
field: 'weeks',
weeksInterval: 2,
triggerAtMinute: 0,
triggerAtHour: 9,
triggerAtDay: [1, 3, 5],
});
expect(result).toEqual('30 0 9 * * 1,3,5');
const result1 = toCronExpression({
field: 'weeks',
weeksInterval: 2,
triggerAtDay: [1, 3, 5],
});
expect(result1).toEqual('30 30 12 * * 1,3,5');
});
it('should return cron expression for months interval', () => {
const result = toCronExpression({
field: 'months',
monthsInterval: 3,
triggerAtMinute: 0,
triggerAtHour: 0,
triggerAtDayOfMonth: 1,
});
expect(result).toEqual('30 0 0 1 */3 *');
const result1 = toCronExpression({
field: 'months',
monthsInterval: 3,
});
expect(result1).toEqual('30 30 12 15 */3 *');
});
});
describe('recurrenceCheck', () => {
it('should return true if activated=false', () => {
const result = recurrenceCheck({ activated: false }, [], 'UTC');
expect(result).toBe(true);
});
it('should return false if intervalSize is falsey', () => {
const result = recurrenceCheck(
{
activated: true,
index: 0,
intervalSize: 0,
typeInterval: 'days',
},
[],
'UTC',
);
expect(result).toBe(false);
});
it('should return true only once for a day cron', () => {
const recurrence: IRecurrenceRule = {
activated: true,
index: 0,
intervalSize: 2,
typeInterval: 'days',
};
const recurrenceRules: number[] = [];
const result1 = recurrenceCheck(recurrence, recurrenceRules, 'UTC');
expect(result1).toBe(true);
const result2 = recurrenceCheck(recurrence, recurrenceRules, 'UTC');
expect(result2).toBe(false);
});
});
describe('intervalToRecurrence', () => {
it('should return recurrence rule for seconds interval', () => {
const result = intervalToRecurrence(
{
field: 'seconds',
secondsInterval: 10,
},
0,
);
expect(result.activated).toBe(false);
});
it('should return recurrence rule for minutes interval', () => {
const result = intervalToRecurrence(
{
field: 'minutes',
minutesInterval: 30,
},
1,
);
expect(result.activated).toBe(false);
});
it('should return recurrence rule for hours interval', () => {
const result = intervalToRecurrence(
{
field: 'hours',
hoursInterval: 3,
triggerAtMinute: 22,
},
2,
);
expect(result).toEqual({
activated: true,
index: 2,
intervalSize: 3,
typeInterval: 'hours',
});
const result1 = intervalToRecurrence(
{
field: 'hours',
hoursInterval: 3,
},
3,
);
expect(result1).toEqual({
activated: true,
index: 3,
intervalSize: 3,
typeInterval: 'hours',
});
});
it('should return recurrence rule for days interval', () => {
const result = intervalToRecurrence(
{
field: 'days',
daysInterval: 4,
triggerAtMinute: 30,
triggerAtHour: 10,
},
4,
);
expect(result).toEqual({
activated: true,
index: 4,
intervalSize: 4,
typeInterval: 'days',
});
const result1 = intervalToRecurrence(
{
field: 'days',
daysInterval: 4,
},
5,
);
expect(result1).toEqual({
activated: true,
index: 5,
intervalSize: 4,
typeInterval: 'days',
});
});
it('should return recurrence rule for weeks interval', () => {
const result = intervalToRecurrence(
{
field: 'weeks',
weeksInterval: 2,
triggerAtMinute: 0,
triggerAtHour: 9,
triggerAtDay: [1, 3, 5],
},
6,
);
expect(result).toEqual({
activated: true,
index: 6,
intervalSize: 2,
typeInterval: 'weeks',
});
});
it('should return recurrence rule for months interval', () => {
const result = intervalToRecurrence(
{
field: 'months',
monthsInterval: 3,
triggerAtMinute: 0,
triggerAtHour: 0,
triggerAtDayOfMonth: 1,
},
8,
);
expect(result).toEqual({
activated: true,
index: 8,
intervalSize: 3,
typeInterval: 'months',
});
});
});

View File

@@ -0,0 +1,83 @@
import * as n8nWorkflow from 'n8n-workflow';
import type { INode, ITriggerFunctions, Workflow } from 'n8n-workflow';
import { returnJsonArray } from 'n8n-core';
import { ScheduledTaskManager } from 'n8n-core/dist/ScheduledTaskManager';
import { mock } from 'jest-mock-extended';
import { ScheduleTrigger } from '../ScheduleTrigger.node';
describe('ScheduleTrigger', () => {
Object.defineProperty(n8nWorkflow, 'randomInt', {
value: (min: number, max: number) => Math.floor((min + max) / 2),
});
const HOUR = 60 * 60 * 1000;
const mockDate = new Date('2023-12-28 12:34:56.789Z');
const timezone = 'Europe/Berlin';
jest.useFakeTimers();
jest.setSystemTime(mockDate);
const node = mock<INode>({ typeVersion: 1 });
const workflow = mock<Workflow>({ timezone });
const scheduledTaskManager = new ScheduledTaskManager();
const helpers = mock<ITriggerFunctions['helpers']>({
returnJsonArray,
registerCron: (cronExpression, onTick) =>
scheduledTaskManager.registerCron(workflow, cronExpression, onTick),
});
const triggerFunctions = mock<ITriggerFunctions>({
helpers,
getTimezone: () => timezone,
getNode: () => node,
getMode: () => 'trigger',
});
const scheduleTrigger = new ScheduleTrigger();
beforeEach(() => {
jest.clearAllMocks();
});
describe('trigger', () => {
it('should emit on defined schedule', async () => {
triggerFunctions.getNodeParameter.calledWith('rule', expect.anything()).mockReturnValueOnce({
interval: [{ field: 'hours', hoursInterval: 3 }],
});
triggerFunctions.getWorkflowStaticData.mockReturnValueOnce({ recurrenceRules: [] });
const result = await scheduleTrigger.trigger.call(triggerFunctions);
// Assert that no manualTriggerFunction is returned
expect(result).toEqual({});
expect(triggerFunctions.emit).not.toHaveBeenCalled();
jest.advanceTimersByTime(HOUR);
expect(triggerFunctions.emit).not.toHaveBeenCalled();
jest.advanceTimersByTime(2 * HOUR);
expect(triggerFunctions.emit).toHaveBeenCalledTimes(1);
const firstTriggerData = triggerFunctions.emit.mock.calls[0][0][0][0];
expect(firstTriggerData.json).toEqual({
'Day of month': '28',
'Day of week': 'Thursday',
Hour: '15',
Minute: '30',
Month: 'December',
'Readable date': 'December 28th 2023, 3:30:30 pm',
'Readable time': '3:30:30 pm',
Second: '30',
Timezone: 'Europe/Berlin (UTC+01:00)',
Year: '2023',
timestamp: '2023-12-28T15:30:30.000+01:00',
});
jest.setSystemTime(new Date(firstTriggerData.json.timestamp as string));
jest.advanceTimersByTime(2 * HOUR);
expect(triggerFunctions.emit).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(HOUR);
expect(triggerFunctions.emit).toHaveBeenCalledTimes(2);
});
});
});