From c75990e0632c581384542610a886ef89621a9403 Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Tue, 24 Sep 2024 18:41:07 +0300 Subject: [PATCH] fix(Google Sheets Node): Insert data if sheet is empty instead of error (#10942) --- .../Google/Sheet/test/v2/node/append.test.ts | 59 +++++++++++ .../Sheet/test/v2/node/appendOrUpdate.test.ts | 98 +++++++++++++++++++ .../v2/actions/sheet/append.operation.ts | 6 +- .../actions/sheet/appendOrUpdate.operation.ts | 14 ++- .../Sheet/v2/actions/versionDescription.ts | 7 ++ 5 files changed, 178 insertions(+), 6 deletions(-) create mode 100644 packages/nodes-base/nodes/Google/Sheet/test/v2/node/append.test.ts create mode 100644 packages/nodes-base/nodes/Google/Sheet/test/v2/node/appendOrUpdate.test.ts diff --git a/packages/nodes-base/nodes/Google/Sheet/test/v2/node/append.test.ts b/packages/nodes-base/nodes/Google/Sheet/test/v2/node/append.test.ts new file mode 100644 index 0000000000..d424f554bd --- /dev/null +++ b/packages/nodes-base/nodes/Google/Sheet/test/v2/node/append.test.ts @@ -0,0 +1,59 @@ +import type { MockProxy } from 'jest-mock-extended'; +import { mock } from 'jest-mock-extended'; +import type { IExecuteFunctions, INode } from 'n8n-workflow'; + +import { execute } from '../../../v2/actions/sheet/append.operation'; +import type { GoogleSheet } from '../../../v2/helpers/GoogleSheet'; + +describe('Google Sheet - Append', () => { + let mockExecuteFunctions: MockProxy; + let mockGoogleSheet: MockProxy; + + beforeEach(() => { + mockExecuteFunctions = mock(); + mockGoogleSheet = mock(); + }); + + it('should insert input data if sheet is empty', async () => { + mockExecuteFunctions.getInputData.mockReturnValueOnce([ + { + json: { + row_number: 3, + name: 'NEW NAME', + text: 'NEW TEXT', + }, + pairedItem: { + item: 0, + input: undefined, + }, + }, + ]); + + mockExecuteFunctions.getNode.mockReturnValueOnce(mock({ typeVersion: 4.5 })); + mockExecuteFunctions.getNodeParameter + .mockReturnValueOnce('USER_ENTERED') // valueInputMode + .mockReturnValueOnce({}); // options + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('defineBelow'); // dataMode + + mockGoogleSheet.getData.mockResolvedValueOnce(undefined); + mockGoogleSheet.getData.mockResolvedValueOnce(undefined); + mockGoogleSheet.updateRows.mockResolvedValueOnce(undefined); + + mockGoogleSheet.updateRows.mockResolvedValueOnce([]); + + mockGoogleSheet.appendEmptyRowsOrColumns.mockResolvedValueOnce([]); + mockGoogleSheet.appendSheetData.mockResolvedValueOnce([]); + + await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234'); + + expect(mockGoogleSheet.updateRows).toHaveBeenCalledWith('Sheet1', [['name', 'text']], 'RAW', 1); + expect(mockGoogleSheet.appendEmptyRowsOrColumns).toHaveBeenCalledWith('1234', 1, 0); + expect(mockGoogleSheet.appendSheetData).toHaveBeenCalledWith({ + inputData: [{ name: 'NEW NAME', text: 'NEW TEXT' }], + keyRowIndex: 1, + lastRow: 2, + range: 'Sheet1', + valueInputMode: 'USER_ENTERED', + }); + }); +}); diff --git a/packages/nodes-base/nodes/Google/Sheet/test/v2/node/appendOrUpdate.test.ts b/packages/nodes-base/nodes/Google/Sheet/test/v2/node/appendOrUpdate.test.ts new file mode 100644 index 0000000000..552567609f --- /dev/null +++ b/packages/nodes-base/nodes/Google/Sheet/test/v2/node/appendOrUpdate.test.ts @@ -0,0 +1,98 @@ +import type { MockProxy } from 'jest-mock-extended'; +import { mock } from 'jest-mock-extended'; +import type { IExecuteFunctions, INode } from 'n8n-workflow'; + +import { execute } from '../../../v2/actions/sheet/appendOrUpdate.operation'; +import type { GoogleSheet } from '../../../v2/helpers/GoogleSheet'; + +describe('Google Sheet - Append or Update', () => { + let mockExecuteFunctions: MockProxy; + let mockGoogleSheet: MockProxy; + + beforeEach(() => { + mockExecuteFunctions = mock(); + mockGoogleSheet = mock(); + }); + + it('should insert input data if sheet is empty', async () => { + mockExecuteFunctions.getInputData.mockReturnValueOnce([ + { + json: { + row_number: 3, + name: 'NEW NAME', + text: 'NEW TEXT', + }, + pairedItem: { + item: 0, + input: undefined, + }, + }, + ]); + + mockExecuteFunctions.getNode.mockReturnValueOnce(mock({ typeVersion: 4.5 })); + mockExecuteFunctions.getNodeParameter + .mockReturnValueOnce('USER_ENTERED') // valueInputMode + .mockReturnValueOnce({}); // options + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('defineBelow'); // dataMode + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce([]); // columns.schema + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce(['row_number']); // columnsToMatchOn + mockExecuteFunctions.getNode.mockReturnValueOnce(mock()); + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce([]); // columns.matchingColumns + + mockGoogleSheet.getData.mockResolvedValueOnce(undefined); + + mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]); + mockGoogleSheet.updateRows.mockResolvedValueOnce([]); + + mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({ + updateData: [], + appendData: [ + { + row_number: 3, + name: 'NEW NAME', + text: 'NEW TEXT', + }, + ], + }); + + mockGoogleSheet.appendEmptyRowsOrColumns.mockResolvedValueOnce([]); + mockGoogleSheet.appendSheetData.mockResolvedValueOnce([]); + + await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234'); + + expect(mockGoogleSheet.getColumnValues).toHaveBeenCalledWith({ + dataStartRowIndex: 1, + keyIndex: -1, + range: 'Sheet1!A:Z', + sheetData: [['name', 'text']], + valueRenderMode: 'UNFORMATTED_VALUE', + }); + + expect(mockGoogleSheet.updateRows).toHaveBeenCalledWith( + 'Sheet1', + [['name', 'text']], + 'USER_ENTERED', + 1, + ); + expect(mockGoogleSheet.prepareDataForUpdateOrUpsert).toHaveBeenCalledWith({ + columnNamesList: [['name', 'text']], + columnValuesList: [], + dataStartRowIndex: 1, + indexKey: 'row_number', + inputData: [{ name: 'NEW NAME', row_number: 3, text: 'NEW TEXT' }], + keyRowIndex: 0, + range: 'Sheet1!A:Z', + upsert: true, + valueRenderMode: 'UNFORMATTED_VALUE', + }); + expect(mockGoogleSheet.appendEmptyRowsOrColumns).toHaveBeenCalledWith('1234', 1, 0); + expect(mockGoogleSheet.appendSheetData).toHaveBeenCalledWith({ + columnNamesList: [['name', 'text']], + inputData: [{ name: 'NEW NAME', row_number: 3, text: 'NEW TEXT' }], + keyRowIndex: 1, + lastRow: 2, + range: 'Sheet1!A:Z', + valueInputMode: 'USER_ENTERED', + }); + }); +}); diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/append.operation.ts b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/append.operation.ts index 03fb7a75a0..907a70b0fc 100644 --- a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/append.operation.ts +++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/append.operation.ts @@ -211,7 +211,7 @@ export async function execute( ): Promise { const items = this.getInputData(); const nodeVersion = this.getNode().typeVersion; - const dataMode = + let dataMode = nodeVersion < 4 ? (this.getNodeParameter('dataMode', 0) as string) : (this.getNodeParameter('columns.mappingMode', 0) as string); @@ -228,6 +228,10 @@ export async function execute( const sheetData = await sheet.getData(range, 'FORMATTED_VALUE'); + if (sheetData === undefined || !sheetData.length) { + dataMode = 'autoMapInputData'; + } + if (nodeVersion >= 4.4 && dataMode !== 'autoMapInputData') { //not possible to refresh columns when mode is autoMapInputData if (sheetData?.[keyRowIndex - 1] === undefined) { diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/appendOrUpdate.operation.ts b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/appendOrUpdate.operation.ts index caa3392230..f6f3e5cd3d 100644 --- a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/appendOrUpdate.operation.ts +++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/appendOrUpdate.operation.ts @@ -257,7 +257,7 @@ export async function execute( } } - const dataMode = + let dataMode = nodeVersion < 4 ? (this.getNodeParameter('dataMode', 0) as string) : (this.getNodeParameter('columns.mappingMode', 0) as string); @@ -267,10 +267,14 @@ export async function execute( const sheetData = (await sheet.getData(sheetName, 'FORMATTED_VALUE')) ?? []; if (!sheetData[keyRowIndex] && dataMode !== 'autoMapInputData') { - throw new NodeOperationError( - this.getNode(), - `Could not retrieve the column names from row ${keyRowIndex + 1}`, - ); + if (!sheetData.length) { + dataMode = 'autoMapInputData'; + } else { + throw new NodeOperationError( + this.getNode(), + `Could not retrieve the column names from row ${keyRowIndex + 1}`, + ); + } } columnNames = sheetData[keyRowIndex] ?? []; diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/actions/versionDescription.ts b/packages/nodes-base/nodes/Google/Sheet/v2/actions/versionDescription.ts index e41a8b9a24..5ac6b0a195 100644 --- a/packages/nodes-base/nodes/Google/Sheet/v2/actions/versionDescription.ts +++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/versionDescription.ts @@ -26,6 +26,13 @@ export const versionDescription: INodeTypeDescription = { whenToDisplay: 'beforeExecution', location: 'outputPane', }, + { + message: 'No columns found in Google Sheet. All rows will be appended', + displayCondition: + '={{ ["appendOrUpdate", "append"].includes($parameter["operation"]) && $parameter?.columns?.mappingMode === "defineBelow" && !$parameter?.columns?.schema?.length }}', + whenToDisplay: 'beforeExecution', + location: 'outputPane', + }, ], credentials: [ {