diff --git a/packages/nodes-base/nodes/DateTime/DateTime.node.ts b/packages/nodes-base/nodes/DateTime/DateTime.node.ts index 7dcbb06655758..478fac354d7d8 100644 --- a/packages/nodes-base/nodes/DateTime/DateTime.node.ts +++ b/packages/nodes-base/nodes/DateTime/DateTime.node.ts @@ -1,579 +1,27 @@ -import type { - IDataObject, - IExecuteFunctions, - ILoadOptionsFunctions, - INodeExecutionData, - INodePropertyOptions, - INodeType, - 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.', - ); -} - -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 they 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') { - 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); +import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow'; +import { VersionedNodeType } from 'n8n-workflow'; + +import { DateTimeV1 } from './V1/DateTimeV1.node'; + +import { DateTimeV2 } from './V2/DateTimeV2.node'; + +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"]}}', + }; + + const nodeVersions: IVersionedNodeType['nodeVersions'] = { + 1: new DateTimeV1(baseDescription), + 2: new DateTimeV2(baseDescription), + }; + + 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 0000000000000..6e039a6fa6382 --- /dev/null +++ b/packages/nodes-base/nodes/DateTime/V1/DateTimeV1.node.ts @@ -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 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.', + }, + ], + }, + ], +}; + +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 { + 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') { + 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); + } +} diff --git a/packages/nodes-base/nodes/DateTime/V2/AddToDateDescription.ts b/packages/nodes-base/nodes/DateTime/V2/AddToDateDescription.ts new file mode 100644 index 0000000000000..7d96fe75ff140 --- /dev/null +++ b/packages/nodes-base/nodes/DateTime/V2/AddToDateDescription.ts @@ -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. {{your_date.plus(5, 'minutes')}}. More info", + 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'], + }, + }, + }, +]; diff --git a/packages/nodes-base/nodes/DateTime/V2/CurrentDateDescription.ts b/packages/nodes-base/nodes/DateTime/V2/CurrentDateDescription.ts new file mode 100644 index 0000000000000..051a1e83225a0 --- /dev/null +++ b/packages/nodes-base/nodes/DateTime/V2/CurrentDateDescription.ts @@ -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 {{$now}} or {{$today}}. More info', + 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.', + }, + ], + }, +]; 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 0000000000000..44a9a68976e39 --- /dev/null +++ b/packages/nodes-base/nodes/DateTime/V2/DateTimeV2.node.ts @@ -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 { + 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); + } +} diff --git a/packages/nodes-base/nodes/DateTime/V2/ExtractDateDescription.ts b/packages/nodes-base/nodes/DateTime/V2/ExtractDateDescription.ts new file mode 100644 index 0000000000000..0fcb7bb20dc5a --- /dev/null +++ b/packages/nodes-base/nodes/DateTime/V2/ExtractDateDescription.ts @@ -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. {{ your_date.extract("month") }}}. More info', + 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'], + }, + }, + }, +]; diff --git a/packages/nodes-base/nodes/DateTime/V2/FormatDateDescription.ts b/packages/nodes-base/nodes/DateTime/V2/FormatDateDescription.ts new file mode 100644 index 0000000000000..dad16abcec35a --- /dev/null +++ b/packages/nodes-base/nodes/DateTime/V2/FormatDateDescription.ts @@ -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. {{your_date.format('yyyy-MM-dd')}}. More info", + 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 More info', + 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", + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/DateTime/V2/GenericFunctions.ts b/packages/nodes-base/nodes/DateTime/V2/GenericFunctions.ts new file mode 100644 index 0000000000000..153d875d3be1a --- /dev/null +++ b/packages/nodes-base/nodes/DateTime/V2/GenericFunctions.ts @@ -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; +} diff --git a/packages/nodes-base/nodes/DateTime/V2/GetTimeBetweenDates.ts b/packages/nodes-base/nodes/DateTime/V2/GetTimeBetweenDates.ts new file mode 100644 index 0000000000000..161858936c4b9 --- /dev/null +++ b/packages/nodes-base/nodes/DateTime/V2/GetTimeBetweenDates.ts @@ -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', + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/DateTime/V2/RoundDateDescription.ts b/packages/nodes-base/nodes/DateTime/V2/RoundDateDescription.ts new file mode 100644 index 0000000000000..5d244efae2723 --- /dev/null +++ b/packages/nodes-base/nodes/DateTime/V2/RoundDateDescription.ts @@ -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. {{ your_date.beginningOf('month') }} or {{ your_date.endOfMonth() }}. More info", + 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'], + }, + }, + }, +]; diff --git a/packages/nodes-base/nodes/DateTime/V2/SubtractFromDateDescription.ts b/packages/nodes-base/nodes/DateTime/V2/SubtractFromDateDescription.ts new file mode 100644 index 0000000000000..247d63b0b06cf --- /dev/null +++ b/packages/nodes-base/nodes/DateTime/V2/SubtractFromDateDescription.ts @@ -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. {{your_date.minus(5, 'minutes')}}. More info", + 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'], + }, + }, + }, +]; diff --git a/packages/nodes-base/nodes/DateTime/test/node/DateTimeWorkflowV2.json b/packages/nodes-base/nodes/DateTime/test/node/DateTimeWorkflowV2.json new file mode 100644 index 0000000000000..fceebea6cc527 --- /dev/null +++ b/packages/nodes-base/nodes/DateTime/test/node/DateTimeWorkflowV2.json @@ -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": [] +} diff --git a/packages/nodes-base/nodes/DateTime/test/node/workflow.timestamp_v2.json b/packages/nodes-base/nodes/DateTime/test/node/workflow.timestamp_v2.json new file mode 100644 index 0000000000000..0ca9ec5e9396c --- /dev/null +++ b/packages/nodes-base/nodes/DateTime/test/node/workflow.timestamp_v2.json @@ -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": [] +} diff --git a/packages/nodes-base/test/nodes/ExecuteWorkflow.ts b/packages/nodes-base/test/nodes/ExecuteWorkflow.ts index d88304a355ea6..5cbe8499b2f8e 100644 --- a/packages/nodes-base/test/nodes/ExecuteWorkflow.ts +++ b/packages/nodes-base/test/nodes/ExecuteWorkflow.ts @@ -13,11 +13,13 @@ export async function executeWorkflow(testData: WorkflowTestData, nodeTypes: INo active: false, nodeTypes, }); - const waitPromise = await createDeferredPromise(); 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); diff --git a/packages/nodes-base/test/nodes/Helpers.ts b/packages/nodes-base/test/nodes/Helpers.ts index 2ef784be0925a..f1ba61aa128ec 100644 --- a/packages/nodes-base/test/nodes/Helpers.ts +++ b/packages/nodes-base/test/nodes/Helpers.ts @@ -145,6 +145,7 @@ export class CredentialsHelper extends ICredentialsHelper { export function WorkflowExecuteAdditionalData( waitPromise: IDeferredPromise, 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) { diff --git a/packages/nodes-base/test/nodes/types.ts b/packages/nodes-base/test/nodes/types.ts index 59f069b9fc0a3..000c34c9b1176 100644 --- a/packages/nodes-base/test/nodes/types.ts +++ b/packages/nodes-base/test/nodes/types.ts @@ -6,6 +6,12 @@ export interface WorkflowTestData { workflowData: { nodes: INode[]; connections: IConnections; + settings?: { + saveManualExecutions: boolean; + callerPolicy: string; + timezone: string; + saveExecutionProgress: string; + }; }; }; output: {