mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
fix(Schedule Trigger Node): Change scheduler behaviour for intervals days and hours (#5133)
* 🐛Fix scheduler for intervals days and week * ♻️ Simplify and move recurrency rules outside trigger node * Remove async and promise from recurency rule * Update correctly the Static data when using recurrency Rule * Fix logic when recurrency is activated * 🎨 Remove useless staticData fix(passed by reference) * 🐛 remove duplicted hour cronJob leading to 2 executions * More fixes, handles multiple execution * 🐛 fixing dayOfYear recurency check * 🐛 fix recurency check for hours/days should not equal lastExecution * Add month interval to the scheduler * Fix flawed logic for comparing interval * 🚨 Fix lint issue type --------- Co-authored-by: Marcus <marcus@n8n.io>
This commit is contained in:
parent
ec7575b032
commit
78bbe2ba27
57
packages/nodes-base/nodes/Schedule/GenericFunctions.ts
Normal file
57
packages/nodes-base/nodes/Schedule/GenericFunctions.ts
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import type { IRecurencyRule } from './SchedulerInterface';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
export function recurencyCheck(
|
||||||
|
recurrency: IRecurencyRule,
|
||||||
|
recurrencyRules: number[],
|
||||||
|
timezone: string,
|
||||||
|
): boolean {
|
||||||
|
const recurrencyRuleIndex = recurrency.index;
|
||||||
|
const intervalSize = recurrency.intervalSize;
|
||||||
|
const typeInterval = recurrency.typeInterval;
|
||||||
|
|
||||||
|
const lastExecution =
|
||||||
|
recurrencyRuleIndex !== undefined ? recurrencyRules[recurrencyRuleIndex] : undefined;
|
||||||
|
|
||||||
|
if (
|
||||||
|
intervalSize &&
|
||||||
|
recurrencyRuleIndex !== undefined &&
|
||||||
|
(typeInterval === 'weeks' || typeInterval === 'undefined')
|
||||||
|
) {
|
||||||
|
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
|
||||||
|
) {
|
||||||
|
recurrencyRules[recurrencyRuleIndex] = moment.tz(timezone).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();
|
||||||
|
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;
|
||||||
|
}
|
|
@ -4,6 +4,8 @@ import { NodeOperationError } from 'n8n-workflow';
|
||||||
|
|
||||||
import { CronJob } from 'cron';
|
import { CronJob } from 'cron';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import type { IRecurencyRule } from './SchedulerInterface';
|
||||||
|
import { recurencyCheck } from './GenericFunctions';
|
||||||
|
|
||||||
export class ScheduleTrigger implements INodeType {
|
export class ScheduleTrigger implements INodeType {
|
||||||
description: INodeTypeDescription = {
|
description: INodeTypeDescription = {
|
||||||
|
@ -416,7 +418,7 @@ export class ScheduleTrigger implements INodeType {
|
||||||
if (!staticData.recurrencyRules) {
|
if (!staticData.recurrencyRules) {
|
||||||
staticData.recurrencyRules = [];
|
staticData.recurrencyRules = [];
|
||||||
}
|
}
|
||||||
const executeTrigger = (recurrencyRuleIndex?: number, weeksInterval?: number) => {
|
const executeTrigger = async (recurency: IRecurencyRule) => {
|
||||||
const resultData = {
|
const resultData = {
|
||||||
timestamp: moment.tz(timezone).toISOString(true),
|
timestamp: moment.tz(timezone).toISOString(true),
|
||||||
'Readable date': moment.tz(timezone).format('MMMM Do YYYY, h:mm:ss a'),
|
'Readable date': moment.tz(timezone).format('MMMM Do YYYY, h:mm:ss a'),
|
||||||
|
@ -430,24 +432,13 @@ export class ScheduleTrigger implements INodeType {
|
||||||
Second: moment.tz(timezone).format('ss'),
|
Second: moment.tz(timezone).format('ss'),
|
||||||
Timezone: moment.tz(timezone).format('z Z'),
|
Timezone: moment.tz(timezone).format('z Z'),
|
||||||
};
|
};
|
||||||
const lastExecutionWeekNumber =
|
|
||||||
recurrencyRuleIndex !== undefined
|
|
||||||
? staticData.recurrencyRules[recurrencyRuleIndex]
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
// Checks if have the right week interval, handles new years as well
|
if (!recurency.activated) {
|
||||||
if (weeksInterval && recurrencyRuleIndex !== undefined) {
|
this.emit([this.helpers.returnJsonArray([resultData])]);
|
||||||
if (
|
} else {
|
||||||
lastExecutionWeekNumber === undefined || // First time executing this rule
|
if (recurencyCheck(recurency, staticData.recurrencyRules, timezone)) {
|
||||||
moment.tz(timezone).week() >= weeksInterval + lastExecutionWeekNumber || // not first time, but minimum interval has passed
|
|
||||||
moment.tz(timezone).week() + 52 >= weeksInterval + lastExecutionWeekNumber // not first time, correct interval but year has passed
|
|
||||||
) {
|
|
||||||
staticData.recurrencyRules[recurrencyRuleIndex] = moment.tz(timezone).week();
|
|
||||||
this.emit([this.helpers.returnJsonArray([resultData])]);
|
this.emit([this.helpers.returnJsonArray([resultData])]);
|
||||||
}
|
}
|
||||||
// There is no else block here since we don't want to emit anything now
|
|
||||||
} else {
|
|
||||||
this.emit([this.helpers.returnJsonArray([resultData])]);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -456,7 +447,13 @@ export class ScheduleTrigger implements INodeType {
|
||||||
if (interval[i].field === 'cronExpression') {
|
if (interval[i].field === 'cronExpression') {
|
||||||
const cronExpression = interval[i].expression as string;
|
const cronExpression = interval[i].expression as string;
|
||||||
try {
|
try {
|
||||||
const cronJob = new CronJob(cronExpression, executeTrigger, undefined, true, timezone);
|
const cronJob = new CronJob(
|
||||||
|
cronExpression,
|
||||||
|
async () => executeTrigger({ activated: false } as IRecurencyRule),
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
timezone,
|
||||||
|
);
|
||||||
cronJobs.push(cronJob);
|
cronJobs.push(cronJob);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new NodeOperationError(this.getNode(), 'Invalid cron expression', {
|
throw new NodeOperationError(this.getNode(), 'Invalid cron expression', {
|
||||||
|
@ -468,34 +465,86 @@ export class ScheduleTrigger implements INodeType {
|
||||||
if (interval[i].field === 'seconds') {
|
if (interval[i].field === 'seconds') {
|
||||||
const seconds = interval[i].secondsInterval as number;
|
const seconds = interval[i].secondsInterval as number;
|
||||||
intervalValue *= seconds;
|
intervalValue *= seconds;
|
||||||
const intervalObj = setInterval(executeTrigger, intervalValue) as NodeJS.Timeout;
|
const intervalObj = setInterval(
|
||||||
|
async () => executeTrigger({ activated: false } as IRecurencyRule),
|
||||||
|
intervalValue,
|
||||||
|
) as NodeJS.Timeout;
|
||||||
intervalArr.push(intervalObj);
|
intervalArr.push(intervalObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (interval[i].field === 'minutes') {
|
if (interval[i].field === 'minutes') {
|
||||||
const minutes = interval[i].minutesInterval as number;
|
const minutes = interval[i].minutesInterval as number;
|
||||||
intervalValue *= 60 * minutes;
|
intervalValue *= 60 * minutes;
|
||||||
const intervalObj = setInterval(executeTrigger, intervalValue);
|
const intervalObj = setInterval(
|
||||||
|
async () => executeTrigger({ activated: false } as IRecurencyRule),
|
||||||
|
intervalValue,
|
||||||
|
) as NodeJS.Timeout;
|
||||||
intervalArr.push(intervalObj);
|
intervalArr.push(intervalObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (interval[i].field === 'hours') {
|
if (interval[i].field === 'hours') {
|
||||||
const hour = interval[i].hoursInterval?.toString() as string;
|
const hour = interval[i].hoursInterval as number;
|
||||||
const minute = interval[i].triggerAtMinute?.toString() as string;
|
const minute = interval[i].triggerAtMinute?.toString() as string;
|
||||||
const cronTimes: string[] = [minute, `*/${hour}`, '*', '*', '*'];
|
const cronTimes: string[] = [minute, '*', '*', '*', '*'];
|
||||||
const cronExpression: string = cronTimes.join(' ');
|
const cronExpression: string = cronTimes.join(' ');
|
||||||
const cronJob = new CronJob(cronExpression, executeTrigger, undefined, true, timezone);
|
if (hour === 1) {
|
||||||
|
const cronJob = new CronJob(
|
||||||
|
cronExpression,
|
||||||
|
async () => executeTrigger({ activated: false } as IRecurencyRule),
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
timezone,
|
||||||
|
);
|
||||||
cronJobs.push(cronJob);
|
cronJobs.push(cronJob);
|
||||||
|
} else {
|
||||||
|
const cronJob = new CronJob(
|
||||||
|
cronExpression,
|
||||||
|
async () =>
|
||||||
|
executeTrigger({
|
||||||
|
activated: true,
|
||||||
|
index: i,
|
||||||
|
intervalSize: hour,
|
||||||
|
typeInterval: 'hours',
|
||||||
|
} as IRecurencyRule),
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
timezone,
|
||||||
|
);
|
||||||
|
cronJobs.push(cronJob);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (interval[i].field === 'days') {
|
if (interval[i].field === 'days') {
|
||||||
const day = interval[i].daysInterval?.toString() as string;
|
const day = interval[i].daysInterval as number;
|
||||||
const hour = interval[i].triggerAtHour?.toString() as string;
|
const hour = interval[i].triggerAtHour?.toString() as string;
|
||||||
const minute = interval[i].triggerAtMinute?.toString() as string;
|
const minute = interval[i].triggerAtMinute?.toString() as string;
|
||||||
const cronTimes: string[] = [minute, hour, `*/${day}`, '*', '*'];
|
const cronTimes: string[] = [minute, hour, '*', '*', '*'];
|
||||||
const cronExpression: string = cronTimes.join(' ');
|
const cronExpression: string = cronTimes.join(' ');
|
||||||
const cronJob = new CronJob(cronExpression, executeTrigger, undefined, true, timezone);
|
if (day === 1) {
|
||||||
|
const cronJob = new CronJob(
|
||||||
|
cronExpression,
|
||||||
|
async () => executeTrigger({ activated: false } as IRecurencyRule),
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
timezone,
|
||||||
|
);
|
||||||
cronJobs.push(cronJob);
|
cronJobs.push(cronJob);
|
||||||
|
} else {
|
||||||
|
const cronJob = new CronJob(
|
||||||
|
cronExpression,
|
||||||
|
async () =>
|
||||||
|
executeTrigger({
|
||||||
|
activated: true,
|
||||||
|
index: i,
|
||||||
|
intervalSize: day,
|
||||||
|
typeInterval: 'days',
|
||||||
|
} as IRecurencyRule),
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
timezone,
|
||||||
|
);
|
||||||
|
cronJobs.push(cronJob);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (interval[i].field === 'weeks') {
|
if (interval[i].field === 'weeks') {
|
||||||
|
@ -507,12 +556,24 @@ export class ScheduleTrigger implements INodeType {
|
||||||
const cronTimes: string[] = [minute, hour, '*', '*', day];
|
const cronTimes: string[] = [minute, hour, '*', '*', day];
|
||||||
const cronExpression = cronTimes.join(' ');
|
const cronExpression = cronTimes.join(' ');
|
||||||
if (week === 1) {
|
if (week === 1) {
|
||||||
const cronJob = new CronJob(cronExpression, executeTrigger, undefined, true, timezone);
|
const cronJob = new CronJob(
|
||||||
|
cronExpression,
|
||||||
|
async () => executeTrigger({ activated: false } as IRecurencyRule),
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
timezone,
|
||||||
|
);
|
||||||
cronJobs.push(cronJob);
|
cronJobs.push(cronJob);
|
||||||
} else {
|
} else {
|
||||||
const cronJob = new CronJob(
|
const cronJob = new CronJob(
|
||||||
cronExpression,
|
cronExpression,
|
||||||
() => executeTrigger(i, week),
|
async () =>
|
||||||
|
executeTrigger({
|
||||||
|
activated: true,
|
||||||
|
index: i,
|
||||||
|
intervalSize: week,
|
||||||
|
typeInterval: 'weeks',
|
||||||
|
} as IRecurencyRule),
|
||||||
undefined,
|
undefined,
|
||||||
true,
|
true,
|
||||||
timezone,
|
timezone,
|
||||||
|
@ -522,14 +583,37 @@ export class ScheduleTrigger implements INodeType {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (interval[i].field === 'months') {
|
if (interval[i].field === 'months') {
|
||||||
const month = interval[i].monthsInterval?.toString() as string;
|
const month = interval[i].monthsInterval;
|
||||||
const day = interval[i].triggerAtDayOfMonth?.toString() as string;
|
const day = interval[i].triggerAtDayOfMonth?.toString() as string;
|
||||||
const hour = interval[i].triggerAtHour?.toString() as string;
|
const hour = interval[i].triggerAtHour?.toString() as string;
|
||||||
const minute = interval[i].triggerAtMinute?.toString() as string;
|
const minute = interval[i].triggerAtMinute?.toString() as string;
|
||||||
const cronTimes: string[] = [minute, hour, day, `*/${month}`, '*'];
|
const cronTimes: string[] = [minute, hour, day, '*', '*'];
|
||||||
const cronExpression: string = cronTimes.join(' ');
|
const cronExpression: string = cronTimes.join(' ');
|
||||||
const cronJob = new CronJob(cronExpression, executeTrigger, undefined, true, timezone);
|
if (month === 1) {
|
||||||
|
const cronJob = new CronJob(
|
||||||
|
cronExpression,
|
||||||
|
async () => executeTrigger({ activated: false } as IRecurencyRule),
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
timezone,
|
||||||
|
);
|
||||||
cronJobs.push(cronJob);
|
cronJobs.push(cronJob);
|
||||||
|
} else {
|
||||||
|
const cronJob = new CronJob(
|
||||||
|
cronExpression,
|
||||||
|
async () =>
|
||||||
|
executeTrigger({
|
||||||
|
activated: true,
|
||||||
|
index: i,
|
||||||
|
intervalSize: month,
|
||||||
|
typeInterval: 'months',
|
||||||
|
} as IRecurencyRule),
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
timezone,
|
||||||
|
);
|
||||||
|
cronJobs.push(cronJob);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -543,7 +627,7 @@ export class ScheduleTrigger implements INodeType {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function manualTriggerFunction() {
|
async function manualTriggerFunction() {
|
||||||
executeTrigger();
|
void executeTrigger({ activated: false } as IRecurencyRule);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
6
packages/nodes-base/nodes/Schedule/SchedulerInterface.ts
Normal file
6
packages/nodes-base/nodes/Schedule/SchedulerInterface.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export interface IRecurencyRule {
|
||||||
|
activated: boolean;
|
||||||
|
index?: number;
|
||||||
|
intervalSize?: number;
|
||||||
|
typeInterval?: string;
|
||||||
|
}
|
Loading…
Reference in a new issue