From 976dbc296b25e0df776066ecb688d19df428e956 Mon Sep 17 00:00:00 2001 From: agobrech Date: Tue, 4 Apr 2023 14:45:06 +0200 Subject: [PATCH] Setup versionized node --- .../nodes/DateTime/DateTime.node.ts | 568 +----------------- .../nodes/DateTime/V1/DateTimeV1.node.ts | 556 +++++++++++++++++ .../nodes/DateTime/V2/DateTimeV2.node.ts | 556 +++++++++++++++++ 3 files changed, 1132 insertions(+), 548 deletions(-) create mode 100644 packages/nodes-base/nodes/DateTime/V1/DateTimeV1.node.ts create mode 100644 packages/nodes-base/nodes/DateTime/V2/DateTimeV2.node.ts 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); + } +}