diff --git a/packages/nodes-base/credentials/GoogleSheetsOAuth2Api.credentials.ts b/packages/nodes-base/credentials/GoogleSheetsOAuth2Api.credentials.ts
index c35847f6c26a4..578cb4f7525fe 100644
--- a/packages/nodes-base/credentials/GoogleSheetsOAuth2Api.credentials.ts
+++ b/packages/nodes-base/credentials/GoogleSheetsOAuth2Api.credentials.ts
@@ -3,6 +3,7 @@ import { ICredentialType, INodeProperties } from 'n8n-workflow';
const scopes = [
'https://www.googleapis.com/auth/drive.file',
'https://www.googleapis.com/auth/spreadsheets',
+ 'https://www.googleapis.com/auth/drive.metadata',
];
export class GoogleSheetsOAuth2Api implements ICredentialType {
diff --git a/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts b/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts
index 960e38d10b108..de374ff9bf0c4 100644
--- a/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts
+++ b/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts
@@ -1,1396 +1,25 @@
-import { IExecuteFunctions } from 'n8n-core';
-
-import {
- ICredentialsDecrypted,
- ICredentialTestFunctions,
- IDataObject,
- ILoadOptionsFunctions,
- INodeCredentialTestResult,
- INodeExecutionData,
- INodePropertyOptions,
- INodeType,
- INodeTypeDescription,
- NodeOperationError,
-} from 'n8n-workflow';
-
-import {
- GoogleSheet,
- ILookupValues,
- ISheetUpdateData,
- IToDelete,
- ValueInputOption,
- ValueRenderOption,
-} from './GoogleSheet';
-
-import {
- getAccessToken,
- googleApiRequest,
- hexToRgb,
- IGoogleAuthCredentials,
-} from './GenericFunctions';
-
-export class GoogleSheets implements INodeType {
- description: INodeTypeDescription = {
- displayName: 'Google Sheets ',
- name: 'googleSheets',
- icon: 'file:googleSheets.svg',
- group: ['input', 'output'],
- version: [1, 2],
- subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
- description: 'Read, update and write data to Google Sheets',
- defaults: {
- name: 'Google Sheets',
- },
- inputs: ['main'],
- outputs: ['main'],
- credentials: [
- {
- name: 'googleApi',
- required: true,
- displayOptions: {
- show: {
- authentication: ['serviceAccount'],
- },
- },
- testedBy: 'googleApiCredentialTest',
- },
- {
- name: 'googleSheetsOAuth2Api',
- required: true,
- displayOptions: {
- show: {
- authentication: ['oAuth2'],
- },
- },
- },
- ],
- properties: [
- {
- displayName: 'Authentication',
- name: 'authentication',
- type: 'options',
- options: [
- {
- name: 'Service Account',
- value: 'serviceAccount',
- },
- {
- name: 'OAuth2',
- value: 'oAuth2',
- },
- ],
- default: 'serviceAccount',
- displayOptions: {
- show: {
- '@version': [1],
- },
- },
- },
- {
- displayName: 'Authentication',
- name: 'authentication',
- type: 'options',
- options: [
- {
- // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
- name: 'OAuth2 (recommended)',
- value: 'oAuth2',
- },
- {
- name: 'Service Account',
- value: 'serviceAccount',
- },
- ],
- default: 'oAuth2',
- displayOptions: {
- show: {
- '@version': [2],
- },
- },
- },
- {
- displayName: 'Resource',
- name: 'resource',
- type: 'options',
- noDataExpression: true,
- options: [
- {
- name: 'Spreadsheet',
- value: 'spreadsheet',
- },
- {
- name: 'Sheet',
- value: 'sheet',
- },
- ],
- default: 'sheet',
- },
- {
- displayName: 'Operation',
- name: 'operation',
- type: 'options',
- noDataExpression: true,
- displayOptions: {
- show: {
- resource: ['sheet'],
- },
- },
- options: [
- {
- name: 'Append',
- value: 'append',
- description: 'Append data to a sheet',
- action: 'Append data to a sheet',
- },
- {
- name: 'Clear',
- value: 'clear',
- description: 'Clear data from a sheet',
- action: 'Clear a sheet',
- },
- {
- name: 'Create',
- value: 'create',
- description: 'Create a new sheet',
- action: 'Create a sheet',
- },
- {
- name: 'Create or Update',
- value: 'upsert',
- description:
- 'Create a new record, or update the current one if it already exists (upsert)',
- action: 'Create or update a sheet',
- },
- {
- name: 'Delete',
- value: 'delete',
- description: 'Delete columns and rows from a sheet',
- action: 'Delete a sheet',
- },
- {
- name: 'Lookup',
- value: 'lookup',
- description: 'Look up a specific column value and return the matching row',
- action: 'Look up a column value in a sheet',
- },
- {
- name: 'Read',
- value: 'read',
- description: 'Read data from a sheet',
- action: 'Read a sheet',
- },
- {
- name: 'Remove',
- value: 'remove',
- description: 'Remove a sheet',
- action: 'Remove a sheet',
- },
- {
- name: 'Update',
- value: 'update',
- description: 'Update rows in a sheet',
- action: 'Update a sheet',
- },
- ],
- default: 'read',
- },
-
- // ----------------------------------
- // All
- // ----------------------------------
- {
- displayName: 'Spreadsheet ID',
- name: 'sheetId',
- type: 'string',
- displayOptions: {
- show: {
- resource: ['sheet'],
- },
- },
- default: '',
- required: true,
- description:
- 'The ID of the Google Spreadsheet. Found as part of the sheet URL https://docs.google.com/spreadsheets/d/{ID}/.',
- },
- {
- displayName: 'Range',
- name: 'range',
- type: 'string',
- displayOptions: {
- show: {
- resource: ['sheet'],
- },
- hide: {
- operation: ['create', 'delete', 'remove'],
- },
- },
- default: 'A:F',
- required: true,
- description:
- 'The table range to read from or to append data to. See the Google documentation for the details. If it contains multiple sheets it can also be added like this: "MySheet!A:F"',
- },
-
- // ----------------------------------
- // Delete
- // ----------------------------------
- {
- displayName: 'To Delete',
- name: 'toDelete',
- placeholder: 'Add Columns/Rows to delete',
- description: 'Deletes columns and rows from a sheet',
- type: 'fixedCollection',
- typeOptions: {
- multipleValues: true,
- },
- displayOptions: {
- show: {
- resource: ['sheet'],
- operation: ['delete'],
- },
- },
- default: {},
- options: [
- {
- displayName: 'Columns',
- name: 'columns',
- values: [
- {
- displayName: 'Sheet Name or ID',
- name: 'sheetId',
- type: 'options',
- typeOptions: {
- loadOptionsMethod: 'getSheets',
- },
- options: [],
- default: '',
- required: true,
- description:
- 'The sheet to delete columns from. Choose from the list, or specify an ID using an expression.',
- },
- {
- displayName: 'Start Index',
- name: 'startIndex',
- type: 'number',
- typeOptions: {
- minValue: 0,
- },
- default: 0,
- description: 'The start index (0 based and inclusive) of column to delete',
- },
- {
- displayName: 'Amount',
- name: 'amount',
- type: 'number',
- typeOptions: {
- minValue: 1,
- },
- default: 1,
- description: 'Number of columns to delete',
- },
- ],
- },
- {
- displayName: 'Rows',
- name: 'rows',
- values: [
- {
- displayName: 'Sheet Name or ID',
- name: 'sheetId',
- type: 'options',
- typeOptions: {
- loadOptionsMethod: 'getSheets',
- },
- options: [],
- default: '',
- required: true,
- description:
- 'The sheet to delete columns from. Choose from the list, or specify an ID using an expression.',
- },
- {
- displayName: 'Start Index',
- name: 'startIndex',
- type: 'number',
- typeOptions: {
- minValue: 0,
- },
- default: 0,
- description: 'The start index (0 based and inclusive) of row to delete',
- },
- {
- displayName: 'Amount',
- name: 'amount',
- type: 'number',
- typeOptions: {
- minValue: 1,
- },
- default: 1,
- description: 'Number of rows to delete',
- },
- ],
- },
- ],
- },
-
- // ----------------------------------
- // Read
- // ----------------------------------
- {
- displayName: 'RAW Data',
- name: 'rawData',
- type: 'boolean',
- displayOptions: {
- show: {
- resource: ['sheet'],
- operation: ['read'],
- },
- },
- default: false,
- description:
- 'Whether the data should be returned RAW instead of parsed into keys according to their header',
- },
- {
- displayName: 'Data Property',
- name: 'dataProperty',
- type: 'string',
- default: 'data',
- displayOptions: {
- show: {
- resource: ['sheet'],
- operation: ['read'],
- rawData: [true],
- },
- },
- description: 'The name of the property into which to write the RAW data',
- },
-
- // ----------------------------------
- // Update
- // ----------------------------------
- {
- displayName: 'RAW Data',
- name: 'rawData',
- type: 'boolean',
- displayOptions: {
- show: {
- resource: ['sheet'],
- operation: ['update', 'upsert'],
- },
- },
- default: false,
- description: 'Whether the data supplied is RAW instead of parsed into keys',
- },
- {
- displayName: 'Data Property',
- name: 'dataProperty',
- type: 'string',
- default: 'data',
- displayOptions: {
- show: {
- resource: ['sheet'],
- operation: ['update', 'upsert'],
- rawData: [true],
- },
- },
- description: 'The name of the property from which to read the RAW data',
- },
-
- // ----------------------------------
- // Read & Update & lookupColumn
- // ----------------------------------
- {
- displayName: 'Data Start Row',
- name: 'dataStartRow',
- type: 'number',
- typeOptions: {
- minValue: 1,
- },
- default: 1,
- displayOptions: {
- show: {
- resource: ['sheet'],
- },
- hide: {
- operation: ['append', 'create', 'clear', 'delete', 'remove'],
- rawData: [true],
- },
- },
- description:
- 'Index of the first row which contains the actual data and not the keys. Starts with 0.',
- },
-
- // ----------------------------------
- // Mixed
- // ----------------------------------
- {
- displayName: 'Key Row',
- name: 'keyRow',
- type: 'number',
- typeOptions: {
- minValue: 0,
- },
- displayOptions: {
- show: {
- resource: ['sheet'],
- },
- hide: {
- operation: ['clear', 'create', 'delete', 'remove'],
- rawData: [true],
- },
- },
- default: 0,
- description:
- 'Index of the row which contains the keys. Starts at 0. The incoming node data is matched to the keys for assignment. The matching is case sensitive.',
- },
-
- // ----------------------------------
- // lookup
- // ----------------------------------
- {
- displayName: 'Lookup Column',
- name: 'lookupColumn',
- type: 'string',
- default: '',
- placeholder: 'Email',
- required: true,
- displayOptions: {
- show: {
- resource: ['sheet'],
- operation: ['lookup'],
- },
- },
- description: 'The name of the column in which to look for value',
- },
- {
- displayName: 'Lookup Value',
- name: 'lookupValue',
- type: 'string',
- default: '',
- placeholder: 'frank@example.com',
- displayOptions: {
- show: {
- resource: ['sheet'],
- operation: ['lookup'],
- },
- },
- description: 'The value to look for in column',
- },
-
- // ----------------------------------
- // Update
- // ----------------------------------
- {
- displayName: 'Key',
- name: 'key',
- type: 'string',
- default: 'id',
- displayOptions: {
- show: {
- resource: ['sheet'],
- operation: ['update', 'upsert'],
- rawData: [false],
- },
- },
- description: 'The name of the key to identify which data should be updated in the sheet',
- },
-
- {
- displayName: 'Options',
- name: 'options',
- type: 'collection',
- placeholder: 'Add Option',
- default: {},
- displayOptions: {
- show: {
- resource: ['sheet'],
- operation: ['append', 'lookup', 'read', 'update', 'upsert'],
- },
- },
- options: [
- {
- displayName: 'Continue If Empty',
- name: 'continue',
- type: 'boolean',
- default: false,
- displayOptions: {
- show: {
- '/operation': ['lookup', 'read'],
- },
- },
- // eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
- description:
- 'By default, the workflow stops executing if the lookup/read does not return values',
- },
- {
- displayName: 'Return All Matches',
- name: 'returnAllMatches',
- type: 'boolean',
- default: false,
- displayOptions: {
- show: {
- '/operation': ['lookup'],
- },
- },
- // eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
- description:
- 'By default only the first result gets returned. If options gets set all found matches get returned.',
- },
- {
- displayName: 'Use Header Names as JSON Paths',
- name: 'usePathForKeyRow',
- type: 'boolean',
- default: false,
- displayOptions: {
- show: {
- '/operation': ['append'],
- },
- },
- description:
- 'Whether you want to match the headers as path, for example, the row header "category.name" will match the "category" object and get the field "name" from it. By default "category.name" will match with the field with exact name, not nested object.',
- },
- {
- displayName: 'Value Input Mode',
- name: 'valueInputMode',
- type: 'options',
- displayOptions: {
- show: {
- '/operation': ['append', 'update', 'upsert'],
- },
- },
- options: [
- {
- name: 'RAW',
- value: 'RAW',
- description: 'The values will not be parsed and will be stored as-is',
- },
- {
- name: 'User Entered',
- value: 'USER_ENTERED',
- description:
- 'The values will be parsed as if the user typed them into the UI. Numbers will stay as numbers, but strings may be converted to numbers, dates, etc. following the same rules that are applied when entering text into a cell via the Google Sheets UI.',
- },
- ],
- default: 'RAW',
- description: 'Determines how data should be interpreted',
- },
- {
- displayName: 'Value Render Mode',
- name: 'valueRenderMode',
- type: 'options',
- displayOptions: {
- show: {
- '/operation': ['lookup', 'read'],
- },
- },
- options: [
- {
- name: 'Formatted Value',
- value: 'FORMATTED_VALUE',
- description:
- "Values will be calculated & formatted in the reply according to the cell's formatting.Formatting is based on the spreadsheet's locale, not the requesting user's locale.For example, if A1 is 1.23 and A2 is =A1 and formatted as currency, then A2 would return \"$1.23\"",
- },
- {
- name: 'Formula',
- value: 'FORMULA',
- description:
- 'Values will not be calculated. The reply will include the formulas. For example, if A1 is 1.23 and A2 is =A1 and formatted as currency, then A2 would return "=A1".',
- },
- {
- name: 'Unformatted Value',
- value: 'UNFORMATTED_VALUE',
- description:
- 'Values will be calculated, but not formatted in the reply. For example, if A1 is 1.23 and A2 is =A1 and formatted as currency, then A2 would return the number 1.23.',
- },
- ],
- default: 'UNFORMATTED_VALUE',
- description: 'Determines how values should be rendered in the output',
- },
- {
- displayName: 'Value Render Mode',
- name: 'valueRenderMode',
- type: 'options',
- displayOptions: {
- show: {
- '/operation': ['update', 'upsert'],
- '/rawData': [false],
- },
- },
- options: [
- {
- name: 'Formatted Value',
- value: 'FORMATTED_VALUE',
- description:
- "Values will be calculated & formatted in the reply according to the cell's formatting.Formatting is based on the spreadsheet's locale, not the requesting user's locale. For example, if A1 is 1.23 and A2 is =A1 and formatted as currency, then A2 would return \"$1.23\".",
- },
- {
- name: 'Formula',
- value: 'FORMULA',
- description:
- 'Values will not be calculated. The reply will include the formulas. For example, if A1 is 1.23 and A2 is =A1 and formatted as currency, then A2 would return "=A1".',
- },
- {
- name: 'Unformatted Value',
- value: 'UNFORMATTED_VALUE',
- description:
- 'Values will be calculated, but not formatted in the reply. For example, if A1 is 1.23 and A2 is =A1 and formatted as currency, then A2 would return the number 1.23.',
- },
- ],
- default: 'UNFORMATTED_VALUE',
- description: 'Determines how values should be rendered in the output',
- },
- ],
- },
-
- {
- displayName: 'Operation',
- name: 'operation',
- type: 'options',
- noDataExpression: true,
- displayOptions: {
- show: {
- resource: ['spreadsheet'],
- },
- },
- options: [
- {
- name: 'Create',
- value: 'create',
- description: 'Create a spreadsheet',
- action: 'Create a spreadsheet',
- },
- ],
- default: 'create',
- },
- // ----------------------------------
- // spreadsheet:create
- // ----------------------------------
- {
- displayName: 'Title',
- name: 'title',
- type: 'string',
- default: '',
- displayOptions: {
- show: {
- resource: ['spreadsheet'],
- operation: ['create'],
- },
- },
- description: 'The title of the spreadsheet',
- },
- {
- displayName: 'Sheets',
- name: 'sheetsUi',
- placeholder: 'Add Sheet',
- type: 'fixedCollection',
- typeOptions: {
- multipleValues: true,
- },
- default: {},
- displayOptions: {
- show: {
- resource: ['spreadsheet'],
- operation: ['create'],
- },
- },
- options: [
- {
- name: 'sheetValues',
- displayName: 'Sheet',
- values: [
- {
- displayName: 'Sheet Properties',
- name: 'propertiesUi',
- placeholder: 'Add Property',
- type: 'collection',
- default: {},
- options: [
- {
- displayName: 'Hidden',
- name: 'hidden',
- type: 'boolean',
- default: false,
- description: 'Whether the Sheet should be hidden in the UI',
- },
- {
- displayName: 'Title',
- name: 'title',
- type: 'string',
- default: '',
- description: 'Title of the property to create',
- },
- ],
- },
- ],
- },
- ],
- },
- {
- displayName: 'Options',
- name: 'options',
- type: 'collection',
- placeholder: 'Add Option',
- default: {},
- displayOptions: {
- show: {
- resource: ['spreadsheet'],
- operation: ['create'],
- },
- },
- options: [
- {
- displayName: 'Locale',
- name: 'locale',
- type: 'string',
- default: '',
- placeholder: 'en_US',
- description: `The locale of the spreadsheet in one of the following formats:
-
- - en (639-1)
- - fil (639-2 if no 639-1 format exists)
- - en_US (combination of ISO language an country)
- `,
- },
- {
- displayName: 'Recalculation Interval',
- name: 'autoRecalc',
- type: 'options',
- options: [
- {
- name: 'Default',
- value: '',
- description: 'Default value',
- },
- {
- name: 'On Change',
- value: 'ON_CHANGE',
- description: 'Volatile functions are updated on every change',
- },
- {
- name: 'Minute',
- value: 'MINUTE',
- description: 'Volatile functions are updated on every change and every minute',
- },
- {
- name: 'Hour',
- value: 'HOUR',
- description: 'Volatile functions are updated on every change and hourly',
- },
- ],
- default: '',
- description: 'Cell recalculation interval options',
- },
- ],
- },
-
- // ----------------------------------
- // sheet:create
- // ----------------------------------
- {
- displayName: 'Simplify',
- name: 'simple',
- type: 'boolean',
- default: true,
- displayOptions: {
- show: {
- resource: ['sheet'],
- operation: ['create'],
- },
- },
- description:
- 'Whether to return a simplified version of the response instead of the raw data',
- },
- {
- displayName: 'Options',
- name: 'options',
- type: 'collection',
- placeholder: 'Add Option',
- default: {},
- displayOptions: {
- show: {
- resource: ['sheet'],
- operation: ['create'],
- },
- },
- options: [
- {
- displayName: 'Grid Properties',
- name: 'gridProperties',
- type: 'collection',
- placeholder: 'Add Property',
- default: {},
- options: [
- {
- displayName: 'Column Count',
- name: 'columnCount',
- type: 'number',
- default: 0,
- description: 'The number of columns in the grid',
- },
- {
- displayName: 'Column Group Control After',
- name: 'columnGroupControlAfter',
- type: 'boolean',
- default: false,
- description: 'Whether the column grouping control toggle is shown after the group',
- },
- {
- displayName: 'Frozen Column Count',
- name: 'frozenColumnCount',
- type: 'number',
- default: 0,
- description: 'The number of columns that are frozen in the grid',
- },
- {
- displayName: 'Frozen Row Count',
- name: 'frozenRowCount',
- type: 'number',
- default: 0,
- description: 'The number of rows that are frozen in the grid',
- },
- {
- displayName: 'Hide Gridlines',
- name: 'hideGridlines',
- type: 'boolean',
- default: false,
- description: "Whether the grid isn't showing gridlines in the UI",
- },
- {
- displayName: 'Row Count',
- name: 'rowCount',
- type: 'number',
- default: 0,
- description: 'The number of rows in the grid',
- },
- {
- displayName: 'Row Group Control After',
- name: 'rowGroupControlAfter',
- type: 'boolean',
- default: false,
- description: 'Whether the row grouping control toggle is shown after the group',
- },
- ],
- description: 'The type of the sheet',
- },
- {
- displayName: 'Hidden',
- name: 'hidden',
- type: 'boolean',
- default: false,
- description: "Whether the sheet is hidden in the UI, false if it's visible",
- },
- {
- displayName: 'Right To Left',
- name: 'rightToLeft',
- type: 'boolean',
- default: false,
- description: 'Whether the sheet is an RTL sheet instead of an LTR sheet',
- },
- {
- displayName: 'Sheet ID',
- name: 'sheetId',
- type: 'number',
- default: 0,
- description:
- 'The ID of the sheet. Must be non-negative. This field cannot be changed once set.',
- },
- {
- displayName: 'Sheet Index',
- name: 'index',
- type: 'number',
- default: 0,
- description: 'The index of the sheet within the spreadsheet',
- },
- {
- displayName: 'Tab Color',
- name: 'tabColor',
- type: 'color',
- default: '0aa55c',
- description: 'The color of the tab in the UI',
- },
- {
- displayName: 'Title',
- name: 'title',
- type: 'string',
- default: '',
- description: 'The Sheet name',
- },
- ],
- },
-
- // ----------------------------------
- // sheet:remove
- // ----------------------------------
- {
- displayName: 'Sheet ID',
- name: 'id',
- type: 'string',
- default: '',
- required: true,
- displayOptions: {
- show: {
- resource: ['sheet'],
- operation: ['remove'],
- },
- },
- description: 'The ID of the sheet to delete',
- },
- ],
- };
-
- methods = {
- loadOptions: {
- // Get all the sheets in a Spreadsheet
- async getSheets(this: ILoadOptionsFunctions): Promise {
- const spreadsheetId = this.getCurrentNodeParameter('sheetId') as string;
-
- const sheet = new GoogleSheet(spreadsheetId, this);
- const responseData = await sheet.spreadsheetGetSheets();
-
- if (responseData === undefined) {
- throw new NodeOperationError(this.getNode(), 'No data got returned');
- }
-
- const returnData: INodePropertyOptions[] = [];
- for (const sheet of responseData.sheets!) {
- if (sheet.properties!.sheetType !== 'GRID') {
- continue;
- }
-
- returnData.push({
- name: sheet.properties!.title as string,
- value: sheet.properties!.sheetId as unknown as string,
- });
- }
-
- return returnData;
- },
- },
- credentialTest: {
- async googleApiCredentialTest(
- this: ICredentialTestFunctions,
- credential: ICredentialsDecrypted,
- ): Promise {
- try {
- const tokenRequest = await getAccessToken.call(
- this,
- credential.data! as unknown as IGoogleAuthCredentials,
- );
- if (!tokenRequest.access_token) {
- return {
- status: 'Error',
- message: 'Could not generate a token from your private key.',
- };
- }
- } catch (err) {
- return {
- status: 'Error',
- message: `Private key validation failed: ${err.message}`,
- };
- }
-
- return {
- status: 'OK',
- message: 'Connection successful!',
- };
- },
- },
- };
-
- async execute(this: IExecuteFunctions): Promise {
- const operation = this.getNodeParameter('operation', 0) as string;
- const resource = this.getNodeParameter('resource', 0) as string;
-
- if (resource === 'sheet') {
- const spreadsheetId = this.getNodeParameter('sheetId', 0) as string;
-
- const sheet = new GoogleSheet(spreadsheetId, this);
-
- let range = '';
- if (!['create', 'delete', 'remove'].includes(operation)) {
- range = this.getNodeParameter('range', 0) as string;
- }
-
- const options = this.getNodeParameter('options', 0, {}) as IDataObject;
-
- const valueInputMode = (options.valueInputMode || 'RAW') as ValueInputOption;
- const valueRenderMode = (options.valueRenderMode || 'UNFORMATTED_VALUE') as ValueRenderOption;
-
- if (operation === 'append') {
- // ----------------------------------
- // append
- // ----------------------------------
- try {
- const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10);
-
- const items = this.getInputData();
-
- const setData: IDataObject[] = [];
- items.forEach((item) => {
- setData.push(item.json);
- });
-
- const usePathForKeyRow = (options.usePathForKeyRow || false) as boolean;
-
- // Convert data into array format
- const _data = await sheet.appendSheetData(
- setData,
- sheet.encodeRange(range),
- keyRow,
- valueInputMode,
- usePathForKeyRow,
- );
-
- // TODO: Should add this data somewhere
- // TODO: Should have something like add metadata which does not get passed through
-
- return this.prepareOutputData(items);
- } catch (error) {
- if (this.continueOnFail()) {
- return this.prepareOutputData([{ json: { error: error.message } }]);
- }
- throw error;
- }
- } else if (operation === 'clear') {
- // ----------------------------------
- // clear
- // ----------------------------------
- try {
- await sheet.clearData(sheet.encodeRange(range));
-
- const items = this.getInputData();
- return this.prepareOutputData(items);
- } catch (error) {
- if (this.continueOnFail()) {
- return this.prepareOutputData([{ json: { error: error.message } }]);
- }
- throw error;
- }
- } else if (operation === 'create') {
- const returnData: IDataObject[] = [];
-
- let responseData;
- for (let i = 0; i < this.getInputData().length; i++) {
- try {
- const spreadsheetId = this.getNodeParameter('sheetId', i) as string;
- const options = this.getNodeParameter('options', i, {}) as IDataObject;
- const simple = this.getNodeParameter('simple', 0) as boolean;
- const properties = { ...options };
-
- if (options.tabColor) {
- const { red, green, blue } = hexToRgb(options.tabColor as string)!;
- properties.tabColor = { red: red / 255, green: green / 255, blue: blue / 255 };
- }
-
- const requests = [
- {
- addSheet: {
- properties,
- },
- },
- ];
-
- responseData = await googleApiRequest.call(
- this,
- 'POST',
- `/v4/spreadsheets/${spreadsheetId}:batchUpdate`,
- { requests },
- );
-
- if (simple === true) {
- Object.assign(responseData, responseData.replies[0].addSheet.properties);
- delete responseData.replies;
- }
- returnData.push(responseData);
- } catch (error) {
- if (this.continueOnFail()) {
- returnData.push({ error: error.message });
- continue;
- }
- throw error;
- }
- }
-
- return [this.helpers.returnJsonArray(returnData)];
- } else if (operation === 'delete') {
- // ----------------------------------
- // delete
- // ----------------------------------
- try {
- const requests: IDataObject[] = [];
-
- const toDelete = this.getNodeParameter('toDelete', 0) as IToDelete;
-
- const deletePropertyToDimensions: IDataObject = {
- columns: 'COLUMNS',
- rows: 'ROWS',
- };
-
- for (const propertyName of Object.keys(deletePropertyToDimensions)) {
- if (toDelete[propertyName] !== undefined) {
- toDelete[propertyName]!.forEach((range) => {
- requests.push({
- deleteDimension: {
- range: {
- sheetId: range.sheetId,
- dimension: deletePropertyToDimensions[propertyName] as string,
- startIndex: range.startIndex,
- endIndex:
- parseInt(range.startIndex.toString(), 10) +
- parseInt(range.amount.toString(), 10),
- },
- },
- });
- });
- }
- }
-
- const _data = await sheet.spreadsheetBatchUpdate(requests);
-
- const items = this.getInputData();
- return this.prepareOutputData(items);
- } catch (error) {
- if (this.continueOnFail()) {
- return this.prepareOutputData([{ json: { error: error.message } }]);
- }
- throw error;
- }
- } else if (operation === 'lookup') {
- // ----------------------------------
- // lookup
- // ----------------------------------
- try {
- const sheetData = await sheet.getData(sheet.encodeRange(range), valueRenderMode);
-
- if (sheetData === undefined) {
- return [];
- }
-
- const dataStartRow = parseInt(this.getNodeParameter('dataStartRow', 0) as string, 10);
- const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10);
-
- const items = this.getInputData();
-
- const lookupValues: ILookupValues[] = [];
- for (let i = 0; i < items.length; i++) {
- lookupValues.push({
- lookupColumn: this.getNodeParameter('lookupColumn', i) as string,
- lookupValue: this.getNodeParameter('lookupValue', i) as string,
- });
- }
-
- let returnData = await sheet.lookupValues(
- sheetData,
- keyRow,
- dataStartRow,
- lookupValues,
- options.returnAllMatches as boolean | undefined,
- );
-
- if (returnData.length === 0 && options.continue && options.returnAllMatches) {
- returnData = [{}];
- } else if (
- returnData.length === 1 &&
- Object.keys(returnData[0]).length === 0 &&
- !options.continue &&
- !options.returnAllMatches
- ) {
- returnData = [];
- }
-
- return [this.helpers.returnJsonArray(returnData)];
- } catch (error) {
- if (this.continueOnFail()) {
- return [this.helpers.returnJsonArray({ error: error.message })];
- }
- throw error;
- }
- } else if (operation === 'read') {
- // ----------------------------------
- // read
- // ----------------------------------
- try {
- const rawData = this.getNodeParameter('rawData', 0) as boolean;
-
- const sheetData = await sheet.getData(sheet.encodeRange(range), valueRenderMode);
-
- let returnData: IDataObject[];
- if (!sheetData) {
- returnData = [];
- } else if (rawData === true) {
- const dataProperty = this.getNodeParameter('dataProperty', 0) as string;
- returnData = [
- {
- [dataProperty]: sheetData,
- },
- ];
- } else {
- const dataStartRow = parseInt(this.getNodeParameter('dataStartRow', 0) as string, 10);
- const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10);
-
- returnData = sheet.structureArrayDataByColumn(sheetData, keyRow, dataStartRow);
- }
-
- if (returnData.length === 0 && options.continue) {
- returnData = [{}];
- }
-
- return [this.helpers.returnJsonArray(returnData)];
- } catch (error) {
- if (this.continueOnFail()) {
- return [this.helpers.returnJsonArray({ error: error.message })];
- }
- throw error;
- }
- } else if (operation === 'remove') {
- const returnData: IDataObject[] = [];
-
- let responseData;
- for (let i = 0; i < this.getInputData().length; i++) {
- try {
- const sheetId = this.getNodeParameter('id', i) as string;
- const spreadsheetId = this.getNodeParameter('sheetId', i) as string;
-
- const requests = [
- {
- deleteSheet: {
- sheetId,
- },
- },
- ];
-
- responseData = await googleApiRequest.call(
- this,
- 'POST',
- `/v4/spreadsheets/${spreadsheetId}:batchUpdate`,
- { requests },
- );
- delete responseData.replies;
- returnData.push(responseData);
- } catch (error) {
- if (this.continueOnFail()) {
- returnData.push({ error: error.message });
- continue;
- }
- throw error;
- }
- }
-
- return [this.helpers.returnJsonArray(returnData)];
- } else if (operation === 'update' || operation === 'upsert') {
- // ----------------------------------
- // update/upsert
- // ----------------------------------
- const upsert = operation === 'upsert' ? true : false;
- try {
- const rawData = this.getNodeParameter('rawData', 0) as boolean;
-
- const items = this.getInputData();
-
- if (rawData === true) {
- const dataProperty = this.getNodeParameter('dataProperty', 0) as string;
-
- const updateData: ISheetUpdateData[] = [];
- for (let i = 0; i < items.length; i++) {
- updateData.push({
- range,
- values: items[i].json[dataProperty] as string[][],
- });
- }
-
- const _data = await sheet.batchUpdate(updateData, valueInputMode);
- } else {
- const keyName = this.getNodeParameter('key', 0) as string;
- const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10);
- const dataStartRow = parseInt(this.getNodeParameter('dataStartRow', 0) as string, 10);
-
- const setData: IDataObject[] = [];
- items.forEach((item) => {
- setData.push(item.json);
- });
-
- const _data = await sheet.updateSheetData(
- setData,
- keyName,
- range,
- keyRow,
- dataStartRow,
- valueInputMode,
- valueRenderMode,
- upsert,
- );
- }
- // TODO: Should add this data somewhere
- // TODO: Should have something like add metadata which does not get passed through
-
- return this.prepareOutputData(items);
- } catch (error) {
- if (this.continueOnFail()) {
- return this.prepareOutputData([{ json: { error: error.message } }]);
- }
- throw error;
- }
- }
- }
-
- if (resource === 'spreadsheet') {
- const returnData: IDataObject[] = [];
-
- let responseData;
-
- if (operation === 'create') {
- // ----------------------------------
- // create
- // ----------------------------------
- // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/create
-
- for (let i = 0; i < this.getInputData().length; i++) {
- try {
- const title = this.getNodeParameter('title', i) as string;
- const sheetsUi = this.getNodeParameter('sheetsUi', i, {}) as IDataObject;
-
- const body = {
- properties: {
- title,
- autoRecalc: undefined as undefined | string,
- locale: undefined as undefined | string,
- },
- sheets: [] as IDataObject[],
- };
-
- const options = this.getNodeParameter('options', i, {}) as IDataObject;
-
- if (Object.keys(sheetsUi).length) {
- const data = [];
- const sheets = sheetsUi.sheetValues as IDataObject[];
- for (const sheet of sheets) {
- const properties = sheet.propertiesUi as IDataObject;
- if (properties) {
- data.push({ properties });
- }
- }
- body.sheets = data;
- }
-
- body.properties!.autoRecalc = options.autoRecalc
- ? (options.autoRecalc as string)
- : undefined;
- body.properties!.locale = options.locale ? (options.locale as string) : undefined;
-
- responseData = await googleApiRequest.call(this, 'POST', `/v4/spreadsheets`, body);
-
- returnData.push(responseData);
- } catch (error) {
- if (this.continueOnFail()) {
- returnData.push({ error: error.message });
- continue;
- }
- throw error;
- }
- }
- }
-
- return [this.helpers.returnJsonArray(returnData)];
- }
-
- return [];
+import { INodeTypeBaseDescription, IVersionedNodeType, VersionedNodeType } from 'n8n-workflow';
+
+import { GoogleSheetsV1 } from './v1/GoogleSheetsV1.node';
+import { GoogleSheetsV2 } from './v2/GoogleSheetsV2.node';
+
+export class GoogleSheets extends VersionedNodeType {
+ constructor() {
+ const baseDescription: INodeTypeBaseDescription = {
+ displayName: 'Google Sheets ',
+ name: 'googleSheets',
+ icon: 'file:googleSheets.svg',
+ group: ['input', 'output'],
+ defaultVersion: 2,
+ subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
+ description: 'Read, update and write data to Google Sheets',
+ };
+
+ const nodeVersions: IVersionedNodeType['nodeVersions'] = {
+ 1: new GoogleSheetsV1(baseDescription),
+ 2: new GoogleSheetsV2(baseDescription),
+ };
+
+ super(nodeVersions, baseDescription);
}
}
diff --git a/packages/nodes-base/nodes/Google/Sheet/GenericFunctions.ts b/packages/nodes-base/nodes/Google/Sheet/v1/GenericFunctions.ts
similarity index 100%
rename from packages/nodes-base/nodes/Google/Sheet/GenericFunctions.ts
rename to packages/nodes-base/nodes/Google/Sheet/v1/GenericFunctions.ts
diff --git a/packages/nodes-base/nodes/Google/Sheet/GoogleSheet.ts b/packages/nodes-base/nodes/Google/Sheet/v1/GoogleSheet.ts
similarity index 100%
rename from packages/nodes-base/nodes/Google/Sheet/GoogleSheet.ts
rename to packages/nodes-base/nodes/Google/Sheet/v1/GoogleSheet.ts
diff --git a/packages/nodes-base/nodes/Google/Sheet/v1/GoogleSheetsV1.node.ts b/packages/nodes-base/nodes/Google/Sheet/v1/GoogleSheetsV1.node.ts
new file mode 100644
index 0000000000000..6b7eb6011c999
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Sheet/v1/GoogleSheetsV1.node.ts
@@ -0,0 +1,502 @@
+import { IExecuteFunctions } from 'n8n-core';
+
+import {
+ ICredentialsDecrypted,
+ ICredentialTestFunctions,
+ IDataObject,
+ ILoadOptionsFunctions,
+ INodeCredentialTestResult,
+ INodeExecutionData,
+ INodePropertyOptions,
+ INodeType,
+ INodeTypeBaseDescription,
+ INodeTypeDescription,
+ NodeOperationError,
+} from 'n8n-workflow';
+
+import {
+ GoogleSheet,
+ ILookupValues,
+ ISheetUpdateData,
+ IToDelete,
+ ValueInputOption,
+ ValueRenderOption,
+} from './GoogleSheet';
+
+import {
+ getAccessToken,
+ googleApiRequest,
+ hexToRgb,
+ IGoogleAuthCredentials,
+} from './GenericFunctions';
+
+import { versionDescription } from './versionDescription';
+
+export class GoogleSheetsV1 implements INodeType {
+ description: INodeTypeDescription;
+
+ constructor(baseDescription: INodeTypeBaseDescription) {
+ this.description = {
+ ...baseDescription,
+ ...versionDescription,
+ };
+ }
+
+ methods = {
+ loadOptions: {
+ // Get all the sheets in a Spreadsheet
+ async getSheets(this: ILoadOptionsFunctions): Promise {
+ const spreadsheetId = this.getCurrentNodeParameter('sheetId') as string;
+
+ const sheet = new GoogleSheet(spreadsheetId, this);
+ const responseData = await sheet.spreadsheetGetSheets();
+
+ if (responseData === undefined) {
+ throw new NodeOperationError(this.getNode(), 'No data got returned');
+ }
+
+ const returnData: INodePropertyOptions[] = [];
+ for (const sheet of responseData.sheets!) {
+ if (sheet.properties!.sheetType !== 'GRID') {
+ continue;
+ }
+
+ returnData.push({
+ name: sheet.properties!.title as string,
+ value: sheet.properties!.sheetId as unknown as string,
+ });
+ }
+
+ return returnData;
+ },
+ },
+ credentialTest: {
+ async googleApiCredentialTest(
+ this: ICredentialTestFunctions,
+ credential: ICredentialsDecrypted,
+ ): Promise {
+ try {
+ const tokenRequest = await getAccessToken.call(
+ this,
+ credential.data! as unknown as IGoogleAuthCredentials,
+ );
+ if (!tokenRequest.access_token) {
+ return {
+ status: 'Error',
+ message: 'Could not generate a token from your private key.',
+ };
+ }
+ } catch (err) {
+ return {
+ status: 'Error',
+ message: `Private key validation failed: ${err.message}`,
+ };
+ }
+
+ return {
+ status: 'OK',
+ message: 'Connection successful!',
+ };
+ },
+ },
+ };
+
+ async execute(this: IExecuteFunctions): Promise {
+ const operation = this.getNodeParameter('operation', 0) as string;
+ const resource = this.getNodeParameter('resource', 0) as string;
+
+ if (resource === 'sheet') {
+ const spreadsheetId = this.getNodeParameter('sheetId', 0) as string;
+
+ const sheet = new GoogleSheet(spreadsheetId, this);
+
+ let range = '';
+ if (!['create', 'delete', 'remove'].includes(operation)) {
+ range = this.getNodeParameter('range', 0) as string;
+ }
+
+ const options = this.getNodeParameter('options', 0, {}) as IDataObject;
+
+ const valueInputMode = (options.valueInputMode || 'RAW') as ValueInputOption;
+ const valueRenderMode = (options.valueRenderMode || 'UNFORMATTED_VALUE') as ValueRenderOption;
+
+ if (operation === 'append') {
+ // ----------------------------------
+ // append
+ // ----------------------------------
+ try {
+ const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10);
+
+ const items = this.getInputData();
+
+ const setData: IDataObject[] = [];
+ items.forEach((item) => {
+ setData.push(item.json);
+ });
+
+ const usePathForKeyRow = (options.usePathForKeyRow || false) as boolean;
+
+ // Convert data into array format
+ const _data = await sheet.appendSheetData(
+ setData,
+ sheet.encodeRange(range),
+ keyRow,
+ valueInputMode,
+ usePathForKeyRow,
+ );
+
+ // TODO: Should add this data somewhere
+ // TODO: Should have something like add metadata which does not get passed through
+
+ return this.prepareOutputData(items);
+ } catch (error) {
+ if (this.continueOnFail()) {
+ return this.prepareOutputData([{ json: { error: error.message } }]);
+ }
+ throw error;
+ }
+ } else if (operation === 'clear') {
+ // ----------------------------------
+ // clear
+ // ----------------------------------
+ try {
+ await sheet.clearData(sheet.encodeRange(range));
+
+ const items = this.getInputData();
+ return this.prepareOutputData(items);
+ } catch (error) {
+ if (this.continueOnFail()) {
+ return this.prepareOutputData([{ json: { error: error.message } }]);
+ }
+ throw error;
+ }
+ } else if (operation === 'create') {
+ const returnData: IDataObject[] = [];
+
+ let responseData;
+ for (let i = 0; i < this.getInputData().length; i++) {
+ try {
+ const spreadsheetId = this.getNodeParameter('sheetId', i) as string;
+ const options = this.getNodeParameter('options', i, {}) as IDataObject;
+ const simple = this.getNodeParameter('simple', 0) as boolean;
+ const properties = { ...options };
+
+ if (options.tabColor) {
+ const { red, green, blue } = hexToRgb(options.tabColor as string)!;
+ properties.tabColor = { red: red / 255, green: green / 255, blue: blue / 255 };
+ }
+
+ const requests = [
+ {
+ addSheet: {
+ properties,
+ },
+ },
+ ];
+
+ responseData = await googleApiRequest.call(
+ this,
+ 'POST',
+ `/v4/spreadsheets/${spreadsheetId}:batchUpdate`,
+ { requests },
+ );
+
+ if (simple === true) {
+ Object.assign(responseData, responseData.replies[0].addSheet.properties);
+ delete responseData.replies;
+ }
+ returnData.push(responseData);
+ } catch (error) {
+ if (this.continueOnFail()) {
+ returnData.push({ error: error.message });
+ continue;
+ }
+ throw error;
+ }
+ }
+
+ return [this.helpers.returnJsonArray(returnData)];
+ } else if (operation === 'delete') {
+ // ----------------------------------
+ // delete
+ // ----------------------------------
+ try {
+ const requests: IDataObject[] = [];
+
+ const toDelete = this.getNodeParameter('toDelete', 0) as IToDelete;
+
+ const deletePropertyToDimensions: IDataObject = {
+ columns: 'COLUMNS',
+ rows: 'ROWS',
+ };
+
+ for (const propertyName of Object.keys(deletePropertyToDimensions)) {
+ if (toDelete[propertyName] !== undefined) {
+ toDelete[propertyName]!.forEach((range) => {
+ requests.push({
+ deleteDimension: {
+ range: {
+ sheetId: range.sheetId,
+ dimension: deletePropertyToDimensions[propertyName] as string,
+ startIndex: range.startIndex,
+ endIndex:
+ parseInt(range.startIndex.toString(), 10) +
+ parseInt(range.amount.toString(), 10),
+ },
+ },
+ });
+ });
+ }
+ }
+
+ const _data = await sheet.spreadsheetBatchUpdate(requests);
+
+ const items = this.getInputData();
+ return this.prepareOutputData(items);
+ } catch (error) {
+ if (this.continueOnFail()) {
+ return this.prepareOutputData([{ json: { error: error.message } }]);
+ }
+ throw error;
+ }
+ } else if (operation === 'lookup') {
+ // ----------------------------------
+ // lookup
+ // ----------------------------------
+ try {
+ const sheetData = await sheet.getData(sheet.encodeRange(range), valueRenderMode);
+
+ if (sheetData === undefined) {
+ return [];
+ }
+
+ const dataStartRow = parseInt(this.getNodeParameter('dataStartRow', 0) as string, 10);
+ const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10);
+
+ const items = this.getInputData();
+
+ const lookupValues: ILookupValues[] = [];
+ for (let i = 0; i < items.length; i++) {
+ lookupValues.push({
+ lookupColumn: this.getNodeParameter('lookupColumn', i) as string,
+ lookupValue: this.getNodeParameter('lookupValue', i) as string,
+ });
+ }
+
+ let returnData = await sheet.lookupValues(
+ sheetData,
+ keyRow,
+ dataStartRow,
+ lookupValues,
+ options.returnAllMatches as boolean | undefined,
+ );
+
+ if (returnData.length === 0 && options.continue && options.returnAllMatches) {
+ returnData = [{}];
+ } else if (
+ returnData.length === 1 &&
+ Object.keys(returnData[0]).length === 0 &&
+ !options.continue &&
+ !options.returnAllMatches
+ ) {
+ returnData = [];
+ }
+
+ return [this.helpers.returnJsonArray(returnData)];
+ } catch (error) {
+ if (this.continueOnFail()) {
+ return [this.helpers.returnJsonArray({ error: error.message })];
+ }
+ throw error;
+ }
+ } else if (operation === 'read') {
+ // ----------------------------------
+ // read
+ // ----------------------------------
+ try {
+ const rawData = this.getNodeParameter('rawData', 0) as boolean;
+
+ const sheetData = await sheet.getData(sheet.encodeRange(range), valueRenderMode);
+
+ let returnData: IDataObject[];
+ if (!sheetData) {
+ returnData = [];
+ } else if (rawData === true) {
+ const dataProperty = this.getNodeParameter('dataProperty', 0) as string;
+ returnData = [
+ {
+ [dataProperty]: sheetData,
+ },
+ ];
+ } else {
+ const dataStartRow = parseInt(this.getNodeParameter('dataStartRow', 0) as string, 10);
+ const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10);
+
+ returnData = sheet.structureArrayDataByColumn(sheetData, keyRow, dataStartRow);
+ }
+
+ if (returnData.length === 0 && options.continue) {
+ returnData = [{}];
+ }
+
+ return [this.helpers.returnJsonArray(returnData)];
+ } catch (error) {
+ if (this.continueOnFail()) {
+ return [this.helpers.returnJsonArray({ error: error.message })];
+ }
+ throw error;
+ }
+ } else if (operation === 'remove') {
+ const returnData: IDataObject[] = [];
+
+ let responseData;
+ for (let i = 0; i < this.getInputData().length; i++) {
+ try {
+ const sheetId = this.getNodeParameter('id', i) as string;
+ const spreadsheetId = this.getNodeParameter('sheetId', i) as string;
+
+ const requests = [
+ {
+ deleteSheet: {
+ sheetId,
+ },
+ },
+ ];
+
+ responseData = await googleApiRequest.call(
+ this,
+ 'POST',
+ `/v4/spreadsheets/${spreadsheetId}:batchUpdate`,
+ { requests },
+ );
+ delete responseData.replies;
+ returnData.push(responseData);
+ } catch (error) {
+ if (this.continueOnFail()) {
+ returnData.push({ error: error.message });
+ continue;
+ }
+ throw error;
+ }
+ }
+
+ return [this.helpers.returnJsonArray(returnData)];
+ } else if (operation === 'update' || operation === 'upsert') {
+ // ----------------------------------
+ // update/upsert
+ // ----------------------------------
+ const upsert = operation === 'upsert' ? true : false;
+ try {
+ const rawData = this.getNodeParameter('rawData', 0) as boolean;
+
+ const items = this.getInputData();
+
+ if (rawData === true) {
+ const dataProperty = this.getNodeParameter('dataProperty', 0) as string;
+
+ const updateData: ISheetUpdateData[] = [];
+ for (let i = 0; i < items.length; i++) {
+ updateData.push({
+ range,
+ values: items[i].json[dataProperty] as string[][],
+ });
+ }
+
+ const _data = await sheet.batchUpdate(updateData, valueInputMode);
+ } else {
+ const keyName = this.getNodeParameter('key', 0) as string;
+ const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10);
+ const dataStartRow = parseInt(this.getNodeParameter('dataStartRow', 0) as string, 10);
+
+ const setData: IDataObject[] = [];
+ items.forEach((item) => {
+ setData.push(item.json);
+ });
+
+ const _data = await sheet.updateSheetData(
+ setData,
+ keyName,
+ range,
+ keyRow,
+ dataStartRow,
+ valueInputMode,
+ valueRenderMode,
+ upsert,
+ );
+ }
+ // TODO: Should add this data somewhere
+ // TODO: Should have something like add metadata which does not get passed through
+
+ return this.prepareOutputData(items);
+ } catch (error) {
+ if (this.continueOnFail()) {
+ return this.prepareOutputData([{ json: { error: error.message } }]);
+ }
+ throw error;
+ }
+ }
+ }
+
+ if (resource === 'spreadsheet') {
+ const returnData: IDataObject[] = [];
+
+ let responseData;
+
+ if (operation === 'create') {
+ // ----------------------------------
+ // create
+ // ----------------------------------
+ // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/create
+
+ for (let i = 0; i < this.getInputData().length; i++) {
+ try {
+ const title = this.getNodeParameter('title', i) as string;
+ const sheetsUi = this.getNodeParameter('sheetsUi', i, {}) as IDataObject;
+
+ const body = {
+ properties: {
+ title,
+ autoRecalc: undefined as undefined | string,
+ locale: undefined as undefined | string,
+ },
+ sheets: [] as IDataObject[],
+ };
+
+ const options = this.getNodeParameter('options', i, {}) as IDataObject;
+
+ if (Object.keys(sheetsUi).length) {
+ const data = [];
+ const sheets = sheetsUi.sheetValues as IDataObject[];
+ for (const sheet of sheets) {
+ const properties = sheet.propertiesUi as IDataObject;
+ if (properties) {
+ data.push({ properties });
+ }
+ }
+ body.sheets = data;
+ }
+
+ body.properties!.autoRecalc = options.autoRecalc
+ ? (options.autoRecalc as string)
+ : undefined;
+ body.properties!.locale = options.locale ? (options.locale as string) : undefined;
+
+ responseData = await googleApiRequest.call(this, 'POST', `/v4/spreadsheets`, body);
+
+ returnData.push(responseData);
+ } catch (error) {
+ if (this.continueOnFail()) {
+ returnData.push({ error: error.message });
+ continue;
+ }
+ throw error;
+ }
+ }
+ }
+
+ return [this.helpers.returnJsonArray(returnData)];
+ }
+
+ return [];
+ }
+}
diff --git a/packages/nodes-base/nodes/Google/Sheet/v1/versionDescription.ts b/packages/nodes-base/nodes/Google/Sheet/v1/versionDescription.ts
new file mode 100644
index 0000000000000..f634d96f1af6a
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Sheet/v1/versionDescription.ts
@@ -0,0 +1,880 @@
+/* eslint-disable n8n-nodes-base/node-filename-against-convention */
+import { INodeTypeDescription } from 'n8n-workflow';
+
+export const versionDescription: INodeTypeDescription = {
+ displayName: 'Google Sheets ',
+ name: 'googleSheets',
+ icon: 'file:googleSheets.svg',
+ group: ['input', 'output'],
+ version: 1,
+ subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
+ description: 'Read, update and write data to Google Sheets',
+ defaults: {
+ name: 'Google Sheets',
+ },
+ inputs: ['main'],
+ outputs: ['main'],
+ credentials: [
+ {
+ name: 'googleApi',
+ required: true,
+ displayOptions: {
+ show: {
+ authentication: ['serviceAccount'],
+ },
+ },
+ testedBy: 'googleApiCredentialTest',
+ },
+ {
+ name: 'googleSheetsOAuth2Api',
+ required: true,
+ displayOptions: {
+ show: {
+ authentication: ['oAuth2'],
+ },
+ },
+ },
+ ],
+ properties: [
+ {
+ displayName: 'Authentication',
+ name: 'authentication',
+ type: 'options',
+ options: [
+ {
+ name: 'Service Account',
+ value: 'serviceAccount',
+ },
+ {
+ name: 'OAuth2',
+ value: 'oAuth2',
+ },
+ ],
+ default: 'serviceAccount',
+ },
+ {
+ displayName: 'Resource',
+ name: 'resource',
+ type: 'options',
+ noDataExpression: true,
+ options: [
+ {
+ name: 'Spreadsheet',
+ value: 'spreadsheet',
+ },
+ {
+ name: 'Sheet',
+ value: 'sheet',
+ },
+ ],
+ default: 'sheet',
+ },
+ {
+ displayName: 'Operation',
+ name: 'operation',
+ type: 'options',
+ noDataExpression: true,
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ },
+ },
+ options: [
+ {
+ name: 'Append',
+ value: 'append',
+ description: 'Append data to a sheet',
+ action: 'Append data to a sheet',
+ },
+ {
+ name: 'Clear',
+ value: 'clear',
+ description: 'Clear data from a sheet',
+ action: 'Clear a sheet',
+ },
+ {
+ name: 'Create',
+ value: 'create',
+ description: 'Create a new sheet',
+ action: 'Create a sheet',
+ },
+ {
+ name: 'Create or Update',
+ value: 'upsert',
+ description:
+ 'Create a new record, or update the current one if it already exists (upsert)',
+ action: 'Create or update a sheet',
+ },
+ {
+ name: 'Delete',
+ value: 'delete',
+ description: 'Delete columns and rows from a sheet',
+ action: 'Delete a sheet',
+ },
+ {
+ name: 'Lookup',
+ value: 'lookup',
+ description: 'Look up a specific column value and return the matching row',
+ action: 'Look up a column value in a sheet',
+ },
+ {
+ name: 'Read',
+ value: 'read',
+ description: 'Read data from a sheet',
+ action: 'Read a sheet',
+ },
+ {
+ name: 'Remove',
+ value: 'remove',
+ description: 'Remove a sheet',
+ action: 'Remove a sheet',
+ },
+ {
+ name: 'Update',
+ value: 'update',
+ description: 'Update rows in a sheet',
+ action: 'Update a sheet',
+ },
+ ],
+ default: 'read',
+ },
+
+ // ----------------------------------
+ // All
+ // ----------------------------------
+ {
+ displayName: 'Spreadsheet ID',
+ name: 'sheetId',
+ type: 'string',
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ },
+ },
+ default: '',
+ required: true,
+ description:
+ 'The ID of the Google Spreadsheet. Found as part of the sheet URL https://docs.google.com/spreadsheets/d/{ID}/.',
+ },
+ {
+ displayName: 'Range',
+ name: 'range',
+ type: 'string',
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ },
+ hide: {
+ operation: ['create', 'delete', 'remove'],
+ },
+ },
+ default: 'A:F',
+ required: true,
+ description:
+ 'The table range to read from or to append data to. See the Google documentation for the details. If it contains multiple sheets it can also be added like this: "MySheet!A:F"',
+ },
+
+ // ----------------------------------
+ // Delete
+ // ----------------------------------
+ {
+ displayName: 'To Delete',
+ name: 'toDelete',
+ placeholder: 'Add Columns/Rows to delete',
+ description: 'Deletes columns and rows from a sheet',
+ type: 'fixedCollection',
+ typeOptions: {
+ multipleValues: true,
+ },
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['delete'],
+ },
+ },
+ default: {},
+ options: [
+ {
+ displayName: 'Columns',
+ name: 'columns',
+ values: [
+ {
+ displayName: 'Sheet Name or ID',
+ name: 'sheetId',
+ type: 'options',
+ typeOptions: {
+ loadOptionsMethod: 'getSheets',
+ },
+ options: [],
+ default: '',
+ required: true,
+ description:
+ 'Choose from the list, or specify an ID using an expression',
+ },
+ {
+ displayName: 'Start Index',
+ name: 'startIndex',
+ type: 'number',
+ typeOptions: {
+ minValue: 0,
+ },
+ default: 0,
+ description: 'The start index (0 based and inclusive) of column to delete',
+ },
+ {
+ displayName: 'Amount',
+ name: 'amount',
+ type: 'number',
+ typeOptions: {
+ minValue: 1,
+ },
+ default: 1,
+ description: 'Number of columns to delete',
+ },
+ ],
+ },
+ {
+ displayName: 'Rows',
+ name: 'rows',
+ values: [
+ {
+ displayName: 'Sheet Name or ID',
+ name: 'sheetId',
+ type: 'options',
+ typeOptions: {
+ loadOptionsMethod: 'getSheets',
+ },
+ options: [],
+ default: '',
+ required: true,
+ description:
+ 'Choose from the list, or specify an ID using an expression',
+ },
+ {
+ displayName: 'Start Index',
+ name: 'startIndex',
+ type: 'number',
+ typeOptions: {
+ minValue: 0,
+ },
+ default: 0,
+ description: 'The start index (0 based and inclusive) of row to delete',
+ },
+ {
+ displayName: 'Amount',
+ name: 'amount',
+ type: 'number',
+ typeOptions: {
+ minValue: 1,
+ },
+ default: 1,
+ description: 'Number of rows to delete',
+ },
+ ],
+ },
+ ],
+ },
+
+ // ----------------------------------
+ // Read
+ // ----------------------------------
+ {
+ displayName: 'RAW Data',
+ name: 'rawData',
+ type: 'boolean',
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['read'],
+ },
+ },
+ default: false,
+ description:
+ 'Whether the data should be returned RAW instead of parsed into keys according to their header',
+ },
+ {
+ displayName: 'Data Property',
+ name: 'dataProperty',
+ type: 'string',
+ default: 'data',
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['read'],
+ rawData: [true],
+ },
+ },
+ description: 'The name of the property into which to write the RAW data',
+ },
+
+ // ----------------------------------
+ // Update
+ // ----------------------------------
+ {
+ displayName: 'RAW Data',
+ name: 'rawData',
+ type: 'boolean',
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['update', 'upsert'],
+ },
+ },
+ default: false,
+ description: 'Whether the data supplied is RAW instead of parsed into keys',
+ },
+ {
+ displayName: 'Data Property',
+ name: 'dataProperty',
+ type: 'string',
+ default: 'data',
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['update', 'upsert'],
+ rawData: [true],
+ },
+ },
+ description: 'The name of the property from which to read the RAW data',
+ },
+
+ // ----------------------------------
+ // Read & Update & lookupColumn
+ // ----------------------------------
+ {
+ displayName: 'Data Start Row',
+ name: 'dataStartRow',
+ type: 'number',
+ typeOptions: {
+ minValue: 1,
+ },
+ default: 1,
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ },
+ hide: {
+ operation: ['append', 'create', 'clear', 'delete', 'remove'],
+ rawData: [true],
+ },
+ },
+ description:
+ 'Index of the first row which contains the actual data and not the keys. Starts with 0.',
+ },
+
+ // ----------------------------------
+ // Mixed
+ // ----------------------------------
+ {
+ displayName: 'Key Row',
+ name: 'keyRow',
+ type: 'number',
+ typeOptions: {
+ minValue: 0,
+ },
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ },
+ hide: {
+ operation: ['clear', 'create', 'delete', 'remove'],
+ rawData: [true],
+ },
+ },
+ default: 0,
+ description:
+ 'Index of the row which contains the keys. Starts at 0. The incoming node data is matched to the keys for assignment. The matching is case sensitive.',
+ },
+
+ // ----------------------------------
+ // lookup
+ // ----------------------------------
+ {
+ displayName: 'Lookup Column',
+ name: 'lookupColumn',
+ type: 'string',
+ default: '',
+ placeholder: 'Email',
+ required: true,
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['lookup'],
+ },
+ },
+ description: 'The name of the column in which to look for value',
+ },
+ {
+ displayName: 'Lookup Value',
+ name: 'lookupValue',
+ type: 'string',
+ default: '',
+ placeholder: 'frank@example.com',
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['lookup'],
+ },
+ },
+ description: 'The value to look for in column',
+ },
+
+ // ----------------------------------
+ // Update
+ // ----------------------------------
+ {
+ displayName: 'Key',
+ name: 'key',
+ type: 'string',
+ default: 'id',
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['update', 'upsert'],
+ rawData: [false],
+ },
+ },
+ description: 'The name of the key to identify which data should be updated in the sheet',
+ },
+
+ {
+ displayName: 'Options',
+ name: 'options',
+ type: 'collection',
+ placeholder: 'Add Option',
+ default: {},
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['append', 'lookup', 'read', 'update', 'upsert'],
+ },
+ },
+ options: [
+ {
+ displayName: 'Continue If Empty',
+ name: 'continue',
+ type: 'boolean',
+ default: false,
+ displayOptions: {
+ show: {
+ '/operation': ['lookup', 'read'],
+ },
+ },
+ // eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
+ description:
+ 'By default, the workflow stops executing if the lookup/read does not return values',
+ },
+ {
+ displayName: 'Return All Matches',
+ name: 'returnAllMatches',
+ type: 'boolean',
+ default: false,
+ displayOptions: {
+ show: {
+ '/operation': ['lookup'],
+ },
+ },
+ // eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
+ description:
+ 'By default only the first result gets returned. If options gets set all found matches get returned.',
+ },
+ {
+ displayName: 'Use Header Names as JSON Paths',
+ name: 'usePathForKeyRow',
+ type: 'boolean',
+ default: false,
+ displayOptions: {
+ show: {
+ '/operation': ['append'],
+ },
+ },
+ description:
+ 'Whether you want to match the headers as path, for example, the row header "category.name" will match the "category" object and get the field "name" from it. By default "category.name" will match with the field with exact name, not nested object.',
+ },
+ {
+ displayName: 'Value Input Mode',
+ name: 'valueInputMode',
+ type: 'options',
+ displayOptions: {
+ show: {
+ '/operation': ['append', 'update', 'upsert'],
+ },
+ },
+ options: [
+ {
+ name: 'RAW',
+ value: 'RAW',
+ description: 'The values will not be parsed and will be stored as-is',
+ },
+ {
+ name: 'User Entered',
+ value: 'USER_ENTERED',
+ description:
+ 'The values will be parsed as if the user typed them into the UI. Numbers will stay as numbers, but strings may be converted to numbers, dates, etc. following the same rules that are applied when entering text into a cell via the Google Sheets UI.',
+ },
+ ],
+ default: 'RAW',
+ description: 'Determines how data should be interpreted',
+ },
+ {
+ displayName: 'Value Render Mode',
+ name: 'valueRenderMode',
+ type: 'options',
+ displayOptions: {
+ show: {
+ '/operation': ['lookup', 'read'],
+ },
+ },
+ options: [
+ {
+ name: 'Formatted Value',
+ value: 'FORMATTED_VALUE',
+ description:
+ "Values will be calculated & formatted in the reply according to the cell's formatting.Formatting is based on the spreadsheet's locale, not the requesting user's locale.For example, if A1 is 1.23 and A2 is =A1 and formatted as currency, then A2 would return \"$1.23\"",
+ },
+ {
+ name: 'Formula',
+ value: 'FORMULA',
+ description:
+ 'Values will not be calculated. The reply will include the formulas. For example, if A1 is 1.23 and A2 is =A1 and formatted as currency, then A2 would return "=A1".',
+ },
+ {
+ name: 'Unformatted Value',
+ value: 'UNFORMATTED_VALUE',
+ description:
+ 'Values will be calculated, but not formatted in the reply. For example, if A1 is 1.23 and A2 is =A1 and formatted as currency, then A2 would return the number 1.23.',
+ },
+ ],
+ default: 'UNFORMATTED_VALUE',
+ description: 'Determines how values should be rendered in the output',
+ },
+ {
+ displayName: 'Value Render Mode',
+ name: 'valueRenderMode',
+ type: 'options',
+ displayOptions: {
+ show: {
+ '/operation': ['update', 'upsert'],
+ '/rawData': [false],
+ },
+ },
+ options: [
+ {
+ name: 'Formatted Value',
+ value: 'FORMATTED_VALUE',
+ description:
+ "Values will be calculated & formatted in the reply according to the cell's formatting.Formatting is based on the spreadsheet's locale, not the requesting user's locale. For example, if A1 is 1.23 and A2 is =A1 and formatted as currency, then A2 would return \"$1.23\".",
+ },
+ {
+ name: 'Formula',
+ value: 'FORMULA',
+ description:
+ 'Values will not be calculated. The reply will include the formulas. For example, if A1 is 1.23 and A2 is =A1 and formatted as currency, then A2 would return "=A1".',
+ },
+ {
+ name: 'Unformatted Value',
+ value: 'UNFORMATTED_VALUE',
+ description:
+ 'Values will be calculated, but not formatted in the reply. For example, if A1 is 1.23 and A2 is =A1 and formatted as currency, then A2 would return the number 1.23.',
+ },
+ ],
+ default: 'UNFORMATTED_VALUE',
+ description: 'Determines how values should be rendered in the output',
+ },
+ ],
+ },
+
+ {
+ displayName: 'Operation',
+ name: 'operation',
+ type: 'options',
+ noDataExpression: true,
+ displayOptions: {
+ show: {
+ resource: ['spreadsheet'],
+ },
+ },
+ options: [
+ {
+ name: 'Create',
+ value: 'create',
+ description: 'Create a spreadsheet',
+ action: 'Create a spreadsheet',
+ },
+ ],
+ default: 'create',
+ },
+ // ----------------------------------
+ // spreadsheet:create
+ // ----------------------------------
+ {
+ displayName: 'Title',
+ name: 'title',
+ type: 'string',
+ default: '',
+ displayOptions: {
+ show: {
+ resource: ['spreadsheet'],
+ operation: ['create'],
+ },
+ },
+ description: 'The title of the spreadsheet',
+ },
+ {
+ displayName: 'Sheets',
+ name: 'sheetsUi',
+ placeholder: 'Add Sheet',
+ type: 'fixedCollection',
+ typeOptions: {
+ multipleValues: true,
+ },
+ default: {},
+ displayOptions: {
+ show: {
+ resource: ['spreadsheet'],
+ operation: ['create'],
+ },
+ },
+ options: [
+ {
+ name: 'sheetValues',
+ displayName: 'Sheet',
+ values: [
+ {
+ displayName: 'Sheet Properties',
+ name: 'propertiesUi',
+ placeholder: 'Add Property',
+ type: 'collection',
+ default: {},
+ options: [
+ {
+ displayName: 'Hidden',
+ name: 'hidden',
+ type: 'boolean',
+ default: false,
+ description: 'Whether the Sheet should be hidden in the UI',
+ },
+ {
+ displayName: 'Title',
+ name: 'title',
+ type: 'string',
+ default: '',
+ description: 'Title of the property to create',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ displayName: 'Options',
+ name: 'options',
+ type: 'collection',
+ placeholder: 'Add Option',
+ default: {},
+ displayOptions: {
+ show: {
+ resource: ['spreadsheet'],
+ operation: ['create'],
+ },
+ },
+ options: [
+ {
+ displayName: 'Locale',
+ name: 'locale',
+ type: 'string',
+ default: '',
+ placeholder: 'en_US',
+ description: `The locale of the spreadsheet in one of the following formats:
+
+ - en (639-1)
+ - fil (639-2 if no 639-1 format exists)
+ - en_US (combination of ISO language an country)
+ `,
+ },
+ {
+ displayName: 'Recalculation Interval',
+ name: 'autoRecalc',
+ type: 'options',
+ options: [
+ {
+ name: 'Default',
+ value: '',
+ description: 'Default value',
+ },
+ {
+ name: 'On Change',
+ value: 'ON_CHANGE',
+ description: 'Volatile functions are updated on every change',
+ },
+ {
+ name: 'Minute',
+ value: 'MINUTE',
+ description: 'Volatile functions are updated on every change and every minute',
+ },
+ {
+ name: 'Hour',
+ value: 'HOUR',
+ description: 'Volatile functions are updated on every change and hourly',
+ },
+ ],
+ default: '',
+ description: 'Cell recalculation interval options',
+ },
+ ],
+ },
+
+ // ----------------------------------
+ // sheet:create
+ // ----------------------------------
+ {
+ displayName: 'Simplify',
+ name: 'simple',
+ type: 'boolean',
+ default: true,
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['create'],
+ },
+ },
+ description: 'Whether to return a simplified version of the response instead of the raw data',
+ },
+ {
+ displayName: 'Options',
+ name: 'options',
+ type: 'collection',
+ placeholder: 'Add Option',
+ default: {},
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['create'],
+ },
+ },
+ options: [
+ {
+ displayName: 'Grid Properties',
+ name: 'gridProperties',
+ type: 'collection',
+ placeholder: 'Add Property',
+ default: {},
+ options: [
+ {
+ displayName: 'Column Count',
+ name: 'columnCount',
+ type: 'number',
+ default: 0,
+ description: 'The number of columns in the grid',
+ },
+ {
+ displayName: 'Column Group Control After',
+ name: 'columnGroupControlAfter',
+ type: 'boolean',
+ default: false,
+ description: 'Whether the column grouping control toggle is shown after the group',
+ },
+ {
+ displayName: 'Frozen Column Count',
+ name: 'frozenColumnCount',
+ type: 'number',
+ default: 0,
+ description: 'The number of columns that are frozen in the grid',
+ },
+ {
+ displayName: 'Frozen Row Count',
+ name: 'frozenRowCount',
+ type: 'number',
+ default: 0,
+ description: 'The number of rows that are frozen in the grid',
+ },
+ {
+ displayName: 'Hide Gridlines',
+ name: 'hideGridlines',
+ type: 'boolean',
+ default: false,
+ description: "Whether the grid isn't showing gridlines in the UI",
+ },
+ {
+ displayName: 'Row Count',
+ name: 'rowCount',
+ type: 'number',
+ default: 0,
+ description: 'The number of rows in the grid',
+ },
+ {
+ displayName: 'Row Group Control After',
+ name: 'rowGroupControlAfter',
+ type: 'boolean',
+ default: false,
+ description: 'Whether the row grouping control toggle is shown after the group',
+ },
+ ],
+ description: 'The type of the sheet',
+ },
+ {
+ displayName: 'Hidden',
+ name: 'hidden',
+ type: 'boolean',
+ default: false,
+ description: "Whether the sheet is hidden in the UI, false if it's visible",
+ },
+ {
+ displayName: 'Right To Left',
+ name: 'rightToLeft',
+ type: 'boolean',
+ default: false,
+ description: 'Whether the sheet is an RTL sheet instead of an LTR sheet',
+ },
+ {
+ displayName: 'Sheet ID',
+ name: 'sheetId',
+ type: 'number',
+ default: 0,
+ description:
+ 'The ID of the sheet. Must be non-negative. This field cannot be changed once set.',
+ },
+ {
+ displayName: 'Sheet Index',
+ name: 'index',
+ type: 'number',
+ default: 0,
+ description: 'The index of the sheet within the spreadsheet',
+ },
+ {
+ displayName: 'Tab Color',
+ name: 'tabColor',
+ type: 'color',
+ default: '0aa55c',
+ description: 'The color of the tab in the UI',
+ },
+ {
+ displayName: 'Title',
+ name: 'title',
+ type: 'string',
+ default: '',
+ description: 'The Sheet name',
+ },
+ ],
+ },
+
+ // ----------------------------------
+ // sheet:remove
+ // ----------------------------------
+ {
+ displayName: 'Sheet ID',
+ name: 'id',
+ type: 'string',
+ default: '',
+ required: true,
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['remove'],
+ },
+ },
+ description: 'The ID of the sheet to delete',
+ },
+ ],
+};
diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/GoogleSheetsV2.node.ts b/packages/nodes-base/nodes/Google/Sheet/v2/GoogleSheetsV2.node.ts
new file mode 100644
index 0000000000000..89289d48efa54
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Sheet/v2/GoogleSheetsV2.node.ts
@@ -0,0 +1,25 @@
+import { IExecuteFunctions } from 'n8n-core';
+import { INodeType, INodeTypeBaseDescription, INodeTypeDescription } from 'n8n-workflow';
+import { versionDescription } from './actions/versionDescription';
+import { credentialTest, listSearch, loadOptions } from './methods';
+import { router } from './actions/router';
+
+export class GoogleSheetsV2 implements INodeType {
+ description: INodeTypeDescription;
+
+ constructor(baseDescription: INodeTypeBaseDescription) {
+ this.description = {
+ ...baseDescription,
+ ...versionDescription,
+ };
+ }
+ methods = {
+ loadOptions,
+ credentialTest,
+ listSearch,
+ };
+
+ async execute(this: IExecuteFunctions) {
+ return await router.call(this);
+ }
+}
diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/actions/router.ts b/packages/nodes-base/nodes/Google/Sheet/v2/actions/router.ts
new file mode 100644
index 0000000000000..3066f383bcab4
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/router.ts
@@ -0,0 +1,68 @@
+import { IExecuteFunctions } from 'n8n-core';
+import { IDataObject, INodeExecutionData } from 'n8n-workflow';
+import * as sheet from './sheet/Sheet.resource';
+import * as spreadsheet from './spreadsheet/SpreadSheet.resource';
+import { GoogleSheet } from '../helpers/GoogleSheet';
+import { getSpreadsheetId } from '../helpers/GoogleSheets.utils';
+import { GoogleSheets, ResourceLocator } from '../helpers/GoogleSheets.types';
+
+export async function router(this: IExecuteFunctions): Promise {
+ const operationResult: INodeExecutionData[] = [];
+
+ try {
+ const resource = this.getNodeParameter('resource', 0);
+ const operation = this.getNodeParameter('operation', 0);
+
+ const googleSheets = {
+ resource,
+ operation,
+ } as GoogleSheets;
+
+ if (googleSheets.resource === 'sheet') {
+ const { mode, value } = this.getNodeParameter('documentId', 0) as IDataObject;
+ const spreadsheetId = getSpreadsheetId(mode as ResourceLocator, value as string);
+
+ const googleSheet = new GoogleSheet(spreadsheetId, this);
+
+ let sheetWithinDocument = '';
+ if (operation !== 'create') {
+ sheetWithinDocument = this.getNodeParameter('sheetName', 0, undefined, {
+ extractValue: true,
+ }) as string;
+ }
+
+ if (sheetWithinDocument === 'gid=0') {
+ sheetWithinDocument = '0';
+ }
+
+ let sheetName = '';
+ switch (operation) {
+ case 'create':
+ sheetName = spreadsheetId;
+ break;
+ case 'delete':
+ sheetName = sheetWithinDocument;
+ break;
+ case 'remove':
+ sheetName = `${spreadsheetId}||${sheetWithinDocument}`;
+ break;
+ default:
+ sheetName = await googleSheet.spreadsheetGetSheetNameById(sheetWithinDocument);
+ }
+
+ operationResult.push(
+ ...(await sheet[googleSheets.operation].execute.call(this, googleSheet, sheetName)),
+ );
+ } else if (googleSheets.resource === 'spreadsheet') {
+ operationResult.push(...(await spreadsheet[googleSheets.operation].execute.call(this)));
+ }
+ } catch (err) {
+ if (this.continueOnFail()) {
+ operationResult.push({ json: this.getInputData(0)[0].json, error: err });
+ } else {
+ throw err;
+ }
+ }
+
+ return [operationResult];
+}
diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/Sheet.resource.ts b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/Sheet.resource.ts
new file mode 100644
index 0000000000000..bc486d8dc6089
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/Sheet.resource.ts
@@ -0,0 +1,199 @@
+import { INodeProperties } from 'n8n-workflow';
+import * as append from './append.operation';
+import * as appendOrUpdate from './appendOrUpdate.operation';
+import * as clear from './clear.operation';
+import * as create from './create.operation';
+import * as del from './delete.operation';
+import * as read from './read.operation';
+import * as remove from './remove.operation';
+import * as update from './update.operation';
+
+export { append, appendOrUpdate, clear, create, del as delete, read, remove, update };
+
+export const descriptions: INodeProperties[] = [
+ {
+ displayName: 'Operation',
+ name: 'operation',
+ type: 'options',
+ noDataExpression: true,
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ },
+ },
+ options: [
+ {
+ name: 'Append',
+ value: 'append',
+ description: 'Append data to a sheet',
+ action: 'Append data to a sheet',
+ },
+ {
+ // eslint-disable-next-line n8n-nodes-base/node-param-option-name-wrong-for-upsert
+ name: 'Append or Update',
+ value: 'appendOrUpdate',
+ description: 'Append a new row or update the current one if it already exists (upsert)',
+ action: 'Append or update a sheet',
+ },
+ {
+ name: 'Clear',
+ value: 'clear',
+ description: 'Clear data from a sheet',
+ action: 'Clear a sheet',
+ },
+ {
+ name: 'Create',
+ value: 'create',
+ description: 'Create a new sheet',
+ action: 'Create a sheet',
+ },
+ {
+ name: 'Delete',
+ value: 'delete',
+ description: 'Delete columns and rows from a sheet',
+ action: 'Delete a sheet',
+ },
+ {
+ name: 'Read Rows',
+ value: 'read',
+ description: 'Read all rows in a sheet',
+ action: 'Read all rows',
+ },
+ {
+ name: 'Remove',
+ value: 'remove',
+ description: 'Remove a sheet',
+ action: 'Remove a sheet',
+ },
+ {
+ name: 'Update',
+ value: 'update',
+ description: 'Update rows in a sheet',
+ action: 'Update a sheet',
+ },
+ ],
+ default: 'read',
+ },
+ {
+ displayName: 'Document',
+ name: 'documentId',
+ type: 'resourceLocator',
+ default: { mode: 'list', value: '' },
+ required: true,
+ modes: [
+ {
+ displayName: 'From List',
+ name: 'list',
+ type: 'list',
+ typeOptions: {
+ searchListMethod: 'spreadSheetsSearch',
+ searchable: true,
+ },
+ },
+ {
+ displayName: 'By URL',
+ name: 'url',
+ type: 'string',
+ extractValue: {
+ type: 'regex',
+ regex:
+ 'https:\\/\\/(?:drive|docs)\\.google\\.com\\/\\w+\\/d\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)',
+ },
+ validation: [
+ {
+ type: 'regex',
+ properties: {
+ regex:
+ 'https:\\/\\/(?:drive|docs)\\.google.com\\/\\w+\\/d\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)',
+ errorMessage: 'Not a valid Google Drive File URL',
+ },
+ },
+ ],
+ },
+ {
+ displayName: 'By ID',
+ name: 'id',
+ type: 'string',
+ validation: [
+ {
+ type: 'regex',
+ properties: {
+ regex: '[a-zA-Z0-9\\-_]{2,}',
+ errorMessage: 'Not a valid Google Drive File ID',
+ },
+ },
+ ],
+ url: '=https://docs.google.com/spreadsheets/d/{{$value}}/edit',
+ },
+ ],
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ },
+ },
+ },
+ {
+ displayName: 'Sheet',
+ name: 'sheetName',
+ type: 'resourceLocator',
+ default: { mode: 'list', value: '' },
+ // default: '', //empty string set to progresivly reveal fields
+ required: true,
+ modes: [
+ {
+ displayName: 'From List',
+ name: 'list',
+ type: 'list',
+ typeOptions: {
+ searchListMethod: 'sheetsSearch',
+ searchable: false,
+ },
+ },
+ {
+ displayName: 'By URL',
+ name: 'url',
+ type: 'string',
+ extractValue: {
+ type: 'regex',
+ regex: `https:\\/\\/docs\\.google\\.com\/spreadsheets\\/d\\/[0-9a-zA-Z\\-_]+\\/edit\\#gid=([0-9]+)`,
+ },
+ validation: [
+ {
+ type: 'regex',
+ properties: {
+ regex: `https:\\/\\/docs\\.google\\.com\/spreadsheets\\/d\\/[0-9a-zA-Z\\-_]+\\/edit\\#gid=([0-9]+)`,
+ errorMessage: 'Not a valid Sheet URL',
+ },
+ },
+ ],
+ },
+ {
+ displayName: 'By ID',
+ name: 'id',
+ type: 'string',
+ validation: [
+ {
+ type: 'regex',
+ properties: {
+ regex: '[0-9]{2,}',
+ errorMessage: 'Not a valid Sheet ID',
+ },
+ },
+ ],
+ },
+ ],
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['append', 'appendOrUpdate', 'clear', 'delete', 'read', 'remove', 'update'],
+ },
+ },
+ },
+ ...append.description,
+ ...clear.description,
+ ...create.description,
+ ...del.description,
+ ...read.description,
+ ...update.description,
+ ...appendOrUpdate.description,
+];
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
new file mode 100644
index 0000000000000..8a851fd5fb962
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/append.operation.ts
@@ -0,0 +1,188 @@
+import { IExecuteFunctions } from 'n8n-core';
+import { SheetProperties, ValueInputOption } from '../../helpers/GoogleSheets.types';
+import { IDataObject, INodeExecutionData } from 'n8n-workflow';
+import { GoogleSheet } from '../../helpers/GoogleSheet';
+import { autoMapInputData, mapFields, untilSheetSelected } from '../../helpers/GoogleSheets.utils';
+import { cellFormat, handlingExtraData } from './commonDescription';
+
+export const description: SheetProperties = [
+ {
+ displayName: 'Data Mode',
+ name: 'dataMode',
+ type: 'options',
+ options: [
+ {
+ name: 'Auto-Map Input Data to Columns',
+ value: 'autoMapInputData',
+ description: 'Use when node input properties match destination column names',
+ },
+ {
+ name: 'Map Each Column Below',
+ value: 'defineBelow',
+ description: 'Set the value for each destination column',
+ },
+ {
+ name: 'Nothing',
+ value: 'nothing',
+ description: 'Do not send anything',
+ },
+ ],
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['append'],
+ },
+ hide: {
+ ...untilSheetSelected,
+ },
+ },
+ default: 'defineBelow',
+ description: 'Whether to insert the input data this node receives in the new row',
+ },
+ {
+ displayName:
+ "In this mode, make sure the incoming data is named the same as the columns in your Sheet. (Use a 'set' node before this node to change it if required.)",
+ name: 'autoMapNotice',
+ type: 'notice',
+ default: '',
+ displayOptions: {
+ show: {
+ operation: ['append'],
+ dataMode: ['autoMapInputData'],
+ },
+ hide: {
+ ...untilSheetSelected,
+ },
+ },
+ },
+ {
+ displayName: 'Fields to Send',
+ name: 'fieldsUi',
+ placeholder: 'Add Field',
+ type: 'fixedCollection',
+ typeOptions: {
+ multipleValueButtonText: 'Add Field to Send',
+ multipleValues: true,
+ },
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['append'],
+ dataMode: ['defineBelow'],
+ },
+ hide: {
+ ...untilSheetSelected,
+ },
+ },
+ default: {},
+ options: [
+ {
+ displayName: 'Field',
+ name: 'fieldValues',
+ values: [
+ {
+ displayName: 'Field Name or ID',
+ name: 'fieldId',
+ type: 'options',
+ description:
+ 'Choose from the list, or specify an ID using an expression',
+ typeOptions: {
+ loadOptionsDependsOn: ['sheetName.value'],
+ loadOptionsMethod: 'getSheetHeaderRowAndSkipEmpty',
+ },
+ default: '',
+ },
+ {
+ displayName: 'Field Value',
+ name: 'fieldValue',
+ type: 'string',
+ default: '',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ displayName: 'Options',
+ name: 'options',
+ type: 'collection',
+ placeholder: 'Add Option',
+ default: {},
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['append'],
+ },
+ hide: {
+ ...untilSheetSelected,
+ },
+ },
+ options: [
+ ...cellFormat,
+ {
+ displayName: 'Data Location on Sheet',
+ name: 'locationDefine',
+ type: 'fixedCollection',
+ placeholder: 'Select Range',
+ default: { values: {} },
+ options: [
+ {
+ displayName: 'Values',
+ name: 'values',
+ values: [
+ {
+ displayName: 'Header Row',
+ name: 'headerRow',
+ type: 'number',
+ typeOptions: {
+ minValue: 1,
+ },
+ default: 1,
+ description:
+ 'Index of the row which contains the keys. Starts at 1. The incoming node data is matched to the keys for assignment. The matching is case sensitive.',
+ },
+ ],
+ },
+ ],
+ },
+ ...handlingExtraData,
+ ],
+ },
+];
+
+export async function execute(
+ this: IExecuteFunctions,
+ sheet: GoogleSheet,
+ sheetName: string,
+): Promise {
+ const items = this.getInputData();
+ const dataMode = this.getNodeParameter('dataMode', 0) as string;
+
+ if (!items.length || dataMode === 'nothing') return [];
+
+ const options = this.getNodeParameter('options', 0, {}) as IDataObject;
+ const locationDefine = ((options.locationDefine as IDataObject) || {}).values as IDataObject;
+
+ let headerRow = 1;
+ if (locationDefine && locationDefine.headerRow) {
+ headerRow = locationDefine.headerRow as number;
+ }
+
+ let setData: IDataObject[] = [];
+
+ if (dataMode === 'autoMapInputData') {
+ setData = await autoMapInputData.call(this, sheetName, sheet, items, options);
+ } else {
+ setData = mapFields.call(this, items.length);
+ }
+
+ await sheet.appendSheetData(
+ setData,
+ sheetName,
+ headerRow,
+ (options.cellFormat as ValueInputOption) || 'RAW',
+ false,
+ );
+
+ return items;
+}
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
new file mode 100644
index 0000000000000..22dd84eb7fbd4
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/appendOrUpdate.operation.ts
@@ -0,0 +1,315 @@
+import { IExecuteFunctions } from 'n8n-core';
+import { ISheetUpdateData, SheetProperties } from '../../helpers/GoogleSheets.types';
+import { IDataObject, INodeExecutionData, NodeOperationError } from 'n8n-workflow';
+import { GoogleSheet } from '../../helpers/GoogleSheet';
+import { ValueInputOption, ValueRenderOption } from '../../helpers/GoogleSheets.types';
+import { untilSheetSelected } from '../../helpers/GoogleSheets.utils';
+import { cellFormat, handlingExtraData, locationDefine } from './commonDescription';
+
+export const description: SheetProperties = [
+ {
+ displayName: 'Data Mode',
+ name: 'dataMode',
+ type: 'options',
+ options: [
+ {
+ name: 'Auto-Map Input Data to Columns',
+ value: 'autoMapInputData',
+ description: 'Use when node input properties match destination column names',
+ },
+ {
+ name: 'Map Each Column Below',
+ value: 'defineBelow',
+ description: 'Set the value for each destination column',
+ },
+ {
+ name: 'Nothing',
+ value: 'nothing',
+ description: 'Do not send anything',
+ },
+ ],
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['appendOrUpdate'],
+ },
+ hide: {
+ ...untilSheetSelected,
+ },
+ },
+ default: 'defineBelow',
+ description: 'Whether to insert the input data this node receives in the new row',
+ },
+ {
+ // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased, n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
+ displayName: 'Column to match on',
+ name: 'columnToMatchOn',
+ type: 'options',
+ description:
+ 'Choose from the list, or specify an ID using an expression',
+ typeOptions: {
+ loadOptionsDependsOn: ['sheetName.value'],
+ loadOptionsMethod: 'getSheetHeaderRowAndSkipEmpty',
+ },
+ default: '',
+ hint: "Used to find the correct row to update. Doesn't get changed.",
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['appendOrUpdate'],
+ },
+ hide: {
+ ...untilSheetSelected,
+ },
+ },
+ },
+ {
+ displayName: 'Value of Column to Match On',
+ name: 'valueToMatchOn',
+ type: 'string',
+ default: '',
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['appendOrUpdate'],
+ dataMode: ['defineBelow'],
+ },
+ hide: {
+ ...untilSheetSelected,
+ },
+ },
+ },
+ {
+ displayName: 'Values to Send',
+ name: 'fieldsUi',
+ placeholder: 'Add Field',
+ type: 'fixedCollection',
+ typeOptions: {
+ multipleValues: true,
+ },
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['appendOrUpdate'],
+ dataMode: ['defineBelow'],
+ },
+ hide: {
+ ...untilSheetSelected,
+ },
+ },
+ default: {},
+ options: [
+ {
+ displayName: 'Field',
+ name: 'values',
+ values: [
+ {
+ // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
+ displayName: 'Column',
+ name: 'column',
+ type: 'options',
+ description:
+ 'Choose from the list, or specify an ID using an expression',
+ typeOptions: {
+ loadOptionsDependsOn: ['sheetName.value', 'columnToMatchOn'],
+ loadOptionsMethod: 'getSheetHeaderRowAndAddColumn',
+ },
+ default: '',
+ },
+ {
+ displayName: 'Column Name',
+ name: 'columnName',
+ type: 'string',
+ default: '',
+ displayOptions: {
+ show: {
+ column: ['newColumn'],
+ },
+ },
+ },
+ {
+ displayName: 'Value',
+ name: 'fieldValue',
+ type: 'string',
+ default: '',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ displayName: 'Options',
+ name: 'options',
+ type: 'collection',
+ placeholder: 'Add Option',
+ default: {},
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['appendOrUpdate'],
+ },
+ hide: {
+ ...untilSheetSelected,
+ },
+ },
+ options: [...cellFormat, ...locationDefine, ...handlingExtraData],
+ },
+];
+
+export async function execute(
+ this: IExecuteFunctions,
+ sheet: GoogleSheet,
+ sheetName: string,
+): Promise {
+ const items = this.getInputData();
+ const valueInputMode = this.getNodeParameter('options.cellFormat', 0, 'RAW') as ValueInputOption;
+ const range = `${sheetName}!A:Z`;
+
+ const options = this.getNodeParameter('options', 0, {}) as IDataObject;
+
+ const valueRenderMode = (options.valueRenderMode || 'UNFORMATTED_VALUE') as ValueRenderOption;
+
+ const locationDefine = ((options.locationDefine as IDataObject) || {}).values as IDataObject;
+
+ let headerRow = 0;
+ let firstDataRow = 1;
+
+ if (locationDefine) {
+ if (locationDefine.headerRow) {
+ headerRow = parseInt(locationDefine.headerRow as string, 10) - 1;
+ }
+ if (locationDefine.firstDataRow) {
+ firstDataRow = parseInt(locationDefine.firstDataRow as string, 10) - 1;
+ }
+ }
+
+ let columnNames: string[] = [];
+
+ const sheetData = await sheet.getData(sheetName, 'FORMATTED_VALUE');
+
+ if (sheetData === undefined || sheetData[headerRow] === undefined) {
+ throw new NodeOperationError(
+ this.getNode(),
+ `Could not retrieve the column names from row ${headerRow + 1}`,
+ );
+ }
+
+ columnNames = sheetData[headerRow];
+ const newColumns = new Set();
+
+ const columnToMatchOn = this.getNodeParameter('columnToMatchOn', 0) as string;
+ const keyIndex = columnNames.indexOf(columnToMatchOn);
+
+ const columnValues = await sheet.getColumnValues(
+ range,
+ keyIndex,
+ firstDataRow,
+ valueRenderMode,
+ sheetData,
+ );
+
+ const updateData: ISheetUpdateData[] = [];
+ const appendData: IDataObject[] = [];
+
+ for (let i = 0; i < items.length; i++) {
+ const dataMode = this.getNodeParameter('dataMode', i) as
+ | 'defineBelow'
+ | 'autoMapInputData'
+ | 'nothing';
+
+ if (dataMode === 'nothing') continue;
+
+ const data: IDataObject[] = [];
+
+ if (dataMode === 'autoMapInputData') {
+ const handlingExtraData = (options.handlingExtraData as string) || 'insertInNewColumn';
+ if (handlingExtraData === 'ignoreIt') {
+ data.push(items[i].json);
+ }
+ if (handlingExtraData === 'error') {
+ Object.keys(items[i].json).forEach((key) => {
+ if (columnNames.includes(key) === false) {
+ throw new NodeOperationError(this.getNode(), `Unexpected fields in node input`, {
+ itemIndex: i,
+ description: `The input field '${key}' doesn't match any column in the Sheet. You can ignore this by changing the 'Handling extra data' field, which you can find under 'Options'.`,
+ });
+ }
+ });
+ data.push(items[i].json);
+ }
+ if (handlingExtraData === 'insertInNewColumn') {
+ Object.keys(items[i].json).forEach((key) => {
+ if (columnNames.includes(key) === false) {
+ newColumns.add(key);
+ }
+ });
+ data.push(items[i].json);
+ }
+ } else {
+ const valueToMatchOn = this.getNodeParameter('valueToMatchOn', i) as string;
+
+ const fields = (this.getNodeParameter('fieldsUi.values', i, {}) as IDataObject[]).reduce(
+ (acc, entry) => {
+ if (entry.column === 'newColumn') {
+ const columnName = entry.columnName as string;
+
+ if (columnNames.includes(columnName) === false) {
+ newColumns.add(columnName);
+ }
+
+ acc[columnName] = entry.fieldValue as string;
+ } else {
+ acc[entry.column as string] = entry.fieldValue as string;
+ }
+ return acc;
+ },
+ {} as IDataObject,
+ );
+
+ fields[columnToMatchOn] = valueToMatchOn;
+
+ data.push(fields);
+ }
+
+ if (newColumns.size) {
+ await sheet.updateRows(
+ sheetName,
+ [columnNames.concat([...newColumns])],
+ (options.cellFormat as ValueInputOption) || 'RAW',
+ headerRow + 1,
+ );
+ }
+
+ const preparedData = await sheet.prepareDataForUpdateOrUpsert(
+ data,
+ columnToMatchOn,
+ range,
+ headerRow,
+ firstDataRow,
+ valueRenderMode,
+ true,
+ [columnNames.concat([...newColumns])],
+ columnValues,
+ );
+
+ updateData.push(...preparedData.updateData);
+ appendData.push(...preparedData.appendData);
+ }
+
+ if (updateData.length) {
+ await sheet.batchUpdate(updateData, valueInputMode);
+ }
+ if (appendData.length) {
+ const lastRow = sheetData.length + 1;
+ await sheet.appendSheetData(
+ appendData,
+ range,
+ headerRow + 1,
+ valueInputMode,
+ false,
+ [columnNames.concat([...newColumns])],
+ lastRow,
+ );
+ }
+ return items;
+}
diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/clear.operation.ts b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/clear.operation.ts
new file mode 100644
index 0000000000000..f0fb325c20221
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/clear.operation.ts
@@ -0,0 +1,210 @@
+import { IExecuteFunctions } from 'n8n-core';
+import { INodeExecutionData } from 'n8n-workflow';
+import { SheetProperties } from '../../helpers/GoogleSheets.types';
+import { GoogleSheet } from '../../helpers/GoogleSheet';
+import {
+ getColumnName,
+ getColumnNumber,
+ untilSheetSelected,
+} from '../../helpers/GoogleSheets.utils';
+
+export const description: SheetProperties = [
+ {
+ displayName: 'Clear',
+ name: 'clear',
+ type: 'options',
+ options: [
+ {
+ name: 'Whole Sheet',
+ value: 'wholeSheet',
+ },
+ {
+ name: 'Specific Rows',
+ value: 'specificRows',
+ },
+ {
+ name: 'Specific Columns',
+ value: 'specificColumns',
+ },
+ {
+ name: 'Specific Range',
+ value: 'specificRange',
+ },
+ ],
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['clear'],
+ },
+ hide: {
+ ...untilSheetSelected,
+ },
+ },
+ default: 'wholeSheet',
+ description: 'What to clear',
+ },
+ {
+ displayName: 'Keep First Row',
+ name: 'keepFirstRow',
+ type: 'boolean',
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['clear'],
+ clear: ['wholeSheet'],
+ },
+ hide: {
+ ...untilSheetSelected,
+ },
+ },
+ default: false,
+ },
+ {
+ displayName: 'Start Row Number',
+ name: 'startIndex',
+ type: 'number',
+ typeOptions: {
+ minValue: 1,
+ },
+ default: 1,
+ description: 'The row number to delete from, The first row is 1',
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['clear'],
+ clear: ['specificRows'],
+ },
+ hide: {
+ ...untilSheetSelected,
+ },
+ },
+ },
+ {
+ displayName: 'Number of Rows to Delete',
+ name: 'rowsToDelete',
+ type: 'number',
+ typeOptions: {
+ minValue: 1,
+ },
+ default: 1,
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['clear'],
+ clear: ['specificRows'],
+ },
+ hide: {
+ ...untilSheetSelected,
+ },
+ },
+ },
+
+ {
+ displayName: 'Start Column',
+ name: 'startIndex',
+ type: 'string',
+ default: 'A',
+ description: 'The column to delete',
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['clear'],
+ clear: ['specificColumns'],
+ },
+ hide: {
+ ...untilSheetSelected,
+ },
+ },
+ },
+ {
+ // Could this be better as "end column"?
+ displayName: 'Number of Columns to Delete',
+ name: 'columnsToDelete',
+ type: 'number',
+ typeOptions: {
+ minValue: 1,
+ },
+ default: 1,
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['clear'],
+ clear: ['specificColumns'],
+ },
+ hide: {
+ ...untilSheetSelected,
+ },
+ },
+ },
+ {
+ displayName: 'Range',
+ name: 'range',
+ type: 'string',
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['clear'],
+ clear: ['specificRange'],
+ },
+ hide: {
+ ...untilSheetSelected,
+ },
+ },
+ default: 'A:F',
+ required: true,
+ description:
+ 'The table range to read from or to append data to. See the Google documentation for the details. If it contains multiple sheets it can also be added like this: "MySheet!A:F"',
+ },
+];
+
+export async function execute(
+ this: IExecuteFunctions,
+ sheet: GoogleSheet,
+ sheetName: string,
+): Promise {
+ const items = this.getInputData();
+
+ for (let i = 0; i < items.length; i++) {
+ const clearType = this.getNodeParameter('clear', i) as string;
+ const keepFirstRow = this.getNodeParameter('keepFirstRow', i, false) as boolean;
+ let range = '';
+
+ if (clearType === 'specificRows') {
+ const startIndex = this.getNodeParameter('startIndex', i) as number;
+ const rowsToDelete = this.getNodeParameter('rowsToDelete', i) as number;
+ const endIndex = rowsToDelete === 1 ? startIndex : startIndex + rowsToDelete - 1;
+
+ range = `${sheetName}!${startIndex}:${endIndex}`;
+ }
+
+ if (clearType === 'specificColumns') {
+ const startIndex = this.getNodeParameter('startIndex', i) as string;
+ const columnsToDelete = this.getNodeParameter('columnsToDelete', i) as number;
+ const columnNumber = getColumnNumber(startIndex);
+ const endIndex = columnsToDelete === 1 ? columnNumber : columnNumber + columnsToDelete - 1;
+
+ range = `${sheetName}!${startIndex}:${getColumnName(endIndex)}`;
+ }
+
+ if (clearType === 'specificRange') {
+ const rangeField = this.getNodeParameter('range', i) as string;
+ const region = rangeField.includes('!') ? rangeField.split('!')[1] || '' : rangeField;
+
+ range = `${sheetName}!${region}`;
+ }
+
+ if (clearType === 'wholeSheet') {
+ range = sheetName;
+ }
+
+ if (keepFirstRow) {
+ const firstRow = await sheet.getData(`${range}!1:1`, 'FORMATTED_VALUE');
+ await sheet.clearData(range);
+ await sheet.updateRows(range, firstRow as string[][], 'RAW', 1);
+ } else {
+ await sheet.clearData(range);
+ }
+ }
+
+ return items;
+}
diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/commonDescription.ts b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/commonDescription.ts
new file mode 100644
index 0000000000000..fc31112e4be52
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/commonDescription.ts
@@ -0,0 +1,274 @@
+import { INodeProperties } from 'n8n-workflow';
+
+export const dataLocationOnSheet: INodeProperties[] = [
+ {
+ displayName: 'Data Location on Sheet',
+ name: 'dataLocationOnSheet',
+ type: 'fixedCollection',
+ placeholder: 'Select Range',
+ default: { values: { rangeDefinition: 'detectAutomatically' } },
+ options: [
+ {
+ displayName: 'Values',
+ name: 'values',
+ values: [
+ {
+ displayName: 'Range Definition',
+ name: 'rangeDefinition',
+ type: 'options',
+ options: [
+ {
+ name: 'Detect Automatically',
+ value: 'detectAutomatically',
+ description: 'Automatically detect the data range',
+ },
+ {
+ name: 'Specify Range (A1 Notation)',
+ value: 'specifyRangeA1',
+ description: 'Manually specify the data range',
+ },
+ {
+ name: 'Specify Range (Rows)',
+ value: 'specifyRange',
+ description: 'Manually specify the data range',
+ },
+ ],
+ default: '',
+ },
+ {
+ displayName: 'Read Rows Until',
+ name: 'readRowsUntil',
+ type: 'options',
+ default: 'lastRowInSheet',
+ options: [
+ {
+ name: 'First Empty Row',
+ value: 'firstEmptyRow',
+ },
+ {
+ name: 'Last Row In Sheet',
+ value: 'lastRowInSheet',
+ },
+ ],
+ displayOptions: {
+ show: {
+ rangeDefinition: ['detectAutomatically'],
+ },
+ },
+ },
+ {
+ displayName: 'Header Row',
+ name: 'headerRow',
+ type: 'number',
+ typeOptions: {
+ minValue: 1,
+ },
+ default: 1,
+ description:
+ 'Index of the row which contains the keys. Starts at 1. The incoming node data is matched to the keys for assignment. The matching is case sensitive.',
+ hint: 'From start of range. First row is row 1',
+ displayOptions: {
+ show: {
+ rangeDefinition: ['specifyRange'],
+ },
+ },
+ },
+ {
+ displayName: 'First Data Row',
+ name: 'firstDataRow',
+ type: 'number',
+ typeOptions: {
+ minValue: 1,
+ },
+ default: 2,
+ description:
+ 'Index of the first row which contains the actual data and not the keys. Starts with 1.',
+ hint: 'From start of range. First row is row 1',
+ displayOptions: {
+ show: {
+ rangeDefinition: ['specifyRange'],
+ },
+ },
+ },
+ {
+ displayName: 'Range',
+ name: 'range',
+ type: 'string',
+ default: '',
+ placeholder: 'A:Z',
+ description:
+ 'The table range to read from or to append data to. See the Google documentation for the details.',
+ hint: 'You can specify both the rows and the columns, e.g. C4:E7',
+ displayOptions: {
+ show: {
+ rangeDefinition: ['specifyRangeA1'],
+ },
+ },
+ },
+ ],
+ },
+ ],
+ },
+];
+
+export const locationDefine: INodeProperties[] = [
+ {
+ displayName: 'Data Location on Sheet',
+ name: 'locationDefine',
+ type: 'fixedCollection',
+ placeholder: 'Select Range',
+ default: { values: {} },
+ options: [
+ {
+ displayName: 'Values',
+ name: 'values',
+ values: [
+ {
+ displayName: 'Header Row',
+ name: 'headerRow',
+ type: 'number',
+ typeOptions: {
+ minValue: 1,
+ },
+ default: 1,
+ description:
+ 'Index of the row which contains the keys. Starts at 1. The incoming node data is matched to the keys for assignment. The matching is case sensitive.',
+ hint: 'From start of range. First row is row 1',
+ },
+ {
+ displayName: 'First Data Row',
+ name: 'firstDataRow',
+ type: 'number',
+ typeOptions: {
+ minValue: 1,
+ },
+ default: 2,
+ description:
+ 'Index of the first row which contains the actual data and not the keys. Starts with 1.',
+ hint: 'From start of range. First row is row 1',
+ },
+ ],
+ },
+ ],
+ },
+];
+
+export const outputFormatting: INodeProperties[] = [
+ {
+ displayName: 'Output Formatting',
+ name: 'outputFormatting',
+ type: 'fixedCollection',
+ placeholder: 'Add Formatting',
+ default: { values: { general: 'UNFORMATTED_VALUE', date: 'FORMATTED_STRING' } },
+ options: [
+ {
+ displayName: 'Values',
+ name: 'values',
+ values: [
+ {
+ displayName: 'General Formatting',
+ name: 'general',
+ type: 'options',
+ options: [
+ {
+ // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
+ name: 'Values (unformatted)',
+ value: 'UNFORMATTED_VALUE',
+ description:
+ 'Numbers stay as numbers, but any currency signs or special formatting is lost',
+ },
+ {
+ // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
+ name: 'Values (formatted)',
+ value: 'FORMATTED_VALUE',
+ description:
+ 'Numbers are turned to text, and displayed as in Google Sheets (e.g. with commas or currency signs)',
+ },
+ {
+ name: 'Formulas',
+ value: 'FORMULA',
+ },
+ ],
+ default: '',
+ description: 'Determines how values should be rendered in the output',
+ },
+ {
+ displayName: 'Date Formatting',
+ name: 'date',
+ type: 'options',
+ default: '',
+ options: [
+ {
+ name: 'Formatted Text',
+ value: 'FORMATTED_STRING',
+ description: "As displayed in Google Sheets, e.g. '01/01/2022'",
+ },
+ {
+ name: 'Serial Number',
+ value: 'SERIAL_NUMBER',
+ description: 'A number representing the number of days since Dec 30, 1899',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+];
+
+export const cellFormat: INodeProperties[] = [
+ {
+ displayName: 'Cell Format',
+ name: 'cellFormat',
+ type: 'options',
+ options: [
+ {
+ // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
+ name: 'Let n8n format',
+ value: 'RAW',
+ description: 'Cells have the same types as the input data',
+ },
+ {
+ // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
+ name: 'Let Google Sheets format',
+ value: 'USER_ENTERED',
+ description: 'Cells are styled as if you typed the values into Google Sheets directly',
+ },
+ ],
+ default: 'RAW',
+ description: 'Determines how data should be interpreted',
+ },
+];
+
+export const handlingExtraData: INodeProperties[] = [
+ {
+ // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
+ displayName: 'Handling extra fields in input',
+ name: 'handlingExtraData',
+ type: 'options',
+ options: [
+ {
+ name: 'Insert in New Column(s)',
+ value: 'insertInNewColumn',
+ description: 'Create a new column for extra data',
+ },
+ {
+ name: 'Ignore Them',
+ value: 'ignoreIt',
+ description: 'Ignore extra data',
+ },
+ {
+ name: 'Error',
+ value: 'error',
+ description: 'Throw an error',
+ },
+ ],
+ displayOptions: {
+ show: {
+ '/dataMode': ['autoMapInputData'],
+ },
+ },
+ default: 'insertInNewColumn',
+ description: "What do to with fields that don't match any columns in the Google Sheet",
+ },
+];
diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/create.operation.ts b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/create.operation.ts
new file mode 100644
index 0000000000000..7c26284485466
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/create.operation.ts
@@ -0,0 +1,127 @@
+import { IExecuteFunctions } from 'n8n-core';
+import { IDataObject, INodeExecutionData } from 'n8n-workflow';
+import { SheetProperties } from '../../helpers/GoogleSheets.types';
+import { apiRequest } from '../../transport';
+import { GoogleSheet } from '../../helpers/GoogleSheet';
+import { getExistingSheetNames, hexToRgb } from '../../helpers/GoogleSheets.utils';
+
+export const description: SheetProperties = [
+ {
+ displayName: 'Title',
+ name: 'title',
+ type: 'string',
+ required: true,
+ default: 'n8n-sheet',
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['create'],
+ },
+ },
+ description: 'The name of the sheet',
+ },
+ {
+ displayName: 'Options',
+ name: 'options',
+ type: 'collection',
+ placeholder: 'Add Option',
+ default: {},
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['create'],
+ },
+ },
+ options: [
+ {
+ displayName: 'Hidden',
+ name: 'hidden',
+ type: 'boolean',
+ default: false,
+ description: "Whether the sheet is hidden in the UI, false if it's visible",
+ },
+ {
+ displayName: 'Right To Left',
+ name: 'rightToLeft',
+ type: 'boolean',
+ default: false,
+ description: 'Whether the sheet is an RTL sheet instead of an LTR sheet',
+ },
+ {
+ displayName: 'Sheet ID',
+ name: 'sheetId',
+ type: 'number',
+ default: 0,
+ description:
+ 'The ID of the sheet. Must be non-negative. This field cannot be changed once set.',
+ },
+ {
+ displayName: 'Sheet Index',
+ name: 'index',
+ type: 'number',
+ default: 0,
+ description: 'The index of the sheet within the spreadsheet',
+ },
+ {
+ displayName: 'Tab Color',
+ name: 'tabColor',
+ type: 'color',
+ default: '0aa55c',
+ description: 'The color of the tab in the UI',
+ },
+ ],
+ },
+];
+
+export async function execute(
+ this: IExecuteFunctions,
+ sheet: GoogleSheet,
+ sheetName: string,
+): Promise {
+ let responseData;
+ const returnData: IDataObject[] = [];
+ const items = this.getInputData();
+
+ const existingSheetNames = await getExistingSheetNames(sheet);
+
+ for (let i = 0; i < items.length; i++) {
+ const sheetTitle = this.getNodeParameter('title', i, {}) as string;
+
+ if (existingSheetNames.includes(sheetTitle)) {
+ continue;
+ }
+
+ const options = this.getNodeParameter('options', i, {}) as IDataObject;
+ const properties = { ...options };
+ properties.title = sheetTitle;
+
+ if (options.tabColor) {
+ const { red, green, blue } = hexToRgb(options.tabColor as string)!;
+ properties.tabColor = { red: red / 255, green: green / 255, blue: blue / 255 };
+ }
+
+ const requests = [
+ {
+ addSheet: {
+ properties,
+ },
+ },
+ ];
+
+ responseData = await apiRequest.call(
+ this,
+ 'POST',
+ `/v4/spreadsheets/${sheetName}:batchUpdate`,
+ { requests },
+ );
+
+ // simplify response
+ Object.assign(responseData, responseData.replies[0].addSheet.properties);
+ delete responseData.replies;
+
+ existingSheetNames.push(sheetTitle);
+
+ returnData.push(responseData);
+ }
+ return this.helpers.returnJsonArray(returnData);
+}
diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/delete.operation.ts b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/delete.operation.ts
new file mode 100644
index 0000000000000..c1d3832ba9e7d
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/delete.operation.ts
@@ -0,0 +1,169 @@
+import { IExecuteFunctions } from 'n8n-core';
+import { IDataObject, INodeExecutionData } from 'n8n-workflow';
+import { SheetProperties } from '../../helpers/GoogleSheets.types';
+import { GoogleSheet } from '../../helpers/GoogleSheet';
+import { getColumnNumber, untilSheetSelected } from '../../helpers/GoogleSheets.utils';
+
+export const description: SheetProperties = [
+ {
+ displayName: 'To Delete',
+ name: 'toDelete',
+ type: 'options',
+ options: [
+ {
+ name: 'Rows',
+ value: 'rows',
+ description: 'Rows to delete',
+ },
+ {
+ name: 'Columns',
+ value: 'columns',
+ description: 'Columns to delete',
+ },
+ ],
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['delete'],
+ },
+ hide: {
+ ...untilSheetSelected,
+ },
+ },
+ default: 'rows',
+ description: 'What to delete',
+ },
+ {
+ displayName: 'Start Row Number',
+ name: 'startIndex',
+ type: 'number',
+ typeOptions: {
+ minValue: 1,
+ },
+ default: 2,
+ description: 'The row number to delete from, The first row is 2',
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['delete'],
+ toDelete: ['rows'],
+ },
+ hide: {
+ ...untilSheetSelected,
+ },
+ },
+ },
+ {
+ displayName: 'Number of Rows to Delete',
+ name: 'numberToDelete',
+ type: 'number',
+ typeOptions: {
+ minValue: 1,
+ },
+ default: 1,
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['delete'],
+ toDelete: ['rows'],
+ },
+ hide: {
+ ...untilSheetSelected,
+ },
+ },
+ },
+ {
+ displayName: 'Start Column',
+ name: 'startIndex',
+ type: 'string',
+ default: 'A',
+ description: 'The column to delete',
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['delete'],
+ toDelete: ['columns'],
+ },
+ hide: {
+ ...untilSheetSelected,
+ },
+ },
+ },
+ {
+ displayName: 'Number of Columns to Delete',
+ name: 'numberToDelete',
+ type: 'number',
+ typeOptions: {
+ minValue: 1,
+ },
+ default: 1,
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['delete'],
+ toDelete: ['columns'],
+ },
+ hide: {
+ ...untilSheetSelected,
+ },
+ },
+ },
+];
+
+export async function execute(
+ this: IExecuteFunctions,
+ sheet: GoogleSheet,
+ sheetName: string,
+): Promise {
+ const items = this.getInputData();
+
+ for (let i = 0; i < items.length; i++) {
+ const requests: IDataObject[] = [];
+ let startIndex, endIndex, numberToDelete;
+ const deleteType = this.getNodeParameter('toDelete', i) as string;
+
+ if (deleteType === 'rows') {
+ startIndex = this.getNodeParameter('startIndex', i) as number;
+ // We start from 1 now...
+ startIndex--;
+ numberToDelete = this.getNodeParameter('numberToDelete', i) as number;
+ if (numberToDelete === 1) {
+ endIndex = startIndex + 1;
+ } else {
+ endIndex = startIndex + numberToDelete;
+ }
+ requests.push({
+ deleteDimension: {
+ range: {
+ sheetId: sheetName,
+ dimension: 'ROWS',
+ startIndex,
+ endIndex,
+ },
+ },
+ });
+ } else if (deleteType === 'columns') {
+ startIndex = this.getNodeParameter('startIndex', i) as string;
+ numberToDelete = this.getNodeParameter('numberToDelete', i) as number;
+ startIndex = getColumnNumber(startIndex) - 1;
+ if (numberToDelete === 1) {
+ endIndex = startIndex + 1;
+ } else {
+ endIndex = startIndex + numberToDelete;
+ }
+ requests.push({
+ deleteDimension: {
+ range: {
+ sheetId: sheetName,
+ dimension: 'COLUMNS',
+ startIndex,
+ endIndex,
+ },
+ },
+ });
+ }
+ await sheet.spreadsheetBatchUpdate(requests);
+ }
+
+ return this.helpers.returnJsonArray({ success: true });
+}
diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/read.operation.ts b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/read.operation.ts
new file mode 100644
index 0000000000000..a12381d438b70
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/read.operation.ts
@@ -0,0 +1,168 @@
+import { IExecuteFunctions } from 'n8n-core';
+import { IDataObject, INodeExecutionData } from 'n8n-workflow';
+import { GoogleSheet } from '../../helpers/GoogleSheet';
+import {
+ getRangeString,
+ prepareSheetData,
+ untilSheetSelected,
+} from '../../helpers/GoogleSheets.utils';
+import { ILookupValues, SheetProperties } from '../../helpers/GoogleSheets.types';
+import { dataLocationOnSheet, outputFormatting } from './commonDescription';
+import {
+ RangeDetectionOptions,
+ SheetRangeData,
+ ValueRenderOption,
+} from '../../helpers/GoogleSheets.types';
+
+export const description: SheetProperties = [
+ {
+ displayName: 'Filters',
+ name: 'filtersUI',
+ placeholder: 'Add Filter',
+ type: 'fixedCollection',
+ typeOptions: {
+ multipleValueButtonText: 'Add Filter',
+ multipleValues: true,
+ },
+ default: {},
+ options: [
+ {
+ displayName: 'Filter',
+ name: 'values',
+ values: [
+ {
+ // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
+ displayName: 'Column',
+ name: 'lookupColumn',
+ type: 'options',
+ typeOptions: {
+ loadOptionsDependsOn: ['sheetName.value'],
+ loadOptionsMethod: 'getSheetHeaderRowWithGeneratedColumnNames',
+ },
+ default: '',
+ description:
+ 'Choose from the list, or specify an ID using an expression',
+ },
+ {
+ displayName: 'Value',
+ name: 'lookupValue',
+ type: 'string',
+ default: '',
+ hint: 'The column must have this value to be matched',
+ },
+ ],
+ },
+ ],
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['read'],
+ },
+ hide: {
+ ...untilSheetSelected,
+ },
+ },
+ },
+ {
+ displayName: 'Options',
+ name: 'options',
+ type: 'collection',
+ placeholder: 'Add Option',
+ default: {},
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['read'],
+ },
+ hide: {
+ ...untilSheetSelected,
+ },
+ },
+ options: [
+ ...dataLocationOnSheet,
+ ...outputFormatting,
+ {
+ displayName: 'When Filter Has Multiple Matches',
+ name: 'returnAllMatches',
+ type: 'options',
+ default: 'returnFirstMatch',
+ options: [
+ {
+ name: 'Return First Match',
+ value: 'returnFirstMatch',
+ description: 'Return only the first match',
+ },
+ {
+ name: 'Return All Matches',
+ value: 'returnAllMatches',
+ description: 'Return all values that match',
+ },
+ ],
+ description:
+ 'By default only the first result gets returned, Set to "Return All Matches" to get multiple matches',
+ },
+ ],
+ },
+];
+
+export async function execute(
+ this: IExecuteFunctions,
+ sheet: GoogleSheet,
+ sheetName: string,
+): Promise {
+ const options = this.getNodeParameter('options', 0, {}) as IDataObject;
+ const outputFormatting =
+ (((options.outputFormatting as IDataObject) || {}).values as IDataObject) || {};
+
+ const dataLocationOnSheetOptions =
+ (((options.dataLocationOnSheet as IDataObject) || {}).values as RangeDetectionOptions) || {};
+
+ if (dataLocationOnSheetOptions.rangeDefinition === undefined) {
+ dataLocationOnSheetOptions.rangeDefinition = 'detectAutomatically';
+ }
+
+ const range = getRangeString(sheetName, dataLocationOnSheetOptions);
+
+ const valueRenderMode = (outputFormatting.general || 'UNFORMATTED_VALUE') as ValueRenderOption;
+ const dateTimeRenderOption = (outputFormatting.date || 'FORMATTED_STRING') as string;
+
+ const sheetData = (await sheet.getData(
+ range,
+ valueRenderMode,
+ dateTimeRenderOption,
+ )) as SheetRangeData;
+
+ if (sheetData === undefined || sheetData.length === 0) {
+ return [];
+ }
+
+ const { data, headerRow, firstDataRow } = prepareSheetData(sheetData, dataLocationOnSheetOptions);
+
+ let returnData = [];
+
+ const lookupValues = this.getNodeParameter('filtersUI.values', 0, []) as ILookupValues[];
+
+ if (lookupValues.length) {
+ const returnAllMatches = options.returnAllMatches === 'returnAllMatches' ? true : false;
+
+ const items = this.getInputData();
+ for (let i = 1; i < items.length; i++) {
+ const itemLookupValues = this.getNodeParameter('filtersUI.values', i, []) as ILookupValues[];
+ if (itemLookupValues.length) {
+ lookupValues.push(...itemLookupValues);
+ }
+ }
+
+ returnData = await sheet.lookupValues(
+ data as string[][],
+ headerRow,
+ firstDataRow,
+ lookupValues,
+ returnAllMatches,
+ );
+ } else {
+ returnData = sheet.structureArrayDataByColumn(data as string[][], headerRow, firstDataRow);
+ }
+
+ return this.helpers.returnJsonArray(returnData);
+}
diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/remove.operation.ts b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/remove.operation.ts
new file mode 100644
index 0000000000000..8030ba7c96a27
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/remove.operation.ts
@@ -0,0 +1,36 @@
+import { IExecuteFunctions } from 'n8n-core';
+import { IDataObject, INodeExecutionData } from 'n8n-workflow';
+import { apiRequest } from '../../transport';
+import { GoogleSheet } from '../../helpers/GoogleSheet';
+
+export async function execute(
+ this: IExecuteFunctions,
+ sheet: GoogleSheet,
+ sheetName: string,
+): Promise {
+ const returnData: IDataObject[] = [];
+ const items = this.getInputData();
+ for (let i = 0; i < items.length; i++) {
+ const [spreadsheetId, sheetWithinDocument] = sheetName.split('||');
+ const requests = [
+ {
+ deleteSheet: {
+ sheetId: sheetWithinDocument,
+ },
+ },
+ ];
+
+ let responseData;
+
+ responseData = await apiRequest.call(
+ this,
+ 'POST',
+ `/v4/spreadsheets/${spreadsheetId}:batchUpdate`,
+ { requests },
+ );
+ delete responseData.replies;
+ returnData.push(responseData);
+ }
+
+ return this.helpers.returnJsonArray(returnData);
+}
diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/update.operation.ts b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/update.operation.ts
new file mode 100644
index 0000000000000..d02557a454d21
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/update.operation.ts
@@ -0,0 +1,302 @@
+import { IExecuteFunctions } from 'n8n-core';
+import { ISheetUpdateData, SheetProperties } from '../../helpers/GoogleSheets.types';
+import { IDataObject, INodeExecutionData, NodeOperationError } from 'n8n-workflow';
+import { GoogleSheet } from '../../helpers/GoogleSheet';
+import { ValueInputOption, ValueRenderOption } from '../../helpers/GoogleSheets.types';
+import { untilSheetSelected } from '../../helpers/GoogleSheets.utils';
+import { cellFormat, handlingExtraData, locationDefine } from './commonDescription';
+
+export const description: SheetProperties = [
+ {
+ displayName: 'Data Mode',
+ name: 'dataMode',
+ type: 'options',
+ options: [
+ {
+ name: 'Auto-Map Input Data to Columns',
+ value: 'autoMapInputData',
+ description: 'Use when node input properties match destination column names',
+ },
+ {
+ name: 'Map Each Column Below',
+ value: 'defineBelow',
+ description: 'Set the value for each destination column',
+ },
+ {
+ name: 'Nothing',
+ value: 'nothing',
+ description: 'Do not send anything',
+ },
+ ],
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['update'],
+ },
+ hide: {
+ ...untilSheetSelected,
+ },
+ },
+ default: 'defineBelow',
+ description: 'Whether to insert the input data this node receives in the new row',
+ },
+ {
+ // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased, n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
+ displayName: 'Column to match on',
+ name: 'columnToMatchOn',
+ type: 'options',
+ description:
+ 'Choose from the list, or specify an ID using an expression',
+ typeOptions: {
+ loadOptionsDependsOn: ['sheetName.value'],
+ loadOptionsMethod: 'getSheetHeaderRowAndSkipEmpty',
+ },
+ default: '',
+ hint: "Used to find the correct row to update. Doesn't get changed.",
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['update'],
+ },
+ hide: {
+ ...untilSheetSelected,
+ },
+ },
+ },
+ {
+ displayName: 'Value of Column to Match On',
+ name: 'valueToMatchOn',
+ type: 'string',
+ default: '',
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['update'],
+ dataMode: ['defineBelow'],
+ },
+ hide: {
+ ...untilSheetSelected,
+ },
+ },
+ },
+ {
+ displayName: 'Values to Send',
+ name: 'fieldsUi',
+ placeholder: 'Add Field',
+ type: 'fixedCollection',
+ typeOptions: {
+ multipleValues: true,
+ },
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['update'],
+ dataMode: ['defineBelow'],
+ },
+ hide: {
+ ...untilSheetSelected,
+ },
+ },
+ default: {},
+ options: [
+ {
+ displayName: 'Field',
+ name: 'values',
+ values: [
+ {
+ // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
+ displayName: 'Column',
+ name: 'column',
+ type: 'options',
+ description:
+ 'Choose from the list, or specify an ID using an expression',
+ typeOptions: {
+ loadOptionsDependsOn: ['sheetName.value', 'columnToMatchOn'],
+ loadOptionsMethod: 'getSheetHeaderRowAndAddColumn',
+ },
+ default: '',
+ },
+ {
+ displayName: 'Column Name',
+ name: 'columnName',
+ type: 'string',
+ default: '',
+ displayOptions: {
+ show: {
+ column: ['newColumn'],
+ },
+ },
+ },
+ {
+ displayName: 'Value',
+ name: 'fieldValue',
+ type: 'string',
+ default: '',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ displayName: 'Options',
+ name: 'options',
+ type: 'collection',
+ placeholder: 'Add Option',
+ default: {},
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['update'],
+ },
+ hide: {
+ ...untilSheetSelected,
+ },
+ },
+ options: [...cellFormat, ...locationDefine, ...handlingExtraData],
+ },
+];
+
+export async function execute(
+ this: IExecuteFunctions,
+ sheet: GoogleSheet,
+ sheetName: string,
+): Promise {
+ const items = this.getInputData();
+ const valueInputMode = this.getNodeParameter('options.cellFormat', 0, 'RAW') as ValueInputOption;
+ const range = `${sheetName}!A:Z`;
+
+ const options = this.getNodeParameter('options', 0, {}) as IDataObject;
+
+ const valueRenderMode = (options.valueRenderMode || 'UNFORMATTED_VALUE') as ValueRenderOption;
+
+ const locationDefine = ((options.locationDefine as IDataObject) || {}).values as IDataObject;
+
+ let headerRow = 0;
+ let firstDataRow = 1;
+
+ if (locationDefine) {
+ if (locationDefine.headerRow) {
+ headerRow = parseInt(locationDefine.headerRow as string, 10) - 1;
+ }
+ if (locationDefine.firstDataRow) {
+ firstDataRow = parseInt(locationDefine.firstDataRow as string, 10) - 1;
+ }
+ }
+
+ let columnNames: string[] = [];
+
+ const sheetData = await sheet.getData(sheetName, 'FORMATTED_VALUE');
+
+ if (sheetData === undefined || sheetData[headerRow] === undefined) {
+ throw new NodeOperationError(
+ this.getNode(),
+ `Could not retrieve the column names from row ${headerRow + 1}`,
+ );
+ }
+
+ columnNames = sheetData[headerRow];
+ const newColumns = new Set();
+
+ const columnToMatchOn = this.getNodeParameter('columnToMatchOn', 0) as string;
+ const keyIndex = columnNames.indexOf(columnToMatchOn);
+
+ const columnValues = await sheet.getColumnValues(
+ range,
+ keyIndex,
+ firstDataRow,
+ valueRenderMode,
+ sheetData,
+ );
+
+ const updateData: ISheetUpdateData[] = [];
+
+ for (let i = 0; i < items.length; i++) {
+ const dataMode = this.getNodeParameter('dataMode', i) as
+ | 'defineBelow'
+ | 'autoMapInputData'
+ | 'nothing';
+
+ if (dataMode === 'nothing') continue;
+
+ const data: IDataObject[] = [];
+
+ if (dataMode === 'autoMapInputData') {
+ const handlingExtraData = (options.handlingExtraData as string) || 'insertInNewColumn';
+ if (handlingExtraData === 'ignoreIt') {
+ data.push(items[i].json);
+ }
+ if (handlingExtraData === 'error') {
+ Object.keys(items[i].json).forEach((key) => {
+ if (columnNames.includes(key) === false) {
+ throw new NodeOperationError(this.getNode(), `Unexpected fields in node input`, {
+ itemIndex: i,
+ description: `The input field '${key}' doesn't match any column in the Sheet. You can ignore this by changing the 'Handling extra data' field, which you can find under 'Options'.`,
+ });
+ }
+ });
+ data.push(items[i].json);
+ }
+ if (handlingExtraData === 'insertInNewColumn') {
+ Object.keys(items[i].json).forEach((key) => {
+ if (columnNames.includes(key) === false) {
+ newColumns.add(key);
+ }
+ });
+ data.push(items[i].json);
+ }
+ } else {
+ const valueToMatchOn = this.getNodeParameter('valueToMatchOn', i) as string;
+
+ const fields = (this.getNodeParameter('fieldsUi.values', i, {}) as IDataObject[]).reduce(
+ (acc, entry) => {
+ if (entry.column === 'newColumn') {
+ const columnName = entry.columnName as string;
+
+ if (columnNames.includes(columnName) === false) {
+ newColumns.add(columnName);
+ }
+
+ acc[columnName] = entry.fieldValue as string;
+ } else {
+ acc[entry.column as string] = entry.fieldValue as string;
+ }
+ return acc;
+ },
+ {} as IDataObject,
+ );
+
+ fields[columnToMatchOn] = valueToMatchOn;
+
+ data.push(fields);
+ }
+
+ if (newColumns.size) {
+ await sheet.updateRows(
+ sheetName,
+ [columnNames.concat([...newColumns])],
+ (options.cellFormat as ValueInputOption) || 'RAW',
+ headerRow + 1,
+ );
+ }
+
+ const preparedData = await sheet.prepareDataForUpdateOrUpsert(
+ data,
+ columnToMatchOn,
+ range,
+ headerRow,
+ firstDataRow,
+ valueRenderMode,
+ false,
+ [columnNames.concat([...newColumns])],
+ columnValues,
+ );
+
+ updateData.push(...preparedData.updateData);
+ }
+
+ if (updateData.length) {
+ await sheet.batchUpdate(updateData, valueInputMode);
+ }
+
+ return items;
+}
diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/actions/spreadsheet/SpreadSheet.resource.ts b/packages/nodes-base/nodes/Google/Sheet/v2/actions/spreadsheet/SpreadSheet.resource.ts
new file mode 100644
index 0000000000000..56933874534f2
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/spreadsheet/SpreadSheet.resource.ts
@@ -0,0 +1,36 @@
+import * as create from './create.operation';
+import * as deleteSpreadsheet from './delete.operation';
+import { INodeProperties } from 'n8n-workflow';
+
+export { create, deleteSpreadsheet };
+
+export const descriptions: INodeProperties[] = [
+ {
+ displayName: 'Operation',
+ name: 'operation',
+ type: 'options',
+ noDataExpression: true,
+ displayOptions: {
+ show: {
+ resource: ['spreadsheet'],
+ },
+ },
+ options: [
+ {
+ name: 'Create',
+ value: 'create',
+ description: 'Create a spreadsheet',
+ action: 'Create a spreadsheet',
+ },
+ {
+ name: 'Delete',
+ value: 'deleteSpreadsheet',
+ description: 'Delete a spreadsheet',
+ action: 'Delete a spreadsheet',
+ },
+ ],
+ default: 'create',
+ },
+ ...create.description,
+ ...deleteSpreadsheet.description,
+];
diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/actions/spreadsheet/create.operation.ts b/packages/nodes-base/nodes/Google/Sheet/v2/actions/spreadsheet/create.operation.ts
new file mode 100644
index 0000000000000..5ed5490bd045a
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/spreadsheet/create.operation.ts
@@ -0,0 +1,153 @@
+import { IExecuteFunctions } from 'n8n-core';
+import { IDataObject, INodeExecutionData } from 'n8n-workflow';
+import { SpreadSheetProperties } from '../../helpers/GoogleSheets.types';
+import { apiRequest } from '../../transport';
+
+export const description: SpreadSheetProperties = [
+ {
+ displayName: 'Title',
+ name: 'title',
+ type: 'string',
+ default: '',
+ displayOptions: {
+ show: {
+ resource: ['spreadsheet'],
+ operation: ['create'],
+ },
+ },
+ description: 'The title of the spreadsheet',
+ },
+ {
+ displayName: 'Sheets',
+ name: 'sheetsUi',
+ placeholder: 'Add Sheet',
+ type: 'fixedCollection',
+ typeOptions: {
+ multipleValues: true,
+ },
+ default: {},
+ options: [
+ {
+ name: 'sheetValues',
+ displayName: 'Sheet',
+ values: [
+ {
+ displayName: 'Title',
+ name: 'title',
+ type: 'string',
+ default: '',
+ description: 'Title of the property to create',
+ },
+ {
+ displayName: 'Hidden',
+ name: 'hidden',
+ type: 'boolean',
+ default: false,
+ description: 'Whether the Sheet should be hidden in the UI',
+ },
+ ],
+ },
+ ],
+ displayOptions: {
+ show: {
+ resource: ['spreadsheet'],
+ operation: ['create'],
+ },
+ },
+ },
+ {
+ displayName: 'Options',
+ name: 'options',
+ type: 'collection',
+ placeholder: 'Add Option',
+ default: {},
+ displayOptions: {
+ show: {
+ resource: ['spreadsheet'],
+ operation: ['create'],
+ },
+ },
+ options: [
+ {
+ displayName: 'Locale',
+ name: 'locale',
+ type: 'string',
+ default: '',
+ placeholder: 'en_US',
+ description: `The locale of the spreadsheet in one of the following formats:
+
+ - en (639-1)
+ - fil (639-2 if no 639-1 format exists)
+ - en_US (combination of ISO language an country)
+ `,
+ },
+ {
+ displayName: 'Recalculation Interval',
+ name: 'autoRecalc',
+ type: 'options',
+ options: [
+ {
+ name: 'Default',
+ value: '',
+ description: 'Default value',
+ },
+ {
+ name: 'On Change',
+ value: 'ON_CHANGE',
+ description: 'Volatile functions are updated on every change',
+ },
+ {
+ name: 'Minute',
+ value: 'MINUTE',
+ description: 'Volatile functions are updated on every change and every minute',
+ },
+ {
+ name: 'Hour',
+ value: 'HOUR',
+ description: 'Volatile functions are updated on every change and hourly',
+ },
+ ],
+ default: '',
+ description: 'Cell recalculation interval options',
+ },
+ ],
+ },
+];
+
+export async function execute(this: IExecuteFunctions): Promise {
+ const items = this.getInputData();
+ const returnData: IDataObject[] = [];
+
+ for (let i = 0; i < items.length; i++) {
+ const title = this.getNodeParameter('title', i) as string;
+ const sheetsUi = this.getNodeParameter('sheetsUi', i, {}) as IDataObject;
+
+ const body = {
+ properties: {
+ title,
+ autoRecalc: undefined as undefined | string,
+ locale: undefined as undefined | string,
+ },
+ sheets: [] as IDataObject[],
+ };
+
+ const options = this.getNodeParameter('options', i, {}) as IDataObject;
+
+ if (Object.keys(sheetsUi).length) {
+ const data = [];
+ const sheets = sheetsUi.sheetValues as IDataObject[];
+ for (const properties of sheets) {
+ data.push({ properties });
+ }
+ body.sheets = data;
+ }
+
+ body.properties!.autoRecalc = options.autoRecalc ? (options.autoRecalc as string) : undefined;
+ body.properties!.locale = options.locale ? (options.locale as string) : undefined;
+
+ const response = await apiRequest.call(this, 'POST', `/v4/spreadsheets`, body);
+ returnData.push(response);
+ }
+
+ return this.helpers.returnJsonArray(returnData);
+}
diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/actions/spreadsheet/delete.operation.ts b/packages/nodes-base/nodes/Google/Sheet/v2/actions/spreadsheet/delete.operation.ts
new file mode 100644
index 0000000000000..319dbd363c893
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/spreadsheet/delete.operation.ts
@@ -0,0 +1,103 @@
+import { IExecuteFunctions } from 'n8n-core';
+import { IDataObject, INodeExecutionData } from 'n8n-workflow';
+import { SpreadSheetProperties } from '../../helpers/GoogleSheets.types';
+import { apiRequest } from '../../transport';
+
+export const description: SpreadSheetProperties = [
+ // {
+ // displayName: 'Spreadsheet ID',
+ // name: 'spreadsheetId',
+ // type: 'string',
+ // default: '',
+ // displayOptions: {
+ // show: {
+ // resource: ['spreadsheet'],
+ // operation: ['deleteSpreadsheet'],
+ // },
+ // },
+ // },
+ {
+ displayName: 'Document',
+ name: 'documentId',
+ type: 'resourceLocator',
+ default: { mode: 'list', value: '' },
+ required: true,
+ modes: [
+ {
+ displayName: 'From List',
+ name: 'list',
+ type: 'list',
+ typeOptions: {
+ searchListMethod: 'spreadSheetsSearch',
+ searchable: true,
+ },
+ },
+ {
+ displayName: 'By URL',
+ name: 'url',
+ type: 'string',
+ extractValue: {
+ type: 'regex',
+ regex:
+ 'https:\\/\\/(?:drive|docs)\\.google\\.com\\/\\w+\\/d\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)',
+ },
+ validation: [
+ {
+ type: 'regex',
+ properties: {
+ regex:
+ 'https:\\/\\/(?:drive|docs)\\.google.com\\/\\w+\\/d\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)',
+ errorMessage: 'Not a valid Google Drive File URL',
+ },
+ },
+ ],
+ },
+ {
+ displayName: 'By ID',
+ name: 'id',
+ type: 'string',
+ validation: [
+ {
+ type: 'regex',
+ properties: {
+ regex: '[a-zA-Z0-9\\-_]{2,}',
+ errorMessage: 'Not a valid Google Drive File ID',
+ },
+ },
+ ],
+ url: '=https://docs.google.com/spreadsheets/d/{{$value}}/edit',
+ },
+ ],
+ displayOptions: {
+ show: {
+ resource: ['spreadsheet'],
+ operation: ['deleteSpreadsheet'],
+ },
+ },
+ },
+];
+
+export async function execute(this: IExecuteFunctions): Promise {
+ const items = this.getInputData();
+ const returnData: IDataObject[] = [];
+
+ for (let i = 0; i < items.length; i++) {
+ // const spreadsheetId = this.getNodeParameter('spreadsheetId', i) as string;
+ const documentId = this.getNodeParameter('documentId', i, undefined, {
+ extractValue: true,
+ }) as string;
+
+ await apiRequest.call(
+ this,
+ 'DELETE',
+ '',
+ {},
+ {},
+ `https://www.googleapis.com/drive/v3/files/${documentId}`,
+ );
+
+ returnData.push({ success: true });
+ }
+
+ return this.helpers.returnJsonArray(returnData);
+}
diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/actions/versionDescription.ts b/packages/nodes-base/nodes/Google/Sheet/v2/actions/versionDescription.ts
new file mode 100644
index 0000000000000..f2d4e5f8be637
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/versionDescription.ts
@@ -0,0 +1,79 @@
+/* eslint-disable n8n-nodes-base/node-filename-against-convention */
+import { INodeTypeDescription } from 'n8n-workflow';
+
+import * as sheet from './sheet/Sheet.resource';
+import * as spreadsheet from './spreadsheet/SpreadSheet.resource';
+
+export const versionDescription: INodeTypeDescription = {
+ displayName: 'Google Sheets',
+ name: 'googleSheets',
+ icon: 'file:googleSheets.svg',
+ group: ['input', 'output'],
+ version: 2,
+ subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
+ description: 'Read, update and write data to Google Sheets',
+ defaults: {
+ name: 'Google Sheets',
+ },
+ inputs: ['main'],
+ outputs: ['main'],
+ credentials: [
+ {
+ name: 'googleApi',
+ required: true,
+ displayOptions: {
+ show: {
+ authentication: ['serviceAccount'],
+ },
+ },
+ testedBy: 'googleApiCredentialTest',
+ },
+ {
+ name: 'googleSheetsOAuth2Api',
+ required: true,
+ displayOptions: {
+ show: {
+ authentication: ['oAuth2'],
+ },
+ },
+ },
+ ],
+ properties: [
+ {
+ displayName: 'Authentication',
+ name: 'authentication',
+ type: 'options',
+ options: [
+ {
+ name: 'Service Account',
+ value: 'serviceAccount',
+ },
+ {
+ // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
+ name: 'OAuth2 (recommended)',
+ value: 'oAuth2',
+ },
+ ],
+ default: 'oAuth2',
+ },
+ {
+ displayName: 'Resource',
+ name: 'resource',
+ type: 'options',
+ noDataExpression: true,
+ options: [
+ {
+ name: 'Document',
+ value: 'spreadsheet',
+ },
+ {
+ name: 'Sheet Within Document',
+ value: 'sheet',
+ },
+ ],
+ default: 'sheet',
+ },
+ ...sheet.descriptions,
+ ...spreadsheet.descriptions,
+ ],
+};
diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/helpers/GoogleSheet.ts b/packages/nodes-base/nodes/Google/Sheet/v2/helpers/GoogleSheet.ts
new file mode 100644
index 0000000000000..96ed5d7d297ba
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Sheet/v2/helpers/GoogleSheet.ts
@@ -0,0 +1,657 @@
+import { IDataObject, NodeOperationError } from 'n8n-workflow';
+import { IExecuteFunctions, ILoadOptionsFunctions } from 'n8n-core';
+import { apiRequest } from '../transport';
+import { utils as xlsxUtils } from 'xlsx';
+import { get } from 'lodash';
+import {
+ ILookupValues,
+ ISheetUpdateData,
+ SheetCellDecoded,
+ SheetRangeData,
+ SheetRangeDecoded,
+ ValueInputOption,
+ ValueRenderOption,
+} from './GoogleSheets.types';
+import { removeEmptyColumns } from './GoogleSheets.utils';
+
+export class GoogleSheet {
+ id: string;
+ executeFunctions: IExecuteFunctions | ILoadOptionsFunctions;
+
+ constructor(spreadsheetId: string, executeFunctions: IExecuteFunctions | ILoadOptionsFunctions) {
+ this.executeFunctions = executeFunctions;
+ this.id = spreadsheetId;
+ }
+
+ /**
+ * Encodes the range that also none latin character work
+ *
+ * @param {string} range
+ * @returns {string}
+ * @memberof GoogleSheet
+ */
+ private encodeRange(range: string): string {
+ if (range.includes('!')) {
+ const [sheet, ranges] = range.split('!');
+ return `${encodeURIComponent(sheet)}!${ranges}`;
+ }
+ return encodeURIComponent(range);
+ }
+
+ /**
+ * Clears values from a sheet
+ *
+ * @param {string} range
+ * @returns {Promise