Skip to content

Commit

Permalink
fix(Google Sheets Node): Insert data if sheet is empty instead of err…
Browse files Browse the repository at this point in the history
…or (#10942)
  • Loading branch information
michael-radency committed Sep 24, 2024
1 parent ad60d49 commit c75990e
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 6 deletions.
59 changes: 59 additions & 0 deletions packages/nodes-base/nodes/Google/Sheet/test/v2/node/append.test.ts
Original file line number Diff line number Diff line change
@@ -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<IExecuteFunctions>;
let mockGoogleSheet: MockProxy<GoogleSheet>;

beforeEach(() => {
mockExecuteFunctions = mock<IExecuteFunctions>();
mockGoogleSheet = mock<GoogleSheet>();
});

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<INode>({ 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',
});
});
});
Original file line number Diff line number Diff line change
@@ -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<IExecuteFunctions>;
let mockGoogleSheet: MockProxy<GoogleSheet>;

beforeEach(() => {
mockExecuteFunctions = mock<IExecuteFunctions>();
mockGoogleSheet = mock<GoogleSheet>();
});

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<INode>({ 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<INode>());
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',
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ export async function execute(
): Promise<INodeExecutionData[]> {
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);
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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] ?? [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
{
Expand Down

0 comments on commit c75990e

Please sign in to comment.