mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 10:31:15 +00:00
feat(Google Sheets Node): Overhaul of node
This commit is contained in:
committed by
GitHub
parent
6eee155ecb
commit
d96d6f11db
@@ -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 <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
|
||||
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 <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
|
||||
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<INodeExecutionData[]> {
|
||||
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<string>();
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user