1
1
mirror of https://github.com/n8n-io/n8n.git synced 2024-10-08 02:28:57 +03:00

feat(Date & Time Node): Overhaul of the node (#5904)

* Setup versionized node

* Fix node naming

* Set all possible actions

* Add Current Date operation

* Add timezone to current date

* feat add to date operator

* Change output field name to camel case

* Fix info box for luxons tip

* Feat subtract to date operation

* Feat format date operation

* Fix to node field for format date

* Feat rounding operation

* Feat get in between date operation

* Feat add extract date operation

* Add generic function for parsing date

* Remove moment methods from operations

* Change moment to luxon for the rest of the operations

* Fix Format date operation

* Fix format value

* Add timezone option for current date

* Add tests, improve workflow settings for testing, toString the results

* Change icon for V2

* Revert "Change icon for V2"

This reverts commit 46b59bea2e.

* Change workflow  test name

* Fix ui bug for custom format

* Fix default value for format operation

* Fix info box for rounding operation

* Change default for units for between time operation

* Inprove fields and resort time units

* Fix extract week number

* Resolve issue with formating and timezones

* Fix field name and unit order

*  restored removed test case, sync v1 with curent master

*  parseDate update to support timestamps, tests

* Keep same field for substract and add time

* Update unit test

* Improve visibility, add iso to string option

* Update option naming

---------

Co-authored-by: Michael Kret <michael.k@radency.com>
This commit is contained in:
agobrech 2023-05-08 17:34:14 +02:00 committed by GitHub
parent 40bc74b6a2
commit 7d1d1f7872
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 2206 additions and 578 deletions

View File

@ -1,579 +1,27 @@
import type {
IDataObject,
IExecuteFunctions,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow';
import { VersionedNodeType } from 'n8n-workflow';
import { deepCopy, NodeOperationError } from 'n8n-workflow';
import { DateTimeV1 } from './V1/DateTimeV1.node';
import set from 'lodash.set';
import { DateTimeV2 } from './V2/DateTimeV2.node';
import moment from 'moment-timezone';
export class DateTime extends VersionedNodeType {
constructor() {
const baseDescription: INodeTypeBaseDescription = {
displayName: 'Date & Time',
name: 'dateTime',
icon: 'fa:clock',
group: ['transform'],
defaultVersion: 2,
description: 'Allows you to manipulate date and time values',
subtitle: '={{$parameter["action"]}}',
};
import { DateTime as LuxonDateTime } from 'luxon';
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
1: new DateTimeV1(baseDescription),
2: new DateTimeV2(baseDescription),
};
function parseDateByFormat(this: IExecuteFunctions, value: string, fromFormat: string) {
const date = moment(value, fromFormat, true);
if (moment(date).isValid()) return date;
throw new NodeOperationError(
this.getNode(),
'Date input cannot be parsed. Please recheck the value and the "From Format" field.',
);
}
function getIsoValue(this: IExecuteFunctions, value: string) {
try {
return new Date(value).toISOString(); // may throw due to unpredictable input
} catch (error) {
throw new NodeOperationError(
this.getNode(),
'Unrecognized date input. Please specify a format in the "From Format" field.',
);
}
}
function parseDateByDefault(this: IExecuteFunctions, value: string) {
const isoValue = getIsoValue.call(this, value);
if (moment(isoValue).isValid()) return moment(isoValue);
throw new NodeOperationError(
this.getNode(),
'Unrecognized date input. Please specify a format in the "From Format" field.',
);
}
export class DateTime implements INodeType {
description: INodeTypeDescription = {
displayName: 'Date & Time',
name: 'dateTime',
icon: 'fa:clock',
group: ['transform'],
version: 1,
description: 'Allows you to manipulate date and time values',
subtitle: '={{$parameter["action"]}}',
defaults: {
name: 'Date & Time',
color: '#408000',
},
inputs: ['main'],
outputs: ['main'],
properties: [
{
displayName:
"More powerful date functionality is available in <a href='https://docs.n8n.io/code-examples/expressions/luxon/' target='_blank'>expressions</a>,</br> e.g. <code>{{ $now.plus(1, 'week') }}</code>",
name: 'noticeDateTime',
type: 'notice',
default: '',
},
{
displayName: 'Action',
name: 'action',
type: 'options',
options: [
{
name: 'Calculate a Date',
description: 'Add or subtract time from a date',
value: 'calculate',
action: 'Add or subtract time from a date',
},
{
name: 'Format a Date',
description: 'Convert a date to a different format',
value: 'format',
action: 'Convert a date to a different format',
},
],
default: 'format',
},
{
displayName: 'Value',
name: 'value',
displayOptions: {
show: {
action: ['format'],
},
},
type: 'string',
default: '',
description: 'The value that should be converted',
required: true,
},
{
displayName: 'Property Name',
name: 'dataPropertyName',
type: 'string',
default: 'data',
required: true,
displayOptions: {
show: {
action: ['format'],
},
},
description: 'Name of the property to which to write the converted date',
},
{
displayName: 'Custom Format',
name: 'custom',
displayOptions: {
show: {
action: ['format'],
},
},
type: 'boolean',
default: false,
description: 'Whether a predefined format should be selected or custom format entered',
},
{
displayName: 'To Format',
name: 'toFormat',
displayOptions: {
show: {
action: ['format'],
custom: [true],
},
},
type: 'string',
default: '',
placeholder: 'YYYY-MM-DD',
description: 'The format to convert the date to',
},
{
displayName: 'To Format',
name: 'toFormat',
type: 'options',
displayOptions: {
show: {
action: ['format'],
custom: [false],
},
},
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
options: [
{
name: 'MM/DD/YYYY',
value: 'MM/DD/YYYY',
description: 'Example: 09/04/1986',
},
{
name: 'YYYY/MM/DD',
value: 'YYYY/MM/DD',
description: 'Example: 1986/04/09',
},
{
name: 'MMMM DD YYYY',
value: 'MMMM DD YYYY',
description: 'Example: April 09 1986',
},
{
name: 'MM-DD-YYYY',
value: 'MM-DD-YYYY',
description: 'Example: 09-04-1986',
},
{
name: 'YYYY-MM-DD',
value: 'YYYY-MM-DD',
description: 'Example: 1986-04-09',
},
{
name: 'Unix Timestamp',
value: 'X',
description: 'Example: 513388800.879',
},
{
name: 'Unix Ms Timestamp',
value: 'x',
description: 'Example: 513388800',
},
],
default: 'MM/DD/YYYY',
description: 'The format to convert the date to',
},
{
displayName: 'Options',
name: 'options',
displayOptions: {
show: {
action: ['format'],
},
},
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'From Format',
name: 'fromFormat',
type: 'string',
default: '',
description: 'In case the input format is not recognized you can provide the format',
},
{
displayName: 'From Timezone Name or ID',
name: 'fromTimezone',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTimezones',
},
default: 'UTC',
description:
'The timezone to convert from. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
},
{
displayName: 'To Timezone Name or ID',
name: 'toTimezone',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTimezones',
},
default: 'UTC',
description:
'The timezone to convert to. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
},
],
},
{
displayName: 'Date Value',
name: 'value',
displayOptions: {
show: {
action: ['calculate'],
},
},
type: 'string',
default: '',
description: 'The date string or timestamp from which you want to add/subtract time',
required: true,
},
{
displayName: 'Operation',
name: 'operation',
displayOptions: {
show: {
action: ['calculate'],
},
},
type: 'options',
noDataExpression: true,
options: [
{
name: 'Add',
value: 'add',
description: 'Add time to Date Value',
action: 'Add time to Date Value',
},
{
name: 'Subtract',
value: 'subtract',
description: 'Subtract time from Date Value',
action: 'Subtract time from Date Value',
},
],
default: 'add',
required: true,
},
{
displayName: 'Duration',
name: 'duration',
displayOptions: {
show: {
action: ['calculate'],
},
},
type: 'number',
typeOptions: {
minValue: 0,
},
default: 0,
required: true,
description: 'E.g. enter “10” then select “Days” if you want to add 10 days to Date Value.',
},
{
displayName: 'Time Unit',
name: 'timeUnit',
description: 'Time unit for Duration parameter above',
displayOptions: {
show: {
action: ['calculate'],
},
},
type: 'options',
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
options: [
{
name: 'Quarters',
value: 'quarters',
},
{
name: 'Years',
value: 'years',
},
{
name: 'Months',
value: 'months',
},
{
name: 'Weeks',
value: 'weeks',
},
{
name: 'Days',
value: 'days',
},
{
name: 'Hours',
value: 'hours',
},
{
name: 'Minutes',
value: 'minutes',
},
{
name: 'Seconds',
value: 'seconds',
},
{
name: 'Milliseconds',
value: 'milliseconds',
},
],
default: 'days',
required: true,
},
{
displayName: 'Property Name',
name: 'dataPropertyName',
type: 'string',
default: 'data',
required: true,
displayOptions: {
show: {
action: ['calculate'],
},
},
description: 'Name of the output property to which to write the converted date',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
displayOptions: {
show: {
action: ['calculate'],
},
},
options: [
{
displayName: 'From Format',
name: 'fromFormat',
type: 'string',
default: '',
description:
'Format for parsing the value as a date. If unrecognized, specify the <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.datetime/#faqs">format</a> for the value.',
},
],
},
],
};
methods = {
loadOptions: {
// Get all the timezones to display them to user so that they can
// select them easily
async getTimezones(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
for (const timezone of moment.tz.names()) {
const timezoneName = timezone;
const timezoneId = timezone;
returnData.push({
name: timezoneName,
value: timezoneId,
});
}
return returnData;
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const length = items.length;
const returnData: INodeExecutionData[] = [];
const workflowTimezone = this.getTimezone();
let item: INodeExecutionData;
for (let i = 0; i < length; i++) {
try {
const action = this.getNodeParameter('action', 0) as string;
item = items[i];
if (action === 'format') {
let currentDate: string | number | LuxonDateTime = this.getNodeParameter(
'value',
i,
) as string;
const dataPropertyName = this.getNodeParameter('dataPropertyName', i);
const toFormat = this.getNodeParameter('toFormat', i) as string;
const options = this.getNodeParameter('options', i);
let newDate;
if ((currentDate as unknown as IDataObject) instanceof LuxonDateTime) {
currentDate = (currentDate as unknown as LuxonDateTime).toISO();
}
// Check if the input is a number
if (!Number.isNaN(Number(currentDate))) {
//input is a number, convert to number in case it is a string
currentDate = Number(currentDate);
// check if the number is a timestamp in float format and convert to integer
if (!Number.isInteger(currentDate)) {
currentDate = currentDate * 1000;
}
}
if (currentDate === undefined) {
continue;
}
if (options.fromFormat === undefined && !moment(currentDate).isValid()) {
throw new NodeOperationError(
this.getNode(),
'The date input format could not be recognized. Please set the "From Format" field',
{ itemIndex: i },
);
}
if (Number.isInteger(currentDate)) {
const timestampLengthInMilliseconds1990 = 12;
// check if the number is a timestamp in seconds or milliseconds and create a moment object accordingly
if (currentDate.toString().length < timestampLengthInMilliseconds1990) {
newDate = moment.unix(currentDate as number);
} else {
newDate = moment(currentDate);
}
} else {
if (options.fromTimezone || options.toTimezone) {
const fromTimezone = options.fromTimezone || workflowTimezone;
if (options.fromFormat) {
newDate = moment.tz(
currentDate as string,
options.fromFormat as string,
fromTimezone as string,
);
} else {
newDate = moment.tz(currentDate, fromTimezone as string);
}
} else {
if (options.fromFormat) {
newDate = moment(currentDate, options.fromFormat as string);
} else {
newDate = moment(currentDate);
}
}
}
if (options.toTimezone || options.fromTimezone) {
// If either a source or a target timezone got defined the
// timezone of the date has to be changed. If a target-timezone
// is set use it else fall back to workflow timezone.
newDate = newDate.tz((options.toTimezone as string) || workflowTimezone);
}
newDate = newDate.format(toFormat);
let newItem: INodeExecutionData;
if (dataPropertyName.includes('.')) {
// Uses dot notation so copy all data
newItem = {
json: deepCopy(item.json),
pairedItem: {
item: i,
},
};
} else {
// Does not use dot notation so shallow copy is enough
newItem = {
json: { ...item.json },
pairedItem: {
item: i,
},
};
}
if (item.binary !== undefined) {
newItem.binary = item.binary;
}
set(newItem, `json.${dataPropertyName}`, newDate);
returnData.push(newItem);
}
if (action === 'calculate') {
const dateValue = this.getNodeParameter('value', i) as string;
const operation = this.getNodeParameter('operation', i) as 'add' | 'subtract';
const duration = this.getNodeParameter('duration', i) as number;
const timeUnit = this.getNodeParameter('timeUnit', i) as moment.DurationInputArg2;
const { fromFormat } = this.getNodeParameter('options', i) as { fromFormat?: string };
const dataPropertyName = this.getNodeParameter('dataPropertyName', i);
const newDate = fromFormat
? parseDateByFormat.call(this, dateValue, fromFormat)
: parseDateByDefault.call(this, dateValue);
operation === 'add'
? newDate.add(duration, timeUnit).utc().format()
: newDate.subtract(duration, timeUnit).utc().format();
let newItem: INodeExecutionData;
if (dataPropertyName.includes('.')) {
// Uses dot notation so copy all data
newItem = {
json: deepCopy(item.json),
pairedItem: {
item: i,
},
};
} else {
// Does not use dot notation so shallow copy is enough
newItem = {
json: { ...item.json },
pairedItem: {
item: i,
},
};
}
if (item.binary !== undefined) {
newItem.binary = item.binary;
}
set(newItem, `json.${dataPropertyName}`, newDate.toISOString());
returnData.push(newItem);
}
} catch (error) {
if (this.continueOnFail()) {
returnData.push({
json: {
error: error.message,
},
pairedItem: {
item: i,
},
});
continue;
}
throw error;
}
}
return this.prepareOutputData(returnData);
super(nodeVersions, baseDescription);
}
}

View File

@ -0,0 +1,590 @@
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
import type {
IDataObject,
IExecuteFunctions,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeBaseDescription,
INodeTypeDescription,
} from 'n8n-workflow';
import { deepCopy, NodeOperationError } from 'n8n-workflow';
import set from 'lodash.set';
import moment from 'moment-timezone';
import { DateTime as LuxonDateTime } from 'luxon';
function parseDateByFormat(this: IExecuteFunctions, value: string, fromFormat: string) {
const date = moment(value, fromFormat, true);
if (moment(date).isValid()) return date;
throw new NodeOperationError(
this.getNode(),
'Date input cannot be parsed. Please recheck the value and the "From Format" field.',
);
}
function getIsoValue(this: IExecuteFunctions, value: string) {
try {
return new Date(value).toISOString(); // may throw due to unpredictable input
} catch (error) {
throw new NodeOperationError(
this.getNode(),
'Unrecognized date input. Please specify a format in the "From Format" field.',
);
}
}
function parseDateByDefault(this: IExecuteFunctions, value: string) {
const isoValue = getIsoValue.call(this, value);
if (moment(isoValue).isValid()) return moment(isoValue);
throw new NodeOperationError(
this.getNode(),
'Unrecognized date input. Please specify a format in the "From Format" field.',
);
}
const versionDescription: INodeTypeDescription = {
displayName: 'Date & Time',
name: 'dateTime',
icon: 'fa:clock',
group: ['transform'],
version: 1,
description: 'Allows you to manipulate date and time values',
subtitle: '={{$parameter["action"]}}',
defaults: {
name: 'Date & Time',
color: '#408000',
},
inputs: ['main'],
outputs: ['main'],
properties: [
{
displayName:
"More powerful date functionality is available in <a href='https://docs.n8n.io/code-examples/expressions/luxon/' target='_blank'>expressions</a>,</br> e.g. <code>{{ $now.plus(1, 'week') }}</code>",
name: 'noticeDateTime',
type: 'notice',
default: '',
},
{
displayName: 'Action',
name: 'action',
type: 'options',
options: [
{
name: 'Calculate a Date',
description: 'Add or subtract time from a date',
value: 'calculate',
action: 'Add or subtract time from a date',
},
{
name: 'Format a Date',
description: 'Convert a date to a different format',
value: 'format',
action: 'Convert a date to a different format',
},
],
default: 'format',
},
{
displayName: 'Value',
name: 'value',
displayOptions: {
show: {
action: ['format'],
},
},
type: 'string',
default: '',
description: 'The value that should be converted',
required: true,
},
{
displayName: 'Property Name',
name: 'dataPropertyName',
type: 'string',
default: 'data',
required: true,
displayOptions: {
show: {
action: ['format'],
},
},
description: 'Name of the property to which to write the converted date',
},
{
displayName: 'Custom Format',
name: 'custom',
displayOptions: {
show: {
action: ['format'],
},
},
type: 'boolean',
default: false,
description: 'Whether a predefined format should be selected or custom format entered',
},
{
displayName: 'To Format',
name: 'toFormat',
displayOptions: {
show: {
action: ['format'],
custom: [true],
},
},
type: 'string',
default: '',
placeholder: 'YYYY-MM-DD',
description: 'The format to convert the date to',
},
{
displayName: 'To Format',
name: 'toFormat',
type: 'options',
displayOptions: {
show: {
action: ['format'],
custom: [false],
},
},
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
options: [
{
name: 'MM/DD/YYYY',
value: 'MM/DD/YYYY',
description: 'Example: 09/04/1986',
},
{
name: 'YYYY/MM/DD',
value: 'YYYY/MM/DD',
description: 'Example: 1986/04/09',
},
{
name: 'MMMM DD YYYY',
value: 'MMMM DD YYYY',
description: 'Example: April 09 1986',
},
{
name: 'MM-DD-YYYY',
value: 'MM-DD-YYYY',
description: 'Example: 09-04-1986',
},
{
name: 'YYYY-MM-DD',
value: 'YYYY-MM-DD',
description: 'Example: 1986-04-09',
},
{
name: 'Unix Timestamp',
value: 'X',
description: 'Example: 513388800.879',
},
{
name: 'Unix Ms Timestamp',
value: 'x',
description: 'Example: 513388800',
},
],
default: 'MM/DD/YYYY',
description: 'The format to convert the date to',
},
{
displayName: 'Options',
name: 'options',
displayOptions: {
show: {
action: ['format'],
},
},
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'From Format',
name: 'fromFormat',
type: 'string',
default: '',
description: 'In case the input format is not recognized you can provide the format',
},
{
displayName: 'From Timezone Name or ID',
name: 'fromTimezone',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTimezones',
},
default: 'UTC',
description:
'The timezone to convert from. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
},
{
displayName: 'To Timezone Name or ID',
name: 'toTimezone',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTimezones',
},
default: 'UTC',
description:
'The timezone to convert to. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
},
],
},
{
displayName: 'Date Value',
name: 'value',
displayOptions: {
show: {
action: ['calculate'],
},
},
type: 'string',
default: '',
description: 'The date string or timestamp from which you want to add/subtract time',
required: true,
},
{
displayName: 'Operation',
name: 'operation',
displayOptions: {
show: {
action: ['calculate'],
},
},
type: 'options',
noDataExpression: true,
options: [
{
name: 'Add',
value: 'add',
description: 'Add time to Date Value',
action: 'Add time to Date Value',
},
{
name: 'Subtract',
value: 'subtract',
description: 'Subtract time from Date Value',
action: 'Subtract time from Date Value',
},
],
default: 'add',
required: true,
},
{
displayName: 'Duration',
name: 'duration',
displayOptions: {
show: {
action: ['calculate'],
},
},
type: 'number',
typeOptions: {
minValue: 0,
},
default: 0,
required: true,
description: 'E.g. enter “10” then select “Days” if you want to add 10 days to Date Value.',
},
{
displayName: 'Time Unit',
name: 'timeUnit',
description: 'Time unit for Duration parameter above',
displayOptions: {
show: {
action: ['calculate'],
},
},
type: 'options',
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
options: [
{
name: 'Quarters',
value: 'quarters',
},
{
name: 'Years',
value: 'years',
},
{
name: 'Months',
value: 'months',
},
{
name: 'Weeks',
value: 'weeks',
},
{
name: 'Days',
value: 'days',
},
{
name: 'Hours',
value: 'hours',
},
{
name: 'Minutes',
value: 'minutes',
},
{
name: 'Seconds',
value: 'seconds',
},
{
name: 'Milliseconds',
value: 'milliseconds',
},
],
default: 'days',
required: true,
},
{
displayName: 'Property Name',
name: 'dataPropertyName',
type: 'string',
default: 'data',
required: true,
displayOptions: {
show: {
action: ['calculate'],
},
},
description: 'Name of the output property to which to write the converted date',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
displayOptions: {
show: {
action: ['calculate'],
},
},
options: [
{
displayName: 'From Format',
name: 'fromFormat',
type: 'string',
default: '',
description:
'Format for parsing the value as a date. If unrecognized, specify the <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.datetime/#faqs">format</a> for the value.',
},
],
},
],
};
export class DateTimeV1 implements INodeType {
description: INodeTypeDescription;
constructor(baseDescription: INodeTypeBaseDescription) {
this.description = {
...baseDescription,
...versionDescription,
};
}
methods = {
loadOptions: {
// Get all the timezones to display them to user so that they can
// select them easily
async getTimezones(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
for (const timezone of moment.tz.names()) {
const timezoneName = timezone;
const timezoneId = timezone;
returnData.push({
name: timezoneName,
value: timezoneId,
});
}
return returnData;
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const length = items.length;
const returnData: INodeExecutionData[] = [];
const workflowTimezone = this.getTimezone();
let item: INodeExecutionData;
for (let i = 0; i < length; i++) {
try {
const action = this.getNodeParameter('action', 0) as string;
item = items[i];
if (action === 'format') {
let currentDate: string | number | LuxonDateTime = this.getNodeParameter(
'value',
i,
) as string;
const dataPropertyName = this.getNodeParameter('dataPropertyName', i);
const toFormat = this.getNodeParameter('toFormat', i) as string;
const options = this.getNodeParameter('options', i);
let newDate;
if ((currentDate as unknown as IDataObject) instanceof LuxonDateTime) {
currentDate = (currentDate as unknown as LuxonDateTime).toISO();
}
// Check if the input is a number
if (!Number.isNaN(Number(currentDate))) {
//input is a number, convert to number in case it is a string
currentDate = Number(currentDate);
// check if the number is a timestamp in float format and convert to integer
if (!Number.isInteger(currentDate)) {
currentDate = currentDate * 1000;
}
}
if (currentDate === undefined) {
continue;
}
if (options.fromFormat === undefined && !moment(currentDate).isValid()) {
throw new NodeOperationError(
this.getNode(),
'The date input format could not be recognized. Please set the "From Format" field',
{ itemIndex: i },
);
}
if (Number.isInteger(currentDate)) {
const timestampLengthInMilliseconds1990 = 12;
// check if the number is a timestamp in seconds or milliseconds and create a moment object accordingly
if (currentDate.toString().length < timestampLengthInMilliseconds1990) {
newDate = moment.unix(currentDate as number);
} else {
newDate = moment(currentDate);
}
} else {
if (options.fromTimezone || options.toTimezone) {
const fromTimezone = options.fromTimezone || workflowTimezone;
if (options.fromFormat) {
newDate = moment.tz(
currentDate as string,
options.fromFormat as string,
fromTimezone as string,
);
} else {
newDate = moment.tz(currentDate, fromTimezone as string);
}
} else {
if (options.fromFormat) {
newDate = moment(currentDate, options.fromFormat as string);
} else {
newDate = moment(currentDate);
}
}
}
if (options.toTimezone || options.fromTimezone) {
// If either a source or a target timezone got defined the
// timezone of the date has to be changed. If a target-timezone
// is set use it else fall back to workflow timezone.
newDate = newDate.tz((options.toTimezone as string) || workflowTimezone);
}
newDate = newDate.format(toFormat);
let newItem: INodeExecutionData;
if (dataPropertyName.includes('.')) {
// Uses dot notation so copy all data
newItem = {
json: deepCopy(item.json),
pairedItem: {
item: i,
},
};
} else {
// Does not use dot notation so shallow copy is enough
newItem = {
json: { ...item.json },
pairedItem: {
item: i,
},
};
}
if (item.binary !== undefined) {
newItem.binary = item.binary;
}
set(newItem, `json.${dataPropertyName}`, newDate);
returnData.push(newItem);
}
if (action === 'calculate') {
const dateValue = this.getNodeParameter('value', i) as string;
const operation = this.getNodeParameter('operation', i) as 'add' | 'subtract';
const duration = this.getNodeParameter('duration', i) as number;
const timeUnit = this.getNodeParameter('timeUnit', i) as moment.DurationInputArg2;
const { fromFormat } = this.getNodeParameter('options', i) as { fromFormat?: string };
const dataPropertyName = this.getNodeParameter('dataPropertyName', i);
const newDate = fromFormat
? parseDateByFormat.call(this, dateValue, fromFormat)
: parseDateByDefault.call(this, dateValue);
operation === 'add'
? newDate.add(duration, timeUnit).utc().format()
: newDate.subtract(duration, timeUnit).utc().format();
let newItem: INodeExecutionData;
if (dataPropertyName.includes('.')) {
// Uses dot notation so copy all data
newItem = {
json: deepCopy(item.json),
pairedItem: {
item: i,
},
};
} else {
// Does not use dot notation so shallow copy is enough
newItem = {
json: { ...item.json },
pairedItem: {
item: i,
},
};
}
if (item.binary !== undefined) {
newItem.binary = item.binary;
}
set(newItem, `json.${dataPropertyName}`, newDate.toISOString());
returnData.push(newItem);
}
} catch (error) {
if (this.continueOnFail()) {
returnData.push({
json: {
error: error.message,
},
pairedItem: {
item: i,
},
});
continue;
}
throw error;
}
}
return this.prepareOutputData(returnData);
}
}

View File

@ -0,0 +1,105 @@
import type { INodeProperties } from 'n8n-workflow';
export const AddToDateDescription: INodeProperties[] = [
{
displayName:
"You can also do this using an expression, e.g. <code>{{your_date.plus(5, 'minutes')}}</code>. <a target='_blank' href='https://docs.n8n.io/code-examples/expressions/luxon/'>More info</a>",
name: 'notice',
type: 'notice',
default: '',
displayOptions: {
show: {
operation: ['addToDate'],
},
},
},
{
displayName: 'Date to Add To',
name: 'magnitude',
type: 'string',
description: 'The date that you want to change',
default: '',
displayOptions: {
show: {
operation: ['addToDate'],
},
},
required: true,
},
{
displayName: 'Time Unit to Add',
name: 'timeUnit',
description: 'Time unit for Duration parameter below',
displayOptions: {
show: {
operation: ['addToDate'],
},
},
type: 'options',
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
options: [
{
name: 'Years',
value: 'years',
},
{
name: 'Quarters',
value: 'quarters',
},
{
name: 'Months',
value: 'months',
},
{
name: 'Weeks',
value: 'weeks',
},
{
name: 'Days',
value: 'days',
},
{
name: 'Hours',
value: 'hours',
},
{
name: 'Minutes',
value: 'minutes',
},
{
name: 'Seconds',
value: 'seconds',
},
{
name: 'Milliseconds',
value: 'milliseconds',
},
],
default: 'days',
required: true,
},
{
displayName: 'Duration',
name: 'duration',
type: 'number',
description: 'The number of time units to add to the date',
default: 0,
displayOptions: {
show: {
operation: ['addToDate'],
},
},
},
{
displayName: 'Output Field Name',
name: 'outputFieldName',
type: 'string',
default: 'newDate',
description: 'Name of the field to put the output in',
displayOptions: {
show: {
operation: ['addToDate'],
},
},
},
];

View File

@ -0,0 +1,63 @@
import type { INodeProperties } from 'n8n-workflow';
export const CurrentDateDescription: INodeProperties[] = [
{
displayName:
'You can also refer to the current date in n8n expressions by using <code>{{$now}}</code> or <code>{{$today}}</code>. <a target="_blank" href="https://docs.n8n.io/code-examples/expressions/luxon/">More info</a>',
name: 'notice',
type: 'notice',
default: '',
displayOptions: {
show: {
operation: ['getCurrentDate'],
},
},
},
{
displayName: 'Include Current Time',
name: 'includeTime',
type: 'boolean',
default: true,
description: 'Whether deactivated, the time will be set to midnight',
displayOptions: {
show: {
operation: ['getCurrentDate'],
},
},
},
{
displayName: 'Output Field Name',
name: 'outputFieldName',
type: 'string',
default: 'currentDate',
description: 'Name of the field to put the output in',
displayOptions: {
show: {
operation: ['getCurrentDate'],
},
},
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
displayOptions: {
show: {
operation: ['getCurrentDate'],
},
},
default: {},
options: [
{
displayName: 'Timezone',
name: 'timezone',
type: 'string',
placeholder: 'America/New_York',
default: '',
description:
'The timezone to use. If not set, the timezone of the n8n instance will be used. Use GMT for +00:00 timezone.',
},
],
},
];

View File

@ -0,0 +1,210 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeType,
INodeTypeBaseDescription,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { CurrentDateDescription } from './CurrentDateDescription';
import { AddToDateDescription } from './AddToDateDescription';
import { SubtractFromDateDescription } from './SubtractFromDateDescription';
import { FormatDateDescription } from './FormatDateDescription';
import { RoundDateDescription } from './RoundDateDescription';
import { GetTimeBetweenDatesDescription } from './GetTimeBetweenDates';
import type { DateTimeUnit, DurationUnit } from 'luxon';
import { DateTime } from 'luxon';
import { ExtractDateDescription } from './ExtractDateDescription';
import { parseDate } from './GenericFunctions';
export class DateTimeV2 implements INodeType {
description: INodeTypeDescription;
constructor(baseDescription: INodeTypeBaseDescription) {
this.description = {
...baseDescription,
version: 2,
defaults: {
name: 'Date & Time',
color: '#408000',
},
inputs: ['main'],
outputs: ['main'],
properties: [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Add to a Date',
value: 'addToDate',
},
{
name: 'Extract Part of a Date',
value: 'extractDate',
},
{
name: 'Format a Date',
value: 'formatDate',
},
{
name: 'Get Current Date',
value: 'getCurrentDate',
},
{
name: 'Get Time Between Dates',
value: 'getTimeBetweenDates',
},
{
name: 'Round a Date',
value: 'roundDate',
},
{
name: 'Subtract From a Date',
value: 'subtractFromDate',
},
],
default: 'getCurrentDate',
},
...CurrentDateDescription,
...AddToDateDescription,
...SubtractFromDateDescription,
...FormatDateDescription,
...RoundDateDescription,
...GetTimeBetweenDatesDescription,
...ExtractDateDescription,
],
};
}
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
const responseData = [];
const operation = this.getNodeParameter('operation', 0);
const workflowTimezone = this.getTimezone();
for (let i = 0; i < items.length; i++) {
if (operation === 'getCurrentDate') {
const includeTime = this.getNodeParameter('includeTime', i) as boolean;
const outputFieldName = this.getNodeParameter('outputFieldName', i) as string;
const { timezone } = this.getNodeParameter('options', i) as {
timezone: string;
};
const newLocal = timezone ? timezone : workflowTimezone;
if (DateTime.now().setZone(newLocal).invalidReason === 'unsupported zone') {
throw new NodeOperationError(
this.getNode(),
`The timezone ${newLocal} is not valid. Please check the timezone.`,
);
}
responseData.push(
includeTime
? { [outputFieldName]: DateTime.now().setZone(newLocal).toString() }
: {
[outputFieldName]: DateTime.now().setZone(newLocal).startOf('day').toString(),
},
);
} else if (operation === 'addToDate') {
const addToDate = this.getNodeParameter('magnitude', i) as string;
const timeUnit = this.getNodeParameter('timeUnit', i) as string;
const duration = this.getNodeParameter('duration', i) as number;
const outputFieldName = this.getNodeParameter('outputFieldName', i) as string;
const dateToAdd = parseDate.call(this, addToDate, workflowTimezone);
const returnedDate = dateToAdd.plus({ [timeUnit]: duration });
responseData.push({ [outputFieldName]: returnedDate.toString() });
} else if (operation === 'subtractFromDate') {
const subtractFromDate = this.getNodeParameter('magnitude', i) as string;
const timeUnit = this.getNodeParameter('timeUnit', i) as string;
const duration = this.getNodeParameter('duration', i) as number;
const outputFieldName = this.getNodeParameter('outputFieldName', i) as string;
const dateToAdd = parseDate.call(this, subtractFromDate, workflowTimezone);
const returnedDate = dateToAdd.minus({ [timeUnit]: duration });
responseData.push({ [outputFieldName]: returnedDate.toString() });
} else if (operation === 'formatDate') {
const date = this.getNodeParameter('date', i) as string;
const format = this.getNodeParameter('format', i) as string;
const outputFieldName = this.getNodeParameter('outputFieldName', i) as string;
const { timezone } = this.getNodeParameter('options', i) as { timezone: boolean };
const dateLuxon = timezone
? parseDate.call(this, date, workflowTimezone)
: parseDate.call(this, date);
if (format === 'custom') {
const customFormat = this.getNodeParameter('customFormat', i) as string;
responseData.push({
[outputFieldName]: dateLuxon.toFormat(customFormat),
});
} else {
responseData.push({
[outputFieldName]: dateLuxon.toFormat(format),
});
}
} else if (operation === 'roundDate') {
const date = this.getNodeParameter('date', i) as string;
const mode = this.getNodeParameter('mode', i) as string;
const outputFieldName = this.getNodeParameter('outputFieldName', i) as string;
const dateLuxon = parseDate.call(this, date, workflowTimezone);
if (mode === 'roundDown') {
const toNearest = this.getNodeParameter('toNearest', i) as string;
responseData.push({
[outputFieldName]: dateLuxon.startOf(toNearest as DateTimeUnit).toString(),
});
} else if (mode === 'roundUp') {
const to = this.getNodeParameter('to', i) as string;
responseData.push({
[outputFieldName]: dateLuxon
.plus({ [to]: 1 })
.startOf(to as DateTimeUnit)
.toString(),
});
}
} else if (operation === 'getTimeBetweenDates') {
const startDate = this.getNodeParameter('startDate', i) as string;
const endDate = this.getNodeParameter('endDate', i) as string;
const unit = this.getNodeParameter('units', i) as DurationUnit[];
const outputFieldName = this.getNodeParameter('outputFieldName', i) as string;
const { isoString } = this.getNodeParameter('options', i) as {
isoString: boolean;
};
const luxonStartDate = parseDate.call(this, startDate, workflowTimezone);
const luxonEndDate = parseDate.call(this, endDate, workflowTimezone);
const duration = luxonEndDate.diff(luxonStartDate, unit);
isoString
? responseData.push({
[outputFieldName]: duration.toString(),
})
: responseData.push({
[outputFieldName]: duration.toObject(),
});
} else if (operation === 'extractDate') {
const date = this.getNodeParameter('date', i) as string | DateTime;
const outputFieldName = this.getNodeParameter('outputFieldName', i) as string;
const part = this.getNodeParameter('part', i) as keyof DateTime | 'week';
const parsedDate = parseDate.call(this, date, workflowTimezone);
const selectedPart = part === 'week' ? parsedDate.weekNumber : parsedDate.get(part);
responseData.push({ [outputFieldName]: selectedPart });
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject[]),
{
itemData: { item: i },
},
);
returnData.push(...executionData);
}
return this.prepareOutputData(returnData);
}
}

View File

@ -0,0 +1,82 @@
import type { INodeProperties } from 'n8n-workflow';
export const ExtractDateDescription: INodeProperties[] = [
{
displayName:
'You can also do this using an expression, e.g. <code>{{ your_date.extract("month") }}}</code>. <a target="_blank" href="https://docs.n8n.io/code-examples/expressions/luxon/">More info</a>',
name: 'notice',
type: 'notice',
default: '',
displayOptions: {
show: {
operation: ['extractDate'],
},
},
},
{
displayName: 'Date',
name: 'date',
type: 'string',
description: 'The date that you want to round',
default: '',
displayOptions: {
show: {
operation: ['extractDate'],
},
},
},
{
displayName: 'Part',
name: 'part',
type: 'options',
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
options: [
{
name: 'Year',
value: 'year',
},
{
name: 'Month',
value: 'month',
},
{
name: 'Week',
value: 'week',
},
{
name: 'Day',
value: 'day',
},
{
name: 'Hour',
value: 'hour',
},
{
name: 'Minute',
value: 'minute',
},
{
name: 'Second',
value: 'second',
},
],
default: 'month',
displayOptions: {
show: {
operation: ['extractDate'],
},
},
},
{
displayName: 'Output Field Name',
name: 'outputFieldName',
type: 'string',
default: 'datePart',
description: 'Name of the field to put the output in',
displayOptions: {
show: {
operation: ['extractDate'],
},
},
},
];

View File

@ -0,0 +1,129 @@
import type { INodeProperties } from 'n8n-workflow';
export const FormatDateDescription: INodeProperties[] = [
{
displayName:
"You can also do this using an expression, e.g. <code>{{your_date.format('yyyy-MM-dd')}}</code>. <a target='_blank' href='https://docs.n8n.io/code-examples/expressions/luxon/'>More info</a>",
name: 'notice',
type: 'notice',
default: '',
displayOptions: {
show: {
operation: ['formatDate'],
},
},
},
{
displayName: 'Date',
name: 'date',
type: 'string',
description: 'The date that you want to format',
default: '',
displayOptions: {
show: {
operation: ['formatDate'],
},
},
},
{
displayName: 'Format',
name: 'format',
type: 'options',
displayOptions: {
show: {
operation: ['formatDate'],
},
},
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
options: [
{
name: 'Custom Format',
value: 'custom',
},
{
name: 'MM/DD/YYYY',
value: 'MM/dd/yyyy',
description: 'Example: 09/04/1986',
},
{
name: 'YYYY/MM/DD',
value: 'yyyy/MM/dd',
description: 'Example: 1986/04/09',
},
{
name: 'MMMM DD YYYY',
value: 'MMMM dd yyyy',
description: 'Example: April 09 1986',
},
{
name: 'MM-DD-YYYY',
value: 'MM-dd-yyyy',
description: 'Example: 09-04-1986',
},
{
name: 'YYYY-MM-DD',
value: 'yyyy-MM-dd',
description: 'Example: 1986-04-09',
},
{
name: 'Unix Timestamp',
value: 'X',
description: 'Example: 1672531200',
},
{
name: 'Unix Ms Timestamp',
value: 'x',
description: 'Example: 1674691200000',
},
],
default: 'MM/dd/yyyy',
description: 'The format to convert the date to',
},
{
displayName: 'Custom Format',
name: 'customFormat',
type: 'string',
displayOptions: {
show: {
format: ['custom'],
operation: ['formatDate'],
},
},
hint: 'List of special tokens <a target="_blank" href="https://moment.github.io/luxon/#/formatting?id=table-of-tokens">More info</a>',
default: '',
placeholder: 'yyyy-MM-dd',
},
{
displayName: 'Output Field Name',
name: 'outputFieldName',
type: 'string',
default: 'formattedDate',
description: 'Name of the field to put the output in',
displayOptions: {
show: {
operation: ['formatDate'],
},
},
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
displayOptions: {
show: {
operation: ['formatDate'],
},
},
default: {},
options: [
{
displayName: 'Use Workflow Timezone',
name: 'timezone',
type: 'boolean',
default: false,
description: "Whether to use the timezone of the input or the workflow's timezone",
},
],
},
];

View File

@ -0,0 +1,50 @@
import { DateTime } from 'luxon';
import moment from 'moment';
import type { IExecuteFunctions } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
export function parseDate(
this: IExecuteFunctions,
date: string | number | DateTime,
timezone?: string,
) {
let parsedDate;
if (date instanceof DateTime) {
parsedDate = date;
} else {
// Check if the input is a number
if (!Number.isNaN(Number(date))) {
//input is a number, convert to number in case it is a string formatted number
date = Number(date);
// check if the number is a timestamp in float format and convert to integer
if (!Number.isInteger(date)) {
date = date * 1000;
}
}
if (Number.isInteger(date)) {
const timestampLengthInMilliseconds1990 = 12;
// check if the number is a timestamp in seconds or milliseconds and create a moment object accordingly
if (date.toString().length < timestampLengthInMilliseconds1990) {
parsedDate = DateTime.fromSeconds(date as number);
} else {
parsedDate = DateTime.fromMillis(date as number);
}
} else {
if (!timezone && (date as string).includes('+')) {
const offset = (date as string).split('+')[1].slice(0, 2) as unknown as number;
timezone = `Etc/GMT-${offset * 1}`;
}
parsedDate = DateTime.fromISO(moment(date).toISOString());
}
parsedDate = parsedDate.setZone(timezone || 'Etc/UTC');
if (parsedDate.invalidReason === 'unparsable') {
throw new NodeOperationError(this.getNode(), 'Invalid date format');
}
}
return parsedDate;
}

View File

@ -0,0 +1,105 @@
import type { INodeProperties } from 'n8n-workflow';
export const GetTimeBetweenDatesDescription: INodeProperties[] = [
{
displayName: 'Start Date',
name: 'startDate',
type: 'string',
default: '',
displayOptions: {
show: {
operation: ['getTimeBetweenDates'],
},
},
},
{
displayName: 'End Date',
name: 'endDate',
type: 'string',
default: '',
displayOptions: {
show: {
operation: ['getTimeBetweenDates'],
},
},
},
{
displayName: 'Units',
name: 'units',
type: 'multiOptions',
// eslint-disable-next-line n8n-nodes-base/node-param-multi-options-type-unsorted-items
options: [
{
name: 'Year',
value: 'year',
},
{
name: 'Month',
value: 'month',
},
{
name: 'Week',
value: 'week',
},
{
name: 'Day',
value: 'day',
},
{
name: 'Hour',
value: 'hour',
},
{
name: 'Minute',
value: 'minute',
},
{
name: 'Second',
value: 'second',
},
{
name: 'Millisecond',
value: 'millisecond',
},
],
displayOptions: {
show: {
operation: ['getTimeBetweenDates'],
},
},
default: ['day'],
},
{
displayName: 'Output Field Name',
name: 'outputFieldName',
type: 'string',
default: 'timeDifference',
description: 'Name of the field to put the output in',
displayOptions: {
show: {
operation: ['getTimeBetweenDates'],
},
},
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
displayOptions: {
show: {
operation: ['getTimeBetweenDates'],
},
},
default: {},
options: [
{
displayName: 'Output as ISO String',
name: 'isoString',
type: 'boolean',
default: false,
description: 'Whether to output the date as ISO string or not',
},
],
},
];

View File

@ -0,0 +1,122 @@
import type { INodeProperties } from 'n8n-workflow';
export const RoundDateDescription: INodeProperties[] = [
{
displayName:
"You can also do this using an expression, e.g. <code>{{ your_date.beginningOf('month') }}</code> or <code>{{ your_date.endOfMonth() }}</code>. <a target='_blank' href='https://docs.n8n.io/code-examples/expressions/luxon/'>More info</a>",
name: 'notice',
type: 'notice',
default: '',
displayOptions: {
show: {
operation: ['roundDate'],
},
},
},
{
displayName: 'Date',
name: 'date',
type: 'string',
description: 'The date that you want to round',
default: '',
displayOptions: {
show: {
operation: ['roundDate'],
},
},
},
{
displayName: 'Mode',
name: 'mode',
type: 'options',
options: [
{
name: 'Round Down',
value: 'roundDown',
},
{
name: 'Round Up',
value: 'roundUp',
},
],
default: 'roundDown',
displayOptions: {
show: {
operation: ['roundDate'],
},
},
},
{
displayName: 'To Nearest',
name: 'toNearest',
type: 'options',
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
options: [
{
name: 'Year',
value: 'year',
},
{
name: 'Month',
value: 'month',
},
{
name: 'Week',
value: 'week',
},
{
name: 'Day',
value: 'day',
},
{
name: 'Hour',
value: 'hour',
},
{
name: 'Minute',
value: 'minute',
},
{
name: 'Second',
value: 'second',
},
],
default: 'month',
displayOptions: {
show: {
operation: ['roundDate'],
mode: ['roundDown'],
},
},
},
{
displayName: 'To',
name: 'to',
type: 'options',
options: [
{
name: 'End of Month',
value: 'month',
},
],
default: 'month',
displayOptions: {
show: {
operation: ['roundDate'],
mode: ['roundUp'],
},
},
},
{
displayName: 'Output Field Name',
name: 'outputFieldName',
type: 'string',
default: 'roundedDate',
description: 'Name of the field to put the output in',
displayOptions: {
show: {
operation: ['roundDate'],
},
},
},
];

View File

@ -0,0 +1,105 @@
import type { INodeProperties } from 'n8n-workflow';
export const SubtractFromDateDescription: INodeProperties[] = [
{
displayName:
"You can also do this using an expression, e.g. <code>{{your_date.minus(5, 'minutes')}}</code>. <a target='_blank' href='https://docs.n8n.io/code-examples/expressions/luxon/'>More info</a>",
name: 'notice',
type: 'notice',
default: '',
displayOptions: {
show: {
operation: ['subtractFromDate'],
},
},
},
{
displayName: 'Date to Subtract From',
name: 'magnitude',
type: 'string',
description: 'The date that you want to change',
default: '',
displayOptions: {
show: {
operation: ['subtractFromDate'],
},
},
required: true,
},
{
displayName: 'Time Unit to Subtract',
name: 'timeUnit',
description: 'Time unit for Duration parameter below',
displayOptions: {
show: {
operation: ['subtractFromDate'],
},
},
type: 'options',
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
options: [
{
name: 'Years',
value: 'years',
},
{
name: 'Quarters',
value: 'quarters',
},
{
name: 'Months',
value: 'months',
},
{
name: 'Weeks',
value: 'weeks',
},
{
name: 'Days',
value: 'days',
},
{
name: 'Hours',
value: 'hours',
},
{
name: 'Minutes',
value: 'minutes',
},
{
name: 'Seconds',
value: 'seconds',
},
{
name: 'Milliseconds',
value: 'milliseconds',
},
],
default: 'days',
required: true,
},
{
displayName: 'Duration',
name: 'duration',
type: 'number',
description: 'The number of time units to subtract from the date',
default: 0,
displayOptions: {
show: {
operation: ['subtractFromDate'],
},
},
},
{
displayName: 'Output Field Name',
name: 'outputFieldName',
type: 'string',
default: 'newDate',
description: 'Name of the field to put the output in',
displayOptions: {
show: {
operation: ['subtractFromDate'],
},
},
},
];

View File

@ -0,0 +1,260 @@
{
"name": "node-360-quick-overhaul-of-date-and-time-node",
"nodes": [
{
"parameters": {},
"id": "21ff2e15-375d-4e68-b1ca-d48a110be238",
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [-420, 20]
},
{
"parameters": {
"operation": "addToDate",
"magnitude": "={{ $json.currentDate }}",
"duration": 2
},
"id": "b99986f1-edeb-434c-b7ed-9cc86eaec522",
"name": "Add to date",
"type": "n8n-nodes-base.dateTime",
"typeVersion": 2,
"position": [140, 40]
},
{
"parameters": {
"operation": "subtractFromDate",
"magnitude": "={{ $json.newDate }}",
"duration": 2
},
"id": "aa75a04b-0d42-46ff-87e7-75d4b4f6c7ea",
"name": "Subtract date",
"type": "n8n-nodes-base.dateTime",
"typeVersion": 2,
"position": [300, 200]
},
{
"parameters": {
"operation": "formatDate",
"date": "={{ $json.newDate }}",
"format": "yyyy/MM/dd"
},
"id": "52076d89-bc6d-4253-8ca4-9aad3a058d17",
"name": "Format Date",
"type": "n8n-nodes-base.dateTime",
"typeVersion": 2,
"position": [420, 40]
},
{
"parameters": {
"operation": "roundDate",
"date": "={{ $json.formattedDate }}",
"toNearest": "day"
},
"id": "10016499-c9da-4984-9a5f-2f8c8844fb63",
"name": "Round Date",
"type": "n8n-nodes-base.dateTime",
"typeVersion": 2,
"position": [560, 200]
},
{
"parameters": {
"operation": "getTimeBetweenDates",
"startDate": "={{ $node['Subtract date'].json.newDate }}",
"endDate": "={{ $node['Add to date'].json.newDate }}",
"units": ["day"]
},
"id": "f62b6d0b-b13a-4fcd-b4eb-3ec7ea85e80c",
"name": "Get between date",
"type": "n8n-nodes-base.dateTime",
"typeVersion": 2,
"position": [660, 40]
},
{
"parameters": {
"operation": "extractDate",
"date": "={{ $node.Code.json.currentDate }}",
"part": "hour",
"outputFieldName": "date"
},
"id": "764e3e08-f71b-4e42-b059-36285076fe10",
"name": "Extract Date",
"type": "n8n-nodes-base.dateTime",
"typeVersion": 2,
"position": [780, 220]
},
{
"parameters": {
"options": {
"fromFormat": ""
}
},
"id": "f0b75198-74a4-4a13-8842-340539f41d80",
"name": "V1",
"type": "n8n-nodes-base.dateTime",
"typeVersion": 1,
"position": [0, -180],
"disabled": true
},
{
"parameters": {
"jsCode": "return {\"currentDate\":\"2023-04-11T13:51:59.965+00:00\"}\n"
},
"id": "7ba0c2a1-a683-4975-a2ca-70904111a3fc",
"name": "Code",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [-140, 140]
}
],
"pinData": {
"Code": [
{
"json": {
"currentDate": "2023-04-11T13:51:59.965+00:00"
}
}
],
"Add to date": [
{
"json": {
"newDate": "2023-04-13T13:51:59.965+00:00"
}
}
],
"Subtract date": [
{
"json": {
"newDate": "2023-04-11T13:51:59.965+00:00"
}
}
],
"Format Date": [
{
"json": {
"formattedDate": "2023/04/11"
}
}
],
"Round Date": [
{
"json": {
"roundedDate": "2023-04-11T00:00:00.000+00:00"
}
}
],
"Get between date": [
{
"json": {
"timeDifference": {
"days": 2
}
}
}
],
"Extract Date": [
{
"json": {
"date": 13
}
}
]
},
"connections": {
"When clicking \"Execute Workflow\"": {
"main": [
[
{
"node": "V1",
"type": "main",
"index": 0
},
{
"node": "Code",
"type": "main",
"index": 0
}
]
]
},
"Add to date": {
"main": [
[
{
"node": "Subtract date",
"type": "main",
"index": 0
}
]
]
},
"Subtract date": {
"main": [
[
{
"node": "Format Date",
"type": "main",
"index": 0
}
]
]
},
"Format Date": {
"main": [
[
{
"node": "Round Date",
"type": "main",
"index": 0
}
]
]
},
"Round Date": {
"main": [
[
{
"node": "Get between date",
"type": "main",
"index": 0
}
]
]
},
"Get between date": {
"main": [
[
{
"node": "Extract Date",
"type": "main",
"index": 0
}
]
]
},
"Code": {
"main": [
[
{
"node": "Add to date",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"saveManualExecutions": false,
"callerPolicy": "workflowsFromSameOwner",
"timezone": "Etc/GMT",
"executionTimeout": -1
},
"versionId": "c21daa0b-83ae-45f1-b680-d2e57423800b",
"id": "48",
"meta": {
"instanceId": "8e9416f42a954d0a370d988ac3c0f916f44074a6e45189164b1a8559394a7516"
},
"tags": []
}

View File

@ -0,0 +1,352 @@
{
"name": "dateTime overhaul",
"nodes": [
{
"parameters": {},
"id": "4ef93910-a6f8-43e2-bba7-8319ef62f9ee",
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [260, 820]
},
{
"parameters": {
"values": {
"number": [
{
"name": "dateMilis",
"value": 1682918315906
},
{
"name": "dateMilisFloat",
"value": 1682918315.906
},
{
"name": "dateUnix",
"value": 1682918315
}
],
"string": [
{
"name": "dateMilisStr",
"value": "1682918315906"
},
{
"name": "dateMilisFloatStr",
"value": "1682918315.906"
},
{
"name": "dateUnixStr",
"value": "1682918315"
}
]
},
"options": {}
},
"id": "1d9bc8b7-9c8d-40c8-92f2-e94ed50d0ae5",
"name": "Set",
"type": "n8n-nodes-base.set",
"typeVersion": 2,
"position": [420, 820]
},
{
"parameters": {
"operation": "formatDate",
"date": "={{ $json.dateMilis }}",
"format": "yyyy/MM/dd",
"outputFieldName": "data",
"additionalFields": {}
},
"id": "c07b9cbd-4aeb-4267-a1d3-b45ecadbd1cb",
"name": "Date & Time6",
"type": "n8n-nodes-base.dateTime",
"typeVersion": 2,
"position": [680, 640]
},
{
"parameters": {
"operation": "formatDate",
"date": "={{ $json.dateMilisFloat }}",
"format": "yyyy/MM/dd",
"outputFieldName": "data",
"additionalFields": {}
},
"id": "a5b7bb44-63e2-4b71-ad91-55bac329e3f6",
"name": "Date & Time",
"type": "n8n-nodes-base.dateTime",
"typeVersion": 2,
"position": [680, 780]
},
{
"parameters": {
"operation": "formatDate",
"date": "={{ $json.dateUnix }}",
"format": "yyyy/MM/dd",
"outputFieldName": "data",
"additionalFields": {}
},
"id": "1306d282-b5f8-4a54-8834-6207ecff65f7",
"name": "Date & Time1",
"type": "n8n-nodes-base.dateTime",
"typeVersion": 2,
"position": [680, 940]
},
{
"parameters": {
"operation": "formatDate",
"date": "={{ $json.dateMilisStr }}",
"format": "yyyy/MM/dd",
"outputFieldName": "data",
"additionalFields": {}
},
"id": "4823c095-1921-406e-9957-a75521bca1e5",
"name": "Date & Time2",
"type": "n8n-nodes-base.dateTime",
"typeVersion": 2,
"position": [680, 1080]
},
{
"parameters": {
"operation": "formatDate",
"date": "={{ $json.dateMilisFloatStr }}",
"format": "yyyy/MM/dd",
"outputFieldName": "data",
"additionalFields": {}
},
"id": "d209ac18-9935-4452-825a-42aa90daaaa5",
"name": "Date & Time3",
"type": "n8n-nodes-base.dateTime",
"typeVersion": 2,
"position": [680, 1220]
},
{
"parameters": {
"operation": "formatDate",
"date": "={{ $json.dateUnixStr }}",
"format": "yyyy/MM/dd",
"outputFieldName": "data",
"additionalFields": {}
},
"id": "b7065dfb-ae7e-4828-a5ea-e9c313302944",
"name": "Date & Time4",
"type": "n8n-nodes-base.dateTime",
"typeVersion": 2,
"position": [680, 1380]
},
{
"parameters": {},
"id": "5ae1bb29-d19e-4e3d-af11-ccc53ee23bfb",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [900, 640]
},
{
"parameters": {},
"id": "8716cc32-d4a6-48d6-af5d-e15646006dd8",
"name": "No Operation, do nothing1",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [900, 780]
},
{
"parameters": {},
"id": "88f0247d-ecc0-49a2-8bae-4e7b99ae8611",
"name": "No Operation, do nothing2",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [900, 920]
},
{
"parameters": {},
"id": "99a04c1d-5426-446e-9171-2d12a5b14a13",
"name": "No Operation, do nothing3",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [900, 1060]
},
{
"parameters": {},
"id": "923e317f-3e7b-4609-883d-c630034bd20c",
"name": "No Operation, do nothing4",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [900, 1200]
},
{
"parameters": {},
"id": "93745a80-a2b6-414b-bcf0-938f2a2da985",
"name": "No Operation, do nothing5",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [900, 1340]
}
],
"pinData": {
"No Operation, do nothing5": [
{
"json": {
"data": "2023/05/01"
}
}
],
"No Operation, do nothing4": [
{
"json": {
"data": "2023/05/01"
}
}
],
"No Operation, do nothing3": [
{
"json": {
"data": "2023/05/01"
}
}
],
"No Operation, do nothing2": [
{
"json": {
"data": "2023/05/01"
}
}
],
"No Operation, do nothing1": [
{
"json": {
"data": "2023/05/01"
}
}
],
"No Operation, do nothing": [
{
"json": {
"data": "2023/05/01"
}
}
]
},
"connections": {
"When clicking \"Execute Workflow\"": {
"main": [
[
{
"node": "Set",
"type": "main",
"index": 0
}
]
]
},
"Set": {
"main": [
[
{
"node": "Date & Time6",
"type": "main",
"index": 0
},
{
"node": "Date & Time",
"type": "main",
"index": 0
},
{
"node": "Date & Time1",
"type": "main",
"index": 0
},
{
"node": "Date & Time2",
"type": "main",
"index": 0
},
{
"node": "Date & Time3",
"type": "main",
"index": 0
},
{
"node": "Date & Time4",
"type": "main",
"index": 0
}
]
]
},
"Date & Time6": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
},
"Date & Time4": {
"main": [
[
{
"node": "No Operation, do nothing5",
"type": "main",
"index": 0
}
]
]
},
"Date & Time3": {
"main": [
[
{
"node": "No Operation, do nothing4",
"type": "main",
"index": 0
}
]
]
},
"Date & Time2": {
"main": [
[
{
"node": "No Operation, do nothing3",
"type": "main",
"index": 0
}
]
]
},
"Date & Time1": {
"main": [
[
{
"node": "No Operation, do nothing2",
"type": "main",
"index": 0
}
]
]
},
"Date & Time": {
"main": [
[
{
"node": "No Operation, do nothing1",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {},
"versionId": "19282890-eff2-40ca-be11-a8fff559c964",
"id": "21",
"meta": {
"instanceId": "6ebec4953fe56f1c009e7c3b107578b375137523af057073c0b5da17350651bd"
},
"tags": []
}

View File

@ -13,11 +13,13 @@ export async function executeWorkflow(testData: WorkflowTestData, nodeTypes: INo
active: false,
nodeTypes,
});
const waitPromise = await createDeferredPromise<IRun>();
const nodeExecutionOrder: string[] = [];
const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise, nodeExecutionOrder);
const additionalData = Helpers.WorkflowExecuteAdditionalData(
waitPromise,
nodeExecutionOrder,
testData,
);
const workflowExecute = new WorkflowExecute(additionalData, executionMode);
const executionData = await workflowExecute.run(workflowInstance);

View File

@ -145,6 +145,7 @@ export class CredentialsHelper extends ICredentialsHelper {
export function WorkflowExecuteAdditionalData(
waitPromise: IDeferredPromise<IRun>,
nodeExecutionOrder: string[],
workflowTestData?: WorkflowTestData,
): IWorkflowExecuteAdditionalData {
const hookFunctions = {
nodeExecuteAfter: [
@ -167,7 +168,6 @@ export function WorkflowExecuteAdditionalData(
nodes: [],
connections: {},
};
return {
credentialsHelper: new CredentialsHelper(credentialTypes),
hooks: new WorkflowHooks(hookFunctions, 'trigger', '1', workflowData),
@ -175,7 +175,7 @@ export function WorkflowExecuteAdditionalData(
sendMessageToUI: (message: string) => {},
restApiUrl: '',
encryptionKey: 'test',
timezone: 'America/New_York',
timezone: workflowTestData?.input.workflowData.settings?.timezone || 'America/New_York',
webhookBaseUrl: 'webhook',
webhookWaitingBaseUrl: 'webhook-waiting',
webhookTestBaseUrl: 'webhook-test',
@ -339,7 +339,6 @@ const preparePinData = (pinData: IDataObject) => {
);
return returnData;
};
export const workflowToTests = (workflowFiles: string[]) => {
const testCases: WorkflowTestData[] = [];
for (const filePath of workflowFiles) {

View File

@ -6,6 +6,12 @@ export interface WorkflowTestData {
workflowData: {
nodes: INode[];
connections: IConnections;
settings?: {
saveManualExecutions: boolean;
callerPolicy: string;
timezone: string;
saveExecutionProgress: string;
};
};
};
output: {