diff --git a/packages/nodes-base/nodes/DateTime/DateTime.node.ts b/packages/nodes-base/nodes/DateTime/DateTime.node.ts
index 26facc9e17..76626a09e7 100644
--- a/packages/nodes-base/nodes/DateTime/DateTime.node.ts
+++ b/packages/nodes-base/nodes/DateTime/DateTime.node.ts
@@ -1,555 +1,27 @@
-import type {
- IExecuteFunctions,
- ILoadOptionsFunctions,
- INodeExecutionData,
- INodePropertyOptions,
- INodeType,
- INodeTypeDescription,
-} from 'n8n-workflow';
-import { deepCopy, NodeOperationError } from 'n8n-workflow';
+import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow';
+import { VersionedNodeType } from 'n8n-workflow';
-import set from 'lodash.set';
+import { DateTimeV1 } from './V1/DateTimeV1.node';
-import moment from 'moment-timezone';
+import { DateTimeV2 } from './V2/DateTimeV2.node';
-function parseDateByFormat(this: IExecuteFunctions, value: string, fromFormat: string) {
- const date = moment(value, fromFormat, true);
- if (moment(date).isValid()) return date;
+export class Slack 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"]}}',
+ };
- throw new NodeOperationError(
- this.getNode(),
- 'Date input cannot be parsed. Please recheck the value and the "From Format" field.',
- );
-}
+ const nodeVersions: IVersionedNodeType['nodeVersions'] = {
+ 1: new DateTimeV1(baseDescription),
+ 2: new DateTimeV2(baseDescription),
+ };
-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 expressions, e.g. {{ $now.plus(1, 'week') }}
",
- 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 expression.',
- },
- {
- 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 expression.',
- },
- ],
- },
- {
- 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 format for the value.',
- },
- ],
- },
- ],
- };
-
- methods = {
- loadOptions: {
- // Get all the timezones to display them to user so that he can
- // select them easily
- async getTimezones(this: ILoadOptionsFunctions): Promise {
- 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 {
- 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') {
- const currentDate = 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 === undefined) {
- continue;
- }
- if (
- options.fromFormat === undefined &&
- !moment(currentDate as string | number).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 as unknown as number)) {
- newDate = moment.unix(currentDate as unknown as number);
- } else {
- if (options.fromTimezone || options.toTimezone) {
- const fromTimezone = options.fromTimezone || workflowTimezone;
- if (options.fromFormat) {
- newDate = moment.tz(
- currentDate,
- 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);
}
}
diff --git a/packages/nodes-base/nodes/DateTime/V1/DateTimeV1.node.ts b/packages/nodes-base/nodes/DateTime/V1/DateTimeV1.node.ts
new file mode 100644
index 0000000000..fbc2f5bfd5
--- /dev/null
+++ b/packages/nodes-base/nodes/DateTime/V1/DateTimeV1.node.ts
@@ -0,0 +1,556 @@
+import type {
+ 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';
+
+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 DateTimeV1 implements INodeType {
+ description: INodeTypeDescription;
+
+ constructor(baseDescription: INodeTypeBaseDescription) {
+ this.description = {
+ ...baseDescription,
+ version: 1,
+ defaults: {
+ name: 'Date & Time',
+ color: '#408000',
+ },
+ inputs: ['main'],
+ outputs: ['main'],
+ properties: [
+ {
+ displayName:
+ "More powerful date functionality is available in expressions, e.g. {{ $now.plus(1, 'week') }}
",
+ 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 expression.',
+ },
+ {
+ 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 expression.',
+ },
+ ],
+ },
+ {
+ 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 format for the value.',
+ },
+ ],
+ },
+ ],
+ };
+ }
+
+ methods = {
+ loadOptions: {
+ // Get all the timezones to display them to user so that he can
+ // select them easily
+ async getTimezones(this: ILoadOptionsFunctions): Promise {
+ 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 {
+ 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') {
+ const currentDate = 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 === undefined) {
+ continue;
+ }
+ if (
+ options.fromFormat === undefined &&
+ !moment(currentDate as string | number).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 as unknown as number)) {
+ newDate = moment.unix(currentDate as unknown as number);
+ } else {
+ if (options.fromTimezone || options.toTimezone) {
+ const fromTimezone = options.fromTimezone || workflowTimezone;
+ if (options.fromFormat) {
+ newDate = moment.tz(
+ currentDate,
+ 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);
+ }
+}
diff --git a/packages/nodes-base/nodes/DateTime/V2/DateTimeV2.node.ts b/packages/nodes-base/nodes/DateTime/V2/DateTimeV2.node.ts
new file mode 100644
index 0000000000..74bb1e9ee7
--- /dev/null
+++ b/packages/nodes-base/nodes/DateTime/V2/DateTimeV2.node.ts
@@ -0,0 +1,556 @@
+import type {
+ 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';
+
+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 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:
+ "More powerful date functionality is available in expressions, e.g. {{ $now.plus(1, 'week') }}
",
+ 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 expression.',
+ },
+ {
+ 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 expression.',
+ },
+ ],
+ },
+ {
+ 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 format for the value.',
+ },
+ ],
+ },
+ ],
+ };
+ }
+
+ methods = {
+ loadOptions: {
+ // Get all the timezones to display them to user so that he can
+ // select them easily
+ async getTimezones(this: ILoadOptionsFunctions): Promise {
+ 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 {
+ 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') {
+ const currentDate = 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 === undefined) {
+ continue;
+ }
+ if (
+ options.fromFormat === undefined &&
+ !moment(currentDate as string | number).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 as unknown as number)) {
+ newDate = moment.unix(currentDate as unknown as number);
+ } else {
+ if (options.fromTimezone || options.toTimezone) {
+ const fromTimezone = options.fromTimezone || workflowTimezone;
+ if (options.fromFormat) {
+ newDate = moment.tz(
+ currentDate,
+ 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);
+ }
+}