fix(Google Sheets Node): Fix for append operation if no empty rows in sheet

This commit is contained in:
Michael Kret 2022-12-01 10:39:03 +02:00 committed by GitHub
parent 6d4e959884
commit 741c7da8b1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 211 additions and 171 deletions

View file

@ -24,15 +24,15 @@ export async function router(this: IExecuteFunctions): Promise<INodeExecutionDat
const googleSheet = new GoogleSheet(spreadsheetId, this);
let sheetWithinDocument = '';
let sheetId = '';
if (operation !== 'create') {
sheetWithinDocument = this.getNodeParameter('sheetName', 0, undefined, {
sheetId = this.getNodeParameter('sheetName', 0, undefined, {
extractValue: true,
}) as string;
}
if (sheetWithinDocument === 'gid=0') {
sheetWithinDocument = '0';
if (sheetId === 'gid=0') {
sheetId = '0';
}
let sheetName = '';
@ -41,17 +41,22 @@ export async function router(this: IExecuteFunctions): Promise<INodeExecutionDat
sheetName = spreadsheetId;
break;
case 'delete':
sheetName = sheetWithinDocument;
sheetName = sheetId;
break;
case 'remove':
sheetName = `${spreadsheetId}||${sheetWithinDocument}`;
sheetName = `${spreadsheetId}||${sheetId}`;
break;
default:
sheetName = await googleSheet.spreadsheetGetSheetNameById(sheetWithinDocument);
sheetName = await googleSheet.spreadsheetGetSheetNameById(sheetId);
}
operationResult.push(
...(await sheet[googleSheets.operation].execute.call(this, googleSheet, sheetName)),
...(await sheet[googleSheets.operation].execute.call(
this,
googleSheet,
sheetName,
sheetId,
)),
);
} else if (googleSheets.resource === 'spreadsheet') {
operationResult.push(...(await spreadsheet[googleSheets.operation].execute.call(this)));
@ -60,6 +65,15 @@ export async function router(this: IExecuteFunctions): Promise<INodeExecutionDat
if (this.continueOnFail()) {
operationResult.push({ json: this.getInputData(0)[0].json, error: err });
} else {
if (
err.message &&
(err.message.toLowerCase().includes('bad request') ||
err.message.toLowerCase().includes('uknown error')) &&
err.description
) {
err.message = err.description;
err.description = undefined;
}
throw err;
}
}

View file

@ -154,6 +154,7 @@ export async function execute(
this: IExecuteFunctions,
sheet: GoogleSheet,
sheetName: string,
sheetId: string,
): Promise<INodeExecutionData[]> {
const items = this.getInputData();
const dataMode = this.getNodeParameter('dataMode', 0) as string;
@ -176,6 +177,12 @@ export async function execute(
setData = mapFields.call(this, items.length);
}
if (setData.length === 0) {
return [];
} else {
await sheet.appendEmptyRowsOrColumns(sheetId, 1, 0);
}
await sheet.appendSheetData(
setData,
sheetName,

View file

@ -160,6 +160,7 @@ export async function execute(
this: IExecuteFunctions,
sheet: GoogleSheet,
sheetName: string,
sheetId: string,
): Promise<INodeExecutionData[]> {
const items = this.getInputData();
const valueInputMode = this.getNodeParameter('options.cellFormat', 0, 'RAW') as ValueInputOption;
@ -248,23 +249,22 @@ export async function execute(
} 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;
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;
if (columnNames.includes(columnName) === false) {
newColumns.add(columnName);
}
return acc;
},
{} as IDataObject,
);
acc[columnName] = entry.fieldValue as string;
} else {
acc[entry.column as string] = entry.fieldValue as string;
}
return acc;
}, {} as IDataObject);
fields[columnToMatchOn] = valueToMatchOn;
@ -300,6 +300,7 @@ export async function execute(
await sheet.batchUpdate(updateData, valueInputMode);
}
if (appendData.length) {
await sheet.appendEmptyRowsOrColumns(sheetId, 1, 0);
const lastRow = sheetData.length + 1;
await sheet.appendSheetData(
appendData,

View file

@ -247,23 +247,22 @@ export async function execute(
} 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;
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;
if (columnNames.includes(columnName) === false) {
newColumns.add(columnName);
}
return acc;
},
{} as IDataObject,
);
acc[columnName] = entry.fieldValue as string;
} else {
acc[entry.column as string] = entry.fieldValue as string;
}
return acc;
}, {} as IDataObject);
fields[columnToMatchOn] = valueToMatchOn;

View file

@ -187,6 +187,43 @@ export class GoogleSheet {
return response;
}
async appendEmptyRowsOrColumns(sheetId: string, rowsToAdd = 1, columnsToAdd = 1) {
const requests: IDataObject[] = [];
if (rowsToAdd > 0) {
requests.push({
appendDimension: {
sheetId,
dimension: 'ROWS',
length: rowsToAdd,
},
});
}
if (columnsToAdd > 0) {
requests.push({
appendDimension: {
sheetId,
dimension: 'COLUMNS',
length: columnsToAdd,
},
});
}
if (requests.length === 0) {
throw new Error('Must specify at least one column or row to add');
}
const response = await apiRequest.call(
this.executeFunctions,
'POST',
`/v4/spreadsheets/${this.id}:batchUpdate`,
{ requests },
);
return response;
}
/**
* Appends the cell values
*/

View file

@ -6,91 +6,83 @@ import {
NodeOperationError,
} from 'n8n-workflow';
import { ResourceLocator } from '../helpers/GoogleSheets.types';
import { getSpreadsheetId, sortLoadOptions } from '../helpers/GoogleSheets.utils';
import { apiRequest, apiRequestAllItems } from '../transport';
import { getSpreadsheetId } from '../helpers/GoogleSheets.utils';
import { apiRequest } from '../transport';
export async function spreadSheetsSearch(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
try {
const returnData: INodeListSearchItems[] = [];
const query: string[] = [];
if (filter) {
query.push(`name contains '${filter.replace("'", "\\'")}'`);
}
query.push("mimeType = 'application/vnd.google-apps.spreadsheet'");
const qs = {
pageSize: 50,
orderBy: 'modifiedTime desc',
fields: 'nextPageToken, files(id, name, webViewLink)',
q: query.join(' and '),
includeItemsFromAllDrives: true,
supportsAllDrives: true,
};
const sheets = await apiRequestAllItems.call(
this,
'files',
'GET',
'',
{},
qs,
'https://www.googleapis.com/drive/v3/files',
);
for (const sheet of sheets) {
returnData.push({
name: sheet.name as string,
value: sheet.id as string,
url: sheet.webViewLink as string,
});
}
return { results: sortLoadOptions(returnData) };
} catch (error) {
return { results: [] };
const query: string[] = [];
if (filter) {
query.push(`name contains '${filter.replace("'", "\\'")}'`);
}
query.push("mimeType = 'application/vnd.google-apps.spreadsheet'");
const qs = {
q: query.join(' and '),
pageToken: (paginationToken as string) || undefined,
fields: 'nextPageToken, files(id, name, webViewLink)',
orderBy: 'name_natural',
includeItemsFromAllDrives: true,
supportsAllDrives: true,
};
const res = await apiRequest.call(
this,
'GET',
'',
{},
qs,
'https://www.googleapis.com/drive/v3/files',
);
return {
results: res.files.map((sheet: IDataObject) => ({
name: sheet.name as string,
value: sheet.id as string,
url: sheet.webViewLink as string,
})),
paginationToken: res.nextPageToken,
};
}
export async function sheetsSearch(
this: ILoadOptionsFunctions,
_filter?: string,
): Promise<INodeListSearchResult> {
try {
const { mode, value } = this.getNodeParameter('documentId', 0) as IDataObject;
const spreadsheetId = getSpreadsheetId(mode as ResourceLocator, value as string);
const { mode, value } = this.getNodeParameter('documentId', 0) as IDataObject;
const spreadsheetId = getSpreadsheetId(mode as ResourceLocator, value as string);
const query = {
fields: 'sheets.properties',
};
const query = {
fields: 'sheets.properties',
};
const responseData = await apiRequest.call(
this,
'GET',
`/v4/spreadsheets/${spreadsheetId}`,
{},
query,
);
const responseData = await apiRequest.call(
this,
'GET',
`/v4/spreadsheets/${spreadsheetId}`,
{},
query,
);
if (responseData === undefined) {
throw new NodeOperationError(this.getNode(), 'No data got returned');
}
const returnData: INodeListSearchItems[] = [];
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 number) || 'gid=0',
//prettier-ignore
url: `https://docs.google.com/spreadsheets/d/${spreadsheetId}/edit#gid=${sheet.properties!.sheetId}`,
});
}
return { results: returnData };
} catch (error) {
return { results: [] };
if (responseData === undefined) {
throw new NodeOperationError(this.getNode(), 'No data got returned');
}
const returnData: INodeListSearchItems[] = [];
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 number) || 'gid=0',
//prettier-ignore
url: `https://docs.google.com/spreadsheets/d/${spreadsheetId}/edit#gid=${sheet.properties!.sheetId}`,
});
}
return { results: returnData };
}

View file

@ -9,73 +9,65 @@ import { getSpreadsheetId } from '../helpers/GoogleSheets.utils';
import { ResourceLocator } from '../helpers/GoogleSheets.types';
export async function getSheets(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
try {
const { mode, value } = this.getNodeParameter('documentId', 0) as IDataObject;
const spreadsheetId = getSpreadsheetId(mode as ResourceLocator, value as string);
const { mode, value } = this.getNodeParameter('documentId', 0) as IDataObject;
const spreadsheetId = getSpreadsheetId(mode as ResourceLocator, value as string);
const sheet = new GoogleSheet(spreadsheetId, this);
const responseData = await sheet.spreadsheetGetSheets();
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;
} catch (error) {
return [];
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;
}
export async function getSheetHeaderRow(
this: ILoadOptionsFunctions,
): Promise<INodePropertyOptions[]> {
try {
const { mode, value } = this.getNodeParameter('documentId', 0) as IDataObject;
const spreadsheetId = getSpreadsheetId(mode as ResourceLocator, value as string);
const { mode, value } = this.getNodeParameter('documentId', 0) as IDataObject;
const spreadsheetId = getSpreadsheetId(mode as ResourceLocator, value as string);
const sheet = new GoogleSheet(spreadsheetId, this);
let sheetWithinDocument = this.getNodeParameter('sheetName', undefined, {
extractValue: true,
}) as string;
const sheet = new GoogleSheet(spreadsheetId, this);
let sheetWithinDocument = this.getNodeParameter('sheetName', undefined, {
extractValue: true,
}) as string;
if (sheetWithinDocument === 'gid=0') {
sheetWithinDocument = '0';
}
const sheetName = await sheet.spreadsheetGetSheetNameById(sheetWithinDocument);
const sheetData = await sheet.getData(`${sheetName}!1:1`, 'FORMATTED_VALUE');
if (sheetData === undefined) {
throw new NodeOperationError(this.getNode(), 'No data got returned');
}
const columns = sheet.testFilter(sheetData, 0, 0);
const returnData: INodePropertyOptions[] = [];
for (const column of columns) {
returnData.push({
name: column as unknown as string,
value: column as unknown as string,
});
}
return returnData;
} catch (error) {
return [];
if (sheetWithinDocument === 'gid=0') {
sheetWithinDocument = '0';
}
const sheetName = await sheet.spreadsheetGetSheetNameById(sheetWithinDocument);
const sheetData = await sheet.getData(`${sheetName}!1:1`, 'FORMATTED_VALUE');
if (sheetData === undefined) {
throw new NodeOperationError(this.getNode(), 'No data got returned');
}
const columns = sheet.testFilter(sheetData, 0, 0);
const returnData: INodePropertyOptions[] = [];
for (const column of columns) {
returnData.push({
name: column as unknown as string,
value: column as unknown as string,
});
}
return returnData;
}
export async function getSheetHeaderRowAndAddColumn(

View file

@ -52,11 +52,10 @@ export async function apiRequest(
);
options.headers!.Authorization = `Bearer ${access_token}`;
//@ts-ignore
return await this.helpers.request(options);
return await this.helpers.request!(options);
} else {
//@ts-ignore
return await this.helpers.requestOAuth2.call(this, 'googleSheetsOAuth2Api', options);
return await this.helpers.requestOAuth2!.call(this, 'googleSheetsOAuth2Api', options);
}
} catch (error) {
if (error.code === 'ERR_OSSL_PEM_NO_START_LINE') {
@ -151,6 +150,5 @@ export function getAccessToken(
json: true,
};
//@ts-ignore
return this.helpers.request(options);
return this.helpers.request!(options);
}