feat(Microsoft Excel 365 Node): Overhaul

This commit is contained in:
Michael Kret
2023-05-02 12:44:25 +03:00
committed by GitHub
parent 25fe14be56
commit 5364a2dff3
75 changed files with 8049 additions and 675 deletions

View File

@@ -0,0 +1,79 @@
import type { INodeProperties } from 'n8n-workflow';
import * as append from './append.operation';
import * as clear from './clear.operation';
import * as deleteWorksheet from './deleteWorksheet.operation';
import * as getAll from './getAll.operation';
import * as readRows from './readRows.operation';
import * as update from './update.operation';
import * as upsert from './upsert.operation';
export { append, clear, deleteWorksheet, getAll, readRows, update, upsert };
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['worksheet'],
},
},
options: [
{
name: 'Append',
value: 'append',
description: 'Append data to sheet',
action: 'Append data to sheet',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-option-name-wrong-for-upsert
name: 'Append or Update',
value: 'upsert',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-upsert
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 sheet',
action: 'Clear sheet',
},
{
name: 'Delete',
value: 'deleteWorksheet',
description: 'Delete sheet',
action: 'Delete sheet',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Get a list of sheets',
action: 'Get sheets',
},
{
name: 'Get Rows',
value: 'readRows',
description: 'Retrieve a list of sheet rows',
action: 'Get rows from sheet',
},
{
name: 'Update',
value: 'update',
description: 'Update rows of a sheet or sheet range',
action: 'Update sheet',
},
],
default: 'getAll',
},
...append.description,
...clear.description,
...deleteWorksheet.description,
...getAll.description,
...readRows.description,
...update.description,
...upsert.description,
];

View File

@@ -0,0 +1,227 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { processJsonInput, updateDisplayOptions } from '../../../../../../utils/utilities';
import type { ExcelResponse } from '../../helpers/interfaces';
import { prepareOutput } from '../../helpers/utils';
import { microsoftApiRequest } from '../../transport';
import { workbookRLC, worksheetRLC } from '../common.descriptions';
const properties: INodeProperties[] = [
workbookRLC,
worksheetRLC,
{
displayName: 'Data Mode',
name: 'dataMode',
type: 'options',
default: 'define',
options: [
{
name: 'Auto-Map Input Data to Columns',
value: 'autoMap',
description: 'Use when node input properties match destination column names',
},
{
name: 'Map Each Column Below',
value: 'define',
description: 'Set the value for each destination column',
},
{
name: 'Raw',
value: 'raw',
description: 'Send raw data as JSON',
},
],
},
{
displayName: 'Data',
name: 'data',
type: 'json',
default: '',
required: true,
placeholder: 'e.g. [["Sara","1/2/2006","Berlin"],["George","5/3/2010","Paris"]]',
description: 'Raw values for the specified range as array of string arrays in JSON format',
displayOptions: {
show: {
dataMode: ['raw'],
},
},
},
{
displayName: 'Values to Send',
name: 'fieldsUi',
placeholder: 'Add Field',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
dataMode: ['define'],
},
},
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: ['worksheet.value'],
loadOptionsMethod: 'getWorksheetColumnRow',
},
default: '',
},
{
displayName: 'Value',
name: 'fieldValue',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'RAW Data',
name: 'rawData',
type: 'boolean',
// eslint-disable-next-line n8n-nodes-base/node-param-default-wrong-for-boolean
default: 0,
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',
required: true,
displayOptions: {
show: {
rawData: [true],
},
},
description: 'The name of the property into which to write the RAW data',
},
],
},
];
const displayOptions = {
show: {
resource: ['worksheet'],
operation: ['append'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
items: INodeExecutionData[],
): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
const workbookId = this.getNodeParameter('workbook', 0, undefined, {
extractValue: true,
}) as string;
const worksheetId = this.getNodeParameter('worksheet', 0, undefined, {
extractValue: true,
}) as string;
const dataMode = this.getNodeParameter('dataMode', 0) as string;
const worksheetData = await microsoftApiRequest.call(
this,
'GET',
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/usedRange`,
);
let values: string[][] = [];
if (dataMode === 'raw') {
const data = this.getNodeParameter('data', 0);
values = processJsonInput(data, 'Data') as string[][];
}
const columnsRow = (worksheetData.values as string[][])[0];
if (dataMode === 'autoMap') {
const itemsData = items.map((item) => item.json);
for (const item of itemsData) {
const updateRow: string[] = [];
for (const column of columnsRow) {
updateRow.push(item[column] as string);
}
values.push(updateRow);
}
}
if (dataMode === 'define') {
const itemsData: IDataObject[] = [];
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
const updateData: IDataObject = {};
const definedFields = this.getNodeParameter('fieldsUi.values', itemIndex, []) as Array<{
column: string;
fieldValue: string;
}>;
for (const entry of definedFields) {
updateData[entry.column] = entry.fieldValue;
}
itemsData.push(updateData);
}
for (const item of itemsData) {
const updateRow: string[] = [];
for (const column of columnsRow) {
updateRow.push(item[column] as string);
}
values.push(updateRow);
}
}
const { address } = worksheetData;
const usedRange = address.split('!')[1];
const [rangeFrom, rangeTo] = usedRange.split(':');
const cellDataFrom = rangeFrom.match(/([a-zA-Z]{1,10})([0-9]{0,10})/) || [];
const cellDataTo = rangeTo.match(/([a-zA-Z]{1,10})([0-9]{0,10})/) || [];
const from = `${cellDataFrom[1]}${Number(cellDataTo[2]) + 1}`;
const to = `${cellDataTo[1]}${Number(cellDataTo[2]) + Number(values.length)}`;
const responseData: ExcelResponse = await microsoftApiRequest.call(
this,
'PATCH',
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${from}:${to}')`,
{ values },
);
const rawData = this.getNodeParameter('options.rawData', 0, false) as boolean;
const dataProperty = this.getNodeParameter('options.dataProperty', 0, 'data') as string;
returnData.push(
...prepareOutput(this.getNode(), responseData, { columnsRow, dataProperty, rawData }),
);
return returnData;
}

View File

@@ -0,0 +1,121 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../../utils/utilities';
import { microsoftApiRequest } from '../../transport';
import { workbookRLC, worksheetRLC } from '../common.descriptions';
const properties: INodeProperties[] = [
workbookRLC,
worksheetRLC,
{
displayName: 'Apply To',
name: 'applyTo',
type: 'options',
//values in capital case as required by api
options: [
{
name: 'All',
value: 'All',
description: 'Clear data in cells and remove all formatting',
},
{
name: 'Formats',
value: 'Formats',
description: 'Clear formatting(e.g. font size, color) of cells',
},
{
name: 'Contents',
value: 'Contents',
description: 'Clear data contained in cells',
},
],
default: 'All',
},
{
displayName: 'Select a Range',
name: 'useRange',
type: 'boolean',
default: false,
},
{
displayName: 'Range',
name: 'range',
type: 'string',
displayOptions: {
show: {
useRange: [true],
},
},
placeholder: 'e.g. A1:B2',
default: '',
description: 'The sheet range that would be cleared, specified using a A1-style notation',
hint: 'Leave blank for entire worksheet',
},
];
const displayOptions = {
show: {
resource: ['worksheet'],
operation: ['clear'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
items: INodeExecutionData[],
): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
for (let i = 0; i < items.length; i++) {
try {
const workbookId = this.getNodeParameter('workbook', i, undefined, {
extractValue: true,
}) as string;
const worksheetId = this.getNodeParameter('worksheet', i, undefined, {
extractValue: true,
}) as string;
const applyTo = this.getNodeParameter('applyTo', i) as string;
const useRange = this.getNodeParameter('useRange', i, false) as boolean;
if (!useRange) {
await microsoftApiRequest.call(
this,
'POST',
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range/clear`,
{ applyTo },
);
} else {
const range = this.getNodeParameter('range', i, '') as string;
await microsoftApiRequest.call(
this,
'POST',
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')/clear`,
{ applyTo },
);
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true }),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} catch (error) {
if (this.continueOnFail()) {
const executionErrorData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ error: error.message }),
{ itemData: { item: i } },
);
returnData.push(...executionErrorData);
continue;
}
throw error;
}
}
return returnData;
}

View File

@@ -0,0 +1,60 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../../utils/utilities';
import { microsoftApiRequest } from '../../transport';
import { workbookRLC, worksheetRLC } from '../common.descriptions';
const properties: INodeProperties[] = [workbookRLC, worksheetRLC];
const displayOptions = {
show: {
resource: ['worksheet'],
operation: ['deleteWorksheet'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
items: INodeExecutionData[],
): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
for (let i = 0; i < items.length; i++) {
try {
const workbookId = this.getNodeParameter('workbook', i, undefined, {
extractValue: true,
}) as string;
const worksheetId = this.getNodeParameter('worksheet', i, undefined, {
extractValue: true,
}) as string;
await microsoftApiRequest.call(
this,
'DELETE',
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}`,
);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true }),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} catch (error) {
if (this.continueOnFail()) {
const executionErrorData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ error: error.message }),
{ itemData: { item: i } },
);
returnData.push(...executionErrorData);
continue;
}
throw error;
}
}
return returnData;
}

View File

@@ -0,0 +1,119 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../../utils/utilities';
import { microsoftApiRequest, microsoftApiRequestAllItems } from '../../transport';
import { workbookRLC } from '../common.descriptions';
const properties: INodeProperties[] = [
workbookRLC,
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'Max number of results to return',
},
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
placeholder: 'Add Filter',
default: {},
options: [
{
displayName: 'Fields',
name: 'fields',
type: 'string',
default: '',
description: 'A comma-separated list of the fields to include in the response',
},
],
},
];
const displayOptions = {
show: {
resource: ['worksheet'],
operation: ['getAll'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
items: INodeExecutionData[],
): Promise<INodeExecutionData[]> {
//https://docs.microsoft.com/en-us/graph/api/workbook-list-worksheets?view=graph-rest-1.0&tabs=http
const returnData: INodeExecutionData[] = [];
for (let i = 0; i < items.length; i++) {
const qs: IDataObject = {};
try {
const returnAll = this.getNodeParameter('returnAll', i);
const workbookId = this.getNodeParameter('workbook', i, undefined, {
extractValue: true,
}) as string;
const filters = this.getNodeParameter('filters', i);
if (filters.fields) {
qs.$select = filters.fields;
}
let responseData;
if (returnAll) {
responseData = await microsoftApiRequestAllItems.call(
this,
'value',
'GET',
`/drive/items/${workbookId}/workbook/worksheets`,
{},
qs,
);
} else {
qs.$top = this.getNodeParameter('limit', i);
responseData = await microsoftApiRequest.call(
this,
'GET',
`/drive/items/${workbookId}/workbook/worksheets`,
{},
qs,
);
responseData = responseData.value;
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject[]),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} catch (error) {
if (this.continueOnFail()) {
const executionErrorData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ error: error.message }),
{ itemData: { item: i } },
);
returnData.push(...executionErrorData);
continue;
}
throw error;
}
}
return returnData;
}

View File

@@ -0,0 +1,199 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../../utils/utilities';
import type { ExcelResponse } from '../../helpers/interfaces';
import { prepareOutput } from '../../helpers/utils';
import { microsoftApiRequest } from '../../transport';
import { workbookRLC, worksheetRLC } from '../common.descriptions';
const properties: INodeProperties[] = [
workbookRLC,
worksheetRLC,
{
displayName: 'Select a Range',
name: 'useRange',
type: 'boolean',
default: false,
},
{
displayName: 'Range',
name: 'range',
type: 'string',
placeholder: 'e.g. A1:B2',
default: '',
description: 'The sheet range to read the data from specified using a A1-style notation',
hint: 'Leave blank to return entire sheet',
displayOptions: {
show: {
useRange: [true],
},
},
},
{
displayName: 'Header Row',
name: 'keyRow',
type: 'number',
typeOptions: {
minValue: 0,
},
default: 0,
hint: 'Index of the row which contains the column names',
description: "Relative to selected 'Range', first row index is 0",
displayOptions: {
show: {
useRange: [true],
},
},
},
{
displayName: 'First Data Row',
name: 'dataStartRow',
type: 'number',
typeOptions: {
minValue: 0,
},
default: 1,
hint: 'Index of first row which contains the actual data',
description: "Relative to selected 'Range', first row index is 0",
displayOptions: {
show: {
useRange: [true],
},
},
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'RAW Data',
name: 'rawData',
type: 'boolean',
// eslint-disable-next-line n8n-nodes-base/node-param-default-wrong-for-boolean
default: 0,
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',
required: true,
displayOptions: {
show: {
rawData: [true],
},
},
description: 'The name of the property into which to write the RAW data',
},
{
displayName: 'Fields',
name: 'fields',
type: 'string',
default: '',
description: 'Fields the response will containt. Multiple can be added separated by ,.',
displayOptions: {
show: {
rawData: [true],
},
},
},
],
},
];
const displayOptions = {
show: {
resource: ['worksheet'],
operation: ['readRows'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
items: INodeExecutionData[],
): Promise<INodeExecutionData[]> {
//https://docs.microsoft.com/en-us/graph/api/worksheet-range?view=graph-rest-1.0&tabs=http
const returnData: INodeExecutionData[] = [];
for (let i = 0; i < items.length; i++) {
const qs: IDataObject = {};
try {
const workbookId = this.getNodeParameter('workbook', i, undefined, {
extractValue: true,
}) as string;
const worksheetId = this.getNodeParameter('worksheet', i, undefined, {
extractValue: true,
}) as string;
const options = this.getNodeParameter('options', i, {});
const range = this.getNodeParameter('range', i, '') as string;
const rawData = (options.rawData as boolean) || false;
if (rawData && options.fields) {
qs.$select = options.fields;
}
let responseData;
if (range) {
responseData = await microsoftApiRequest.call(
this,
'GET',
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`,
{},
qs,
);
} else {
responseData = await microsoftApiRequest.call(
this,
'GET',
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/usedRange`,
{},
qs,
);
}
if (!rawData) {
const keyRow = this.getNodeParameter('keyRow', i, 0) as number;
const firstDataRow = this.getNodeParameter('dataStartRow', i, 1) as number;
returnData.push(
...prepareOutput(this.getNode(), responseData as ExcelResponse, {
rawData,
keyRow,
firstDataRow,
}),
);
} else {
const dataProperty = (options.dataProperty as string) || 'data';
returnData.push(
...prepareOutput(this.getNode(), responseData as ExcelResponse, {
rawData,
dataProperty,
}),
);
}
} catch (error) {
if (this.continueOnFail()) {
const executionErrorData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ error: error.message }),
{ itemData: { item: i } },
);
returnData.push(...executionErrorData);
continue;
}
throw error;
}
}
return returnData;
}

View File

@@ -0,0 +1,376 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { processJsonInput, updateDisplayOptions } from '../../../../../../utils/utilities';
import type { ExcelResponse, UpdateSummary } from '../../helpers/interfaces';
import { prepareOutput, updateByAutoMaping, updateByDefinedValues } from '../../helpers/utils';
import { microsoftApiRequest } from '../../transport';
import { workbookRLC, worksheetRLC } from '../common.descriptions';
const properties: INodeProperties[] = [
workbookRLC,
worksheetRLC,
{
displayName: 'Select a Range',
name: 'useRange',
type: 'boolean',
default: false,
},
{
displayName: 'Range',
name: 'range',
type: 'string',
displayOptions: {
show: {
dataMode: ['autoMap', 'define'],
useRange: [true],
},
},
placeholder: 'e.g. A1:B2',
default: '',
description:
'The sheet range to read the data from specified using a A1-style notation. Leave blank to use whole used range in the sheet.',
hint: 'First row must contain column names',
},
{
displayName: 'Range',
name: 'range',
type: 'string',
displayOptions: {
show: {
dataMode: ['raw'],
useRange: [true],
},
},
placeholder: 'e.g. A1:B2',
default: '',
description: 'The sheet range to read the data from specified using a A1-style notation',
hint: 'Leave blank for entire worksheet',
},
{
displayName: 'Data Mode',
name: 'dataMode',
type: 'options',
default: 'define',
options: [
{
name: 'Auto-Map Input Data to Columns',
value: 'autoMap',
description: 'Use when node input properties match destination column names',
},
{
name: 'Map Each Column Below',
value: 'define',
description: 'Set the value for each destination column',
},
{
name: 'Raw',
value: 'raw',
description:
'Send raw data as JSON, the whole selected range would be updated with the new values',
},
],
},
{
displayName: 'Data',
name: 'data',
type: 'json',
default: '',
required: true,
placeholder: 'e.g. [["Sara","1/2/2006","Berlin"],["George","5/3/2010","Paris"]]',
description:
'Raw values for the specified range as array of string arrays in JSON format. Should match the specified range: one array item for each row.',
displayOptions: {
show: {
dataMode: ['raw'],
},
},
},
{
// 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: ['worksheet.value', 'workbook.value', 'range'],
loadOptionsMethod: 'getWorksheetColumnRow',
},
default: '',
hint: "Used to find the correct row to update. Doesn't get changed.",
displayOptions: {
show: {
dataMode: ['autoMap', 'define'],
},
},
},
{
displayName: 'Value of Column to Match On',
name: 'valueToMatchOn',
type: 'string',
default: '',
displayOptions: {
show: {
dataMode: ['define'],
},
},
},
{
displayName: 'Values to Send',
name: 'fieldsUi',
placeholder: 'Add Field',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
dataMode: ['define'],
},
},
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: ['columnToMatchOn', 'range'],
loadOptionsMethod: 'getWorksheetColumnRowSkipColumnToMatchOn',
},
default: '',
},
{
displayName: 'Value',
name: 'fieldValue',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'RAW Data',
name: 'rawData',
type: 'boolean',
// eslint-disable-next-line n8n-nodes-base/node-param-default-wrong-for-boolean
default: 0,
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',
required: true,
displayOptions: {
show: {
rawData: [true],
},
},
description: 'The name of the property into which to write the RAW data',
},
{
displayName: 'Fields',
name: 'fields',
type: 'string',
default: '',
description: 'Fields the response will containt. Multiple can be added separated by ,.',
displayOptions: {
show: {
rawData: [true],
},
},
},
{
displayName: 'Update All Matches',
name: 'updateAll',
type: 'boolean',
default: false,
description: 'Whether to update all matching rows or just the first match',
displayOptions: {
hide: {
'/dataMode': ['raw'],
},
},
},
],
},
];
const displayOptions = {
show: {
resource: ['worksheet'],
operation: ['update'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
items: INodeExecutionData[],
): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
try {
const options = this.getNodeParameter('options', 0, {});
const rawData = options.rawData as boolean;
const dataProperty = options.dataProperty ? (options.dataProperty as string) : 'data';
const qs: IDataObject = {};
if (rawData && options.fields) {
qs.$select = options.fields;
}
const workbookId = this.getNodeParameter('workbook', 0, undefined, {
extractValue: true,
}) as string;
const worksheetId = this.getNodeParameter('worksheet', 0, undefined, {
extractValue: true,
}) as string;
let range = this.getNodeParameter('range', 0, '') as string;
const dataMode = this.getNodeParameter('dataMode', 0) as string;
let worksheetData: IDataObject = {};
if (range && dataMode !== 'raw') {
worksheetData = await microsoftApiRequest.call(
this,
'PATCH',
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`,
);
}
//get used range if range not provided; if 'raw' mode fetch only address information
if (range === '') {
const query: IDataObject = {};
if (dataMode === 'raw') {
query.select = 'address';
}
worksheetData = await microsoftApiRequest.call(
this,
'GET',
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/usedRange`,
undefined,
query,
);
range = (worksheetData.address as string).split('!')[1];
}
let responseData;
if (dataMode === 'raw') {
const data = this.getNodeParameter('data', 0);
const values = processJsonInput(data, 'Data') as string[][];
responseData = await microsoftApiRequest.call(
this,
'PATCH',
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`,
{ values },
qs,
);
returnData.push(
...prepareOutput(this.getNode(), responseData as ExcelResponse, {
rawData,
dataProperty,
}),
);
} else {
if (worksheetData.values === undefined || (worksheetData.values as string[][]).length <= 1) {
throw new NodeOperationError(
this.getNode(),
'No data found in the specified range, mapping not possible, you can use raw mode instead to update selected range',
);
}
const updateAll = this.getNodeParameter('options.updateAll', 0, false) as boolean;
let updateSummary: UpdateSummary = {
updatedData: [],
updatedRows: [],
appendData: [],
};
if (dataMode === 'define') {
updateSummary = updateByDefinedValues.call(
this,
items.length,
worksheetData.values as string[][],
updateAll,
);
}
if (dataMode === 'autoMap') {
const columnToMatchOn = this.getNodeParameter('columnToMatchOn', 0) as string;
if (!items.some(({ json }) => json[columnToMatchOn] !== undefined)) {
throw new NodeOperationError(
this.getNode(),
`Any item in input data contains column '${columnToMatchOn}', that is selected to match on`,
);
}
updateSummary = updateByAutoMaping(
items,
worksheetData.values as string[][],
columnToMatchOn,
updateAll,
);
}
responseData = await microsoftApiRequest.call(
this,
'PATCH',
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`,
{ values: updateSummary.updatedData },
);
const { updatedRows } = updateSummary;
returnData.push(
...prepareOutput(this.getNode(), responseData as ExcelResponse, {
updatedRows,
rawData,
dataProperty,
}),
);
}
} catch (error) {
if (this.continueOnFail()) {
const executionErrorData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ error: error.message }),
{ itemData: { item: 0 } },
);
returnData.push(...executionErrorData);
} else {
throw error;
}
}
return returnData;
}

View File

@@ -0,0 +1,333 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { processJsonInput, updateDisplayOptions } from '../../../../../../utils/utilities';
import type { ExcelResponse, UpdateSummary } from '../../helpers/interfaces';
import { prepareOutput, updateByAutoMaping, updateByDefinedValues } from '../../helpers/utils';
import { microsoftApiRequest } from '../../transport';
import { workbookRLC, worksheetRLC } from '../common.descriptions';
const properties: INodeProperties[] = [
workbookRLC,
worksheetRLC,
{
displayName: 'Select a Range',
name: 'useRange',
type: 'boolean',
default: false,
},
{
displayName: 'Range',
name: 'range',
type: 'string',
displayOptions: {
show: {
dataMode: ['autoMap', 'define'],
useRange: [true],
},
},
placeholder: 'e.g. A1:B2',
default: '',
description:
'The sheet range to read the data from specified using a A1-style notation. Leave blank to use whole used range in the sheet.',
hint: 'First row must contain column names',
},
{
displayName: 'Data Mode',
name: 'dataMode',
type: 'options',
default: 'define',
options: [
{
name: 'Auto-Map Input Data to Columns',
value: 'autoMap',
description: 'Use when node input properties match destination column names',
},
{
name: 'Map Each Column Below',
value: 'define',
description: 'Set the value for each destination column',
},
],
},
{
// 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: ['worksheet.value', 'workbook.value', 'range'],
loadOptionsMethod: 'getWorksheetColumnRow',
},
default: '',
hint: "Used to find the correct row to update. Doesn't get changed.",
displayOptions: {
show: {
dataMode: ['autoMap', 'define'],
},
},
},
{
displayName: 'Value of Column to Match On',
name: 'valueToMatchOn',
type: 'string',
default: '',
displayOptions: {
show: {
dataMode: ['define'],
},
},
},
{
displayName: 'Values to Send',
name: 'fieldsUi',
placeholder: 'Add Field',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
dataMode: ['define'],
},
},
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: ['columnToMatchOn', 'range'],
loadOptionsMethod: 'getWorksheetColumnRowSkipColumnToMatchOn',
},
default: '',
},
{
displayName: 'Value',
name: 'fieldValue',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'RAW Data',
name: 'rawData',
type: 'boolean',
// eslint-disable-next-line n8n-nodes-base/node-param-default-wrong-for-boolean
default: 0,
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',
required: true,
displayOptions: {
show: {
rawData: [true],
},
},
description: 'The name of the property into which to write the RAW data',
},
{
displayName: 'Update All Matches',
name: 'updateAll',
type: 'boolean',
default: false,
description: 'Whether to update all matching rows or just the first match',
},
],
},
];
const displayOptions = {
show: {
resource: ['worksheet'],
operation: ['upsert'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
items: INodeExecutionData[],
): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
try {
const workbookId = this.getNodeParameter('workbook', 0, undefined, {
extractValue: true,
}) as string;
const worksheetId = this.getNodeParameter('worksheet', 0, undefined, {
extractValue: true,
}) as string;
let range = this.getNodeParameter('range', 0, '') as string;
const dataMode = this.getNodeParameter('dataMode', 0) as string;
let worksheetData: IDataObject = {};
if (range && dataMode !== 'raw') {
worksheetData = await microsoftApiRequest.call(
this,
'PATCH',
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`,
);
}
//get used range if range not provided; if 'raw' mode fetch only address information
if (range === '') {
const query: IDataObject = {};
if (dataMode === 'raw') {
query.select = 'address';
}
worksheetData = await microsoftApiRequest.call(
this,
'GET',
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/usedRange`,
undefined,
query,
);
range = (worksheetData.address as string).split('!')[1];
}
let responseData;
if (dataMode === 'raw') {
const data = this.getNodeParameter('data', 0);
const values = processJsonInput(data, 'Data') as string[][];
responseData = await microsoftApiRequest.call(
this,
'PATCH',
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`,
{ values },
);
}
if (
dataMode !== 'raw' &&
(worksheetData.values === undefined || (worksheetData.values as string[][]).length <= 1)
) {
throw new NodeOperationError(
this.getNode(),
'No data found in the specified range, mapping not possible, you can use raw mode instead to update selected range',
);
}
const updateAll = this.getNodeParameter('options.updateAll', 0, false) as boolean;
let updateSummary: UpdateSummary = {
updatedData: [],
updatedRows: [],
appendData: [],
};
if (dataMode === 'define') {
updateSummary = updateByDefinedValues.call(
this,
items.length,
worksheetData.values as string[][],
updateAll,
);
}
if (dataMode === 'autoMap') {
const columnToMatchOn = this.getNodeParameter('columnToMatchOn', 0) as string;
if (!items.some(({ json }) => json[columnToMatchOn] !== undefined)) {
throw new NodeOperationError(
this.getNode(),
`Any item in input data contains column '${columnToMatchOn}', that is selected to match on`,
);
}
updateSummary = updateByAutoMaping(
items,
worksheetData.values as string[][],
columnToMatchOn,
updateAll,
);
}
if (updateSummary.appendData.length) {
const appendValues: string[][] = [];
const columnsRow = (worksheetData.values as string[][])[0];
for (const [index, item] of updateSummary.appendData.entries()) {
const updateRow: string[] = [];
for (const column of columnsRow) {
updateRow.push(item[column] as string);
}
appendValues.push(updateRow);
updateSummary.updatedRows.push(index + updateSummary.updatedData.length);
}
updateSummary.updatedData = updateSummary.updatedData.concat(appendValues);
const [rangeFrom, rangeTo] = range.split(':');
const cellDataTo = rangeTo.match(/([a-zA-Z]{1,10})([0-9]{0,10})/) || [];
range = `${rangeFrom}:${cellDataTo[1]}${Number(cellDataTo[2]) + appendValues.length}`;
}
responseData = await microsoftApiRequest.call(
this,
'PATCH',
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`,
{ values: updateSummary.updatedData },
);
const { updatedRows } = updateSummary;
const rawData = this.getNodeParameter('options.rawData', 0, false) as boolean;
const dataProperty = this.getNodeParameter('options.dataProperty', 0, 'data') as string;
returnData.push(
...prepareOutput(this.getNode(), responseData as ExcelResponse, {
updatedRows,
rawData,
dataProperty,
}),
);
} catch (error) {
if (this.continueOnFail()) {
const executionErrorData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ error: error.message }),
{ itemData: { item: 0 } },
);
returnData.push(...executionErrorData);
} else {
throw error;
}
}
return returnData;
}