From 2b9ca0d240b403a5f12b115956bbc11672f3a04a Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Mon, 3 Apr 2023 12:48:11 +0300 Subject: [PATCH] fix(Gmail Node): Gmail luxon object support, fix for timestamp --- .../nodes/Google/Gmail/GenericFunctions.ts | 128 ++++++++++-------- .../nodes/Google/Gmail/GmailTrigger.node.ts | 2 +- .../nodes/Google/Gmail/test/v2/utils.test.ts | 111 +++++++++++++++ .../nodes/Google/Gmail/v2/GmailV2.node.ts | 4 +- 4 files changed, 184 insertions(+), 61 deletions(-) create mode 100644 packages/nodes-base/nodes/Google/Gmail/test/v2/utils.test.ts diff --git a/packages/nodes-base/nodes/Google/Gmail/GenericFunctions.ts b/packages/nodes-base/nodes/Google/Gmail/GenericFunctions.ts index 99fcce1943..e1fd977b2d 100644 --- a/packages/nodes-base/nodes/Google/Gmail/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Google/Gmail/GenericFunctions.ts @@ -9,6 +9,7 @@ import type { IExecuteFunctions, IExecuteSingleFunctions, ILoadOptionsFunctions, + INode, INodeExecutionData, IPollFunctions, JsonObject, @@ -351,9 +352,64 @@ export function extractEmail(s: string) { return s; } +export const prepareTimestamp = ( + node: INode, + itemIndex: number, + query: string, + dateValue: string | number | DateTime, + label: 'after' | 'before', +) => { + if (dateValue instanceof DateTime) { + dateValue = dateValue.toISO(); + } + + let timestamp = DateTime.fromISO(dateValue as string).toSeconds(); + const timestampLengthInMilliseconds1990 = 12; + + if (typeof timestamp === 'number') { + timestamp = Math.round(timestamp); + } + + if ( + !timestamp && + typeof dateValue === 'number' && + dateValue.toString().length < timestampLengthInMilliseconds1990 + ) { + timestamp = dateValue; + } + + if (!timestamp && (dateValue as string).length < timestampLengthInMilliseconds1990) { + timestamp = parseInt(dateValue as string, 10); + } + + if (!timestamp) { + timestamp = Math.floor(DateTime.fromMillis(parseInt(dateValue as string, 10)).toSeconds()); + } + + if (!timestamp) { + const description = `'${dateValue}' isn't a valid date and time. If you're using an expression, be sure to set an ISO date string or a timestamp.`; + throw new NodeOperationError( + node, + `Invalid date/time in 'Received ${label[0].toUpperCase() + label.slice(1)}' field`, + { + description, + itemIndex, + }, + ); + } + + if (query) { + query += ` ${label}:${timestamp}`; + } else { + query = `${label}:${timestamp}`; + } + return query; +}; + export function prepareQuery( this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IPollFunctions, fields: IDataObject, + itemIndex: number, ) { const qs: IDataObject = { ...fields }; if (qs.labelIds) { @@ -383,68 +439,24 @@ export function prepareQuery( } if (qs.receivedAfter) { - let timestamp = DateTime.fromISO(qs.receivedAfter as string).toSeconds(); - const timestampLengthInMilliseconds1990 = 12; - - if ( - !timestamp && - typeof qs.receivedAfter === 'number' && - qs.receivedAfter.toString().length < timestampLengthInMilliseconds1990 - ) { - timestamp = qs.receivedAfter; - } - - if (!timestamp && (qs.receivedAfter as string).length < timestampLengthInMilliseconds1990) { - timestamp = parseInt(qs.receivedAfter as string, 10); - } - - if (!timestamp) { - timestamp = Math.floor( - DateTime.fromMillis(parseInt(qs.receivedAfter as string, 10)).toSeconds(), - ); - } - - if (!timestamp) { - const description = `'${qs.receivedAfter}' isn't a valid date and time. If you're using an expression, be sure to set an ISO date string or a timestamp.`; - throw new NodeOperationError(this.getNode(), "Invalid date/time in 'Received After' field", { - description, - }); - } - - if (qs.q) { - qs.q += ` after:${timestamp}`; - } else { - qs.q = `after:${timestamp}`; - } + qs.q = prepareTimestamp( + this.getNode(), + itemIndex, + qs.q as string, + qs.receivedAfter as string, + 'after', + ); delete qs.receivedAfter; } if (qs.receivedBefore) { - let timestamp = DateTime.fromISO(qs.receivedBefore as string).toSeconds(); - const timestampLengthInMilliseconds1990 = 12; - - if (!timestamp && (qs.receivedBefore as string).length < timestampLengthInMilliseconds1990) { - timestamp = parseInt(qs.receivedBefore as string, 10); - } - - if (!timestamp) { - timestamp = Math.floor( - DateTime.fromMillis(parseInt(qs.receivedBefore as string, 10)).toSeconds(), - ); - } - - if (!timestamp) { - const description = `'${qs.receivedBefore}' isn't a valid date and time. If you're using an expression, be sure to set an ISO date string or a timestamp.`; - throw new NodeOperationError(this.getNode(), "Invalid date/time in 'Received Before' field", { - description, - }); - } - - if (qs.q) { - qs.q += ` before:${timestamp}`; - } else { - qs.q = `before:${timestamp}`; - } + qs.q = prepareTimestamp( + this.getNode(), + itemIndex, + qs.q as string, + qs.receivedBefore as string, + 'before', + ); delete qs.receivedBefore; } diff --git a/packages/nodes-base/nodes/Google/Gmail/GmailTrigger.node.ts b/packages/nodes-base/nodes/Google/Gmail/GmailTrigger.node.ts index ed2834d13e..73cef44222 100644 --- a/packages/nodes-base/nodes/Google/Gmail/GmailTrigger.node.ts +++ b/packages/nodes-base/nodes/Google/Gmail/GmailTrigger.node.ts @@ -246,7 +246,7 @@ export class GmailTrigger implements INodeType { delete filters.receivedAfter; } - Object.assign(qs, prepareQuery.call(this, filters), options); + Object.assign(qs, prepareQuery.call(this, filters, 0), options); responseData = await googleApiRequest.call( this, diff --git a/packages/nodes-base/nodes/Google/Gmail/test/v2/utils.test.ts b/packages/nodes-base/nodes/Google/Gmail/test/v2/utils.test.ts new file mode 100644 index 0000000000..91813b0273 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Gmail/test/v2/utils.test.ts @@ -0,0 +1,111 @@ +import type { INode } from 'n8n-workflow'; +import { prepareTimestamp } from '../../GenericFunctions'; + +import { DateTime } from 'luxon'; + +const node: INode = { + id: '1', + name: 'Gmail node', + typeVersion: 2, + type: 'n8n-nodes-base.gmail', + position: [50, 50], + parameters: { + operation: 'getAll', + }, +}; + +describe('Google Gmail v2, prepareTimestamp', () => { + it('should return a valid timestamp from ISO', () => { + const dateInput = '2020-01-01T00:00:00.000Z'; + const timestampBefore = prepareTimestamp(node, 0, '', dateInput, 'before'); + const timestampAfter = prepareTimestamp(node, 0, '', dateInput, 'after'); + + expect(timestampBefore).toBeDefined(); + expect(timestampBefore).toBe('before:1577836800'); + + expect(timestampAfter).toBeDefined(); + expect(timestampAfter).toBe('after:1577836800'); + }); + + it('should return a valid timestamp from integer in miliseconds', () => { + const dateInput = 1577836800000; + const timestampBefore = prepareTimestamp(node, 0, '', dateInput, 'before'); + const timestampAfter = prepareTimestamp(node, 0, '', dateInput, 'after'); + + expect(timestampBefore).toBeDefined(); + expect(timestampBefore).toBe('before:1577836800'); + + expect(timestampAfter).toBeDefined(); + expect(timestampAfter).toBe('after:1577836800'); + }); + + it('should return a valid timestamp from integer in seconds', () => { + const dateInput = 1577836800; + const timestampBefore = prepareTimestamp(node, 0, '', dateInput, 'before'); + const timestampAfter = prepareTimestamp(node, 0, '', dateInput, 'after'); + + expect(timestampBefore).toBeDefined(); + expect(timestampBefore).toBe('before:1577836800'); + + expect(timestampAfter).toBeDefined(); + expect(timestampAfter).toBe('after:1577836800'); + }); + + it('should return a valid timestamp from string in miliseconds', () => { + const dateInput = '1577836800000'; + const timestampBefore = prepareTimestamp(node, 0, '', dateInput, 'before'); + const timestampAfter = prepareTimestamp(node, 0, '', dateInput, 'after'); + + expect(timestampBefore).toBeDefined(); + expect(timestampBefore).toBe('before:1577836800'); + + expect(timestampAfter).toBeDefined(); + expect(timestampAfter).toBe('after:1577836800'); + }); + + it('should return a valid timestamp from string in seconds', () => { + const dateInput = '1577836800'; + const timestampBefore = prepareTimestamp(node, 0, '', dateInput, 'before'); + const timestampAfter = prepareTimestamp(node, 0, '', dateInput, 'after'); + + expect(timestampBefore).toBeDefined(); + expect(timestampBefore).toBe('before:1577836800'); + + expect(timestampAfter).toBeDefined(); + expect(timestampAfter).toBe('after:1577836800'); + }); + + it('should return a valid timestamp from luxon DateTime', () => { + const dateInput = DateTime.fromISO('2020-01-01T00:00:00.000Z'); + const timestampBefore = prepareTimestamp(node, 0, '', dateInput, 'before'); + const timestampAfter = prepareTimestamp(node, 0, '', dateInput, 'after'); + + expect(timestampBefore).toBeDefined(); + expect(timestampBefore).toBe('before:1577836800'); + + expect(timestampAfter).toBeDefined(); + expect(timestampAfter).toBe('after:1577836800'); + }); + + it('should return a valid timestamp from luxon DateTime ISO', () => { + const dateInput = DateTime.fromISO('2020-01-01T00:00:00.000Z').toISO(); + const timestampBefore = prepareTimestamp(node, 0, '', dateInput, 'before'); + const timestampAfter = prepareTimestamp(node, 0, '', dateInput, 'after'); + + expect(timestampBefore).toBeDefined(); + expect(timestampBefore).toBe('before:1577836800'); + + expect(timestampAfter).toBeDefined(); + expect(timestampAfter).toBe('after:1577836800'); + }); + + it('should throw error on invalid data', () => { + const dateInput = 'invalid'; + expect(() => prepareTimestamp(node, 0, '', dateInput, 'before')).toThrow( + "Invalid date/time in 'Received Before' field", + ); + expect(() => prepareTimestamp(node, 0, '', dateInput, 'after')).toThrow( + "Invalid date/time in 'Received After' field", + ); + }); +}); diff --git a/packages/nodes-base/nodes/Google/Gmail/v2/GmailV2.node.ts b/packages/nodes-base/nodes/Google/Gmail/v2/GmailV2.node.ts index 43a01aacb7..a21f3898c2 100644 --- a/packages/nodes-base/nodes/Google/Gmail/v2/GmailV2.node.ts +++ b/packages/nodes-base/nodes/Google/Gmail/v2/GmailV2.node.ts @@ -382,7 +382,7 @@ export class GmailV2 implements INodeType { const options = this.getNodeParameter('options', i, {}); const filters = this.getNodeParameter('filters', i, {}); const qs: IDataObject = {}; - Object.assign(qs, prepareQuery.call(this, filters), options); + Object.assign(qs, prepareQuery.call(this, filters, i), options, i); if (returnAll) { responseData = await googleApiRequestAllItems.call( @@ -708,7 +708,7 @@ export class GmailV2 implements INodeType { const returnAll = this.getNodeParameter('returnAll', i); const filters = this.getNodeParameter('filters', i); const qs: IDataObject = {}; - Object.assign(qs, prepareQuery.call(this, filters)); + Object.assign(qs, prepareQuery.call(this, filters, i)); if (returnAll) { responseData = await googleApiRequestAllItems.call(