From 5364a2dff32e05147b8e9dd392038eb36791e5dc Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Tue, 2 May 2023 12:44:25 +0300 Subject: [PATCH] feat(Microsoft Excel 365 Node): Overhaul --- .../Microsoft/Excel/MicrosoftExcel.node.ts | 700 +----------------- .../Excel/test/v2/node/table/addTable.test.ts | 72 ++ .../test/v2/node/table/addTable.workflow.json | 78 ++ .../Excel/test/v2/node/table/append.test.ts | 93 +++ .../test/v2/node/table/append.workflow.json | 104 +++ .../test/v2/node/table/convertToRange.test.ts | 68 ++ .../node/table/convertToRange.workflow.json | 79 ++ .../test/v2/node/table/deleteTable.test.ts | 59 ++ .../v2/node/table/deleteTable.workflow.json | 72 ++ .../test/v2/node/table/getColumns.test.ts | 73 ++ .../v2/node/table/getColumns.workflow.json | 88 +++ .../Excel/test/v2/node/table/getRows.test.ts | 97 +++ .../test/v2/node/table/getRows.workflow.json | 85 +++ .../Excel/test/v2/node/table/lookup.test.ts | 114 +++ .../test/v2/node/table/lookup.workflow.json | 88 +++ .../v2/node/workbook/addWorksheet.test.ts | 89 +++ .../node/workbook/addWorksheet.workflow.json | 65 ++ .../v2/node/workbook/deleteWorkbook.test.ts | 59 ++ .../workbook/deleteWorkbook.workflow.json | 59 ++ .../test/v2/node/workbook/getAll.test.ts | 74 ++ .../v2/node/workbook/getAll.workflow.json | 63 ++ .../test/v2/node/worksheet/append.test.ts | 68 ++ .../v2/node/worksheet/append.workflow.json | 166 +++++ .../test/v2/node/worksheet/clear.test.ts | 68 ++ .../v2/node/worksheet/clear.workflow.json | 66 ++ .../v2/node/worksheet/deleteWorksheet.test.ts | 67 ++ .../worksheet/deleteWorksheet.workflow.json | 66 ++ .../test/v2/node/worksheet/getAll.test.ts | 76 ++ .../v2/node/worksheet/getAll.workflow.json | 76 ++ .../test/v2/node/worksheet/readRows.test.ts | 55 ++ .../v2/node/worksheet/readRows.workflow.json | 137 ++++ .../test/v2/node/worksheet/update.test.ts | 68 ++ .../v2/node/worksheet/update.workflow.json | 154 ++++ .../test/v2/node/worksheet/upsert.test.ts | 70 ++ .../v2/node/worksheet/upsert.workflow.json | 162 ++++ .../Excel/test/v2/utils/utils.test.ts | 536 ++++++++++++++ .../Excel/{ => v1}/GenericFunctions.ts | 2 +- .../Excel/v1/MicrosoftExcelV1.node.ts | 692 +++++++++++++++++ .../Excel/{ => v1}/TableDescription.ts | 0 .../Excel/{ => v1}/WorkbookDescription.ts | 0 .../Excel/{ => v1}/WorksheetDescription.ts | 0 .../Excel/v2/MicrosoftExcelV2.node.ts | 25 + .../Excel/v2/actions/common.descriptions.ts | 140 ++++ .../Microsoft/Excel/v2/actions/node.type.ts | 20 + .../Microsoft/Excel/v2/actions/router.ts | 37 + .../Excel/v2/actions/table/Table.resource.ts | 77 ++ .../v2/actions/table/addTable.operation.ts | 127 ++++ .../v2/actions/table/append.operation.ts | 282 +++++++ .../actions/table/convertToRange.operation.ts | 64 ++ .../v2/actions/table/deleteTable.operation.ts | 64 ++ .../v2/actions/table/getColumns.operation.ts | 165 +++++ .../v2/actions/table/getRows.operation.ts | 223 ++++++ .../v2/actions/table/lookup.operation.ts | 156 ++++ .../Excel/v2/actions/versionDescription.ts | 63 ++ .../v2/actions/workbook/Workbook.resource.ts | 45 ++ .../workbook/addWorksheet.operation.ts | 109 +++ .../workbook/deleteWorkbook.operation.ts | 77 ++ .../v2/actions/workbook/getAll.operation.ts | 122 +++ .../actions/worksheet/Worksheet.resource.ts | 79 ++ .../v2/actions/worksheet/append.operation.ts | 227 ++++++ .../v2/actions/worksheet/clear.operation.ts | 121 +++ .../worksheet/deleteWorksheet.operation.ts | 60 ++ .../v2/actions/worksheet/getAll.operation.ts | 119 +++ .../actions/worksheet/readRows.operation.ts | 199 +++++ .../v2/actions/worksheet/update.operation.ts | 376 ++++++++++ .../v2/actions/worksheet/upsert.operation.ts | 333 +++++++++ .../Microsoft/Excel/v2/helpers/interfaces.ts | 14 + .../nodes/Microsoft/Excel/v2/helpers/utils.ts | 208 ++++++ .../nodes/Microsoft/Excel/v2/methods/index.ts | 2 + .../Microsoft/Excel/v2/methods/listSearch.ts | 134 ++++ .../Microsoft/Excel/v2/methods/loadOptions.ts | 89 +++ .../Microsoft/Excel/v2/transport/index.ts | 82 ++ packages/nodes-base/test/nodes/Helpers.ts | 8 + .../nodes-base/test/utils/utilities.test.ts | 41 +- packages/nodes-base/utils/utilities.ts | 36 +- 75 files changed, 8053 insertions(+), 679 deletions(-) create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/addTable.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/addTable.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/append.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/append.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/convertToRange.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/convertToRange.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/deleteTable.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/deleteTable.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/getColumns.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/getColumns.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/getRows.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/getRows.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/lookup.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/lookup.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/addWorksheet.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/addWorksheet.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/deleteWorkbook.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/deleteWorkbook.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/getAll.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/getAll.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/append.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/append.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/clear.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/clear.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/deleteWorksheet.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/deleteWorksheet.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/getAll.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/getAll.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/readRows.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/readRows.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/update.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/update.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/upsert.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/upsert.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/test/v2/utils/utils.test.ts rename packages/nodes-base/nodes/Microsoft/Excel/{ => v1}/GenericFunctions.ts (99%) create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v1/MicrosoftExcelV1.node.ts rename packages/nodes-base/nodes/Microsoft/Excel/{ => v1}/TableDescription.ts (100%) rename packages/nodes-base/nodes/Microsoft/Excel/{ => v1}/WorkbookDescription.ts (100%) rename packages/nodes-base/nodes/Microsoft/Excel/{ => v1}/WorksheetDescription.ts (100%) create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/MicrosoftExcelV2.node.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/actions/common.descriptions.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/actions/node.type.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/actions/router.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/Table.resource.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/addTable.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/append.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/convertToRange.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/deleteTable.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/getColumns.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/getRows.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/lookup.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/actions/versionDescription.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/actions/workbook/Workbook.resource.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/actions/workbook/addWorksheet.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/actions/workbook/deleteWorkbook.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/actions/workbook/getAll.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/Worksheet.resource.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/append.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/clear.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/deleteWorksheet.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/getAll.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/readRows.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/update.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/upsert.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/helpers/interfaces.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/helpers/utils.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/methods/index.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/methods/listSearch.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/methods/loadOptions.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Excel/v2/transport/index.ts diff --git a/packages/nodes-base/nodes/Microsoft/Excel/MicrosoftExcel.node.ts b/packages/nodes-base/nodes/Microsoft/Excel/MicrosoftExcel.node.ts index 086bbfdce760b..9c9d0318756de 100644 --- a/packages/nodes-base/nodes/Microsoft/Excel/MicrosoftExcel.node.ts +++ b/packages/nodes-base/nodes/Microsoft/Excel/MicrosoftExcel.node.ts @@ -1,678 +1,26 @@ -import type { - IExecuteFunctions, - IDataObject, - ILoadOptionsFunctions, - INodeExecutionData, - INodePropertyOptions, - INodeType, - INodeTypeDescription, - JsonObject, -} from 'n8n-workflow'; -import { NodeApiError } from 'n8n-workflow'; - -import { - microsoftApiRequest, - microsoftApiRequestAllItems, - microsoftApiRequestAllItemsSkip, -} from './GenericFunctions'; - -import { workbookFields, workbookOperations } from './WorkbookDescription'; - -import { worksheetFields, worksheetOperations } from './WorksheetDescription'; - -import { tableFields, tableOperations } from './TableDescription'; - -export class MicrosoftExcel implements INodeType { - description: INodeTypeDescription = { - displayName: 'Microsoft Excel', - name: 'microsoftExcel', - icon: 'file:excel.svg', - group: ['input'], - version: 1, - subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Microsoft Excel API', - defaults: { - name: 'Microsoft Excel', - }, - inputs: ['main'], - outputs: ['main'], - credentials: [ - { - name: 'microsoftExcelOAuth2Api', - required: true, - }, - ], - properties: [ - { - displayName: 'Resource', - name: 'resource', - type: 'options', - noDataExpression: true, - options: [ - { - name: 'Table', - value: 'table', - description: 'Represents an Excel table', - }, - { - name: 'Workbook', - value: 'workbook', - description: - 'Workbook is the top level object which contains related workbook objects such as worksheets, tables, ranges, etc', - }, - { - name: 'Worksheet', - value: 'worksheet', - description: - 'An Excel worksheet is a grid of cells. It can contain data, tables, charts, etc.', - }, - ], - default: 'workbook', - }, - ...workbookOperations, - ...workbookFields, - ...worksheetOperations, - ...worksheetFields, - ...tableOperations, - ...tableFields, - ], - }; - - methods = { - loadOptions: { - // Get all the workbooks to display them to user so that they can - // select them easily - async getWorkbooks(this: ILoadOptionsFunctions): Promise { - const qs: IDataObject = { - select: 'id,name', - }; - const returnData: INodePropertyOptions[] = []; - const workbooks = await microsoftApiRequestAllItems.call( - this, - 'value', - 'GET', - "/drive/root/search(q='.xlsx')", - {}, - qs, - ); - for (const workbook of workbooks) { - const workbookName = workbook.name; - const workbookId = workbook.id; - returnData.push({ - name: workbookName, - value: workbookId, - }); - } - return returnData; - }, - // Get all the worksheets to display them to user so that they can - // select them easily - async getworksheets(this: ILoadOptionsFunctions): Promise { - const workbookId = this.getCurrentNodeParameter('workbook'); - const qs: IDataObject = { - select: 'id,name', - }; - const returnData: INodePropertyOptions[] = []; - const worksheets = await microsoftApiRequestAllItems.call( - this, - 'value', - 'GET', - `/drive/items/${workbookId}/workbook/worksheets`, - {}, - qs, - ); - for (const worksheet of worksheets) { - const worksheetName = worksheet.name; - const worksheetId = worksheet.id; - returnData.push({ - name: worksheetName, - value: worksheetId, - }); - } - return returnData; - }, - // Get all the tables to display them to user so that they can - // select them easily - async getTables(this: ILoadOptionsFunctions): Promise { - const workbookId = this.getCurrentNodeParameter('workbook'); - const worksheetId = this.getCurrentNodeParameter('worksheet'); - const qs: IDataObject = { - select: 'id,name', - }; - const returnData: INodePropertyOptions[] = []; - const tables = await microsoftApiRequestAllItems.call( - this, - 'value', - 'GET', - `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables`, - {}, - qs, - ); - for (const table of tables) { - const tableName = table.name; - const tableId = table.id; - returnData.push({ - name: tableName, - value: tableId, - }); - } - return returnData; - }, - }, - }; - - async execute(this: IExecuteFunctions): Promise { - const items = this.getInputData(); - const returnData: INodeExecutionData[] = []; - const length = items.length; - let qs: IDataObject = {}; - const result: IDataObject[] = []; - let responseData; - const resource = this.getNodeParameter('resource', 0); - const operation = this.getNodeParameter('operation', 0); - - if (resource === 'table') { - //https://docs.microsoft.com/en-us/graph/api/table-post-rows?view=graph-rest-1.0&tabs=http - if (operation === 'addRow') { - try { - // TODO: At some point it should be possible to use item dependent parameters. - // Is however important to then not make one separate request each. - const workbookId = this.getNodeParameter('workbook', 0) as string; - const worksheetId = this.getNodeParameter('worksheet', 0) as string; - const tableId = this.getNodeParameter('table', 0) as string; - const additionalFields = this.getNodeParameter('additionalFields', 0); - const body: IDataObject = {}; - - if (additionalFields.index) { - body.index = additionalFields.index as number; - } - - // Get table columns to eliminate any columns not needed on the input - responseData = await microsoftApiRequest.call( - this, - 'GET', - `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, - {}, - qs, - ); - const columns = responseData.value.map((column: IDataObject) => column.name); - - const rows: any[][] = []; - - // Bring the items into the correct format - for (const item of items) { - const row = []; - for (const column of columns) { - row.push(item.json[column]); - } - rows.push(row); - } - - body.values = rows; - const { id } = await microsoftApiRequest.call( - this, - 'POST', - `/drive/items/${workbookId}/workbook/createSession`, - { persistChanges: true }, - ); - responseData = await microsoftApiRequest.call( - this, - 'POST', - `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows/add`, - body, - {}, - '', - { 'workbook-session-id': id }, - ); - await microsoftApiRequest.call( - this, - 'POST', - `/drive/items/${workbookId}/workbook/closeSession`, - {}, - {}, - '', - { 'workbook-session-id': id }, - ); - - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData as IDataObject[]), - { itemData: { item: 0 } }, - ); - - returnData.push(...executionData); - } 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; - } - } - } - //https://docs.microsoft.com/en-us/graph/api/table-list-columns?view=graph-rest-1.0&tabs=http - if (operation === 'getColumns') { - for (let i = 0; i < length; i++) { - try { - qs = {}; - const workbookId = this.getNodeParameter('workbook', i) as string; - const worksheetId = this.getNodeParameter('worksheet', i) as string; - const tableId = this.getNodeParameter('table', i) as string; - const returnAll = this.getNodeParameter('returnAll', i); - const rawData = this.getNodeParameter('rawData', i); - if (rawData) { - const filters = this.getNodeParameter('filters', i); - if (filters.fields) { - qs.$select = filters.fields; - } - } - if (returnAll) { - responseData = await microsoftApiRequestAllItemsSkip.call( - this, - 'value', - 'GET', - `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, - {}, - qs, - ); - } else { - qs.$top = this.getNodeParameter('limit', i); - responseData = await microsoftApiRequest.call( - this, - 'GET', - `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, - {}, - qs, - ); - responseData = responseData.value; - } - if (!rawData) { - responseData = responseData.map((column: IDataObject) => ({ name: column.name })); - } else { - const dataProperty = this.getNodeParameter('dataProperty', i) as string; - responseData = { [dataProperty]: responseData }; - } - - 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; - } - } - } - //https://docs.microsoft.com/en-us/graph/api/table-list-rows?view=graph-rest-1.0&tabs=http - if (operation === 'getRows') { - for (let i = 0; i < length; i++) { - qs = {}; - try { - const workbookId = this.getNodeParameter('workbook', i) as string; - const worksheetId = this.getNodeParameter('worksheet', i) as string; - const tableId = this.getNodeParameter('table', i) as string; - const returnAll = this.getNodeParameter('returnAll', i); - const rawData = this.getNodeParameter('rawData', i); - if (rawData) { - const filters = this.getNodeParameter('filters', i); - if (filters.fields) { - qs.$select = filters.fields; - } - } - if (returnAll) { - responseData = await microsoftApiRequestAllItemsSkip.call( - this, - 'value', - 'GET', - `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`, - {}, - qs, - ); - } else { - const rowsQs = { ...qs }; - rowsQs.$top = this.getNodeParameter('limit', i); - responseData = await microsoftApiRequest.call( - this, - 'GET', - `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`, - {}, - rowsQs, - ); - responseData = responseData.value; - } - if (!rawData) { - const columnsQs = { ...qs }; - columnsQs.$select = 'name'; - // TODO: That should probably be cached in the future - let columns = await microsoftApiRequestAllItemsSkip.call( - this, - 'value', - 'GET', - `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, - {}, - columnsQs, - ); - //@ts-ignore - columns = columns.map((column) => column.name); - for (let index = 0; index < responseData.length; index++) { - const object: IDataObject = {}; - for (let y = 0; y < columns.length; y++) { - object[columns[y]] = responseData[index].values[0][y]; - } - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ ...object }), - { itemData: { item: index } }, - ); - - returnData.push(...executionData); - } - } else { - const dataProperty = this.getNodeParameter('dataProperty', i) as string; - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ [dataProperty]: responseData }), - { 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; - } - } - } - if (operation === 'lookup') { - for (let i = 0; i < length; i++) { - qs = {}; - try { - const workbookId = this.getNodeParameter('workbook', i) as string; - const worksheetId = this.getNodeParameter('worksheet', i) as string; - const tableId = this.getNodeParameter('table', i) as string; - const lookupColumn = this.getNodeParameter('lookupColumn', i) as string; - const lookupValue = this.getNodeParameter('lookupValue', i) as string; - const options = this.getNodeParameter('options', i); - - responseData = await microsoftApiRequestAllItemsSkip.call( - this, - 'value', - 'GET', - `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`, - {}, - {}, - ); - - qs.$select = 'name'; - // TODO: That should probably be cached in the future - let columns = await microsoftApiRequestAllItemsSkip.call( - this, - 'value', - 'GET', - `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, - {}, - qs, - ); - columns = columns.map((column: IDataObject) => column.name); - - if (!columns.includes(lookupColumn)) { - throw new NodeApiError(this.getNode(), responseData as JsonObject, { - message: `Column ${lookupColumn} does not exist on the table selected`, - }); - } - - result.length = 0; - for (let index = 0; index < responseData.length; index++) { - const object: IDataObject = {}; - for (let y = 0; y < columns.length; y++) { - object[columns[y]] = responseData[index].values[0][y]; - } - result.push({ ...object }); - } - - if (options.returnAllMatches) { - responseData = result.filter((data: IDataObject) => { - return data[lookupColumn]?.toString() === lookupValue; - }); - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData), - { itemData: { item: i } }, - ); - - returnData.push(...executionData); - } else { - responseData = result.find((data: IDataObject) => { - return data[lookupColumn]?.toString() === lookupValue; - }); - 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; - } - } - } - } - if (resource === 'workbook') { - for (let i = 0; i < length; i++) { - qs = {}; - try { - //https://docs.microsoft.com/en-us/graph/api/worksheetcollection-add?view=graph-rest-1.0&tabs=http - if (operation === 'addWorksheet') { - const workbookId = this.getNodeParameter('workbook', i) as string; - const additionalFields = this.getNodeParameter('additionalFields', i); - const body: IDataObject = {}; - if (additionalFields.name) { - body.name = additionalFields.name; - } - const { id } = await microsoftApiRequest.call( - this, - 'POST', - `/drive/items/${workbookId}/workbook/createSession`, - { persistChanges: true }, - ); - responseData = await microsoftApiRequest.call( - this, - 'POST', - `/drive/items/${workbookId}/workbook/worksheets/add`, - body, - {}, - '', - { 'workbook-session-id': id }, - ); - await microsoftApiRequest.call( - this, - 'POST', - `/drive/items/${workbookId}/workbook/closeSession`, - {}, - {}, - '', - { 'workbook-session-id': id }, - ); - } - if (operation === 'getAll') { - const returnAll = this.getNodeParameter('returnAll', i); - const filters = this.getNodeParameter('filters', i); - if (filters.fields) { - qs.$select = filters.fields; - } - if (returnAll) { - responseData = await microsoftApiRequestAllItems.call( - this, - 'value', - 'GET', - "/drive/root/search(q='.xlsx')", - {}, - qs, - ); - } else { - qs.$top = this.getNodeParameter('limit', i); - responseData = await microsoftApiRequest.call( - this, - 'GET', - "/drive/root/search(q='.xlsx')", - {}, - qs, - ); - responseData = responseData.value; - } - } - - if (Array.isArray(responseData)) { - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData), - { itemData: { item: i } }, - ); - - returnData.push(...executionData); - } else if (responseData !== undefined) { - 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; - } - } - } - if (resource === 'worksheet') { - for (let i = 0; i < length; i++) { - qs = {}; - try { - //https://docs.microsoft.com/en-us/graph/api/workbook-list-worksheets?view=graph-rest-1.0&tabs=http - if (operation === 'getAll') { - const returnAll = this.getNodeParameter('returnAll', i); - const workbookId = this.getNodeParameter('workbook', i) as string; - const filters = this.getNodeParameter('filters', i); - if (filters.fields) { - qs.$select = filters.fields; - } - 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; - } - } - //https://docs.microsoft.com/en-us/graph/api/worksheet-range?view=graph-rest-1.0&tabs=http - if (operation === 'getContent') { - const workbookId = this.getNodeParameter('workbook', i) as string; - const worksheetId = this.getNodeParameter('worksheet', i) as string; - const range = this.getNodeParameter('range', i) as string; - const rawData = this.getNodeParameter('rawData', i); - if (rawData) { - const filters = this.getNodeParameter('filters', i); - if (filters.fields) { - qs.$select = filters.fields; - } - } - - responseData = await microsoftApiRequest.call( - this, - 'GET', - `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`, - {}, - qs, - ); - - if (!rawData) { - const keyRow = this.getNodeParameter('keyRow', i) as number; - const dataStartRow = this.getNodeParameter('dataStartRow', i) as number; - if (responseData.values === null) { - throw new NodeApiError(this.getNode(), responseData as JsonObject, { - message: 'Range did not return data', - }); - } - const keyValues = responseData.values[keyRow]; - for (let index = dataStartRow; index < responseData.values.length; index++) { - const object: IDataObject = {}; - for (let y = 0; y < keyValues.length; y++) { - object[keyValues[y]] = responseData.values[index][y]; - } - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ ...object }), - { itemData: { item: index } }, - ); - - returnData.push(...executionData); - } - } else { - const dataProperty = this.getNodeParameter('dataProperty', i) as string; - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ [dataProperty]: responseData }), - { 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 this.prepareOutputData(returnData); +import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow'; +import { VersionedNodeType } from 'n8n-workflow'; + +import { MicrosoftExcelV1 } from './v1/MicrosoftExcelV1.node'; +import { MicrosoftExcelV2 } from './v2/MicrosoftExcelV2.node'; + +export class MicrosoftExcel extends VersionedNodeType { + constructor() { + const baseDescription: INodeTypeBaseDescription = { + displayName: 'Microsoft Excel 365', + name: 'microsoftExcel', + icon: 'file:excel.svg', + group: ['input'], + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Microsoft Excel API', + defaultVersion: 2, + }; + + const nodeVersions: IVersionedNodeType['nodeVersions'] = { + 1: new MicrosoftExcelV1(baseDescription), + 2: new MicrosoftExcelV2(baseDescription), + }; + + super(nodeVersions, baseDescription); } } diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/addTable.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/addTable.test.ts new file mode 100644 index 0000000000000..6cf70144f4a3e --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/addTable.test.ts @@ -0,0 +1,72 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import { getResultNodeData, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; +import type { WorkflowTestData } from '../../../../../../../test/nodes/types'; +import { executeWorkflow } from '../../../../../../../test/nodes/ExecuteWorkflow'; + +import * as transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'POST') { + return { + style: 'TableStyleMedium2', + name: 'Table3', + showFilterButton: true, + id: '{317CA469-7D1C-4A5D-9B0B-424444BF0336}', + highlightLastColumn: false, + highlightFirstColumn: false, + legacyId: '3', + showBandedColumns: false, + showBandedRows: true, + showHeaders: true, + showTotals: false, + }; + } + }), + }; +}); + +describe('Test MicrosoftExcelV2, table => addTable', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/table/addTable.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'POST', + '/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/{A0883CFE-D27E-4ECC-B94B-981830AAD55B}/tables/add', + { address: 'A1:D4', hasHeaders: true }, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/addTable.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/addTable.workflow.json new file mode 100644 index 0000000000000..eb8fbbd3fe5cf --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/addTable.workflow.json @@ -0,0 +1,78 @@ +{ + "name": "My workflow 5", + "nodes": [ + { + "parameters": {}, + "id": "875e8784-eb59-40d8-ba45-129a5e29881c", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [380, 140] + }, + { + "parameters": { + "resource": "table", + "operation": "addTable", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ПРРО копія" + }, + "worksheet": { + "__rl": true, + "value": "{A0883CFE-D27E-4ECC-B94B-981830AAD55B}", + "mode": "list", + "cachedResultName": "Sheet4" + }, + "selectRange": "manual", + "range": "A1:D4" + }, + "id": "0e0ac1d2-242c-486a-9287-c70307645acc", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [860, 140], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "style": "TableStyleMedium2", + "name": "Table3", + "showFilterButton": true, + "id": "{317CA469-7D1C-4A5D-9B0B-424444BF0336}", + "highlightLastColumn": false, + "highlightFirstColumn": false, + "legacyId": "3", + "showBandedColumns": false, + "showBandedRows": true, + "showHeaders": true, + "showTotals": false + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {} +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/append.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/append.test.ts new file mode 100644 index 0000000000000..1eddfd1b38a48 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/append.test.ts @@ -0,0 +1,93 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import { getResultNodeData, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; +import type { WorkflowTestData } from '../../../../../../../test/nodes/types'; +import { executeWorkflow } from '../../../../../../../test/nodes/ExecuteWorkflow'; + +import * as transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string, resource: string) { + if (method === 'GET') { + return { + value: [ + { + name: 'id', + }, + { + name: 'name', + }, + { + name: 'age', + }, + { + name: 'data', + }, + ], + }; + } + if (method === 'POST' && resource.includes('createSession')) { + return { + id: 12345, + }; + } + if (method === 'POST' && resource.includes('add')) { + return { + index: 3, + values: [[3, 'Donald', 99, 'data 5']], + }; + } + if (method === 'POST' && resource.includes('closeSession')) { + return; + } + }), + }; +}); + +describe('Test MicrosoftExcelV2, table => append', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/table/append.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(4); + + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'POST', + '/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/{A0883CFE-D27E-4ECC-B94B-981830AAD55B}/tables/{317CA469-7D1C-4A5D-9B0B-424444BF0336}/rows/add', + { values: [['3', 'Donald', '99', 'data 5']] }, + {}, + '', + { 'workbook-session-id': 12345 }, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/append.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/append.workflow.json new file mode 100644 index 0000000000000..1a69bac4dd087 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/append.workflow.json @@ -0,0 +1,104 @@ +{ + "name": "My workflow 5", + "nodes": [ + { + "parameters": {}, + "id": "875e8784-eb59-40d8-ba45-129a5e29881c", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [380, 140] + }, + { + "parameters": { + "resource": "table", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ПРРО копія", + "cachedResultUrl": "https://5w1hb7-my.sharepoint.com/personal/michaeldevsandbox_5w1hb7_onmicrosoft_com/_layouts/15/Doc.aspx?sourcedoc=%7BECC4041C-3AB6-4CF7-B079-0926470A1388%7D&file=%D0%9F%D0%A0%D0%A0%D0%9E%20%D0%BA%D0%BE%D0%BF%D1%96%D1%8F.xlsx&action=default&mobileredirect=true&DefaultItemOpen=1" + }, + "worksheet": { + "__rl": true, + "value": "{A0883CFE-D27E-4ECC-B94B-981830AAD55B}", + "mode": "list", + "cachedResultName": "Sheet4", + "cachedResultUrl": "https://5w1hb7-my.sharepoint.com/personal/michaeldevsandbox_5w1hb7_onmicrosoft_com/_layouts/15/Doc.aspx?sourcedoc=%7BECC4041C-3AB6-4CF7-B079-0926470A1388%7D&file=%D0%9F%D0%A0%D0%A0%D0%9E%20%D0%BA%D0%BE%D0%BF%D1%96%D1%8F.xlsx&action=default&mobileredirect=true&DefaultItemOpen=1&activeCell=Sheet4!A1" + }, + "table": { + "__rl": true, + "value": "{317CA469-7D1C-4A5D-9B0B-424444BF0336}", + "mode": "list", + "cachedResultName": "Table3", + "cachedResultUrl": "https://5w1hb7-my.sharepoint.com/personal/michaeldevsandbox_5w1hb7_onmicrosoft_com/_layouts/15/Doc.aspx?sourcedoc=%7BECC4041C-3AB6-4CF7-B079-0926470A1388%7D&file=%D0%9F%D0%A0%D0%A0%D0%9E%20%D0%BA%D0%BE%D0%BF%D1%96%D1%8F.xlsx&action=default&mobileredirect=true&DefaultItemOpen=1&activeCell=Sheet4!A1:D4" + }, + "fieldsUi": { + "values": [ + { + "column": "id", + "fieldValue": "3" + }, + { + "column": "name", + "fieldValue": "Donald" + }, + { + "column": "age", + "fieldValue": "99" + }, + { + "column": "data", + "fieldValue": "data 5" + } + ] + }, + "options": {} + }, + "id": "0e0ac1d2-242c-486a-9287-c70307645acc", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [860, 140], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "id": 3, + "name": "Donald", + "age": 99, + "data": "data 5" + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {}, + "versionId": "b9eda2d8-e1a5-4a54-aaa9-5e81adaae909", + "id": "135", + "meta": { + "instanceId": "36203ea1ce3cef713fa25999bd9874ae26b9e4c2c3a90a365f2882a154d031d0" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/convertToRange.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/convertToRange.test.ts new file mode 100644 index 0000000000000..0c100f20526cd --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/convertToRange.test.ts @@ -0,0 +1,68 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import { getResultNodeData, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; +import type { WorkflowTestData } from '../../../../../../../test/nodes/types'; +import { executeWorkflow } from '../../../../../../../test/nodes/ExecuteWorkflow'; + +import * as transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'POST') { + return { + address: 'Sheet4!A1:D5', + values: [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Sam', 34, 'data 4'], + [3, 'Donald', 99, 'data 5'], + ], + }; + } + }), + }; +}); + +describe('Test MicrosoftExcelV2, table => convertToRange', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/table/convertToRange.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'POST', + '/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/{A0883CFE-D27E-4ECC-B94B-981830AAD55B}/tables/{6321EE4A-AC21-48AD-87D9-B527637D94B3}/convertToRange', + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/convertToRange.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/convertToRange.workflow.json new file mode 100644 index 0000000000000..e5a02b985bbd4 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/convertToRange.workflow.json @@ -0,0 +1,79 @@ +{ + "name": "My workflow 5", + "nodes": [ + { + "parameters": {}, + "id": "875e8784-eb59-40d8-ba45-129a5e29881c", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [580, 140] + }, + { + "parameters": { + "resource": "table", + "operation": "convertToRange", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ПРРО копія" + }, + "worksheet": { + "__rl": true, + "value": "{A0883CFE-D27E-4ECC-B94B-981830AAD55B}", + "mode": "list", + "cachedResultName": "Sheet4" + }, + "table": { + "__rl": true, + "value": "{6321EE4A-AC21-48AD-87D9-B527637D94B3}", + "mode": "list", + "cachedResultName": "Table3" + } + }, + "id": "0e0ac1d2-242c-486a-9287-c70307645acc", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [860, 140], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "address": "Sheet4!A1:D5", + "values": [ + ["id", "name", "age", "data"], + [1, "Sam", 33, "data 1"], + [2, "Jon", 44, "data 2"], + [3, "Sam", 34, "data 4"], + [3, "Donald", 99, "data 5"] + ] + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {} +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/deleteTable.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/deleteTable.test.ts new file mode 100644 index 0000000000000..77d13225eb16f --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/deleteTable.test.ts @@ -0,0 +1,59 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import { getResultNodeData, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; +import type { WorkflowTestData } from '../../../../../../../test/nodes/types'; +import { executeWorkflow } from '../../../../../../../test/nodes/ExecuteWorkflow'; + +import * as transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'DELETE') { + return; + } + }), + }; +}); + +describe('Test MicrosoftExcelV2, table => deleteTable', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/table/deleteTable.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'DELETE', + '/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/{A0883CFE-D27E-4ECC-B94B-981830AAD55B}/tables/{92FBE3F5-3180-47EE-8549-40892C38DA7F}', + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/deleteTable.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/deleteTable.workflow.json new file mode 100644 index 0000000000000..2a9c585e613d4 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/deleteTable.workflow.json @@ -0,0 +1,72 @@ +{ + "name": "My workflow 5", + "nodes": [ + { + "parameters": {}, + "id": "875e8784-eb59-40d8-ba45-129a5e29881c", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [580, 140] + }, + { + "parameters": { + "resource": "table", + "operation": "deleteTable", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ПРРО копія" + }, + "worksheet": { + "__rl": true, + "value": "{A0883CFE-D27E-4ECC-B94B-981830AAD55B}", + "mode": "list", + "cachedResultName": "Sheet4" + }, + "table": { + "__rl": true, + "value": "{92FBE3F5-3180-47EE-8549-40892C38DA7F}", + "mode": "list", + "cachedResultName": "Table3" + } + }, + "id": "0e0ac1d2-242c-486a-9287-c70307645acc", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [860, 140], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "success": true + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {} +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/getColumns.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/getColumns.test.ts new file mode 100644 index 0000000000000..148dc44f9d23a --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/getColumns.test.ts @@ -0,0 +1,73 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import { getResultNodeData, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; +import type { WorkflowTestData } from '../../../../../../../test/nodes/types'; +import { executeWorkflow } from '../../../../../../../test/nodes/ExecuteWorkflow'; + +import * as transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequestAllItemsSkip: jest.fn(async function () { + return [ + { + name: 'country', + }, + { + name: 'browser', + }, + { + name: 'session_duration', + }, + { + name: 'visits', + }, + ]; + }), + }; +}); + +describe('Test MicrosoftExcelV2, table => getColumns', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/table/getColumns.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequestAllItemsSkip).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequestAllItemsSkip).toHaveBeenCalledWith( + 'value', + 'GET', + '/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/{00000000-0001-0000-0000-000000000000}/tables/{613E8967-D581-44ED-81D3-82A01AA6A05C}/columns', + {}, + {}, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/getColumns.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/getColumns.workflow.json new file mode 100644 index 0000000000000..ec2094099a147 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/getColumns.workflow.json @@ -0,0 +1,88 @@ +{ + "name": "My workflow 5", + "nodes": [ + { + "parameters": {}, + "id": "875e8784-eb59-40d8-ba45-129a5e29881c", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [580, 140] + }, + { + "parameters": { + "resource": "table", + "operation": "getColumns", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ПРРО копія" + }, + "worksheet": { + "__rl": true, + "value": "{00000000-0001-0000-0000-000000000000}", + "mode": "list", + "cachedResultName": "Sheet1" + }, + "table": { + "__rl": true, + "value": "{613E8967-D581-44ED-81D3-82A01AA6A05C}", + "mode": "list", + "cachedResultName": "Table1" + }, + "returnAll": true + }, + "id": "0e0ac1d2-242c-486a-9287-c70307645acc", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [860, 140], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "name": "country" + } + }, + { + "json": { + "name": "browser" + } + }, + { + "json": { + "name": "session_duration" + } + }, + { + "json": { + "name": "visits" + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {} +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/getRows.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/getRows.test.ts new file mode 100644 index 0000000000000..1ab8a1f39c01a --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/getRows.test.ts @@ -0,0 +1,97 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import { getResultNodeData, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; +import type { WorkflowTestData } from '../../../../../../../test/nodes/types'; +import { executeWorkflow } from '../../../../../../../test/nodes/ExecuteWorkflow'; + +import * as transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'GET') { + return { + value: [ + { + index: 0, + values: [['uk', 'firefox', 1, 1]], + }, + { + index: 1, + values: [['us', 'chrome', 1, 12]], + }, + ], + }; + } + }), + microsoftApiRequestAllItemsSkip: jest.fn(async function () { + return [ + { + name: 'country', + }, + { + name: 'browser', + }, + { + name: 'session_duration', + }, + { + name: 'visits', + }, + ]; + }), + }; +}); + +describe('Test MicrosoftExcelV2, table => getRows', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/table/getRows.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'GET', + '/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/{00000000-0001-0000-0000-000000000000}/tables/{613E8967-D581-44ED-81D3-82A01AA6A05C}/rows', + {}, + { $top: 2 }, + ); + + expect(transport.microsoftApiRequestAllItemsSkip).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequestAllItemsSkip).toHaveBeenCalledWith( + 'value', + 'GET', + '/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/{00000000-0001-0000-0000-000000000000}/tables/{613E8967-D581-44ED-81D3-82A01AA6A05C}/columns', + {}, + { $select: 'name' }, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/getRows.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/getRows.workflow.json new file mode 100644 index 0000000000000..77836894d1401 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/getRows.workflow.json @@ -0,0 +1,85 @@ +{ + "name": "My workflow 5", + "nodes": [ + { + "parameters": {}, + "id": "875e8784-eb59-40d8-ba45-129a5e29881c", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [580, 140] + }, + { + "parameters": { + "resource": "table", + "operation": "getRows", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ПРРО копія" + }, + "worksheet": { + "__rl": true, + "value": "{00000000-0001-0000-0000-000000000000}", + "mode": "list", + "cachedResultName": "Sheet1" + }, + "table": { + "__rl": true, + "value": "{613E8967-D581-44ED-81D3-82A01AA6A05C}", + "mode": "list", + "cachedResultName": "Table1" + }, + "limit": 2, + "filters": {} + }, + "id": "0e0ac1d2-242c-486a-9287-c70307645acc", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [860, 140], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "country": "uk", + "browser": "firefox", + "session_duration": 1, + "visits": 1 + } + }, + { + "json": { + "country": "us", + "browser": "chrome", + "session_duration": 1, + "visits": 12 + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {} +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/lookup.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/lookup.test.ts new file mode 100644 index 0000000000000..17785d65ef5f2 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/lookup.test.ts @@ -0,0 +1,114 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import { getResultNodeData, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; +import type { WorkflowTestData } from '../../../../../../../test/nodes/types'; +import { executeWorkflow } from '../../../../../../../test/nodes/ExecuteWorkflow'; + +import * as transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequestAllItemsSkip: jest.fn(async function ( + _property: string, + _method: string, + endpoint: string, + ) { + if (endpoint.includes('columns')) { + return [ + { + name: 'country', + }, + { + name: 'browser', + }, + { + name: 'session_duration', + }, + { + name: 'visits', + }, + ]; + } + if (endpoint.includes('rows')) { + return [ + { + index: 0, + values: [['uk', 'firefox', 1, 1]], + }, + { + index: 1, + values: [['us', 'chrome', 1, 12]], + }, + { + index: 2, + values: [['test', 'test', 55, 123]], + }, + { + index: 3, + values: [['ua', 'chrome', 1, 3]], + }, + { + index: 4, + values: [['ua', 'firefox', 1, 4]], + }, + { + index: 5, + values: [['uk', 'chrome', 1, 55]], + }, + ]; + } + }), + }; +}); + +describe('Test MicrosoftExcelV2, table => lookup', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/table/lookup.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequestAllItemsSkip).toHaveBeenCalledTimes(2); + expect(transport.microsoftApiRequestAllItemsSkip).toHaveBeenCalledWith( + 'value', + 'GET', + '/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/{00000000-0001-0000-0000-000000000000}/tables/{613E8967-D581-44ED-81D3-82A01AA6A05C}/rows', + {}, + {}, + ); + expect(transport.microsoftApiRequestAllItemsSkip).toHaveBeenCalledWith( + 'value', + 'GET', + '/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/{00000000-0001-0000-0000-000000000000}/tables/{613E8967-D581-44ED-81D3-82A01AA6A05C}/columns', + {}, + { $select: 'name' }, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/lookup.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/lookup.workflow.json new file mode 100644 index 0000000000000..339c927fdc77a --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/lookup.workflow.json @@ -0,0 +1,88 @@ +{ + "name": "My workflow 5", + "nodes": [ + { + "parameters": {}, + "id": "875e8784-eb59-40d8-ba45-129a5e29881c", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [580, 140] + }, + { + "parameters": { + "resource": "table", + "operation": "lookup", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ПРРО копія" + }, + "worksheet": { + "__rl": true, + "value": "{00000000-0001-0000-0000-000000000000}", + "mode": "list", + "cachedResultName": "Sheet1" + }, + "table": { + "__rl": true, + "value": "{613E8967-D581-44ED-81D3-82A01AA6A05C}", + "mode": "list", + "cachedResultName": "Table1" + }, + "lookupColumn": "country", + "lookupValue": "uk", + "options": { + "returnAllMatches": true + } + }, + "id": "0e0ac1d2-242c-486a-9287-c70307645acc", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [860, 140], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "country": "uk", + "browser": "firefox", + "session_duration": 1, + "visits": 1 + } + }, + { + "json": { + "country": "uk", + "browser": "chrome", + "session_duration": 1, + "visits": 55 + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {} +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/addWorksheet.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/addWorksheet.test.ts new file mode 100644 index 0000000000000..b6eea1cb54f01 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/addWorksheet.test.ts @@ -0,0 +1,89 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import { getResultNodeData, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; +import type { WorkflowTestData } from '../../../../../../../test/nodes/types'; +import { executeWorkflow } from '../../../../../../../test/nodes/ExecuteWorkflow'; + +import * as transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string, resource: string) { + if (method === 'POST' && resource.includes('createSession')) { + return { + id: 12345, + }; + } + if (method === 'POST' && resource.includes('add')) { + return { + id: '{266ADAB7-25B6-4F28-A2D1-FD5BFBD7A4F0}', + name: 'Sheet42', + position: 8, + visibility: 'Visible', + }; + } + if (method === 'POST' && resource.includes('closeSession')) { + return; + } + }), + }; +}); + +describe('Test MicrosoftExcelV2, workbook => addWorksheet', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/workbook/addWorksheet.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(3); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'POST', + '/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/createSession', + { persistChanges: true }, + ); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'POST', + '/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/add', + { name: 'Sheet42' }, + {}, + '', + { 'workbook-session-id': 12345 }, + ); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'POST', + '/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/closeSession', + {}, + {}, + '', + { 'workbook-session-id': 12345 }, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/addWorksheet.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/addWorksheet.workflow.json new file mode 100644 index 0000000000000..0744375981399 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/addWorksheet.workflow.json @@ -0,0 +1,65 @@ +{ + "name": "My workflow 5", + "nodes": [ + { + "parameters": {}, + "id": "875e8784-eb59-40d8-ba45-129a5e29881c", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [380, 140] + }, + { + "parameters": { + "operation": "addWorksheet", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ПРРО копія" + }, + "additionalFields": { + "name": "Sheet42" + } + }, + "id": "0e0ac1d2-242c-486a-9287-c70307645acc", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [860, 140], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "id": "{266ADAB7-25B6-4F28-A2D1-FD5BFBD7A4F0}", + "name": "Sheet42", + "position": 8, + "visibility": "Visible" + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {} +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/deleteWorkbook.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/deleteWorkbook.test.ts new file mode 100644 index 0000000000000..a4e63b1ebb4ab --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/deleteWorkbook.test.ts @@ -0,0 +1,59 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import { getResultNodeData, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; +import type { WorkflowTestData } from '../../../../../../../test/nodes/types'; +import { executeWorkflow } from '../../../../../../../test/nodes/ExecuteWorkflow'; + +import * as transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'DELETE') { + return; + } + }), + }; +}); + +describe('Test MicrosoftExcelV2, workbook => deleteWorkbook', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/workbook/deleteWorkbook.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'DELETE', + '/drive/items/01FUWX3BXJLISGF2CFWBGYPHXFCXPXOJUK', + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/deleteWorkbook.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/deleteWorkbook.workflow.json new file mode 100644 index 0000000000000..1ac63b01748fa --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/deleteWorkbook.workflow.json @@ -0,0 +1,59 @@ +{ + "name": "My workflow 5", + "nodes": [ + { + "parameters": {}, + "id": "875e8784-eb59-40d8-ba45-129a5e29881c", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [380, 140] + }, + { + "parameters": { + "operation": "deleteWorkbook", + "workbook": { + "__rl": true, + "value": "01FUWX3BXJLISGF2CFWBGYPHXFCXPXOJUK", + "mode": "list", + "cachedResultName": "Book" + } + }, + "id": "0e0ac1d2-242c-486a-9287-c70307645acc", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [860, 140], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "success": true + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {} +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/getAll.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/getAll.test.ts new file mode 100644 index 0000000000000..a78e3b1d9e5c5 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/getAll.test.ts @@ -0,0 +1,74 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import { getResultNodeData, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; +import type { WorkflowTestData } from '../../../../../../../test/nodes/types'; +import { executeWorkflow } from '../../../../../../../test/nodes/ExecuteWorkflow'; + +import * as transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'GET') { + return { + value: [ + { + '@odata.type': '#microsoft.graph.driveItem', + name: 'ПРРО копія.xlsx', + }, + , + { + '@odata.type': '#microsoft.graph.driveItem', + name: 'Book 3.xlsx', + }, + , + ], + }; + } + }), + }; +}); + +describe('Test MicrosoftExcelV2, workbook => getAll', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/workbook/getAll.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'GET', + "/drive/root/search(q='.xlsx')", + {}, + { $select: 'name', $top: 2 }, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/getAll.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/getAll.workflow.json new file mode 100644 index 0000000000000..5499173661842 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/getAll.workflow.json @@ -0,0 +1,63 @@ +{ + "name": "My workflow 5", + "nodes": [ + { + "parameters": {}, + "id": "875e8784-eb59-40d8-ba45-129a5e29881c", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [380, 140] + }, + { + "parameters": { + "limit": 2, + "filters": { + "fields": "name" + } + }, + "id": "0e0ac1d2-242c-486a-9287-c70307645acc", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [860, 140], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "@odata.type": "#microsoft.graph.driveItem", + "name": "ПРРО копія.xlsx" + } + }, + { + "json": { + "@odata.type": "#microsoft.graph.driveItem", + "name": "Book 3.xlsx" + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {} +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/append.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/append.test.ts new file mode 100644 index 0000000000000..108f278800de3 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/append.test.ts @@ -0,0 +1,68 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import type { IDataObject } from 'n8n-workflow'; + +import { equalityTest, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; + +// eslint-disable-next-line unused-imports/no-unused-imports +import * as transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function ( + method: string, + resource: string, + body?: IDataObject, + qs?: IDataObject, + uri?: string, + headers?: IDataObject, + ) { + if (method === 'GET' && resource.includes('usedRange')) { + return { + address: 'Sheet4!A1:D6', + values: [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Ron', 55, 'data 3'], + ], + }; + } + + if (method === 'PATCH' && resource.includes('{A0883CFE-D27E-4ECC-B94B-981830AAD55B}')) { + return { + values: [[4, 'Sam', 34, 'data 4']], + }; + } + + if (method === 'PATCH' && resource.includes('{426949D7-797F-43A9-A8A4-8FE283495A82}')) { + return { + values: [[4, 'Don', 37, 'data 44']], + }; + } + }), + }; +}); + +describe('Test MicrosoftExcelV2, worksheet => append', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/worksheet/append.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + for (const testData of tests) { + test(testData.description, async () => equalityTest(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/append.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/append.workflow.json new file mode 100644 index 0000000000000..73d2a1f2d9649 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/append.workflow.json @@ -0,0 +1,166 @@ +{ + "name": "microsoft excel 365 - tests", + "nodes": [ + { + "parameters": {}, + "id": "2e1ec8f6-a2e2-4aa9-909c-d0a279584131", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [800, 260] + }, + { + "parameters": { + "resource": "worksheet", + "operation": "append", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ПРРО копія" + }, + "worksheet": { + "__rl": true, + "value": "={A0883CFE-D27E-4ECC-B94B-981830AAD55B}", + "mode": "id" + }, + "fieldsUi": { + "values": [ + { + "column": "id", + "fieldValue": "4" + }, + { + "column": "name", + "fieldValue": "Sam" + }, + { + "column": "age", + "fieldValue": "34" + }, + { + "column": "data", + "fieldValue": "data 4" + } + ] + }, + "options": {} + }, + "id": "86f2a240-3acf-45c2-b97f-63dd655d296b", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [1280, 260], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + }, + { + "parameters": { + "resource": "worksheet", + "operation": "append", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ПРРО копія" + }, + "worksheet": { + "__rl": true, + "value": "{426949D7-797F-43A9-A8A4-8FE283495A82}", + "mode": "list", + "cachedResultName": "Sheet5" + }, + "dataMode": "autoMap", + "options": {} + }, + "id": "531949d8-1ffa-4e1c-ae3e-032360b74f06", + "name": "Microsoft Excel 3651", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [1280, 500], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + }, + { + "parameters": { + "jsCode": "return {\n id: 4,\n name: 'Don',\n age: 37,\n data: 'data 44',\n};" + }, + "id": "2919f9b9-e3ac-42cd-a792-774738fd2195", + "name": "Code", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [1080, 500] + } + ], + "pinData": { + "Microsoft Excel 3651": [ + { + "json": { + "id": 4, + "name": "Don", + "age": 37, + "data": "data 44" + } + } + ], + "Microsoft Excel 365": [ + { + "json": { + "id": 4, + "name": "Sam", + "age": 34, + "data": "data 4" + } + } + ], + "Code": [ + { + "json": { + "id": 4, + "name": "Don", + "age": 37, + "data": "data 44" + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + }, + { + "node": "Code", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code": { + "main": [ + [ + { + "node": "Microsoft Excel 3651", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {} +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/clear.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/clear.test.ts new file mode 100644 index 0000000000000..0cd232a074bd6 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/clear.test.ts @@ -0,0 +1,68 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import { getResultNodeData, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; +import type { WorkflowTestData } from '../../../../../../../test/nodes/types'; +import { executeWorkflow } from '../../../../../../../test/nodes/ExecuteWorkflow'; + +import * as transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'POST') { + return { + values: [ + { + json: { + success: true, + }, + }, + ], + }; + } + }), + }; +}); + +describe('Test MicrosoftExcelV2, worksheet => clear', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/worksheet/clear.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'POST', + '/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/{F7AF92FE-D42D-452F-8E4A-901B1D1EBF3F}/range/clear', + { applyTo: 'All' }, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/clear.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/clear.workflow.json new file mode 100644 index 0000000000000..0578a49a68da4 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/clear.workflow.json @@ -0,0 +1,66 @@ +{ + "name": "My workflow 5", + "nodes": [ + { + "parameters": {}, + "id": "f0857ec9-0709-4657-a2f4-059837c94060", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [540, 220] + }, + { + "parameters": { + "resource": "worksheet", + "operation": "clear", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ПРРО копія" + }, + "worksheet": { + "__rl": true, + "value": "{F7AF92FE-D42D-452F-8E4A-901B1D1EBF3F}", + "mode": "list", + "cachedResultName": "Sheet2" + } + }, + "id": "426ed055-0c9b-4ae2-a9fe-a6cce875d5ee", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [1020, 220], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "success": true + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {} +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/deleteWorksheet.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/deleteWorksheet.test.ts new file mode 100644 index 0000000000000..0b12ff7c59c6a --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/deleteWorksheet.test.ts @@ -0,0 +1,67 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import { getResultNodeData, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; +import type { WorkflowTestData } from '../../../../../../../test/nodes/types'; +import { executeWorkflow } from '../../../../../../../test/nodes/ExecuteWorkflow'; + +import * as transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'DELETE') { + return { + values: [ + { + json: { + success: true, + }, + }, + ], + }; + } + }), + }; +}); + +describe('Test MicrosoftExcelV2, worksheet => deleteWorksheet', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/worksheet/deleteWorksheet.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'DELETE', + '/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/{88D9C37A-4180-4B23-8996-BF11F32EB63C}', + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/deleteWorksheet.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/deleteWorksheet.workflow.json new file mode 100644 index 0000000000000..1446f9a89a937 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/deleteWorksheet.workflow.json @@ -0,0 +1,66 @@ +{ + "name": "My workflow 5", + "nodes": [ + { + "parameters": {}, + "id": "f0857ec9-0709-4657-a2f4-059837c94060", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [540, 220] + }, + { + "parameters": { + "resource": "worksheet", + "operation": "deleteWorksheet", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ПРРО копія" + }, + "worksheet": { + "__rl": true, + "value": "{88D9C37A-4180-4B23-8996-BF11F32EB63C}", + "mode": "list", + "cachedResultName": "188" + } + }, + "id": "426ed055-0c9b-4ae2-a9fe-a6cce875d5ee", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [1020, 220], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "success": true + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {} +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/getAll.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/getAll.test.ts new file mode 100644 index 0000000000000..a045e8675e716 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/getAll.test.ts @@ -0,0 +1,76 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import { getResultNodeData, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; +import type { WorkflowTestData } from '../../../../../../../test/nodes/types'; +import { executeWorkflow } from '../../../../../../../test/nodes/ExecuteWorkflow'; + +import * as transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'GET') { + return { + value: [ + { + id: '{00000000-0001-0000-0000-000000000000}', + name: 'Sheet1', + }, + { + id: '{F7AF92FE-D42D-452F-8E4A-901B1D1EBF3F}', + name: 'Sheet2', + }, + { + id: '{BF7BD843-4912-4B81-A0AC-4FBBC2783E20}', + name: 'foo2', + }, + ], + }; + } + }), + }; +}); + +describe('Test MicrosoftExcelV2, worksheet => getAll', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/worksheet/getAll.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'GET', + '/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets', + {}, + { $select: 'name', $top: 3 }, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/getAll.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/getAll.workflow.json new file mode 100644 index 0000000000000..ef2f7bfe5a68c --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/getAll.workflow.json @@ -0,0 +1,76 @@ +{ + "name": "My workflow 5", + "nodes": [ + { + "parameters": {}, + "id": "f0857ec9-0709-4657-a2f4-059837c94060", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [540, 220] + }, + { + "parameters": { + "resource": "worksheet", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ПРРО копія" + }, + "limit": 3, + "filters": { + "fields": "name" + } + }, + "id": "426ed055-0c9b-4ae2-a9fe-a6cce875d5ee", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [1020, 220], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "id": "{00000000-0001-0000-0000-000000000000}", + "name": "Sheet1" + } + }, + { + "json": { + "id": "{F7AF92FE-D42D-452F-8E4A-901B1D1EBF3F}", + "name": "Sheet2" + } + }, + { + "json": { + "id": "{BF7BD843-4912-4B81-A0AC-4FBBC2783E20}", + "name": "foo2" + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {} +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/readRows.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/readRows.test.ts new file mode 100644 index 0000000000000..014ff8d658686 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/readRows.test.ts @@ -0,0 +1,55 @@ +import { equalityTest, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; + +// eslint-disable-next-line unused-imports/no-unused-imports +import * as _transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string, resource: string) { + { + if (method === 'GET' && resource.includes('usedRange')) { + return { + values: [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Ron', 55, 'data 3'], + ], + }; + } + + return { + values: [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + ], + }; + } + }), + }; +}); + +describe('Test MicrosoftExcelV2, worksheet => readRows', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/worksheet/readRows.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + for (const testData of tests) { + test(testData.description, async () => equalityTest(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/readRows.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/readRows.workflow.json new file mode 100644 index 0000000000000..8ea8e6bdb8a86 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/readRows.workflow.json @@ -0,0 +1,137 @@ +{ + "name": "microsoft excel 365 - read rows", + "nodes": [ + { + "parameters": {}, + "id": "2e1ec8f6-a2e2-4aa9-909c-d0a279584131", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [820, 380] + }, + { + "parameters": { + "resource": "worksheet", + "operation": "readRows", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ПРРО копія" + }, + "worksheet": { + "__rl": true, + "value": "{A0883CFE-D27E-4ECC-B94B-981830AAD55B}", + "mode": "list", + "cachedResultName": "Sheet4" + }, + "options": {} + }, + "id": "86f2a240-3acf-45c2-b97f-63dd655d296b", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [1100, 260], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + }, + { + "parameters": { + "resource": "worksheet", + "operation": "readRows", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ПРРО копія", + "cachedResultUrl": "https://5w1hb7-my.sharepoint.com/personal/michaeldevsandbox_5w1hb7_onmicrosoft_com/_layouts/15/Doc.aspx?sourcedoc=%7BECC4041C-3AB6-4CF7-B079-0926470A1388%7D&file=%D0%9F%D0%A0%D0%A0%D0%9E%20%D0%BA%D0%BE%D0%BF%D1%96%D1%8F.xlsx&action=default&mobileredirect=true&DefaultItemOpen=1" + }, + "worksheet": { + "__rl": true, + "value": "{A0883CFE-D27E-4ECC-B94B-981830AAD55B}", + "mode": "list", + "cachedResultName": "Sheet4", + "cachedResultUrl": "https://5w1hb7-my.sharepoint.com/personal/michaeldevsandbox_5w1hb7_onmicrosoft_com/_layouts/15/Doc.aspx?sourcedoc=%7BECC4041C-3AB6-4CF7-B079-0926470A1388%7D&file=%D0%9F%D0%A0%D0%A0%D0%9E%20%D0%BA%D0%BE%D0%BF%D1%96%D1%8F.xlsx&action=default&mobileredirect=true&DefaultItemOpen=1&activeCell=Sheet4!A1" + }, + "useRange": true, + "range": "A1:D3", + "dataStartRow": 2, + "options": {} + }, + "id": "8ce6ab42-8f38-452b-90da-598d8a958c2b", + "name": "Microsoft Excel 3651", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [1100, 520], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "id": 1, + "name": "Sam", + "age": 33, + "data": "data 1" + } + }, + { + "json": { + "id": 2, + "name": "Jon", + "age": 44, + "data": "data 2" + } + }, + { + "json": { + "id": 3, + "name": "Ron", + "age": 55, + "data": "data 3" + } + } + ], + "Microsoft Excel 3651": [ + { + "json": { + "id": 2, + "name": "Jon", + "age": 44, + "data": "data 2" + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + }, + { + "node": "Microsoft Excel 3651", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {}, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/update.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/update.test.ts new file mode 100644 index 0000000000000..ca062b7784c6b --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/update.test.ts @@ -0,0 +1,68 @@ +import { equalityTest, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; + +// eslint-disable-next-line unused-imports/no-unused-imports +import * as _transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string, resource: string) { + if (method === 'GET' && resource.includes('usedRange')) { + return { + address: 'Sheet4!A1:D6', + values: [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Ron', 55, 'data 3'], + ], + }; + } + + if (method === 'PATCH' && resource.includes('{A0883CFE-D27E-4ECC-B94B-981830AAD55B}')) { + return { + values: [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Sam', 34, 'data 4'], + ], + }; + } + + if (method === 'PATCH' && resource.includes('{426949D7-797F-43A9-A8A4-8FE283495A82}')) { + return { + values: [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Don', 37, 'data 44'], + ], + }; + } + }), + }; +}); + +describe('Test MicrosoftExcelV2, worksheet => update', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/worksheet/update.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + for (const testData of tests) { + test(testData.description, async () => equalityTest(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/update.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/update.workflow.json new file mode 100644 index 0000000000000..5c94f2b42e28b --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/update.workflow.json @@ -0,0 +1,154 @@ +{ + "name": "My workflow 5", + "nodes": [ + { + "parameters": {}, + "id": "875e8784-eb59-40d8-ba45-129a5e29881c", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [380, 140] + }, + { + "parameters": { + "resource": "worksheet", + "operation": "update", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ПРРО копія" + }, + "worksheet": { + "__rl": true, + "value": "={A0883CFE-D27E-4ECC-B94B-981830AAD55B}", + "mode": "id" + }, + "columnToMatchOn": "id", + "valueToMatchOn": "3", + "fieldsUi": { + "values": [ + { + "column": "name", + "fieldValue": "Sam" + }, + { + "column": "age", + "fieldValue": "34" + }, + { + "column": "data", + "fieldValue": "data 4" + } + ] + }, + "options": {} + }, + "id": "0e0ac1d2-242c-486a-9287-c70307645acc", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [860, 140], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + }, + { + "parameters": { + "resource": "worksheet", + "operation": "update", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ПРРО копія" + }, + "worksheet": { + "__rl": true, + "value": "={426949D7-797F-43A9-A8A4-8FE283495A82}", + "mode": "id" + }, + "dataMode": "autoMap", + "columnToMatchOn": "id", + "options": {} + }, + "id": "d3209da3-cfaf-40a6-a318-c66c2931a28a", + "name": "Microsoft Excel 3651", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [860, 380], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + }, + { + "parameters": { + "jsCode": "return {\n id: 3,\n name: 'Don',\n age: 37,\n data: 'data 44',\n};" + }, + "id": "eb908630-7324-46a5-890d-b5cfccf17cb2", + "name": "Code", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [660, 380] + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "id": 3, + "name": "Sam", + "age": 34, + "data": "data 4" + } + } + ], + "Microsoft Excel 3651": [ + { + "json": { + "id": 3, + "name": "Don", + "age": 37, + "data": "data 44" + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + }, + { + "node": "Code", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code": { + "main": [ + [ + { + "node": "Microsoft Excel 3651", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {} +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/upsert.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/upsert.test.ts new file mode 100644 index 0000000000000..61351fd1ff635 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/upsert.test.ts @@ -0,0 +1,70 @@ +import { equalityTest, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; + +// eslint-disable-next-line unused-imports/no-unused-imports +import * as _transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string, resource: string) { + if (method === 'GET' && resource.includes('usedRange')) { + return { + address: 'Sheet4!A1:D6', + values: [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Ron', 55, 'data 3'], + ], + }; + } + + if (method === 'PATCH' && resource.includes('{A0883CFE-D27E-4ECC-B94B-981830AAD55B}')) { + return { + values: [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Ron', 55, 'data 3'], + [4, 'Sam', 34, 'data 4'], + ], + }; + } + + if (method === 'PATCH' && resource.includes('{426949D7-797F-43A9-A8A4-8FE283495A82}')) { + return { + values: [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Ron', 55, 'data 3'], + [4, 'Don', 37, 'data 44'], + ], + }; + } + }), + }; +}); + +describe('Test MicrosoftExcelV2, worksheet => upsert', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/worksheet/upsert.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + for (const testData of tests) { + test(testData.description, async () => equalityTest(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/upsert.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/upsert.workflow.json new file mode 100644 index 0000000000000..2e8a762c02984 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/upsert.workflow.json @@ -0,0 +1,162 @@ +{ + "name": "My workflow 5", + "nodes": [ + { + "parameters": {}, + "id": "f0857ec9-0709-4657-a2f4-059837c94060", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [540, 220] + }, + { + "parameters": { + "resource": "worksheet", + "operation": "upsert", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ПРРО копія", + "cachedResultUrl": "https://5w1hb7-my.sharepoint.com/personal/michaeldevsandbox_5w1hb7_onmicrosoft_com/_layouts/15/Doc.aspx?sourcedoc=%7BECC4041C-3AB6-4CF7-B079-0926470A1388%7D&file=%D0%9F%D0%A0%D0%A0%D0%9E%20%D0%BA%D0%BE%D0%BF%D1%96%D1%8F.xlsx&action=default&mobileredirect=true&DefaultItemOpen=1" + }, + "worksheet": { + "__rl": true, + "value": "={A0883CFE-D27E-4ECC-B94B-981830AAD55B}", + "mode": "id" + }, + "columnToMatchOn": "id", + "valueToMatchOn": "4", + "fieldsUi": { + "values": [ + { + "column": "name", + "fieldValue": "Sam" + }, + { + "column": "age", + "fieldValue": "34" + }, + { + "column": "data", + "fieldValue": "data 4" + } + ] + }, + "options": {} + }, + "id": "426ed055-0c9b-4ae2-a9fe-a6cce875d5ee", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [1020, 220], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + }, + { + "parameters": { + "resource": "worksheet", + "operation": "upsert", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ПРРО копія", + "cachedResultUrl": "https://5w1hb7-my.sharepoint.com/personal/michaeldevsandbox_5w1hb7_onmicrosoft_com/_layouts/15/Doc.aspx?sourcedoc=%7BECC4041C-3AB6-4CF7-B079-0926470A1388%7D&file=%D0%9F%D0%A0%D0%A0%D0%9E%20%D0%BA%D0%BE%D0%BF%D1%96%D1%8F.xlsx&action=default&mobileredirect=true&DefaultItemOpen=1" + }, + "worksheet": { + "__rl": true, + "value": "={426949D7-797F-43A9-A8A4-8FE283495A82}", + "mode": "id" + }, + "dataMode": "autoMap", + "columnToMatchOn": "id", + "options": {} + }, + "id": "0b10bfae-4e15-48c5-a2e6-7bec1c2687ec", + "name": "Microsoft Excel 3651", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [1020, 460], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + }, + { + "parameters": { + "jsCode": "return {\n id: 4,\n name: 'Don',\n age: 37,\n data: 'data 44',\n};" + }, + "id": "93453ccb-5ac3-425b-8ac4-d20f0dfe9bab", + "name": "Code", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [820, 460] + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "id": 4, + "name": "Sam", + "age": 34, + "data": "data 4" + } + } + ], + "Microsoft Excel 3651": [ + { + "json": { + "id": 4, + "name": "Don", + "age": 37, + "data": "data 44" + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + }, + { + "node": "Code", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code": { + "main": [ + [ + { + "node": "Microsoft Excel 3651", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {}, + "versionId": "f24273bf-ef07-49da-960b-a68b63961d4a", + "id": "135", + "meta": { + "instanceId": "36203ea1ce3cef713fa25999bd9874ae26b9e4c2c3a90a365f2882a154d031d0" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/utils/utils.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/utils/utils.test.ts new file mode 100644 index 0000000000000..5786e9c2d6589 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/utils/utils.test.ts @@ -0,0 +1,536 @@ +import { get } from 'lodash'; +import type { IDataObject, IExecuteFunctions, IGetNodeParameterOptions, INode } from 'n8n-workflow'; +import { + prepareOutput, + updateByAutoMaping, + updateByDefinedValues, +} from '../../../v2/helpers/utils'; + +const node: INode = { + id: '1', + name: 'Microsoft Excel 365', + typeVersion: 2, + type: 'n8n-nodes-base.microsoftExcel', + position: [60, 760], + parameters: {}, +}; + +const fakeExecute = (nodeParameters: IDataObject[]) => { + const fakeExecuteFunction = { + getNodeParameter( + parameterName: string, + itemIndex: number, + fallbackValue?: IDataObject | undefined, + options?: IGetNodeParameterOptions | undefined, + ) { + const parameter = options?.extractValue ? `${parameterName}.value` : parameterName; + return get(nodeParameters[itemIndex], parameter, fallbackValue); + }, + } as unknown as IExecuteFunctions; + return fakeExecuteFunction; +}; + +const responseData = { + address: 'Sheet4!A1:D4', + addressLocal: 'Sheet4!A1:D4', + columnCount: 4, + cellCount: 16, + columnHidden: false, + rowHidden: false, + numberFormat: [ + ['General', 'General', 'General', 'General'], + ['General', 'General', 'General', 'General'], + ['General', 'General', 'General', 'General'], + ['General', 'General', 'General', 'General'], + ], + columnIndex: 0, + text: [ + ['id', 'name', 'age', 'data'], + ['1', 'Sam', '33', 'data 1'], + ['2', 'Jon', '44', 'data 2'], + ['3', 'Ron', '55', 'data 3'], + ], + formulas: [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Ron', 55, 'data 3'], + ], + formulasLocal: [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Ron', 55, 'data 3'], + ], + formulasR1C1: [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Ron', 55, 'data 3'], + ], + hidden: false, + rowCount: 4, + rowIndex: 0, + valueTypes: [ + ['String', 'String', 'String', 'String'], + ['Double', 'String', 'Double', 'String'], + ['Double', 'String', 'Double', 'String'], + ['Double', 'String', 'Double', 'String'], + ], + values: [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Ron', 55, 'data 3'], + ], +}; + +describe('Test MicrosoftExcelV2, prepareOutput', () => { + it('should return empty array', () => { + const output = prepareOutput(node, { values: [] }, { rawData: false }); + expect(output).toBeDefined(); + expect(output).toEqual([]); + }); + + it('should return raw response', () => { + const output = prepareOutput(node, responseData, { rawData: true }); + expect(output).toBeDefined(); + expect(output[0].json.data).toEqual(responseData); + }); + + it('should return raw response in custom property', () => { + const customKey = 'customKey'; + const output = prepareOutput(node, responseData, { rawData: true, dataProperty: customKey }); + expect(output).toBeDefined(); + expect(output[0].json.customKey).toEqual(responseData); + }); + + it('should return formated response', () => { + const output = prepareOutput(node, responseData, { rawData: false }); + expect(output).toBeDefined(); + expect(output.length).toEqual(3); + expect(output[0].json).toEqual({ + id: 1, + name: 'Sam', + age: 33, + data: 'data 1', + }); + }); + + it('should return response with selected first data row', () => { + const output = prepareOutput(node, responseData, { rawData: false, firstDataRow: 3 }); + expect(output).toBeDefined(); + expect(output.length).toEqual(1); + expect(output[0].json).toEqual({ + id: 3, + name: 'Ron', + age: 55, + data: 'data 3', + }); + }); + + it('should return response with selected first data row', () => { + const [firstRow, ...rest] = responseData.values; + const response = { values: [...rest, firstRow] }; + const output = prepareOutput(node, response, { rawData: false, keyRow: 3, firstDataRow: 0 }); + expect(output).toBeDefined(); + expect(output.length).toEqual(3); + expect(output[0].json).toEqual({ + id: 1, + name: 'Sam', + age: 33, + data: 'data 1', + }); + }); +}); + +describe('Test MicrosoftExcelV2, updateByDefinedValues', () => { + it('should update single row', () => { + const nodeParameters = [ + { + columnToMatchOn: 'id', + valueToMatchOn: 2, + fieldsUi: { + values: [ + { + column: 'name', + fieldValue: 'Donald', + }, + ], + }, + }, + ]; + + const sheetData = responseData.values; + + const updateSummary = updateByDefinedValues.call( + fakeExecute(nodeParameters), + nodeParameters.length, + sheetData, + false, + ); + + expect(updateSummary).toBeDefined(); + expect(updateSummary.updatedRows).toContain(0); //header row + expect(updateSummary.updatedRows).toContain(2); //updated row + expect(updateSummary.updatedRows).toHaveLength(2); + expect(updateSummary.updatedData[2][1]).toEqual('Donald'); // updated value + }); + + it('should update multiple rows', () => { + const nodeParameters = [ + { + columnToMatchOn: 'id', + valueToMatchOn: 2, + fieldsUi: { + values: [ + { + column: 'name', + fieldValue: 'Donald', + }, + ], + }, + }, + { + columnToMatchOn: 'id', + valueToMatchOn: 3, + fieldsUi: { + values: [ + { + column: 'name', + fieldValue: 'Eduard', + }, + ], + }, + }, + { + columnToMatchOn: 'id', + valueToMatchOn: 4, + fieldsUi: { + values: [ + { + column: 'name', + fieldValue: 'Ismael', + }, + ], + }, + }, + ]; + + const sheetData = [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Ron', 55, 'data 3'], + [4, 'Ron', 55, 'data 3'], + ]; + + const updateSummary = updateByDefinedValues.call( + fakeExecute(nodeParameters), + nodeParameters.length, + sheetData, + false, + ); + + expect(updateSummary).toBeDefined(); + expect(updateSummary.updatedRows).toContain(0); //header row + expect(updateSummary.updatedRows).toContain(2); //updated row + expect(updateSummary.updatedRows).toContain(3); //updated row + expect(updateSummary.updatedRows).toContain(4); //updated row + expect(updateSummary.updatedRows).toHaveLength(4); + expect(updateSummary.updatedData[2][1]).toEqual('Donald'); // updated value + expect(updateSummary.updatedData[3][1]).toEqual('Eduard'); // updated value + expect(updateSummary.updatedData[4][1]).toEqual('Ismael'); // updated value + }); + + it('should update all occurances', () => { + const nodeParameters = [ + { + columnToMatchOn: 'data', + valueToMatchOn: 'data 3', + fieldsUi: { + values: [ + { + column: 'name', + fieldValue: 'Donald', + }, + ], + }, + }, + ]; + + const sheetData = [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 55, 'data 3'], + [2, 'Jon', 77, 'data 3'], + [3, 'Ron', 44, 'data 3'], + [4, 'Ron', 33, 'data 3'], + ]; + + const updateSummary = updateByDefinedValues.call( + fakeExecute(nodeParameters), + nodeParameters.length, + sheetData, + true, + ); + + expect(updateSummary).toBeDefined(); + expect(updateSummary.updatedRows).toContain(0); //header row + expect(updateSummary.updatedRows).toHaveLength(5); + + for (let i = 1; i < updateSummary.updatedRows.length; i++) { + expect(updateSummary.updatedData[i][1]).toEqual('Donald'); // updated value + } + }); + + it('should append rows', () => { + const nodeParameters = [ + { + columnToMatchOn: 'id', + valueToMatchOn: 4, + fieldsUi: { + values: [ + { + column: 'name', + fieldValue: 'Donald', + }, + { + column: 'age', + fieldValue: 45, + }, + { + column: 'data', + fieldValue: 'data 4', + }, + ], + }, + }, + { + columnToMatchOn: 'id', + valueToMatchOn: 5, + fieldsUi: { + values: [ + { + column: 'name', + fieldValue: 'Victor', + }, + { + column: 'age', + fieldValue: 67, + }, + { + column: 'data', + fieldValue: 'data 5', + }, + ], + }, + }, + ]; + + const sheetData = [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 55, 'data 3'], + [2, 'Jon', 77, 'data 3'], + [3, 'Ron', 44, 'data 3'], + ]; + + const updateSummary = updateByDefinedValues.call( + fakeExecute(nodeParameters), + nodeParameters.length, + sheetData, + true, + ); + + expect(updateSummary).toBeDefined(); + expect(updateSummary.updatedRows).toContain(0); + expect(updateSummary.updatedRows.length).toEqual(1); + expect(updateSummary.appendData[0]).toEqual({ id: 4, name: 'Donald', age: 45, data: 'data 4' }); + expect(updateSummary.appendData[1]).toEqual({ id: 5, name: 'Victor', age: 67, data: 'data 5' }); + }); +}); + +describe('Test MicrosoftExcelV2, updateByAutoMaping', () => { + it('should update single row', () => { + const items = [ + { + json: { + id: 2, + name: 'Donald', + }, + }, + ]; + + const sheetData = [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Ron', 55, 'data 3'], + ]; + + const updateSummary = updateByAutoMaping(items, sheetData, 'id'); + + expect(updateSummary).toBeDefined(); + expect(updateSummary.updatedRows).toHaveLength(2); + expect(updateSummary.updatedRows).toContain(0); //header row + expect(updateSummary.updatedRows).toContain(2); //updated row + expect(updateSummary.updatedData[2][1]).toEqual('Donald'); // updated value + }); + + it('should append single row', () => { + const items = [ + { + json: { + id: 5, + name: 'Donald', + }, + }, + ]; + + const sheetData = [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Ron', 55, 'data 3'], + ]; + + const updateSummary = updateByAutoMaping(items, sheetData, 'id'); + + expect(updateSummary).toBeDefined(); + expect(updateSummary.updatedRows).toHaveLength(1); + expect(updateSummary.updatedRows).toContain(0); //header row + expect(updateSummary.appendData[0]).toEqual({ id: 5, name: 'Donald' }); + }); + + it('should append skip row with match column undefined', () => { + const items = [ + { + json: { + id: 5, + name: 'Donald', + }, + }, + ]; + + const sheetData = [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Ron', 55, 'data 3'], + ]; + + const updateSummary = updateByAutoMaping(items, sheetData, 'idd'); + + expect(updateSummary).toBeDefined(); + expect(updateSummary.updatedRows).toHaveLength(1); + expect(updateSummary.updatedRows).toContain(0); //header row + expect(updateSummary.appendData.length).toEqual(0); + }); + + it('should update multiple rows', () => { + const items = [ + { + json: { + id: 2, + name: 'Donald', + }, + }, + { + json: { + id: 3, + name: 'Eduard', + }, + }, + { + json: { + id: 4, + name: 'Ismael', + }, + }, + ]; + + const sheetData = [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Ron', 55, 'data 3'], + [4, 'Ron', 55, 'data 3'], + ]; + + const updateSummary = updateByAutoMaping(items, sheetData, 'id'); + + expect(updateSummary).toBeDefined(); + expect(updateSummary.updatedRows).toContain(0); //header row + expect(updateSummary.updatedRows).toContain(2); //updated row + expect(updateSummary.updatedRows).toContain(3); //updated row + expect(updateSummary.updatedRows).toContain(4); //updated row + expect(updateSummary.updatedRows).toHaveLength(4); + expect(updateSummary.updatedData[2][1]).toEqual('Donald'); // updated value + expect(updateSummary.updatedData[3][1]).toEqual('Eduard'); // updated value + expect(updateSummary.updatedData[4][1]).toEqual('Ismael'); // updated value + }); + + it('should update all occurances', () => { + const items = [ + { + json: { + data: 'data 3', + name: 'Donald', + }, + }, + ]; + + const sheetData = [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 55, 'data 3'], + [2, 'Jon', 77, 'data 3'], + [3, 'Ron', 44, 'data 3'], + [4, 'Ron', 33, 'data 3'], + ]; + + const updateSummary = updateByAutoMaping(items, sheetData, 'data', true); + + expect(updateSummary).toBeDefined(); + expect(updateSummary.updatedRows).toContain(0); //header row + expect(updateSummary.updatedRows).toHaveLength(5); + + for (let i = 1; i < updateSummary.updatedRows.length; i++) { + expect(updateSummary.updatedData[i][1]).toEqual('Donald'); // updated value + } + }); + + it('should append rows', () => { + const items = [ + { + json: { + id: 4, + data: 'data 4', + name: 'Donald', + age: 45, + }, + }, + { + json: { + id: 5, + data: 'data 5', + name: 'Victor', + age: 67, + }, + }, + ]; + + const sheetData = [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 55, 'data 3'], + [2, 'Jon', 77, 'data 3'], + [3, 'Ron', 44, 'data 3'], + ]; + + const updateSummary = updateByAutoMaping(items, sheetData, 'data', true); + + expect(updateSummary).toBeDefined(); + expect(updateSummary.updatedRows).toContain(0); + expect(updateSummary.updatedRows.length).toEqual(1); + expect(updateSummary.appendData[0]).toEqual({ id: 4, name: 'Donald', age: 45, data: 'data 4' }); + expect(updateSummary.appendData[1]).toEqual({ id: 5, name: 'Victor', age: 67, data: 'data 5' }); + }); +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/GenericFunctions.ts b/packages/nodes-base/nodes/Microsoft/Excel/v1/GenericFunctions.ts similarity index 99% rename from packages/nodes-base/nodes/Microsoft/Excel/GenericFunctions.ts rename to packages/nodes-base/nodes/Microsoft/Excel/v1/GenericFunctions.ts index f29ed80272cb8..5b5d79bb0aca6 100644 --- a/packages/nodes-base/nodes/Microsoft/Excel/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Microsoft/Excel/v1/GenericFunctions.ts @@ -32,7 +32,7 @@ export async function microsoftApiRequest( if (Object.keys(headers).length !== 0) { options.headers = Object.assign({}, options.headers, headers); } - //@ts-ignore + return await this.helpers.requestOAuth2.call(this, 'microsoftExcelOAuth2Api', options); } catch (error) { throw new NodeApiError(this.getNode(), error as JsonObject); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v1/MicrosoftExcelV1.node.ts b/packages/nodes-base/nodes/Microsoft/Excel/v1/MicrosoftExcelV1.node.ts new file mode 100644 index 0000000000000..0def98076b2bc --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v1/MicrosoftExcelV1.node.ts @@ -0,0 +1,692 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ +import type { IExecuteFunctions } from 'n8n-core'; + +import type { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeBaseDescription, + INodeTypeDescription, + JsonObject, +} from 'n8n-workflow'; +import { NodeApiError } from 'n8n-workflow'; + +import { + microsoftApiRequest, + microsoftApiRequestAllItems, + microsoftApiRequestAllItemsSkip, +} from './GenericFunctions'; + +import { workbookFields, workbookOperations } from './WorkbookDescription'; + +import { worksheetFields, worksheetOperations } from './WorksheetDescription'; + +import { tableFields, tableOperations } from './TableDescription'; + +import { oldVersionNotice } from '../../../../utils/descriptions'; + +const versionDescription: INodeTypeDescription = { + displayName: 'Microsoft Excel', + name: 'microsoftExcel', + icon: 'file:excel.svg', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Microsoft Excel API', + defaults: { + name: 'Microsoft Excel', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'microsoftExcelOAuth2Api', + required: true, + }, + ], + properties: [ + oldVersionNotice, + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Table', + value: 'table', + description: 'Represents an Excel table', + }, + { + name: 'Workbook', + value: 'workbook', + description: + 'Workbook is the top level object which contains related workbook objects such as worksheets, tables, ranges, etc', + }, + { + name: 'Worksheet', + value: 'worksheet', + description: + 'An Excel worksheet is a grid of cells. It can contain data, tables, charts, etc.', + }, + ], + default: 'workbook', + }, + ...workbookOperations, + ...workbookFields, + ...worksheetOperations, + ...worksheetFields, + ...tableOperations, + ...tableFields, + ], +}; +export class MicrosoftExcelV1 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + ...versionDescription, + }; + } + + methods = { + loadOptions: { + // Get all the workbooks to display them to user so that he can + // select them easily + async getWorkbooks(this: ILoadOptionsFunctions): Promise { + const qs: IDataObject = { + select: 'id,name', + }; + const returnData: INodePropertyOptions[] = []; + const workbooks = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + "/drive/root/search(q='.xlsx')", + {}, + qs, + ); + for (const workbook of workbooks) { + const workbookName = workbook.name; + const workbookId = workbook.id; + returnData.push({ + name: workbookName, + value: workbookId, + }); + } + return returnData; + }, + // Get all the worksheets to display them to user so that he can + // select them easily + async getworksheets(this: ILoadOptionsFunctions): Promise { + const workbookId = this.getCurrentNodeParameter('workbook'); + const qs: IDataObject = { + select: 'id,name', + }; + const returnData: INodePropertyOptions[] = []; + const worksheets = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + `/drive/items/${workbookId}/workbook/worksheets`, + {}, + qs, + ); + for (const worksheet of worksheets) { + const worksheetName = worksheet.name; + const worksheetId = worksheet.id; + returnData.push({ + name: worksheetName, + value: worksheetId, + }); + } + return returnData; + }, + // Get all the tables to display them to user so that he can + // select them easily + async getTables(this: ILoadOptionsFunctions): Promise { + const workbookId = this.getCurrentNodeParameter('workbook'); + const worksheetId = this.getCurrentNodeParameter('worksheet'); + const qs: IDataObject = { + select: 'id,name', + }; + const returnData: INodePropertyOptions[] = []; + const tables = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables`, + {}, + qs, + ); + for (const table of tables) { + const tableName = table.name; + const tableId = table.id; + returnData.push({ + name: tableName, + value: tableId, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; + const length = items.length; + let qs: IDataObject = {}; + const result: IDataObject[] = []; + let responseData; + const resource = this.getNodeParameter('resource', 0); + const operation = this.getNodeParameter('operation', 0); + + if (resource === 'table') { + //https://docs.microsoft.com/en-us/graph/api/table-post-rows?view=graph-rest-1.0&tabs=http + if (operation === 'addRow') { + try { + // TODO: At some point it should be possible to use item dependent parameters. + // Is however important to then not make one separate request each. + const workbookId = this.getNodeParameter('workbook', 0) as string; + const worksheetId = this.getNodeParameter('worksheet', 0) as string; + const tableId = this.getNodeParameter('table', 0) as string; + const additionalFields = this.getNodeParameter('additionalFields', 0); + const body: IDataObject = {}; + + if (additionalFields.index) { + body.index = additionalFields.index as number; + } + + // Get table columns to eliminate any columns not needed on the input + responseData = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, + {}, + qs, + ); + const columns = responseData.value.map((column: IDataObject) => column.name); + + const rows: any[][] = []; + + // Bring the items into the correct format + for (const item of items) { + const row = []; + for (const column of columns) { + row.push(item.json[column]); + } + rows.push(row); + } + + body.values = rows; + const { id } = await microsoftApiRequest.call( + this, + 'POST', + `/drive/items/${workbookId}/workbook/createSession`, + { persistChanges: true }, + ); + responseData = await microsoftApiRequest.call( + this, + 'POST', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows/add`, + body, + {}, + '', + { 'workbook-session-id': id }, + ); + await microsoftApiRequest.call( + this, + 'POST', + `/drive/items/${workbookId}/workbook/closeSession`, + {}, + {}, + '', + { 'workbook-session-id': id }, + ); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject[]), + { itemData: { item: 0 } }, + ); + + returnData.push(...executionData); + } 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; + } + } + } + //https://docs.microsoft.com/en-us/graph/api/table-list-columns?view=graph-rest-1.0&tabs=http + if (operation === 'getColumns') { + for (let i = 0; i < length; i++) { + try { + qs = {}; + const workbookId = this.getNodeParameter('workbook', i) as string; + const worksheetId = this.getNodeParameter('worksheet', i) as string; + const tableId = this.getNodeParameter('table', i) as string; + const returnAll = this.getNodeParameter('returnAll', i); + const rawData = this.getNodeParameter('rawData', i); + if (rawData) { + const filters = this.getNodeParameter('filters', i); + if (filters.fields) { + qs.$select = filters.fields; + } + } + if (returnAll) { + responseData = await microsoftApiRequestAllItemsSkip.call( + this, + 'value', + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, + {}, + qs, + ); + } else { + qs.$top = this.getNodeParameter('limit', i); + responseData = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, + {}, + qs, + ); + responseData = responseData.value; + } + if (!rawData) { + responseData = responseData.map((column: IDataObject) => ({ name: column.name })); + } else { + const dataProperty = this.getNodeParameter('dataProperty', i) as string; + responseData = { [dataProperty]: responseData }; + } + + 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; + } + } + } + //https://docs.microsoft.com/en-us/graph/api/table-list-rows?view=graph-rest-1.0&tabs=http + if (operation === 'getRows') { + for (let i = 0; i < length; i++) { + qs = {}; + try { + const workbookId = this.getNodeParameter('workbook', i) as string; + const worksheetId = this.getNodeParameter('worksheet', i) as string; + const tableId = this.getNodeParameter('table', i) as string; + const returnAll = this.getNodeParameter('returnAll', i); + const rawData = this.getNodeParameter('rawData', i); + if (rawData) { + const filters = this.getNodeParameter('filters', i); + if (filters.fields) { + qs.$select = filters.fields; + } + } + if (returnAll) { + responseData = await microsoftApiRequestAllItemsSkip.call( + this, + 'value', + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`, + {}, + qs, + ); + } else { + const rowsQs = { ...qs }; + rowsQs.$top = this.getNodeParameter('limit', i); + responseData = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`, + {}, + rowsQs, + ); + responseData = responseData.value; + } + if (!rawData) { + const columnsQs = { ...qs }; + columnsQs.$select = 'name'; + // TODO: That should probably be cached in the future + let columns = await microsoftApiRequestAllItemsSkip.call( + this, + 'value', + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, + {}, + columnsQs, + ); + + columns = (columns as IDataObject[]).map((column) => column.name); + for (let index = 0; index < responseData.length; index++) { + const object: IDataObject = {}; + for (let y = 0; y < columns.length; y++) { + object[columns[y]] = responseData[index].values[0][y]; + } + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ ...object }), + { itemData: { item: index } }, + ); + + returnData.push(...executionData); + } + } else { + const dataProperty = this.getNodeParameter('dataProperty', i) as string; + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ [dataProperty]: responseData }), + { 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; + } + } + } + if (operation === 'lookup') { + for (let i = 0; i < length; i++) { + qs = {}; + try { + const workbookId = this.getNodeParameter('workbook', i) as string; + const worksheetId = this.getNodeParameter('worksheet', i) as string; + const tableId = this.getNodeParameter('table', i) as string; + const lookupColumn = this.getNodeParameter('lookupColumn', i) as string; + const lookupValue = this.getNodeParameter('lookupValue', i) as string; + const options = this.getNodeParameter('options', i); + + responseData = await microsoftApiRequestAllItemsSkip.call( + this, + 'value', + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`, + {}, + {}, + ); + + qs.$select = 'name'; + // TODO: That should probably be cached in the future + let columns = await microsoftApiRequestAllItemsSkip.call( + this, + 'value', + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, + {}, + qs, + ); + columns = columns.map((column: IDataObject) => column.name); + + if (!columns.includes(lookupColumn)) { + throw new NodeApiError(this.getNode(), responseData as JsonObject, { + message: `Column ${lookupColumn} does not exist on the table selected`, + }); + } + + result.length = 0; + for (let index = 0; index < responseData.length; index++) { + const object: IDataObject = {}; + for (let y = 0; y < columns.length; y++) { + object[columns[y]] = responseData[index].values[0][y]; + } + result.push({ ...object }); + } + + if (options.returnAllMatches) { + responseData = result.filter((data: IDataObject) => { + return data[lookupColumn]?.toString() === lookupValue; + }); + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } else { + responseData = result.find((data: IDataObject) => { + return data[lookupColumn]?.toString() === lookupValue; + }); + 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; + } + } + } + } + if (resource === 'workbook') { + for (let i = 0; i < length; i++) { + qs = {}; + try { + //https://docs.microsoft.com/en-us/graph/api/worksheetcollection-add?view=graph-rest-1.0&tabs=http + if (operation === 'addWorksheet') { + const workbookId = this.getNodeParameter('workbook', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i); + const body: IDataObject = {}; + if (additionalFields.name) { + body.name = additionalFields.name; + } + const { id } = await microsoftApiRequest.call( + this, + 'POST', + `/drive/items/${workbookId}/workbook/createSession`, + { persistChanges: true }, + ); + responseData = await microsoftApiRequest.call( + this, + 'POST', + `/drive/items/${workbookId}/workbook/worksheets/add`, + body, + {}, + '', + { 'workbook-session-id': id }, + ); + await microsoftApiRequest.call( + this, + 'POST', + `/drive/items/${workbookId}/workbook/closeSession`, + {}, + {}, + '', + { 'workbook-session-id': id }, + ); + } + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i); + const filters = this.getNodeParameter('filters', i); + if (filters.fields) { + qs.$select = filters.fields; + } + if (returnAll) { + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + "/drive/root/search(q='.xlsx')", + {}, + qs, + ); + } else { + qs.$top = this.getNodeParameter('limit', i); + responseData = await microsoftApiRequest.call( + this, + 'GET', + "/drive/root/search(q='.xlsx')", + {}, + qs, + ); + responseData = responseData.value; + } + } + + if (Array.isArray(responseData)) { + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } else if (responseData !== undefined) { + 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; + } + } + } + if (resource === 'worksheet') { + for (let i = 0; i < length; i++) { + qs = {}; + try { + //https://docs.microsoft.com/en-us/graph/api/workbook-list-worksheets?view=graph-rest-1.0&tabs=http + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i); + const workbookId = this.getNodeParameter('workbook', i) as string; + const filters = this.getNodeParameter('filters', i); + if (filters.fields) { + qs.$select = filters.fields; + } + 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; + } + } + //https://docs.microsoft.com/en-us/graph/api/worksheet-range?view=graph-rest-1.0&tabs=http + if (operation === 'getContent') { + const workbookId = this.getNodeParameter('workbook', i) as string; + const worksheetId = this.getNodeParameter('worksheet', i) as string; + const range = this.getNodeParameter('range', i) as string; + const rawData = this.getNodeParameter('rawData', i); + if (rawData) { + const filters = this.getNodeParameter('filters', i); + if (filters.fields) { + qs.$select = filters.fields; + } + } + + responseData = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`, + {}, + qs, + ); + + if (!rawData) { + const keyRow = this.getNodeParameter('keyRow', i) as number; + const dataStartRow = this.getNodeParameter('dataStartRow', i) as number; + if (responseData.values === null) { + throw new NodeApiError(this.getNode(), responseData as JsonObject, { + message: 'Range did not return data', + }); + } + const keyValues = responseData.values[keyRow]; + for (let index = dataStartRow; index < responseData.values.length; index++) { + const object: IDataObject = {}; + for (let y = 0; y < keyValues.length; y++) { + object[keyValues[y]] = responseData.values[index][y]; + } + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ ...object }), + { itemData: { item: index } }, + ); + + returnData.push(...executionData); + } + } else { + const dataProperty = this.getNodeParameter('dataProperty', i) as string; + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ [dataProperty]: responseData }), + { 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 this.prepareOutputData(returnData); + } +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/TableDescription.ts b/packages/nodes-base/nodes/Microsoft/Excel/v1/TableDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Microsoft/Excel/TableDescription.ts rename to packages/nodes-base/nodes/Microsoft/Excel/v1/TableDescription.ts diff --git a/packages/nodes-base/nodes/Microsoft/Excel/WorkbookDescription.ts b/packages/nodes-base/nodes/Microsoft/Excel/v1/WorkbookDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Microsoft/Excel/WorkbookDescription.ts rename to packages/nodes-base/nodes/Microsoft/Excel/v1/WorkbookDescription.ts diff --git a/packages/nodes-base/nodes/Microsoft/Excel/WorksheetDescription.ts b/packages/nodes-base/nodes/Microsoft/Excel/v1/WorksheetDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Microsoft/Excel/WorksheetDescription.ts rename to packages/nodes-base/nodes/Microsoft/Excel/v1/WorksheetDescription.ts diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/MicrosoftExcelV2.node.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/MicrosoftExcelV2.node.ts new file mode 100644 index 0000000000000..660ca4fe871a9 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/MicrosoftExcelV2.node.ts @@ -0,0 +1,25 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ +import type { IExecuteFunctions } from 'n8n-core'; + +import type { INodeType, INodeTypeBaseDescription, INodeTypeDescription } from 'n8n-workflow'; + +import { listSearch, loadOptions } from './methods'; +import { versionDescription } from './actions/versionDescription'; +import { router } from './actions/router'; + +export class MicrosoftExcelV2 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + ...versionDescription, + }; + } + + methods = { listSearch, loadOptions }; + + async execute(this: IExecuteFunctions) { + return router.call(this); + } +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/common.descriptions.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/common.descriptions.ts new file mode 100644 index 0000000000000..adad75623e1d3 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/common.descriptions.ts @@ -0,0 +1,140 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const workbookRLC: INodeProperties = { + displayName: 'Workbook', + name: 'workbook', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchWorkbooks', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Not a valid Workbook ID', + }, + }, + ], + }, + ], +}; + +export const worksheetRLC: INodeProperties = { + displayName: 'Sheet', + name: 'worksheet', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'getWorksheetsList', + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '{[a-zA-Z0-9\\-_]{2,}}', + errorMessage: 'Not a valid Sheet ID', + }, + }, + ], + }, + ], +}; + +export const tableRLC: INodeProperties = { + displayName: 'Table', + name: 'table', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'getWorksheetTables', + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '{[a-zA-Z0-9\\-_]{2,}}', + errorMessage: 'Not a valid Table ID', + }, + }, + ], + }, + ], +}; + +export const rawDataOutput: INodeProperties = { + displayName: 'Raw Data Output', + name: 'rawDataOutput', + type: 'fixedCollection', + default: { values: { rawData: false } }, + options: [ + { + displayName: 'Values', + name: 'values', + values: [ + { + 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', + }, + ], + }, + ], + displayOptions: { + hide: { + '/dataMode': ['nothing'], + }, + }, +}; diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/node.type.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/node.type.ts new file mode 100644 index 0000000000000..dcf09d02eec04 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/node.type.ts @@ -0,0 +1,20 @@ +import type { AllEntities, Entity } from 'n8n-workflow'; + +type MicrosoftExcelMap = { + table: + | 'append' + | 'addTable' + | 'convertToRange' + | 'deleteTable' + | 'getColumns' + | 'getRows' + | 'lookup'; + workbook: 'addWorksheet' | 'deleteWorkbook' | 'getAll'; + worksheet: 'append' | 'clear' | 'deleteWorksheet' | 'getAll' | 'readRows' | 'update' | 'upsert'; +}; + +export type MicrosoftExcel = AllEntities; + +export type MicrosoftExcelChannel = Entity; +export type MicrosoftExcelMessage = Entity; +export type MicrosoftExcelMember = Entity; diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/router.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/router.ts new file mode 100644 index 0000000000000..681b1e3bab271 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/router.ts @@ -0,0 +1,37 @@ +import type { IExecuteFunctions } from 'n8n-core'; +import type { INodeExecutionData } from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; +import type { MicrosoftExcel } from './node.type'; + +import * as table from './table/Table.resource'; +import * as workbook from './workbook/Workbook.resource'; +import * as worksheet from './worksheet/Worksheet.resource'; + +export async function router(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + let returnData: INodeExecutionData[] = []; + + const resource = this.getNodeParameter('resource', 0); + const operation = this.getNodeParameter('operation', 0); + + const microsoftExcel = { + resource, + operation, + } as MicrosoftExcel; + + switch (microsoftExcel.resource) { + case 'table': + returnData = await table[microsoftExcel.operation].execute.call(this, items); + break; + case 'workbook': + returnData = await workbook[microsoftExcel.operation].execute.call(this, items); + break; + case 'worksheet': + returnData = await worksheet[microsoftExcel.operation].execute.call(this, items); + break; + default: + throw new NodeOperationError(this.getNode(), `The resource "${resource}" is not known`); + } + + return this.prepareOutputData(returnData); +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/Table.resource.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/Table.resource.ts new file mode 100644 index 0000000000000..6fc4497805454 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/Table.resource.ts @@ -0,0 +1,77 @@ +import type { INodeProperties } from 'n8n-workflow'; + +import * as append from './append.operation'; +import * as addTable from './addTable.operation'; +import * as convertToRange from './convertToRange.operation'; +import * as deleteTable from './deleteTable.operation'; +import * as getColumns from './getColumns.operation'; +import * as getRows from './getRows.operation'; +import * as lookup from './lookup.operation'; + +export { append, addTable, convertToRange, deleteTable, getColumns, getRows, lookup }; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['table'], + }, + }, + options: [ + { + name: 'Append', + value: 'append', + description: 'Add rows to the end of the table', + action: 'Append rows to table', + }, + { + name: 'Convert to Range', + value: 'convertToRange', + description: 'Convert a table to a range', + action: 'Convert to range', + }, + { + name: 'Create', + value: 'addTable', + description: 'Add a table based on range', + action: 'Create a table', + }, + { + name: 'Delete', + value: 'deleteTable', + description: 'Delete a table', + action: 'Delete a table', + }, + { + name: 'Get Columns', + value: 'getColumns', + description: 'Retrieve a list of table columns', + action: 'Get columns', + }, + { + name: 'Get Rows', + value: 'getRows', + description: 'Retrieve a list of table rows', + action: 'Get rows', + }, + { + name: 'Lookup', + value: 'lookup', + description: 'Look for rows that match a given value in a column', + action: 'Lookup a column', + }, + ], + default: 'append', + }, + ...append.description, + ...addTable.description, + ...convertToRange.description, + ...deleteTable.description, + ...getColumns.description, + ...getRows.description, + ...lookup.description, +]; diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/addTable.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/addTable.operation.ts new file mode 100644 index 0000000000000..da1d44554720a --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/addTable.operation.ts @@ -0,0 +1,127 @@ +import type { IExecuteFunctions } from 'n8n-core'; +import type { IDataObject, 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: 'Select Range', + name: 'selectRange', + type: 'options', + options: [ + { + name: 'Automatically', + value: 'auto', + description: 'The whole used range on the selected sheet will be converted into a table', + }, + { + name: 'Manually', + value: 'manual', + description: 'Select a range that will be converted into a table', + }, + ], + default: 'auto', + }, + { + displayName: 'Range', + name: 'range', + type: 'string', + default: '', + placeholder: 'A1:B2', + description: 'The range of cells that will be converted to a table', + displayOptions: { + show: { + selectRange: ['manual'], + }, + }, + }, + { + displayName: 'Has Headers', + name: 'hasHeaders', + type: 'boolean', + default: true, + description: + 'Whether the range has column labels. When this property set to false Excel will automatically generate header shifting the data down by one row.', + }, +]; + +const displayOptions = { + show: { + resource: ['table'], + operation: ['addTable'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise { + //https://learn.microsoft.com/en-us/graph/api/worksheet-post-tables?view=graph-rest-1.0 + 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 selectRange = this.getNodeParameter('selectRange', i) as string; + + const hasHeaders = this.getNodeParameter('hasHeaders', i) as boolean; + + let range = ''; + if (selectRange === 'auto') { + const { address } = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/usedRange`, + undefined, + { + select: 'address', + }, + ); + range = address.split('!')[1]; + } else { + range = this.getNodeParameter('range', i) as string; + } + + const responseData = await microsoftApiRequest.call( + this, + 'POST', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/add`, + { + address: range, + hasHeaders, + }, + ); + + 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; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/append.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/append.operation.ts new file mode 100644 index 0000000000000..e97b2d18e405f --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/append.operation.ts @@ -0,0 +1,282 @@ +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 { tableRLC, workbookRLC, worksheetRLC } from '../common.descriptions'; + +const properties: INodeProperties[] = [ + workbookRLC, + worksheetRLC, + tableRLC, + { + 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 expression', + typeOptions: { + loadOptionsDependsOn: ['table.value', 'worksheet.value', 'workbook.value'], + loadOptionsMethod: 'getTableColumns', + }, + default: '', + }, + { + displayName: 'Value', + name: 'fieldValue', + type: 'string', + default: '', + requiresDataPath: 'single', + }, + ], + }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Index', + name: 'index', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: + 'Specifies the relative position of the new row. If not defined, the addition happens at the end. Any row below the inserted row will be shifted downwards. First row index is 0.', + }, + { + 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: ['table'], + operation: ['append'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise { + //https://docs.microsoft.com/en-us/graph/api/table-post-rows?view=graph-rest-1.0&tabs=http + const returnData: INodeExecutionData[] = []; + + try { + // TODO: At some point it should be possible to use item dependent parameters. + // Is however important to then not make one separate request each. + const workbookId = this.getNodeParameter('workbook', 0, undefined, { + extractValue: true, + }) as string; + + const worksheetId = this.getNodeParameter('worksheet', 0, undefined, { + extractValue: true, + }) as string; + + const tableId = this.getNodeParameter('table', 0, undefined, { + extractValue: true, + }) as string; + + const dataMode = this.getNodeParameter('dataMode', 0) as string; + + // Get table columns to eliminate any columns not needed on the input + const columnsData = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, + {}, + ); + const columnsRow = columnsData.value.map((column: IDataObject) => column.name); + + const body: IDataObject = {}; + + let values: string[][] = []; + + if (dataMode === 'raw') { + const data = this.getNodeParameter('data', 0); + values = processJsonInput(data, 'Data') as string[][]; + } + + 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); + } + } + + body.values = values; + + const options = this.getNodeParameter('options', 0); + + if (options.index) { + body.index = options.index as number; + } + + const { id } = await microsoftApiRequest.call( + this, + 'POST', + `/drive/items/${workbookId}/workbook/createSession`, + { persistChanges: true }, + ); + const responseData = await microsoftApiRequest.call( + this, + 'POST', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows/add`, + body, + {}, + '', + { 'workbook-session-id': id }, + ); + await microsoftApiRequest.call( + this, + 'POST', + `/drive/items/${workbookId}/workbook/closeSession`, + {}, + {}, + '', + { 'workbook-session-id': id }, + ); + + const rawData = options.rawData as boolean; + const dataProperty = (options.dataProperty as string) || 'data'; + + returnData.push( + ...prepareOutput(this.getNode(), responseData as ExcelResponse, { + columnsRow, + dataProperty, + rawData, + }), + ); + } 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; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/convertToRange.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/convertToRange.operation.ts new file mode 100644 index 0000000000000..281b15461d0df --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/convertToRange.operation.ts @@ -0,0 +1,64 @@ +import type { IExecuteFunctions } from 'n8n-core'; +import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../../utils/utilities'; +import { microsoftApiRequest } from '../../transport'; +import { tableRLC, workbookRLC, worksheetRLC } from '../common.descriptions'; + +const properties: INodeProperties[] = [workbookRLC, worksheetRLC, tableRLC]; + +const displayOptions = { + show: { + resource: ['table'], + operation: ['convertToRange'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise { + 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 tableId = this.getNodeParameter('table', i, undefined, { + extractValue: true, + }) as string; + + const responseData = await microsoftApiRequest.call( + this, + 'POST', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/convertToRange`, + ); + + 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; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/deleteTable.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/deleteTable.operation.ts new file mode 100644 index 0000000000000..171c80044c9d5 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/deleteTable.operation.ts @@ -0,0 +1,64 @@ +import type { IExecuteFunctions } from 'n8n-core'; +import type { INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../../utils/utilities'; +import { microsoftApiRequest } from '../../transport'; +import { tableRLC, workbookRLC, worksheetRLC } from '../common.descriptions'; + +const properties: INodeProperties[] = [workbookRLC, worksheetRLC, tableRLC]; + +const displayOptions = { + show: { + resource: ['table'], + operation: ['deleteTable'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise { + 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 tableId = this.getNodeParameter('table', i, undefined, { + extractValue: true, + }) as string; + + await microsoftApiRequest.call( + this, + 'DELETE', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}`, + ); + + 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; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/getColumns.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/getColumns.operation.ts new file mode 100644 index 0000000000000..18260d84d254d --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/getColumns.operation.ts @@ -0,0 +1,165 @@ +import type { IExecuteFunctions } from 'n8n-core'; +import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../../utils/utilities'; +import { microsoftApiRequest, microsoftApiRequestAllItemsSkip } from '../../transport'; +import { tableRLC, workbookRLC, worksheetRLC } from '../common.descriptions'; + +const properties: INodeProperties[] = [ + workbookRLC, + worksheetRLC, + tableRLC, + { + 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: 'RAW Data', + name: 'rawData', + type: 'boolean', + 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: { + rawData: [true], + }, + }, + description: 'The name of the property into which to write the RAW data', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + rawData: [true], + }, + }, + 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: ['table'], + operation: ['getColumns'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise { + //https://docs.microsoft.com/en-us/graph/api/table-list-columns?view=graph-rest-1.0&tabs=http + const returnData: INodeExecutionData[] = []; + + for (let i = 0; i < items.length; i++) { + try { + const qs: IDataObject = {}; + const workbookId = this.getNodeParameter('workbook', i, undefined, { + extractValue: true, + }) as string; + + const worksheetId = this.getNodeParameter('worksheet', i, undefined, { + extractValue: true, + }) as string; + + const tableId = this.getNodeParameter('table', i, undefined, { + extractValue: true, + }) as string; + + const returnAll = this.getNodeParameter('returnAll', i); + const rawData = this.getNodeParameter('rawData', i); + if (rawData) { + const filters = this.getNodeParameter('filters', i); + if (filters.fields) { + qs.$select = filters.fields; + } + } + + let responseData; + if (returnAll) { + responseData = await microsoftApiRequestAllItemsSkip.call( + this, + 'value', + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, + {}, + qs, + ); + } else { + qs.$top = this.getNodeParameter('limit', i); + responseData = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, + {}, + qs, + ); + responseData = responseData.value; + } + if (!rawData) { + responseData = responseData.map((column: IDataObject) => ({ name: column.name })); + } else { + const dataProperty = this.getNodeParameter('dataProperty', i) as string; + responseData = { [dataProperty]: responseData }; + } + + 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; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/getRows.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/getRows.operation.ts new file mode 100644 index 0000000000000..bc0c56f940365 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/getRows.operation.ts @@ -0,0 +1,223 @@ +import type { IExecuteFunctions } from 'n8n-core'; +import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../../utils/utilities'; +import { microsoftApiRequest, microsoftApiRequestAllItemsSkip } from '../../transport'; +import { tableRLC, workbookRLC, worksheetRLC } from '../common.descriptions'; + +const properties: INodeProperties[] = [ + workbookRLC, + worksheetRLC, + tableRLC, + { + 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: 'RAW Data', + name: 'rawData', + type: 'boolean', + 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: { + rawData: [true], + }, + }, + description: 'The name of the property into which to write the RAW data', + }, + { + 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', + displayOptions: { + show: { + '/rawData': [true], + }, + }, + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Column Names or IDs', + name: 'column', + type: 'multiOptions', + description: + 'Choose from the list, or specify an ID using an expression. Choose from the list, or specify IDs using an expression.', + typeOptions: { + loadOptionsDependsOn: ['table.value', 'worksheet.value', 'workbook.value'], + loadOptionsMethod: 'getTableColumns', + }, + default: [], + displayOptions: { + show: { + '/rawData': [false], + }, + }, + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['table'], + operation: ['getRows'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise { + //https://docs.microsoft.com/en-us/graph/api/table-list-rows?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 tableId = this.getNodeParameter('table', i, undefined, { + extractValue: true, + }) as string; + + const filters = this.getNodeParameter('filters', i); + const returnAll = this.getNodeParameter('returnAll', i); + const rawData = this.getNodeParameter('rawData', i); + + if (rawData) { + if (filters.fields) { + qs.$select = filters.fields; + } + } + + let responseData; + if (returnAll) { + responseData = await microsoftApiRequestAllItemsSkip.call( + this, + 'value', + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`, + {}, + qs, + ); + } else { + const rowsQs = { ...qs }; + rowsQs.$top = this.getNodeParameter('limit', i); + responseData = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`, + {}, + rowsQs, + ); + responseData = responseData.value; + } + if (!rawData) { + const columnsQs = { ...qs }; + columnsQs.$select = 'name'; + // TODO: That should probably be cached in the future + let columns = await microsoftApiRequestAllItemsSkip.call( + this, + 'value', + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, + {}, + columnsQs, + ); + + columns = (columns as IDataObject[]).map((column) => column.name); + + let rows: INodeExecutionData[] = []; + for (let index = 0; index < responseData.length; index++) { + const object: IDataObject = {}; + for (let y = 0; y < columns.length; y++) { + object[columns[y]] = responseData[index].values[0][y]; + } + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ ...object }), + { itemData: { item: index } }, + ); + + rows.push(...executionData); + } + + if ((filters?.column as string[])?.length) { + rows = rows.map((row) => { + const rowData: IDataObject = {}; + Object.keys(row.json).forEach((key) => { + if ((filters.column as string[]).includes(key)) { + rowData[key] = row.json[key]; + } + }); + return { ...rowData, json: rowData }; + }); + } + + returnData.push(...rows); + } else { + const dataProperty = this.getNodeParameter('dataProperty', i) as string; + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ [dataProperty]: responseData }), + { 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; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/lookup.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/lookup.operation.ts new file mode 100644 index 0000000000000..6229930ad88cc --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/lookup.operation.ts @@ -0,0 +1,156 @@ +import type { IExecuteFunctions } from 'n8n-core'; +import type { IDataObject, INodeExecutionData, INodeProperties, JsonObject } from 'n8n-workflow'; +import { NodeApiError } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../../utils/utilities'; +import { microsoftApiRequestAllItemsSkip } from '../../transport'; +import { tableRLC, workbookRLC, worksheetRLC } from '../common.descriptions'; + +const properties: INodeProperties[] = [ + workbookRLC, + worksheetRLC, + tableRLC, + { + displayName: 'Lookup Column', + name: 'lookupColumn', + type: 'string', + default: '', + placeholder: 'Email', + required: true, + description: 'The name of the column in which to look for value', + }, + { + displayName: 'Lookup Value', + name: 'lookupValue', + type: 'string', + default: '', + placeholder: 'frank@example.com', + required: true, + description: 'The value to look for in column', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Return All Matches', + name: 'returnAllMatches', + type: 'boolean', + default: false, + // 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.', + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['table'], + operation: ['lookup'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise { + 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 tableId = this.getNodeParameter('table', i, undefined, { + extractValue: true, + }) as string; + + const lookupColumn = this.getNodeParameter('lookupColumn', i) as string; + const lookupValue = this.getNodeParameter('lookupValue', i) as string; + const options = this.getNodeParameter('options', i); + + let responseData = await microsoftApiRequestAllItemsSkip.call( + this, + 'value', + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`, + {}, + {}, + ); + + qs.$select = 'name'; + // TODO: That should probably be cached in the future + let columns = await microsoftApiRequestAllItemsSkip.call( + this, + 'value', + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, + {}, + qs, + ); + columns = columns.map((column: IDataObject) => column.name); + + if (!columns.includes(lookupColumn)) { + throw new NodeApiError(this.getNode(), responseData as JsonObject, { + message: `Column ${lookupColumn} does not exist on the table selected`, + }); + } + + const result: IDataObject[] = []; + + for (let index = 0; index < responseData.length; index++) { + const object: IDataObject = {}; + for (let y = 0; y < columns.length; y++) { + object[columns[y]] = responseData[index].values[0][y]; + } + result.push({ ...object }); + } + + if (options.returnAllMatches) { + responseData = result.filter((data: IDataObject) => { + return data[lookupColumn]?.toString() === lookupValue; + }); + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } else { + responseData = result.find((data: IDataObject) => { + return data[lookupColumn]?.toString() === lookupValue; + }); + 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; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/versionDescription.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/versionDescription.ts new file mode 100644 index 0000000000000..128c25231ef5b --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/versionDescription.ts @@ -0,0 +1,63 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ +import type { INodeTypeDescription } from 'n8n-workflow'; + +import * as table from './table/Table.resource'; +import * as workbook from './workbook/Workbook.resource'; +import * as worksheet from './worksheet/Worksheet.resource'; + +export const versionDescription: INodeTypeDescription = { + displayName: 'Microsoft Excel 365', + name: 'microsoftExcel', + icon: 'file:excel.svg', + group: ['input'], + version: 2, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Microsoft Excel API', + defaults: { + name: 'Microsoft Excel 365', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'microsoftExcelOAuth2Api', + required: true, + }, + ], + properties: [ + { + displayName: + 'This node connects to the Microsoft 365 cloud platform. Use the \'Spreadsheet File\' node to directly manipulate spreadsheet files (.xls, .csv, etc). More info.', + name: 'notice', + type: 'notice', + default: '', + }, + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Table', + value: 'table', + description: 'Represents an Excel table', + }, + { + name: 'Workbook', + value: 'workbook', + description: 'A workbook is the top level object which contains one or more worksheets', + }, + { + name: 'Sheet', + value: 'worksheet', + description: 'A sheet is a grid of cells which can contain data, tables, charts, etc', + }, + ], + default: 'workbook', + }, + ...table.description, + ...workbook.description, + ...worksheet.description, + ], +}; diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/workbook/Workbook.resource.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/workbook/Workbook.resource.ts new file mode 100644 index 0000000000000..be20b4c444e5c --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/workbook/Workbook.resource.ts @@ -0,0 +1,45 @@ +import type { INodeProperties } from 'n8n-workflow'; + +import * as addWorksheet from './addWorksheet.operation'; +import * as deleteWorkbook from './deleteWorkbook.operation'; +import * as getAll from './getAll.operation'; + +export { addWorksheet, deleteWorkbook, getAll }; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['workbook'], + }, + }, + options: [ + { + name: 'Add Sheet', + value: 'addWorksheet', + description: 'Add a new sheet to the workbook', + action: 'Add a sheet to a workbook', + }, + { + name: 'Delete', + value: 'deleteWorkbook', + description: 'Delete workbook', + action: 'Delete workbook', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Get workbooks', + action: 'Get workbooks', + }, + ], + default: 'getAll', + }, + ...addWorksheet.description, + ...deleteWorkbook.description, + ...getAll.description, +]; diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/workbook/addWorksheet.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/workbook/addWorksheet.operation.ts new file mode 100644 index 0000000000000..856a909f28874 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/workbook/addWorksheet.operation.ts @@ -0,0 +1,109 @@ +import type { IExecuteFunctions } from 'n8n-core'; +import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../../utils/utilities'; +import { microsoftApiRequest } from '../../transport'; +import { workbookRLC } from '../common.descriptions'; + +const properties: INodeProperties[] = [ + workbookRLC, + { + displayName: 'Options', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: + 'The name of the sheet to be added. The name should be unique. If not specified, Excel will determine the name of the new worksheet.', + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['workbook'], + operation: ['addWorksheet'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise { + //https://docs.microsoft.com/en-us/graph/api/worksheetcollection-add?view=graph-rest-1.0&tabs=http + const returnData: INodeExecutionData[] = []; + + for (let i = 0; i < items.length; i++) { + try { + const workbookId = this.getNodeParameter('workbook', i, undefined, { + extractValue: true, + }) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i); + const body: IDataObject = {}; + if (additionalFields.name) { + body.name = additionalFields.name; + } + const { id } = await microsoftApiRequest.call( + this, + 'POST', + `/drive/items/${workbookId}/workbook/createSession`, + { persistChanges: true }, + ); + const responseData = await microsoftApiRequest.call( + this, + 'POST', + `/drive/items/${workbookId}/workbook/worksheets/add`, + body, + {}, + '', + { 'workbook-session-id': id }, + ); + await microsoftApiRequest.call( + this, + 'POST', + `/drive/items/${workbookId}/workbook/closeSession`, + {}, + {}, + '', + { 'workbook-session-id': id }, + ); + + if (Array.isArray(responseData)) { + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } else if (responseData !== undefined) { + 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; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/workbook/deleteWorkbook.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/workbook/deleteWorkbook.operation.ts new file mode 100644 index 0000000000000..b97acce63a518 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/workbook/deleteWorkbook.operation.ts @@ -0,0 +1,77 @@ +import type { IExecuteFunctions } from 'n8n-core'; +import type { INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../../utils/utilities'; +import { microsoftApiRequest } from '../../transport'; +import { workbookRLC } from '../common.descriptions'; + +const properties: INodeProperties[] = [workbookRLC]; + +const displayOptions = { + show: { + resource: ['workbook'], + operation: ['deleteWorkbook'], + }, +}; +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise { + const returnData: INodeExecutionData[] = []; + + for (let i = 0; i < items.length; i++) { + try { + const workbookId = this.getNodeParameter('workbook', i, undefined, { + extractValue: true, + }) as string; + + try { + await microsoftApiRequest.call(this, 'DELETE', `/drive/items/${workbookId}`); + } catch (error) { + if (error?.description.includes('Lock token does not match existing lock')) { + const errorDescription = + 'Lock token does not match existing lock, this error could happen if the file is opened in the browser or the Office client, please close file and try again.'; + + throw new NodeOperationError(this.getNode(), error as Error, { + itemIndex: i, + description: errorDescription, + }); + } else { + throw error; + } + } + + const responseData = { success: true }; + + if (Array.isArray(responseData)) { + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } else if (responseData !== undefined) { + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData), + { 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; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/workbook/getAll.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/workbook/getAll.operation.ts new file mode 100644 index 0000000000000..bcff50920a95a --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/workbook/getAll.operation.ts @@ -0,0 +1,122 @@ +import type { IExecuteFunctions } from 'n8n-core'; +import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../../utils/utilities'; +import { microsoftApiRequest, microsoftApiRequestAllItems } from '../../transport'; + +const properties: INodeProperties[] = [ + { + 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: ['workbook'], + operation: ['getAll'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise { + const returnData: INodeExecutionData[] = []; + + for (let i = 0; i < items.length; i++) { + try { + const returnAll = this.getNodeParameter('returnAll', i); + const filters = this.getNodeParameter('filters', i); + const qs: IDataObject = {}; + if (filters.fields) { + qs.$select = filters.fields; + } + let responseData; + if (returnAll) { + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + "/drive/root/search(q='.xlsx')", + {}, + qs, + ); + } else { + qs.$top = this.getNodeParameter('limit', i); + responseData = await microsoftApiRequest.call( + this, + 'GET', + "/drive/root/search(q='.xlsx')", + {}, + qs, + ); + responseData = responseData.value; + } + + if (Array.isArray(responseData)) { + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } else if (responseData !== undefined) { + 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; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/Worksheet.resource.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/Worksheet.resource.ts new file mode 100644 index 0000000000000..7f8a901b55f81 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/Worksheet.resource.ts @@ -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, +]; diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/append.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/append.operation.ts new file mode 100644 index 0000000000000..7cb57d6782e97 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/append.operation.ts @@ -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 expression', + 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 { + 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; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/clear.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/clear.operation.ts new file mode 100644 index 0000000000000..4e9b5ab8cbce7 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/clear.operation.ts @@ -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 { + 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; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/deleteWorksheet.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/deleteWorksheet.operation.ts new file mode 100644 index 0000000000000..e9305e219ffd1 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/deleteWorksheet.operation.ts @@ -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 { + 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; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/getAll.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/getAll.operation.ts new file mode 100644 index 0000000000000..16cc37fab04cc --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/getAll.operation.ts @@ -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 { + //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; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/readRows.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/readRows.operation.ts new file mode 100644 index 0000000000000..eeb791c1ae2b9 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/readRows.operation.ts @@ -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 { + //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; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/update.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/update.operation.ts new file mode 100644 index 0000000000000..58805994c6cb0 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/update.operation.ts @@ -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 expression', + 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 expression', + 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 { + 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; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/upsert.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/upsert.operation.ts new file mode 100644 index 0000000000000..fa635aac8385e --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/upsert.operation.ts @@ -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 expression', + 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 expression', + 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 { + 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; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/helpers/interfaces.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/helpers/interfaces.ts new file mode 100644 index 0000000000000..f3d84f3869b0f --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/helpers/interfaces.ts @@ -0,0 +1,14 @@ +import type { IDataObject } from 'n8n-workflow'; + +export type SheetRow = Array; +export type SheetData = SheetRow[]; + +export type ExcelResponse = { + values: SheetData; +}; + +export type UpdateSummary = { + updatedData: SheetData; + appendData: IDataObject[]; + updatedRows: number[]; +}; diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/helpers/utils.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/helpers/utils.ts new file mode 100644 index 0000000000000..650ac8ad7fa68 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/helpers/utils.ts @@ -0,0 +1,208 @@ +import type { IExecuteFunctions } from 'n8n-core'; +import type { IDataObject, INode, INodeExecutionData } from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; +import type { ExcelResponse, SheetData, UpdateSummary } from './interfaces'; +import { constructExecutionMetaData } from 'n8n-core'; +import { wrapData } from '../../../../../utils/utilities'; + +type PrepareOutputConfig = { + rawData: boolean; + dataProperty?: string; + keyRow?: number; + firstDataRow?: number; + columnsRow?: string[]; + updatedRows?: number[]; +}; + +export function prepareOutput( + node: INode, + responseData: ExcelResponse, + config: PrepareOutputConfig, +) { + const returnData: INodeExecutionData[] = []; + + const { rawData, keyRow, firstDataRow, columnsRow, updatedRows } = { + keyRow: 0, + firstDataRow: 1, + columnsRow: undefined, + updatedRows: undefined, + ...config, + }; + + if (!rawData) { + let values = responseData.values; + if (values === null) { + throw new NodeOperationError(node, 'Operation did not return data'); + } + + let columns = []; + + if (columnsRow?.length) { + columns = columnsRow; + values = [columns, ...values]; + } else { + columns = values[keyRow]; + } + + if (updatedRows) { + values = values.filter((_, index) => updatedRows.includes(index)); + } + + for (let rowIndex = firstDataRow; rowIndex < values.length; rowIndex++) { + if (rowIndex === keyRow) continue; + const data: IDataObject = {}; + for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) { + data[columns[columnIndex] as string] = values[rowIndex][columnIndex]; + } + const executionData = constructExecutionMetaData(wrapData({ ...data }), { + itemData: { item: rowIndex }, + }); + + returnData.push(...executionData); + } + } else { + const executionData = constructExecutionMetaData( + wrapData({ [config.dataProperty || 'data']: responseData }), + { itemData: { item: 0 } }, + ); + + returnData.push(...executionData); + } + + return returnData; +} +// update values of spreadsheet when update mode is 'define' +export function updateByDefinedValues( + this: IExecuteFunctions, + itemslength: number, + sheetData: SheetData, + updateAllOccurences: boolean, +): UpdateSummary { + const [columns, ...originalValues] = sheetData; + const updateValues: SheetData = originalValues.map((row) => row.map(() => null)); + + const updatedRowsIndexes = new Set(); + const appendData: IDataObject[] = []; + + for (let itemIndex = 0; itemIndex < itemslength; itemIndex++) { + const columnToMatchOn = this.getNodeParameter('columnToMatchOn', itemIndex) as string; + const valueToMatchOn = this.getNodeParameter('valueToMatchOn', itemIndex) as string; + + const definedFields = this.getNodeParameter('fieldsUi.values', itemIndex, []) as Array<{ + column: string; + fieldValue: string; + }>; + + const columnToMatchOnIndex = columns.indexOf(columnToMatchOn); + + const rowIndexes: number[] = []; + if (updateAllOccurences) { + for (const [index, row] of originalValues.entries()) { + if ( + row[columnToMatchOnIndex] === valueToMatchOn || + Number(row[columnToMatchOnIndex]) === Number(valueToMatchOn) + ) { + rowIndexes.push(index); + } + } + } else { + const rowIndex = originalValues.findIndex( + (row) => + row[columnToMatchOnIndex] === valueToMatchOn || + Number(row[columnToMatchOnIndex]) === Number(valueToMatchOn), + ); + + if (rowIndex !== -1) { + rowIndexes.push(rowIndex); + } + } + + if (!rowIndexes.length) { + const appendItem: IDataObject = {}; + appendItem[columnToMatchOn] = valueToMatchOn; + + for (const entry of definedFields) { + appendItem[entry.column] = entry.fieldValue; + } + appendData.push(appendItem); + continue; + } + + for (const rowIndex of rowIndexes) { + for (const entry of definedFields) { + const columnIndex = columns.indexOf(entry.column); + if (rowIndex === -1) continue; + updateValues[rowIndex][columnIndex] = entry.fieldValue; + //add rows index and shift by 1 to account for header row + updatedRowsIndexes.add(rowIndex + 1); + } + } + } + + const updatedData = [columns, ...updateValues]; + const updatedRows = [0, ...Array.from(updatedRowsIndexes)]; + + const summary: UpdateSummary = { updatedData, appendData, updatedRows }; + + return summary; +} + +// update values of spreadsheet when update mode is 'autoMap' +export function updateByAutoMaping( + items: INodeExecutionData[], + sheetData: SheetData, + columnToMatchOn: string, + updateAllOccurences = false, +): UpdateSummary { + const [columns, ...values] = sheetData; + const matchColumnIndex = columns.indexOf(columnToMatchOn); + const matchValuesMap = values.map((row) => row[matchColumnIndex]); + + const updatedRowsIndexes = new Set(); + const appendData: IDataObject[] = []; + + for (const { json } of items) { + const columnValue = json[columnToMatchOn] as string; + if (columnValue === undefined) continue; + + const rowIndexes: number[] = []; + if (updateAllOccurences) { + matchValuesMap.forEach((value, index) => { + if (value === columnValue || Number(value) === Number(columnValue)) { + rowIndexes.push(index); + } + }); + } else { + const rowIndex = matchValuesMap.findIndex( + (value) => value === columnValue || Number(value) === Number(columnValue), + ); + + if (rowIndex !== -1) rowIndexes.push(rowIndex); + } + + if (!rowIndexes.length) { + appendData.push(json); + continue; + } + + const updatedRow: Array = []; + + for (const columnName of columns as string[]) { + const updateValue = json[columnName] === undefined ? null : (json[columnName] as string); + updatedRow.push(updateValue); + } + + for (const rowIndex of rowIndexes) { + values[rowIndex] = updatedRow as string[]; + //add rows index and shift by 1 to account for header row + updatedRowsIndexes.add(rowIndex + 1); + } + } + + const updatedData = [columns, ...values]; + const updatedRows = [0, ...Array.from(updatedRowsIndexes)]; + + const summary: UpdateSummary = { updatedData, appendData, updatedRows }; + + return summary; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/methods/index.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/methods/index.ts new file mode 100644 index 0000000000000..a5508a3e0fa86 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/methods/index.ts @@ -0,0 +1,2 @@ +export * as loadOptions from './loadOptions'; +export * as listSearch from './listSearch'; diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/methods/listSearch.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/methods/listSearch.ts new file mode 100644 index 0000000000000..031c3e07d5596 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/methods/listSearch.ts @@ -0,0 +1,134 @@ +import type { + IDataObject, + ILoadOptionsFunctions, + INodeListSearchItems, + INodeListSearchResult, +} from 'n8n-workflow'; +import { microsoftApiRequest } from '../transport'; + +export async function searchWorkbooks( + this: ILoadOptionsFunctions, + filter?: string, + paginationToken?: string, +): Promise { + const q = filter ? encodeURI(`.xlsx AND ${filter}`) : '.xlsx'; + + let response: IDataObject = {}; + + if (paginationToken) { + response = await microsoftApiRequest.call( + this, + 'GET', + '', + undefined, + undefined, + paginationToken, // paginationToken contains the full URL + ); + } else { + response = await microsoftApiRequest.call( + this, + 'GET', + `/drive/root/search(q='${q}')`, + undefined, + { + select: 'id,name,webUrl', + $top: 100, + }, + ); + } + + return { + results: (response.value as IDataObject[]).map((workbook: IDataObject) => { + return { + name: (workbook.name as string).replace('.xlsx', ''), + value: workbook.id as string, + url: workbook.webUrl as string, + }; + }), + paginationToken: response['@odata.nextLink'], + }; +} + +export async function getWorksheetsList( + this: ILoadOptionsFunctions, +): Promise { + const workbookRLC = this.getNodeParameter('workbook') as IDataObject; + const workbookId = workbookRLC.value as string; + let workbookURL = workbookRLC.cachedResultUrl as string; + + if (workbookURL.includes('1drv.ms')) { + workbookURL = `https://onedrive.live.com/edit.aspx?resid=${workbookId}`; + } + + let response: IDataObject = {}; + + response = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets`, + undefined, + { + select: 'id,name', + }, + ); + + return { + results: (response.value as IDataObject[]).map((worksheet: IDataObject) => ({ + name: worksheet.name as string, + value: worksheet.id as string, + url: `${workbookURL}&activeCell=${encodeURIComponent(worksheet.name as string)}!A1`, + })), + }; +} + +export async function getWorksheetTables( + this: ILoadOptionsFunctions, +): Promise { + const workbookRLC = this.getNodeParameter('workbook') as IDataObject; + const workbookId = workbookRLC.value as string; + let workbookURL = workbookRLC.cachedResultUrl as string; + + if (workbookURL.includes('1drv.ms')) { + workbookURL = `https://onedrive.live.com/edit.aspx?resid=${workbookId}`; + } + + const worksheetId = this.getNodeParameter('worksheet', undefined, { + extractValue: true, + }) as string; + + let response: IDataObject = {}; + + response = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables`, + undefined, + ); + + const results: INodeListSearchItems[] = []; + + for (const table of response.value as IDataObject[]) { + const name = table.name as string; + const value = table.id as string; + + const { address } = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${value}/range`, + undefined, + { + select: 'address', + }, + ); + + const [sheetName, sheetRange] = address.split('!' as string); + + const url = `${workbookURL}&activeCell=${encodeURIComponent(sheetName as string)}${ + sheetRange ? '!' + (sheetRange as string) : '' + }`; + + results.push({ name, value, url }); + } + + return { results }; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/methods/loadOptions.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/methods/loadOptions.ts new file mode 100644 index 0000000000000..576f02c8c293c --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/methods/loadOptions.ts @@ -0,0 +1,89 @@ +import type { IDataObject, ILoadOptionsFunctions, INodePropertyOptions } from 'n8n-workflow'; +import { microsoftApiRequest } from '../transport'; + +export async function getWorksheetColumnRow( + this: ILoadOptionsFunctions, +): Promise { + const workbookId = this.getNodeParameter('workbook', undefined, { + extractValue: true, + }) as string; + + const worksheetId = this.getNodeParameter('worksheet', undefined, { + extractValue: true, + }) as string; + + let range = this.getNodeParameter('range', '') as string; + let columns: string[] = []; + + if (range === '') { + const worksheetData = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/usedRange`, + undefined, + { select: 'values' }, + ); + + columns = worksheetData.values[0] as string[]; + } else { + const [rangeFrom, rangeTo] = range.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})/) || []; + + range = `${rangeFrom}:${cellDataTo[1]}${cellDataFrom[2]}`; + + const worksheetData = await microsoftApiRequest.call( + this, + 'PATCH', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`, + { select: 'values' }, + ); + + columns = worksheetData.values[0] as string[]; + } + + const returnData: INodePropertyOptions[] = []; + for (const column of columns) { + returnData.push({ + name: column, + value: column, + }); + } + return returnData; +} + +export async function getWorksheetColumnRowSkipColumnToMatchOn( + this: ILoadOptionsFunctions, +): Promise { + const returnData = await getWorksheetColumnRow.call(this); + const columnToMatchOn = this.getNodeParameter('columnToMatchOn', 0) as string; + return returnData.filter((column) => column.value !== columnToMatchOn); +} + +export async function getTableColumns( + this: ILoadOptionsFunctions, +): Promise { + const workbookId = this.getNodeParameter('workbook', undefined, { + extractValue: true, + }) as string; + + const worksheetId = this.getNodeParameter('worksheet', undefined, { + extractValue: true, + }) as string; + + const tableId = this.getNodeParameter('table', undefined, { + extractValue: true, + }) as string; + + const response = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, + {}, + ); + + return (response.value as IDataObject[]).map((column) => ({ + name: column.name as string, + value: column.name as string, + })); +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/transport/index.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/transport/index.ts new file mode 100644 index 0000000000000..f28816ef8b0c1 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/transport/index.ts @@ -0,0 +1,82 @@ +import type { OptionsWithUri } from 'request'; +import type { IExecuteFunctions, IExecuteSingleFunctions, ILoadOptionsFunctions } from 'n8n-core'; +import type { IDataObject, JsonObject } from 'n8n-workflow'; +import { NodeApiError } from 'n8n-workflow'; + +export async function microsoftApiRequest( + this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, + method: string, + resource: string, + body: any = {}, + qs: IDataObject = {}, + uri?: string, + headers: IDataObject = {}, +): Promise { + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: uri || `https://graph.microsoft.com/v1.0/me${resource}`, + json: true, + }; + try { + if (Object.keys(headers).length !== 0) { + options.headers = Object.assign({}, options.headers, headers); + } + return await this.helpers.requestOAuth2.call(this, 'microsoftExcelOAuth2Api', options); + } catch (error) { + throw new NodeApiError(this.getNode(), error as JsonObject); + } +} + +export async function microsoftApiRequestAllItems( + this: IExecuteFunctions | ILoadOptionsFunctions, + propertyName: string, + method: string, + endpoint: string, + body: any = {}, + query: IDataObject = {}, +): Promise { + const returnData: IDataObject[] = []; + + let responseData; + let uri: string | undefined; + query.$top = 100; + + do { + responseData = await microsoftApiRequest.call(this, method, endpoint, body, query, uri); + uri = responseData['@odata.nextLink']; + if (uri?.includes('$top')) { + delete query.$top; + } + returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]); + } while (responseData['@odata.nextLink'] !== undefined); + + return returnData; +} + +export async function microsoftApiRequestAllItemsSkip( + this: IExecuteFunctions | ILoadOptionsFunctions, + propertyName: string, + method: string, + endpoint: string, + body: any = {}, + query: IDataObject = {}, +): Promise { + const returnData: IDataObject[] = []; + + let responseData; + query.$top = 100; + query.$skip = 0; + + do { + responseData = await microsoftApiRequest.call(this, method, endpoint, body, query); + query.$skip += query.$top; + returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]); + } while (responseData.value.length !== 0); + + return returnData; +} diff --git a/packages/nodes-base/test/nodes/Helpers.ts b/packages/nodes-base/test/nodes/Helpers.ts index 134a648404adf..4ecf384a65f3e 100644 --- a/packages/nodes-base/test/nodes/Helpers.ts +++ b/packages/nodes-base/test/nodes/Helpers.ts @@ -235,6 +235,14 @@ export function setup(testData: WorkflowTestData[] | WorkflowTestData) { export function getResultNodeData(result: IRun, testData: WorkflowTestData) { return Object.keys(testData.output.nodeData).map((nodeName) => { if (result.data.resultData.runData[nodeName] === undefined) { + // log errors from other nodes + Object.keys(result.data.resultData.runData).forEach((key) => { + const error = result.data.resultData.runData[key][0]?.error; + if (error) { + console.log(`Node ${key}\n`, error); + } + }); + throw new Error(`Data for node "${nodeName}" is missing!`); } const resultData = result.data.resultData.runData[nodeName].map((nodeData) => { diff --git a/packages/nodes-base/test/utils/utilities.test.ts b/packages/nodes-base/test/utils/utilities.test.ts index b99e786f7d0b0..e98ca10b20011 100644 --- a/packages/nodes-base/test/utils/utilities.test.ts +++ b/packages/nodes-base/test/utils/utilities.test.ts @@ -1,4 +1,4 @@ -import { fuzzyCompare, keysToLowercase } from '../../utils/utilities'; +import { fuzzyCompare, keysToLowercase, wrapData } from '../../utils/utilities'; //most test cases for fuzzyCompare are done in Compare Datasets node tests describe('Test fuzzyCompare', () => { @@ -30,6 +30,45 @@ describe('Test fuzzyCompare', () => { }); }); +describe('Test wrapData', () => { + it('should wrap object in json', () => { + const data = { + id: 1, + name: 'Name', + }; + const wrappedData = wrapData(data); + expect(wrappedData).toBeDefined(); + expect(wrappedData).toEqual([{ json: data }]); + }); + it('should wrap each object in array in json', () => { + const data = [ + { + id: 1, + name: 'Name', + }, + { + id: 2, + name: 'Name 2', + }, + ]; + const wrappedData = wrapData(data); + expect(wrappedData).toBeDefined(); + expect(wrappedData).toEqual([{ json: data[0] }, { json: data[1] }]); + }); + it('json key from source should be inside json', () => { + const data = { + json: { + id: 1, + name: 'Name', + }, + }; + const wrappedData = wrapData(data); + expect(wrappedData).toBeDefined(); + expect(wrappedData).toEqual([{ json: data }]); + expect(Object.keys(wrappedData[0].json)).toContain('json'); + }); +}); + describe('Test keysToLowercase', () => { it('should convert keys to lowercase', () => { const headers = { diff --git a/packages/nodes-base/utils/utilities.ts b/packages/nodes-base/utils/utilities.ts index 8350fdb2bc4bd..cca0d35084680 100644 --- a/packages/nodes-base/utils/utilities.ts +++ b/packages/nodes-base/utils/utilities.ts @@ -1,4 +1,10 @@ -import type { IDataObject, IDisplayOptions, INodeProperties } from 'n8n-workflow'; +import type { + IDataObject, + IDisplayOptions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; + import { jsonParse } from 'n8n-workflow'; import { isEqual, isNull, merge } from 'lodash'; @@ -73,6 +79,25 @@ export function updateDisplayOptions( }); } +export function processJsonInput(jsonData: T, inputName?: string) { + let values; + const input = `'${inputName}' ` || ''; + + if (typeof jsonData === 'string') { + try { + values = jsonParse(jsonData); + } catch (error) { + throw new Error(`Input ${input}must contain a valid JSON`); + } + } else if (typeof jsonData === 'object') { + values = jsonData; + } else { + throw new Error(`Input ${input}must contain a valid JSON`); + } + + return values; +} + function isFalsy(value: T) { if (isNull(value)) return true; if (typeof value === 'string' && value === '') return true; @@ -173,6 +198,15 @@ export const fuzzyCompare = (useFuzzyCompare: boolean, compareVersion = 1) => { }; }; +export function wrapData(data: IDataObject | IDataObject[]): INodeExecutionData[] { + if (!Array.isArray(data)) { + return [{ json: data }]; + } + return data.map((item) => ({ + json: item, + })); +} + export const keysToLowercase = (headers: T) => { if (typeof headers !== 'object' || Array.isArray(headers) || headers === null) return headers; return Object.entries(headers).reduce((acc, [key, value]) => {