mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-23 11:44:06 -08:00
feat(Node): add the Scheduler Node (#4223)
* ✨ Create Schedule node with MVP structure * ✨ Add 24 increments for hours picker * 🚨 Lintfix * Add timestamp, add hour minute and cron expression * Fix bug where there was one extra interval object * Fix default value from fixedCollection * 🐛 UI fixes * 🎨 Changed logic to reflect UI fixes * Fix auto intitialising * Deprecated interval and cron in favor of schedule node * 🐛 Ui fixes * 🐛 Fix issue with week intervals * 🚨 Lint fixes * change order of days in the week to chronological order
This commit is contained in:
parent
1aa21ed3df
commit
128c3b83df
|
@ -128,7 +128,7 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"alias": ["Time", "Scheduler", "Polling"],
|
||||
"alias": ["Time", "Scheduler", "Polling", "Cron", "Interval"],
|
||||
"subcategories": {
|
||||
"Core Nodes": ["Flow"]
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ export class Cron implements INodeType {
|
|||
icon: 'fa:calendar',
|
||||
group: ['trigger', 'schedule'],
|
||||
version: 1,
|
||||
hidden: true,
|
||||
description: 'Triggers the workflow at a specific time',
|
||||
eventTriggerDescription: '',
|
||||
activationMessage:
|
||||
|
@ -69,7 +70,9 @@ export class Cron implements INodeType {
|
|||
const timezone = this.getTimezone();
|
||||
|
||||
// Start the cron-jobs
|
||||
const cronJobs = cronTimes.map(cronTime => new CronJob(cronTime, executeTrigger, undefined, true, timezone));
|
||||
const cronJobs = cronTimes.map(
|
||||
(cronTime) => new CronJob(cronTime, executeTrigger, undefined, true, timezone),
|
||||
);
|
||||
|
||||
// Stop the cron-jobs
|
||||
async function closeFunction() {
|
||||
|
|
|
@ -13,6 +13,7 @@ export class Interval implements INodeType {
|
|||
icon: 'fa:hourglass',
|
||||
group: ['trigger', 'schedule'],
|
||||
version: 1,
|
||||
hidden: true,
|
||||
description: 'Triggers the workflow in a given interval',
|
||||
eventTriggerDescription: '',
|
||||
activationMessage:
|
||||
|
|
9
packages/nodes-base/nodes/Schedule/CronInterface.ts
Normal file
9
packages/nodes-base/nodes/Schedule/CronInterface.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { IDataObject } from 'n8n-workflow';
|
||||
|
||||
export type ICronExpression = [
|
||||
string | Date,
|
||||
string | Date,
|
||||
string | Date,
|
||||
string | Date,
|
||||
string | Date,
|
||||
];
|
18
packages/nodes-base/nodes/Schedule/ScheduleTrigger.node.json
Normal file
18
packages/nodes-base/nodes/Schedule/ScheduleTrigger.node.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"node": "n8n-nodes-base.scheduleTrigger",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Core Nodes"],
|
||||
"resources": {
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.scheduleTrigger/"
|
||||
}
|
||||
],
|
||||
"generic": []
|
||||
},
|
||||
"alias": ["Time", "Scheduler", "Polling", "Cron", "Interval"],
|
||||
"subcategories": {
|
||||
"Core Nodes": ["Flow"]
|
||||
}
|
||||
}
|
520
packages/nodes-base/nodes/Schedule/ScheduleTrigger.node.ts
Normal file
520
packages/nodes-base/nodes/Schedule/ScheduleTrigger.node.ts
Normal file
|
@ -0,0 +1,520 @@
|
|||
import { ITriggerFunctions } from 'n8n-core';
|
||||
import {
|
||||
IDataObject,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
ITriggerResponse,
|
||||
NodeOperationError,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { CronJob } from 'cron';
|
||||
import { ICronExpression } from './CronInterface';
|
||||
import moment from 'moment';
|
||||
|
||||
export class ScheduleTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Schedule Trigger',
|
||||
name: 'scheduleTrigger',
|
||||
icon: 'file:schedule.svg',
|
||||
group: ['trigger', 'schedule'],
|
||||
version: 1,
|
||||
description: 'Triggers the workflow on a given schedule',
|
||||
eventTriggerDescription: '',
|
||||
activationMessage:
|
||||
'Your schedule trigger will now trigger executions on the schedule you have defined.',
|
||||
defaults: {
|
||||
name: 'Schedule Trigger',
|
||||
color: '#00FF00',
|
||||
},
|
||||
// eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node
|
||||
inputs: [],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName:
|
||||
'This workflow will run on the schedule you define here once you <a data-key="activate">activate</a> it.<br><br>For testing, you can also trigger it manually: by going back to the canvas and clicking ‘execute workflow’',
|
||||
name: 'notice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Trigger Rules',
|
||||
name: 'rule',
|
||||
placeholder: 'Add Rule',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
default: {
|
||||
interval: [
|
||||
{
|
||||
field: 'days',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'interval',
|
||||
displayName: 'Trigger Interval',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Trigger Interval',
|
||||
name: 'field',
|
||||
type: 'options',
|
||||
default: 'days',
|
||||
options: [
|
||||
{
|
||||
name: 'Custom (Cron)',
|
||||
value: 'cronExpression',
|
||||
},
|
||||
{
|
||||
name: 'Days',
|
||||
value: 'days',
|
||||
},
|
||||
{
|
||||
name: 'Hours',
|
||||
value: 'hours',
|
||||
},
|
||||
{
|
||||
name: 'Minutes',
|
||||
value: 'minutes',
|
||||
},
|
||||
{
|
||||
name: 'Months',
|
||||
value: 'months',
|
||||
},
|
||||
{
|
||||
name: 'Seconds',
|
||||
value: 'seconds',
|
||||
},
|
||||
{
|
||||
name: 'Weeks',
|
||||
value: 'weeks',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Seconds Between Triggers',
|
||||
name: 'secondsInterval',
|
||||
type: 'number',
|
||||
default: 30,
|
||||
displayOptions: {
|
||||
show: {
|
||||
field: ['seconds'],
|
||||
},
|
||||
},
|
||||
description: 'Number of seconds between each workflow trigger',
|
||||
},
|
||||
{
|
||||
displayName: 'Minutes Between Triggers',
|
||||
name: 'minutesInterval',
|
||||
type: 'number',
|
||||
default: 5,
|
||||
displayOptions: {
|
||||
show: {
|
||||
field: ['minutes'],
|
||||
},
|
||||
},
|
||||
description: 'Number of minutes between each workflow trigger',
|
||||
},
|
||||
{
|
||||
displayName: 'Hours Between Triggers',
|
||||
name: 'hoursInterval',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
show: {
|
||||
field: ['hours'],
|
||||
},
|
||||
},
|
||||
default: 1,
|
||||
description: 'Number of hours between each workflow trigger',
|
||||
},
|
||||
{
|
||||
displayName: 'Days Between Triggers',
|
||||
name: 'daysInterval',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
show: {
|
||||
field: ['days'],
|
||||
},
|
||||
},
|
||||
default: 1,
|
||||
description: 'Number of days between each workflow trigger',
|
||||
},
|
||||
{
|
||||
displayName: 'Weeks Between Triggers',
|
||||
name: 'weeksInterval',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
show: {
|
||||
field: ['weeks'],
|
||||
},
|
||||
},
|
||||
default: 1,
|
||||
description: 'Would run every week unless specified otherwise',
|
||||
},
|
||||
{
|
||||
displayName: 'Months Between Triggers',
|
||||
name: 'monthsInterval',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
show: {
|
||||
field: ['months'],
|
||||
},
|
||||
},
|
||||
default: 1,
|
||||
description: 'Would run every month unless specified otherwise',
|
||||
},
|
||||
{
|
||||
displayName: 'Trigger at Day of Month',
|
||||
name: 'triggerAtDayOfMonth',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
show: {
|
||||
field: ['months'],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 31,
|
||||
},
|
||||
default: 1,
|
||||
description: 'The day of the month to trigger (1-31)',
|
||||
hint: 'If a month doesn’t have this day, the node won’t trigger',
|
||||
},
|
||||
{
|
||||
displayName: 'Trigger on Weekdays',
|
||||
name: 'triggerAtDay',
|
||||
type: 'multiOptions',
|
||||
displayOptions: {
|
||||
show: {
|
||||
field: ['weeks'],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
maxValue: 7,
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Monday',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
name: 'Tuesday',
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
name: 'Wednesday',
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
name: 'Thursday',
|
||||
value: 4,
|
||||
},
|
||||
{
|
||||
name: 'Friday',
|
||||
value: 5,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Saturday',
|
||||
value: 6,
|
||||
},
|
||||
{
|
||||
name: 'Sunday',
|
||||
value: 7,
|
||||
},
|
||||
],
|
||||
default: [7],
|
||||
},
|
||||
{
|
||||
displayName: 'Trigger at Hour',
|
||||
name: 'triggerAtHour',
|
||||
type: 'options',
|
||||
default: 0,
|
||||
displayOptions: {
|
||||
show: {
|
||||
field: ['days', 'weeks', 'months'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Midnight',
|
||||
displayName: 'Midnight',
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
name: '1am',
|
||||
displayName: '1am',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
name: '2am',
|
||||
displayName: '2am',
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
name: '3am',
|
||||
displayName: '3am',
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
name: '4am',
|
||||
displayName: '4am',
|
||||
value: 4,
|
||||
},
|
||||
{
|
||||
name: '5am',
|
||||
displayName: '5am',
|
||||
value: 5,
|
||||
},
|
||||
{
|
||||
name: '6am',
|
||||
displayName: '6am',
|
||||
value: 6,
|
||||
},
|
||||
{
|
||||
name: '7am',
|
||||
displayName: '7am',
|
||||
value: 7,
|
||||
},
|
||||
{
|
||||
name: '8am',
|
||||
displayName: '8am',
|
||||
value: 8,
|
||||
},
|
||||
{
|
||||
name: '9am',
|
||||
displayName: '9am',
|
||||
value: 9,
|
||||
},
|
||||
{
|
||||
name: '10am',
|
||||
displayName: '10am',
|
||||
value: 10,
|
||||
},
|
||||
{
|
||||
name: '11am',
|
||||
displayName: '11am',
|
||||
value: 11,
|
||||
},
|
||||
{
|
||||
name: 'Noon',
|
||||
displayName: 'Noon',
|
||||
value: 12,
|
||||
},
|
||||
{
|
||||
name: '1pm',
|
||||
displayName: '1pm',
|
||||
value: 13,
|
||||
},
|
||||
{
|
||||
name: '2pm',
|
||||
displayName: '2pm',
|
||||
value: 14,
|
||||
},
|
||||
{
|
||||
name: '3pm',
|
||||
displayName: '3pm',
|
||||
value: 15,
|
||||
},
|
||||
{
|
||||
name: '4pm',
|
||||
displayName: '4pm',
|
||||
value: 16,
|
||||
},
|
||||
{
|
||||
name: '5pm',
|
||||
displayName: '5pm',
|
||||
value: 17,
|
||||
},
|
||||
{
|
||||
name: '6pm',
|
||||
displayName: '6pm',
|
||||
value: 18,
|
||||
},
|
||||
{
|
||||
name: '7pm',
|
||||
displayName: '7pm',
|
||||
value: 19,
|
||||
},
|
||||
{
|
||||
name: '8pm',
|
||||
displayName: '8pm',
|
||||
value: 20,
|
||||
},
|
||||
{
|
||||
name: '9pm',
|
||||
displayName: '9pm',
|
||||
value: 21,
|
||||
},
|
||||
{
|
||||
name: '10pm',
|
||||
displayName: '10pm',
|
||||
value: 22,
|
||||
},
|
||||
{
|
||||
name: '11pm',
|
||||
displayName: '11pm',
|
||||
value: 23,
|
||||
},
|
||||
],
|
||||
description: 'The hour of the day to trigger',
|
||||
},
|
||||
{
|
||||
displayName: 'Trigger at Minute',
|
||||
name: 'triggerAtMinute',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
displayOptions: {
|
||||
show: {
|
||||
field: ['hours', 'days', 'weeks', 'months'],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 0,
|
||||
maxValue: 59,
|
||||
},
|
||||
description: 'The minute past the hour to trigger (0-59)',
|
||||
},
|
||||
{
|
||||
displayName:
|
||||
'You can find help generating your cron expression <a href="http://www.cronmaker.com/?1" target="_blank">here</a>',
|
||||
name: 'notice',
|
||||
type: 'notice',
|
||||
displayOptions: {
|
||||
show: {
|
||||
field: ['cronExpression'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Expression',
|
||||
name: 'expression',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'eg. 0 15 * 1 sun',
|
||||
displayOptions: {
|
||||
show: {
|
||||
field: ['cronExpression'],
|
||||
},
|
||||
},
|
||||
hint: 'Format: [Minute] [Hour] [Day of Month] [Month] [Day of Week]',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> {
|
||||
const rule = this.getNodeParameter('rule', []) as IDataObject;
|
||||
const interval = rule.interval as IDataObject[];
|
||||
const timezone = this.getTimezone();
|
||||
const date = moment.tz(timezone).week();
|
||||
const cronJobs: CronJob[] = [];
|
||||
let intervalObj: NodeJS.Timeout;
|
||||
const executeTrigger = () => {
|
||||
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'),
|
||||
};
|
||||
this.emit([this.helpers.returnJsonArray([resultData])]);
|
||||
};
|
||||
for (let i = 0; i < interval.length; i++) {
|
||||
let intervalValue = 1000;
|
||||
if (interval[i].field === 'cronExpression') {
|
||||
const cronExpression = interval[i].expression as string;
|
||||
try {
|
||||
const cronJob = new CronJob(cronExpression, executeTrigger, undefined, true, timezone);
|
||||
cronJobs.push(cronJob);
|
||||
} catch (error) {
|
||||
throw new NodeOperationError(this.getNode(), 'Invalid cron expression', {
|
||||
description: 'More information on how to build them at http://www.cronmaker.com',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (interval[i].field === 'seconds') {
|
||||
const seconds = interval[i].secondsInterval as number;
|
||||
intervalValue *= seconds;
|
||||
intervalObj = setInterval(executeTrigger, intervalValue);
|
||||
}
|
||||
|
||||
if (interval[i].field === 'minutes') {
|
||||
const minutes = interval[i].minutesInterval as number;
|
||||
intervalValue *= 60 * minutes;
|
||||
intervalObj = setInterval(executeTrigger, intervalValue);
|
||||
}
|
||||
|
||||
if (interval[i].field === 'hours') {
|
||||
const hour = interval[i].triggerAtHour?.toString() as string;
|
||||
const minute = interval[i].triggerAtMinute?.toString() as string;
|
||||
const week = interval[i].triggerAtWeek as number;
|
||||
const cronTimes: ICronExpression = [minute, hour, `*/${week * 7}`, '*', '*'];
|
||||
const cronExpression = cronTimes.join(' ');
|
||||
const cronJob = new CronJob(cronExpression, executeTrigger, undefined, true, timezone);
|
||||
cronJobs.push(cronJob);
|
||||
}
|
||||
|
||||
if (interval[i].field === 'days') {
|
||||
const day = interval[i].daysInterval?.toString() as string;
|
||||
const hour = interval[i].triggerAtHour?.toString() as string;
|
||||
const minute = interval[i].triggerAtMinute?.toString() as string;
|
||||
const cronTimes: ICronExpression = [minute, hour, `*/${day}`, '*', '*'];
|
||||
const cronExpression: string = cronTimes.join(' ');
|
||||
const cronJob = new CronJob(cronExpression, executeTrigger, undefined, true, timezone);
|
||||
cronJobs.push(cronJob);
|
||||
}
|
||||
|
||||
if (interval[i].field === 'weeks') {
|
||||
const days = interval[i].triggerAtDay as IDataObject[];
|
||||
const day = days.join(',') as string;
|
||||
const hour = interval[i].triggerAtHour?.toString() as string;
|
||||
const minute = interval[i].triggerAtMinute?.toString() as string;
|
||||
const cronTimes: ICronExpression = [minute, hour, '*', '*', day];
|
||||
const cronExpression: string = cronTimes.join(' ');
|
||||
const cronJob = new CronJob(cronExpression, executeTrigger, undefined, true, timezone);
|
||||
cronJobs.push(cronJob);
|
||||
}
|
||||
|
||||
if (interval[i].field === 'months') {
|
||||
const month = interval[i].monthsInterval?.toString() as string;
|
||||
const day = interval[i].triggerAtDayOfMonth?.toString() as string;
|
||||
const hour = interval[i].triggerAtHour?.toString() as string;
|
||||
const minute = interval[i].triggerAtMinute?.toString() as string;
|
||||
const cronTimes: ICronExpression = [minute, hour, day, `*/${month}`, '*'];
|
||||
const cronExpression: string = cronTimes.join(' ');
|
||||
const cronJob = new CronJob(cronExpression, executeTrigger, undefined, true, timezone);
|
||||
cronJobs.push(cronJob);
|
||||
}
|
||||
}
|
||||
|
||||
async function closeFunction() {
|
||||
for (const cronJob of cronJobs) {
|
||||
cronJob.stop();
|
||||
}
|
||||
clearInterval(intervalObj);
|
||||
}
|
||||
|
||||
async function manualTriggerFunction() {
|
||||
executeTrigger();
|
||||
}
|
||||
|
||||
return {
|
||||
closeFunction,
|
||||
manualTriggerFunction,
|
||||
};
|
||||
}
|
||||
}
|
1
packages/nodes-base/nodes/Schedule/schedule.svg
Normal file
1
packages/nodes-base/nodes/Schedule/schedule.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:svgjs="http://svgjs.com/svgjs" xmlns:xlink="http://www.w3.org/1999/xlink" width="288" height="288"><svg xmlns="http://www.w3.org/2000/svg" width="288" height="288" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc.--><path fill="#4ebf32" d="M232 120C232 106.7 242.7 96 256 96C269.3 96 280 106.7 280 120V243.2L365.3 300C376.3 307.4 379.3 322.3 371.1 333.3C364.6 344.3 349.7 347.3 338.7 339.1L242.7 275.1C236 271.5 232 264 232 255.1L232 120zM256 0C397.4 0 512 114.6 512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256C0 114.6 114.6 0 256 0zM48 256C48 370.9 141.1 464 256 464C370.9 464 464 370.9 464 256C464 141.1 370.9 48 256 48C141.1 48 48 141.1 48 256z" class="color000 svgShape"/></svg></svg>
|
After Width: | Height: | Size: 883 B |
|
@ -630,6 +630,7 @@
|
|||
"dist/nodes/S3/S3.node.js",
|
||||
"dist/nodes/Salesforce/Salesforce.node.js",
|
||||
"dist/nodes/Salesmate/Salesmate.node.js",
|
||||
"dist/nodes/Schedule/ScheduleTrigger.node.js",
|
||||
"dist/nodes/SeaTable/SeaTable.node.js",
|
||||
"dist/nodes/SeaTable/SeaTableTrigger.node.js",
|
||||
"dist/nodes/SecurityScorecard/SecurityScorecard.node.js",
|
||||
|
|
Loading…
Reference in a new issue