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:
agobrech 2022-10-18 13:59:17 +02:00 committed by GitHub
parent 1aa21ed3df
commit 128c3b83df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 555 additions and 2 deletions

View file

@ -128,7 +128,7 @@
} }
] ]
}, },
"alias": ["Time", "Scheduler", "Polling"], "alias": ["Time", "Scheduler", "Polling", "Cron", "Interval"],
"subcategories": { "subcategories": {
"Core Nodes": ["Flow"] "Core Nodes": ["Flow"]
} }

View file

@ -17,6 +17,7 @@ export class Cron implements INodeType {
icon: 'fa:calendar', icon: 'fa:calendar',
group: ['trigger', 'schedule'], group: ['trigger', 'schedule'],
version: 1, version: 1,
hidden: true,
description: 'Triggers the workflow at a specific time', description: 'Triggers the workflow at a specific time',
eventTriggerDescription: '', eventTriggerDescription: '',
activationMessage: activationMessage:
@ -69,7 +70,9 @@ export class Cron implements INodeType {
const timezone = this.getTimezone(); const timezone = this.getTimezone();
// Start the cron-jobs // 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 // Stop the cron-jobs
async function closeFunction() { async function closeFunction() {

View file

@ -13,6 +13,7 @@ export class Interval implements INodeType {
icon: 'fa:hourglass', icon: 'fa:hourglass',
group: ['trigger', 'schedule'], group: ['trigger', 'schedule'],
version: 1, version: 1,
hidden: true,
description: 'Triggers the workflow in a given interval', description: 'Triggers the workflow in a given interval',
eventTriggerDescription: '', eventTriggerDescription: '',
activationMessage: activationMessage:

View file

@ -0,0 +1,9 @@
import { IDataObject } from 'n8n-workflow';
export type ICronExpression = [
string | Date,
string | Date,
string | Date,
string | Date,
string | Date,
];

View 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"]
}
}

View 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 doesnt have this day, the node wont 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,
};
}
}

View 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

View file

@ -630,6 +630,7 @@
"dist/nodes/S3/S3.node.js", "dist/nodes/S3/S3.node.js",
"dist/nodes/Salesforce/Salesforce.node.js", "dist/nodes/Salesforce/Salesforce.node.js",
"dist/nodes/Salesmate/Salesmate.node.js", "dist/nodes/Salesmate/Salesmate.node.js",
"dist/nodes/Schedule/ScheduleTrigger.node.js",
"dist/nodes/SeaTable/SeaTable.node.js", "dist/nodes/SeaTable/SeaTable.node.js",
"dist/nodes/SeaTable/SeaTableTrigger.node.js", "dist/nodes/SeaTable/SeaTableTrigger.node.js",
"dist/nodes/SecurityScorecard/SecurityScorecard.node.js", "dist/nodes/SecurityScorecard/SecurityScorecard.node.js",