n8n/packages/nodes-base/nodes/Google/Sheet/GoogleSheetsTrigger.utils.ts

209 lines
5.7 KiB
TypeScript
Raw Normal View History

import type { IDataObject, IPollFunctions } from 'n8n-workflow';
import { apiRequest } from './v2/transport';
import type { SheetDataRow, SheetRangeData } from './v2/helpers/GoogleSheets.types';
import * as XLSX from 'xlsx';
import isEqual from 'lodash.isequal';
import zip from 'lodash.zip';
export const BINARY_MIME_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
type DiffData = Array<{
rowIndex: number;
previous: SheetDataRow;
current: SheetDataRow;
changeType: string;
}>;
export async function getRevisionFile(this: IPollFunctions, exportLink: string) {
const mimeType = BINARY_MIME_TYPE;
const response = await apiRequest.call(
this,
'GET',
'',
undefined,
{ mimeType },
exportLink,
undefined,
{
resolveWithFullResponse: true,
encoding: null,
json: false,
},
);
return Buffer.from(response.body as string);
}
export function sheetBinaryToArrayOfArrays(
data: Buffer,
sheetName: string,
range: string | undefined,
) {
const workbook = XLSX.read(data, { type: 'buffer', sheets: [sheetName] });
const sheet = workbook.Sheets[sheetName];
const sheetData: string[][] = sheet['!ref']
? XLSX.utils.sheet_to_json(sheet, { header: 1, defval: '', range })
: [];
const lastDataRowIndex = sheetData.reduce((lastRowIndex, row, rowIndex) => {
if (row.some((cell) => cell !== '')) {
return rowIndex;
}
return lastRowIndex;
}, 0);
return sheetData.slice(0, lastDataRowIndex + 1);
}
export function arrayOfArraysToJson(sheetData: SheetRangeData, columns: SheetDataRow) {
const returnData: IDataObject[] = [];
for (let rowIndex = 0; rowIndex < sheetData.length; rowIndex++) {
const rowData: IDataObject = {};
for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) {
const columnName = columns[columnIndex];
const cellValue = sheetData[rowIndex][columnIndex] || '';
rowData[columnName] = cellValue;
}
returnData.push(rowData);
}
return returnData;
}
const getSpecificColumns = (
row: SheetDataRow,
selectedColumns: SheetDataRow,
columns: SheetDataRow,
) => {
return row ? selectedColumns.map((column) => row[columns.indexOf(column) - 1]) : [];
};
const extractVersionData = (
data: DiffData,
version: 'previous' | 'current',
triggerEvent: string,
) => {
if (triggerEvent === 'anyUpdate') {
return data.map(({ [version]: entry, rowIndex, changeType }) =>
entry ? [rowIndex, changeType, ...entry] : [rowIndex, changeType],
);
}
return data.map(({ [version]: entry, rowIndex }) => (entry ? [rowIndex, ...entry] : [rowIndex]));
};
export function compareRevisions(
previous: SheetRangeData,
current: SheetRangeData,
keyRow: number,
includeInOutput: string,
columnsToWatch: string[],
dataStartIndex: number,
event: string,
) {
const dataLength = current.length > previous.length ? current.length : previous.length;
const columnsRowIndex = keyRow - 1;
const columnsInCurrent = current[columnsRowIndex];
const columnsInPrevious = previous[columnsRowIndex];
let columns: SheetDataRow =
event === 'anyUpdate' ? ['row_number', 'change_type'] : ['row_number'];
if (columnsInCurrent !== undefined && columnsInPrevious !== undefined) {
columns =
columnsInCurrent.length > columnsInPrevious.length
? columns.concat(columnsInCurrent)
: columns.concat(columnsInPrevious);
} else if (columnsInCurrent !== undefined) {
columns = columns.concat(columnsInCurrent);
} else if (columnsInPrevious !== undefined) {
columns = columns.concat(columnsInPrevious);
}
const diffData: DiffData = [];
for (let i = dataStartIndex; i < dataLength; i++) {
if (i === columnsRowIndex) {
continue;
}
// sheets API omits trailing empty columns, xlsx does not - so we need to pad the shorter array
if (Array.isArray(current[i]) && Array.isArray(previous[i])) {
while (current[i].length < previous[i].length) {
current[i].push('');
}
}
// if columnsToWatch is defined, only compare those columns
if (columnsToWatch?.length) {
const currentRow = getSpecificColumns(current[i], columnsToWatch, columns);
const previousRow = getSpecificColumns(previous[i], columnsToWatch, columns);
if (isEqual(currentRow, previousRow)) continue;
} else {
if (isEqual(current[i], previous[i])) continue;
}
if (event === 'rowUpdate' && (!previous[i] || previous[i].every((cell) => cell === '')))
continue;
let changeType = 'updated';
if (previous[i] === undefined) {
previous[i] = current[i].map(() => '');
changeType = 'added';
}
if (current[i] === undefined) continue;
diffData.push({
rowIndex: i + 1,
previous: previous[i],
current: current[i],
changeType,
});
}
if (includeInOutput === 'old') {
return arrayOfArraysToJson(extractVersionData(diffData, 'previous', event), columns);
}
if (includeInOutput === 'both') {
const previousData = arrayOfArraysToJson(
extractVersionData(diffData, 'previous', event),
columns,
).map((row) => ({ previous: row }));
const currentData = arrayOfArraysToJson(
extractVersionData(diffData, 'current', event),
columns,
).map((row) => ({ current: row }));
const differences = currentData.map(({ current: currentRow }, index) => {
const { row_number, ...rest } = currentRow;
const returnData: IDataObject = {};
returnData.row_number = row_number;
Object.keys(rest).forEach((key) => {
const previousValue = previousData[index].previous[key];
const currentValue = currentRow[key];
if (isEqual(previousValue, currentValue)) return;
returnData[key] = {
previous: previousValue,
current: currentValue,
};
});
return { differences: returnData };
});
return zip(previousData, currentData, differences).map((row) => Object.assign({}, ...row));
}
return arrayOfArraysToJson(extractVersionData(diffData, 'current', event), columns);
}