mirror of
https://github.com/n8n-io/n8n.git
synced 2026-07-28 03:24:59 +02:00
fix(Microsoft Excel Node): Resolve service principal target per input item (#34112)
This commit is contained in:
parent
bf3c2816c2
commit
afc8a87a60
|
|
@ -48,6 +48,9 @@ describe('Microsoft Excel V2 - loadOptions', () => {
|
|||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/usedRange`,
|
||||
undefined,
|
||||
{ select: 'values' },
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
|
|
@ -79,6 +82,10 @@ describe('Microsoft Excel V2 - loadOptions', () => {
|
|||
'PATCH',
|
||||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`,
|
||||
{ select: 'values' },
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
import { credentials } from '../../../credentials';
|
||||
|
||||
// Pins the whole-batch rule: table:append sends ONE request-set for all input items and
|
||||
// reads every param at item 0 — so even with per-item target expressions that differ,
|
||||
// all four session requests go to item 0's (alice's) root. Bob's root is never mocked,
|
||||
// so any request there fails (disableNetConnect); empty pendingMocks pins the exact set.
|
||||
describe('Test MicrosoftExcelV2, Service Principal table => append uses item 0 target for the whole batch', () => {
|
||||
nock('https://graph.microsoft.com/v1.0')
|
||||
.matchHeader('Authorization', 'Bearer test-access-token')
|
||||
.get(
|
||||
'/users/alice%40contoso.com/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/%7BA0883CFE-D27E-4ECC-B94B-981830AAD55B%7D/tables/%7B317CA469-7D1C-4A5D-9B0B-424444BF0336%7D/columns',
|
||||
)
|
||||
.reply(200, { value: [{ name: 'row' }] });
|
||||
|
||||
nock('https://graph.microsoft.com/v1.0')
|
||||
.matchHeader('Authorization', 'Bearer test-access-token')
|
||||
.post(
|
||||
'/users/alice%40contoso.com/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/createSession',
|
||||
{
|
||||
persistChanges: true,
|
||||
},
|
||||
)
|
||||
.reply(200, { id: 'session-1' });
|
||||
|
||||
// Both items' rows land in ONE add call under alice's root.
|
||||
nock('https://graph.microsoft.com/v1.0')
|
||||
.matchHeader('Authorization', 'Bearer test-access-token')
|
||||
.matchHeader('workbook-session-id', 'session-1')
|
||||
.post(
|
||||
'/users/alice%40contoso.com/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/%7BA0883CFE-D27E-4ECC-B94B-981830AAD55B%7D/tables/%7B317CA469-7D1C-4A5D-9B0B-424444BF0336%7D/rows/add',
|
||||
{ values: [['r1'], ['r2']] },
|
||||
)
|
||||
.reply(200, { index: 0, values: [['r1'], ['r2']] });
|
||||
|
||||
nock('https://graph.microsoft.com/v1.0')
|
||||
.matchHeader('Authorization', 'Bearer test-access-token')
|
||||
.matchHeader('workbook-session-id', 'session-1')
|
||||
.post(
|
||||
'/users/alice%40contoso.com/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/closeSession',
|
||||
{},
|
||||
)
|
||||
.reply(200);
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['append.sp.batch.workflow.json'],
|
||||
customAssertions: () => expect(nock.pendingMocks()).toEqual([]),
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
{
|
||||
"name": "microsoft excel 365 - table append (service principal, whole-batch item-0 target)",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "9a000000-0000-4000-8000-000000000001",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"data": [
|
||||
{ "user": "alice@contoso.com", "row": "r1" },
|
||||
{ "user": "bob@contoso.com", "row": "r2" }
|
||||
]
|
||||
},
|
||||
"id": "9a000000-0000-4000-8000-000000000002",
|
||||
"name": "Test Data",
|
||||
"type": "n8n-nodes-testing.testData",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 0]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"authentication": "microsoftEntraServicePrincipalApi",
|
||||
"userTarget": {
|
||||
"__rl": true,
|
||||
"value": "={{ $json.user }}",
|
||||
"mode": "id"
|
||||
},
|
||||
"resource": "table",
|
||||
"operation": "append",
|
||||
"workbook": {
|
||||
"__rl": true,
|
||||
"value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I",
|
||||
"mode": "id"
|
||||
},
|
||||
"worksheet": {
|
||||
"__rl": true,
|
||||
"value": "{A0883CFE-D27E-4ECC-B94B-981830AAD55B}",
|
||||
"mode": "id"
|
||||
},
|
||||
"table": {
|
||||
"__rl": true,
|
||||
"value": "{317CA469-7D1C-4A5D-9B0B-424444BF0336}",
|
||||
"mode": "id"
|
||||
},
|
||||
"dataMode": "autoMap",
|
||||
"options": {}
|
||||
},
|
||||
"id": "9a000000-0000-4000-8000-000000000003",
|
||||
"name": "Microsoft Excel 365",
|
||||
"type": "n8n-nodes-base.microsoftExcel",
|
||||
"typeVersion": 2,
|
||||
"position": [400, 0],
|
||||
"credentials": {
|
||||
"microsoftEntraServicePrincipalApi": {
|
||||
"id": "1",
|
||||
"name": "Microsoft Entra Service Principal account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Microsoft Excel 365": [
|
||||
{
|
||||
"json": {
|
||||
"row": "r1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"row": "r2"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [[{ "node": "Test Data", "type": "main", "index": 0 }]]
|
||||
},
|
||||
"Test Data": {
|
||||
"main": [[{ "node": "Microsoft Excel 365", "type": "main", "index": 0 }]]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {},
|
||||
"tags": []
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
import { credentials } from '../../../credentials';
|
||||
|
||||
// Drive-mode per-item resolution: with `Access As: Drive` the target id IS the drive,
|
||||
// addressed as `/drives/{id}` (no `/drive` suffix). Two items with distinct drive-id
|
||||
// expressions must each read from their own drive root. Consume-once + empty
|
||||
// pendingMocks pin the exact request set.
|
||||
describe('Test MicrosoftExcelV2, Service Principal worksheet => readRows per-item drive target', () => {
|
||||
nock('https://graph.microsoft.com/v1.0')
|
||||
.matchHeader('Authorization', 'Bearer test-access-token')
|
||||
.get(
|
||||
'/drives/b!driveA/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/%7BA0883CFE-D27E-4ECC-B94B-981830AAD55B%7D/usedRange',
|
||||
)
|
||||
.reply(200, { values: [['row'], ['drive-a-row']] });
|
||||
|
||||
nock('https://graph.microsoft.com/v1.0')
|
||||
.matchHeader('Authorization', 'Bearer test-access-token')
|
||||
.get(
|
||||
'/drives/b!driveB/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/%7BA0883CFE-D27E-4ECC-B94B-981830AAD55B%7D/usedRange',
|
||||
)
|
||||
.reply(200, { values: [['row'], ['drive-b-row']] });
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['readRows.sp.drive.perItem.workflow.json'],
|
||||
customAssertions: () => expect(nock.pendingMocks()).toEqual([]),
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
{
|
||||
"name": "microsoft excel 365 - read rows (service principal, per-item drive target)",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "8a000000-0000-4000-8000-000000000001",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"data": [{ "drive": "b!driveA" }, { "drive": "b!driveB" }]
|
||||
},
|
||||
"id": "8a000000-0000-4000-8000-000000000002",
|
||||
"name": "Test Data",
|
||||
"type": "n8n-nodes-testing.testData",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 0]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"authentication": "microsoftEntraServicePrincipalApi",
|
||||
"resourceTarget": "drive",
|
||||
"driveTarget": {
|
||||
"__rl": true,
|
||||
"value": "={{ $json.drive }}",
|
||||
"mode": "id"
|
||||
},
|
||||
"resource": "worksheet",
|
||||
"operation": "readRows",
|
||||
"workbook": {
|
||||
"__rl": true,
|
||||
"value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I",
|
||||
"mode": "id"
|
||||
},
|
||||
"worksheet": {
|
||||
"__rl": true,
|
||||
"value": "{A0883CFE-D27E-4ECC-B94B-981830AAD55B}",
|
||||
"mode": "id"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "8a000000-0000-4000-8000-000000000003",
|
||||
"name": "Microsoft Excel 365",
|
||||
"type": "n8n-nodes-base.microsoftExcel",
|
||||
"typeVersion": 2,
|
||||
"position": [400, 0],
|
||||
"credentials": {
|
||||
"microsoftEntraServicePrincipalApi": {
|
||||
"id": "1",
|
||||
"name": "Microsoft Entra Service Principal account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Microsoft Excel 365": [
|
||||
{
|
||||
"json": {
|
||||
"row": "drive-a-row"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"row": "drive-b-row"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [[{ "node": "Test Data", "type": "main", "index": 0 }]]
|
||||
},
|
||||
"Test Data": {
|
||||
"main": [[{ "node": "Microsoft Excel 365", "type": "main", "index": 0 }]]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {},
|
||||
"tags": []
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
import { credentials } from '../../../credentials';
|
||||
|
||||
// The target RLC accepts expressions, so it must be resolved per input item: the Test
|
||||
// Data node fans out to two users and each item's read must be rebased onto that item's
|
||||
// `/users/{id}/drive` root (encoding pin preserved). Consume-once interceptors + empty
|
||||
// pendingMocks pin the exact request set in both directions.
|
||||
describe('Test MicrosoftExcelV2, Service Principal worksheet => readRows per-item target', () => {
|
||||
nock('https://graph.microsoft.com/v1.0')
|
||||
.matchHeader('Authorization', 'Bearer test-access-token')
|
||||
.get(
|
||||
'/users/jane%40contoso.com/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/%7BA0883CFE-D27E-4ECC-B94B-981830AAD55B%7D/usedRange',
|
||||
)
|
||||
.reply(200, { values: [['row'], ['jane-row']] });
|
||||
|
||||
nock('https://graph.microsoft.com/v1.0')
|
||||
.matchHeader('Authorization', 'Bearer test-access-token')
|
||||
.get(
|
||||
'/users/john%40contoso.com/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/%7BA0883CFE-D27E-4ECC-B94B-981830AAD55B%7D/usedRange',
|
||||
)
|
||||
.reply(200, { values: [['row'], ['john-row']] });
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['readRows.sp.perItem.workflow.json'],
|
||||
customAssertions: () => expect(nock.pendingMocks()).toEqual([]),
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
{
|
||||
"name": "microsoft excel 365 - read rows (service principal, per-item target)",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "6a000000-0000-4000-8000-000000000001",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"data": [{ "user": "jane@contoso.com" }, { "user": "john@contoso.com" }]
|
||||
},
|
||||
"id": "6a000000-0000-4000-8000-000000000002",
|
||||
"name": "Test Data",
|
||||
"type": "n8n-nodes-testing.testData",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 0]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"authentication": "microsoftEntraServicePrincipalApi",
|
||||
"userTarget": {
|
||||
"__rl": true,
|
||||
"value": "={{ $json.user }}",
|
||||
"mode": "id"
|
||||
},
|
||||
"resource": "worksheet",
|
||||
"operation": "readRows",
|
||||
"workbook": {
|
||||
"__rl": true,
|
||||
"value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I",
|
||||
"mode": "id"
|
||||
},
|
||||
"worksheet": {
|
||||
"__rl": true,
|
||||
"value": "{A0883CFE-D27E-4ECC-B94B-981830AAD55B}",
|
||||
"mode": "id"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "6a000000-0000-4000-8000-000000000003",
|
||||
"name": "Microsoft Excel 365",
|
||||
"type": "n8n-nodes-base.microsoftExcel",
|
||||
"typeVersion": 2,
|
||||
"position": [400, 0],
|
||||
"credentials": {
|
||||
"microsoftEntraServicePrincipalApi": {
|
||||
"id": "1",
|
||||
"name": "Microsoft Entra Service Principal account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Microsoft Excel 365": [
|
||||
{
|
||||
"json": {
|
||||
"row": "jane-row"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"row": "john-row"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [[{ "node": "Test Data", "type": "main", "index": 0 }]]
|
||||
},
|
||||
"Test Data": {
|
||||
"main": [[{ "node": "Microsoft Excel 365", "type": "main", "index": 0 }]]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {},
|
||||
"tags": []
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
import { credentials } from '../../../credentials';
|
||||
|
||||
// Per-item error isolation under continueOnFail: item 0's bogus target ("not a user"
|
||||
// fails the shape validators) must produce an error item for item 0 only, while item 1
|
||||
// still reads from its own user's drive. Only jane is mocked; consume-once + empty
|
||||
// pendingMocks pin the exact request set.
|
||||
describe('Test MicrosoftExcelV2, Service Principal worksheet => readRows fails only the bad item under continueOnFail', () => {
|
||||
nock('https://graph.microsoft.com/v1.0')
|
||||
.matchHeader('Authorization', 'Bearer test-access-token')
|
||||
.get(
|
||||
'/users/jane%40contoso.com/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/%7BA0883CFE-D27E-4ECC-B94B-981830AAD55B%7D/usedRange',
|
||||
)
|
||||
.reply(200, { values: [['row'], ['jane-row']] });
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['readRows.sp.perItemContinueOnFail.workflow.json'],
|
||||
customAssertions: () => expect(nock.pendingMocks()).toEqual([]),
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
{
|
||||
"name": "microsoft excel 365 - read rows (service principal, per-item target, continueOnFail)",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "7a000000-0000-4000-8000-000000000001",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"data": [{ "user": "not a user" }, { "user": "jane@contoso.com" }]
|
||||
},
|
||||
"id": "7a000000-0000-4000-8000-000000000002",
|
||||
"name": "Test Data",
|
||||
"type": "n8n-nodes-testing.testData",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 0]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"authentication": "microsoftEntraServicePrincipalApi",
|
||||
"userTarget": {
|
||||
"__rl": true,
|
||||
"value": "={{ $json.user }}",
|
||||
"mode": "id"
|
||||
},
|
||||
"resource": "worksheet",
|
||||
"operation": "readRows",
|
||||
"workbook": {
|
||||
"__rl": true,
|
||||
"value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I",
|
||||
"mode": "id"
|
||||
},
|
||||
"worksheet": {
|
||||
"__rl": true,
|
||||
"value": "{A0883CFE-D27E-4ECC-B94B-981830AAD55B}",
|
||||
"mode": "id"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "7a000000-0000-4000-8000-000000000003",
|
||||
"name": "Microsoft Excel 365",
|
||||
"type": "n8n-nodes-base.microsoftExcel",
|
||||
"typeVersion": 2,
|
||||
"position": [400, 0],
|
||||
"onError": "continueRegularOutput",
|
||||
"credentials": {
|
||||
"microsoftEntraServicePrincipalApi": {
|
||||
"id": "1",
|
||||
"name": "Microsoft Entra Service Principal account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Microsoft Excel 365": [
|
||||
{
|
||||
"json": {
|
||||
"error": "The target ID is not valid"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"row": "jane-row"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [[{ "node": "Test Data", "type": "main", "index": 0 }]]
|
||||
},
|
||||
"Test Data": {
|
||||
"main": [[{ "node": "Microsoft Excel 365", "type": "main", "index": 0 }]]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {},
|
||||
"tags": []
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
import { credentials } from '../../../credentials';
|
||||
|
||||
// Pins the whole-batch rule for worksheet:upsert: one request set for all input items and
|
||||
// every request-level parameter (including the SP target) is read at item 0 — so even with
|
||||
// per-item target expressions that differ, all three requests (usedRange read, append-range
|
||||
// probe, final write) go to item 0's (alice's) root. Bob's root is never mocked, so any
|
||||
// request there fails (disableNetConnect); empty pendingMocks pins the exact set.
|
||||
describe('Test MicrosoftExcelV2, Service Principal worksheet => upsert uses item 0 target for the whole batch', () => {
|
||||
nock('https://graph.microsoft.com/v1.0')
|
||||
.matchHeader('Authorization', 'Bearer test-access-token')
|
||||
.get(
|
||||
'/users/alice%40contoso.com/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/%7BA0883CFE-D27E-4ECC-B94B-981830AAD55B%7D/usedRange',
|
||||
)
|
||||
.reply(200, {
|
||||
address: 'Sheet1!A1:B3',
|
||||
values: [
|
||||
['id', 'data'],
|
||||
['1', 'old1'],
|
||||
['2', 'old2'],
|
||||
],
|
||||
});
|
||||
|
||||
// Item 1 appends, so the node re-reads the used range (address only) — still under alice.
|
||||
nock('https://graph.microsoft.com/v1.0')
|
||||
.matchHeader('Authorization', 'Bearer test-access-token')
|
||||
.get(
|
||||
'/users/alice%40contoso.com/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/%7BA0883CFE-D27E-4ECC-B94B-981830AAD55B%7D/usedRange',
|
||||
)
|
||||
.query({ select: 'address' })
|
||||
.reply(200, { address: 'Sheet1!A1:B3' });
|
||||
|
||||
// Both items' rows (item 0's update + item 1's append) land in ONE write under alice's root.
|
||||
nock('https://graph.microsoft.com/v1.0')
|
||||
.matchHeader('Authorization', 'Bearer test-access-token')
|
||||
.patch(
|
||||
"/users/alice%40contoso.com/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/%7BA0883CFE-D27E-4ECC-B94B-981830AAD55B%7D/range(address='A1:B4')",
|
||||
{
|
||||
values: [
|
||||
['id', 'data'],
|
||||
['1', 'new1'],
|
||||
['2', 'old2'],
|
||||
['3', 'new3'],
|
||||
],
|
||||
},
|
||||
)
|
||||
.reply(200, {
|
||||
values: [
|
||||
['id', 'data'],
|
||||
['1', 'new1'],
|
||||
['2', 'old2'],
|
||||
['3', 'new3'],
|
||||
],
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['upsert.sp.batch.workflow.json'],
|
||||
customAssertions: () => expect(nock.pendingMocks()).toEqual([]),
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
{
|
||||
"name": "microsoft excel 365 - worksheet upsert (service principal, whole-batch item-0 target)",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "5a000000-0000-4000-8000-000000000001",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"data": [
|
||||
{ "user": "alice@contoso.com", "id": "1", "data": "new1" },
|
||||
{ "user": "bob@contoso.com", "id": "3", "data": "new3" }
|
||||
]
|
||||
},
|
||||
"id": "5a000000-0000-4000-8000-000000000002",
|
||||
"name": "Test Data",
|
||||
"type": "n8n-nodes-testing.testData",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 0]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"authentication": "microsoftEntraServicePrincipalApi",
|
||||
"userTarget": {
|
||||
"__rl": true,
|
||||
"value": "={{ $json.user }}",
|
||||
"mode": "id"
|
||||
},
|
||||
"resource": "worksheet",
|
||||
"operation": "upsert",
|
||||
"workbook": {
|
||||
"__rl": true,
|
||||
"value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I",
|
||||
"mode": "id"
|
||||
},
|
||||
"worksheet": {
|
||||
"__rl": true,
|
||||
"value": "{A0883CFE-D27E-4ECC-B94B-981830AAD55B}",
|
||||
"mode": "id"
|
||||
},
|
||||
"dataMode": "autoMap",
|
||||
"columnToMatchOn": "id",
|
||||
"options": {}
|
||||
},
|
||||
"id": "5a000000-0000-4000-8000-000000000003",
|
||||
"name": "Microsoft Excel 365",
|
||||
"type": "n8n-nodes-base.microsoftExcel",
|
||||
"typeVersion": 2.1,
|
||||
"position": [400, 0],
|
||||
"credentials": {
|
||||
"microsoftEntraServicePrincipalApi": {
|
||||
"id": "1",
|
||||
"name": "Microsoft Entra Service Principal account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Microsoft Excel 365": [
|
||||
{
|
||||
"json": {
|
||||
"id": "1",
|
||||
"data": "new1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": "3",
|
||||
"data": "new3"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [[{ "node": "Test Data", "type": "main", "index": 0 }]]
|
||||
},
|
||||
"Test Data": {
|
||||
"main": [[{ "node": "Microsoft Excel 365", "type": "main", "index": 0 }]]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {},
|
||||
"tags": []
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import type {
|
|||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { stampItemIndexOnError } from '../../../../GenericFunctions';
|
||||
import { microsoftApiRequest } from '../../transport';
|
||||
import { workbookRLC, worksheetRLC } from '../common.descriptions';
|
||||
|
||||
|
|
@ -94,6 +95,9 @@ export async function execute(
|
|||
{
|
||||
select: 'address',
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
i,
|
||||
);
|
||||
range = address.split('!')[1];
|
||||
} else {
|
||||
|
|
@ -108,6 +112,10 @@ export async function execute(
|
|||
address: range,
|
||||
hasHeaders,
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
i,
|
||||
);
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
|
|
@ -125,7 +133,8 @@ export async function execute(
|
|||
returnData.push(...executionErrorData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
// A NodeError from the transport may be missing the itemIndex, add it
|
||||
throw stampItemIndexOnError(error, i);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@ export async function execute(
|
|||
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.
|
||||
// Whole-batch operation: one request set for all items; request-level params (including the SP target) are read at item 0.
|
||||
const workbookId = this.getNodeParameter('workbook', 0, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
|
@ -179,6 +180,10 @@ export async function execute(
|
|||
'GET',
|
||||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`,
|
||||
{},
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
const columnsRow = columnsData.value.map((column: IDataObject) => column.name);
|
||||
|
||||
|
|
@ -242,6 +247,10 @@ export async function execute(
|
|||
'POST',
|
||||
`/drive/items/${workbookId}/workbook/createSession`,
|
||||
{ persistChanges: true },
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
const responseData = await microsoftApiRequest.call(
|
||||
this,
|
||||
|
|
@ -251,6 +260,7 @@ export async function execute(
|
|||
{},
|
||||
'',
|
||||
{ 'workbook-session-id': id },
|
||||
0,
|
||||
);
|
||||
await microsoftApiRequest.call(
|
||||
this,
|
||||
|
|
@ -260,6 +270,7 @@ export async function execute(
|
|||
{},
|
||||
'',
|
||||
{ 'workbook-session-id': id },
|
||||
0,
|
||||
);
|
||||
|
||||
const rawData = options.rawData as boolean;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import type {
|
|||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { stampItemIndexOnError } from '../../../../GenericFunctions';
|
||||
import { microsoftApiRequest } from '../../transport';
|
||||
import { tableRLC, workbookRLC, worksheetRLC } from '../common.descriptions';
|
||||
|
||||
|
|
@ -45,6 +46,11 @@ export async function execute(
|
|||
this,
|
||||
'POST',
|
||||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/convertToRange`,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
i,
|
||||
);
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
|
|
@ -62,7 +68,8 @@ export async function execute(
|
|||
returnData.push(...executionErrorData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
// A NodeError from the transport may be missing the itemIndex, add it
|
||||
throw stampItemIndexOnError(error, i);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n
|
|||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { stampItemIndexOnError } from '../../../../GenericFunctions';
|
||||
import { microsoftApiRequest } from '../../transport';
|
||||
import { tableRLC, workbookRLC, worksheetRLC } from '../common.descriptions';
|
||||
|
||||
|
|
@ -40,6 +41,11 @@ export async function execute(
|
|||
this,
|
||||
'DELETE',
|
||||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}`,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
i,
|
||||
);
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
|
|
@ -57,7 +63,8 @@ export async function execute(
|
|||
returnData.push(...executionErrorData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
// A NodeError from the transport may be missing the itemIndex, add it
|
||||
throw stampItemIndexOnError(error, i);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import type {
|
|||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { stampItemIndexOnError } from '../../../../GenericFunctions';
|
||||
import { microsoftApiRequest, microsoftApiRequestAllItemsSkip } from '../../transport';
|
||||
import { tableRLC, workbookRLC, worksheetRLC } from '../common.descriptions';
|
||||
|
||||
|
|
@ -129,6 +130,7 @@ export async function execute(
|
|||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`,
|
||||
{},
|
||||
qs,
|
||||
i,
|
||||
);
|
||||
} else {
|
||||
qs.$top = this.getNodeParameter('limit', i);
|
||||
|
|
@ -138,6 +140,9 @@ export async function execute(
|
|||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`,
|
||||
{},
|
||||
qs,
|
||||
undefined,
|
||||
undefined,
|
||||
i,
|
||||
);
|
||||
responseData = responseData.value;
|
||||
}
|
||||
|
|
@ -163,7 +168,8 @@ export async function execute(
|
|||
returnData.push(...executionErrorData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
// A NodeError from the transport may be missing the itemIndex, add it
|
||||
throw stampItemIndexOnError(error, i);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import type {
|
|||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { stampItemIndexOnError } from '../../../../GenericFunctions';
|
||||
import { microsoftApiRequest, microsoftApiRequestAllItemsSkip } from '../../transport';
|
||||
import { tableRLC, workbookRLC, worksheetRLC } from '../common.descriptions';
|
||||
|
||||
|
|
@ -147,6 +148,7 @@ export async function execute(
|
|||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`,
|
||||
{},
|
||||
qs,
|
||||
i,
|
||||
);
|
||||
} else {
|
||||
const rowsQs = { ...qs };
|
||||
|
|
@ -157,6 +159,9 @@ export async function execute(
|
|||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`,
|
||||
{},
|
||||
rowsQs,
|
||||
undefined,
|
||||
undefined,
|
||||
i,
|
||||
);
|
||||
responseData = responseData.value;
|
||||
}
|
||||
|
|
@ -171,6 +176,7 @@ export async function execute(
|
|||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`,
|
||||
{},
|
||||
columnsQs,
|
||||
i,
|
||||
);
|
||||
|
||||
columns = (columns as IDataObject[]).map((column) => column.name);
|
||||
|
|
@ -220,7 +226,8 @@ export async function execute(
|
|||
returnData.push(...executionErrorData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
// A NodeError from the transport may be missing the itemIndex, add it
|
||||
throw stampItemIndexOnError(error, i);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { NodeApiError } from 'n8n-workflow';
|
|||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { stampItemIndexOnError } from '../../../../GenericFunctions';
|
||||
import { microsoftApiRequestAllItemsSkip } from '../../transport';
|
||||
import { tableRLC, workbookRLC, worksheetRLC } from '../common.descriptions';
|
||||
|
||||
|
|
@ -95,6 +96,7 @@ export async function execute(
|
|||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`,
|
||||
{},
|
||||
{},
|
||||
i,
|
||||
);
|
||||
|
||||
qs.$select = 'name';
|
||||
|
|
@ -106,6 +108,7 @@ export async function execute(
|
|||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`,
|
||||
{},
|
||||
qs,
|
||||
i,
|
||||
);
|
||||
columns = columns.map((column: IDataObject) => column.name);
|
||||
|
||||
|
|
@ -155,7 +158,8 @@ export async function execute(
|
|||
returnData.push(...executionErrorData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
// A NodeError from the transport may be missing the itemIndex, add it
|
||||
throw stampItemIndexOnError(error, i);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import type {
|
|||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { stampItemIndexOnError } from '../../../../GenericFunctions';
|
||||
import { microsoftApiRequest } from '../../transport';
|
||||
import { workbookRLC } from '../common.descriptions';
|
||||
|
||||
|
|
@ -63,6 +64,10 @@ export async function execute(
|
|||
'POST',
|
||||
`/drive/items/${workbookId}/workbook/createSession`,
|
||||
{ persistChanges: true },
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
i,
|
||||
);
|
||||
const responseData = await microsoftApiRequest.call(
|
||||
this,
|
||||
|
|
@ -72,6 +77,7 @@ export async function execute(
|
|||
{},
|
||||
'',
|
||||
{ 'workbook-session-id': id },
|
||||
i,
|
||||
);
|
||||
await microsoftApiRequest.call(
|
||||
this,
|
||||
|
|
@ -81,6 +87,7 @@ export async function execute(
|
|||
{},
|
||||
'',
|
||||
{ 'workbook-session-id': id },
|
||||
i,
|
||||
);
|
||||
|
||||
if (Array.isArray(responseData)) {
|
||||
|
|
@ -107,7 +114,8 @@ export async function execute(
|
|||
returnData.push(...executionErrorData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
// A NodeError from the transport may be missing the itemIndex, add it
|
||||
throw stampItemIndexOnError(error, i);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { NodeOperationError } from 'n8n-workflow';
|
|||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { stampItemIndexOnError } from '../../../../GenericFunctions';
|
||||
import { microsoftApiRequest } from '../../transport';
|
||||
import { workbookRLC } from '../common.descriptions';
|
||||
|
||||
|
|
@ -29,7 +30,16 @@ export async function execute(
|
|||
}) as string;
|
||||
|
||||
try {
|
||||
await microsoftApiRequest.call(this, 'DELETE', `/drive/items/${workbookId}`);
|
||||
await microsoftApiRequest.call(
|
||||
this,
|
||||
'DELETE',
|
||||
`/drive/items/${workbookId}`,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
i,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error?.description.includes('Lock token does not match existing lock')) {
|
||||
const errorDescription =
|
||||
|
|
@ -70,7 +80,8 @@ export async function execute(
|
|||
returnData.push(...executionErrorData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
// A NodeError from the transport may be missing the itemIndex, add it
|
||||
throw stampItemIndexOnError(error, i);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { NodeOperationError } from 'n8n-workflow';
|
|||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { stampItemIndexOnError } from '../../../../GenericFunctions';
|
||||
import {
|
||||
getExcelCredentialType,
|
||||
microsoftApiRequest,
|
||||
|
|
@ -100,6 +101,7 @@ export async function execute(
|
|||
"/drive/root/search(q='.xlsx')",
|
||||
{},
|
||||
qs,
|
||||
i,
|
||||
);
|
||||
} else {
|
||||
qs.$top = this.getNodeParameter('limit', i);
|
||||
|
|
@ -109,6 +111,9 @@ export async function execute(
|
|||
"/drive/root/search(q='.xlsx')",
|
||||
{},
|
||||
qs,
|
||||
undefined,
|
||||
undefined,
|
||||
i,
|
||||
);
|
||||
responseData = responseData.value;
|
||||
}
|
||||
|
|
@ -137,7 +142,8 @@ export async function execute(
|
|||
returnData.push(...executionErrorData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
// A NodeError from the transport may be missing the itemIndex, add it
|
||||
throw stampItemIndexOnError(error, i);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ export async function execute(
|
|||
|
||||
const nodeVersion = this.getNode().typeVersion;
|
||||
|
||||
// Whole-batch operation: one request set for all items; request-level params (including the SP target) are read at item 0.
|
||||
const workbookId = this.getNodeParameter('workbook', 0, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
|
@ -159,6 +160,11 @@ export async function execute(
|
|||
this,
|
||||
'GET',
|
||||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/usedRange`,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
let values: string[][] = [];
|
||||
|
|
@ -254,6 +260,10 @@ export async function execute(
|
|||
'PATCH',
|
||||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`,
|
||||
{ values },
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
const rawData = this.getNodeParameter('options.rawData', 0, false) as boolean;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { INodeExecutionData, IExecuteFunctions, INodeProperties } from 'n8n
|
|||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { stampItemIndexOnError } from '../../../../GenericFunctions';
|
||||
import { microsoftApiRequest } from '../../transport';
|
||||
import { workbookRLC, worksheetRLC } from '../common.descriptions';
|
||||
|
||||
|
|
@ -88,6 +89,10 @@ export async function execute(
|
|||
'POST',
|
||||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range/clear`,
|
||||
{ applyTo },
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
i,
|
||||
);
|
||||
} else {
|
||||
const range = this.getNodeParameter('range', i, '') as string;
|
||||
|
|
@ -96,6 +101,10 @@ export async function execute(
|
|||
'POST',
|
||||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')/clear`,
|
||||
{ applyTo },
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
i,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -114,7 +123,8 @@ export async function execute(
|
|||
returnData.push(...executionErrorData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
// A NodeError from the transport may be missing the itemIndex, add it
|
||||
throw stampItemIndexOnError(error, i);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { INodeExecutionData, IExecuteFunctions, INodeProperties } from 'n8n
|
|||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { stampItemIndexOnError } from '../../../../GenericFunctions';
|
||||
import { microsoftApiRequest } from '../../transport';
|
||||
import { workbookRLC, worksheetRLC } from '../common.descriptions';
|
||||
|
||||
|
|
@ -36,6 +37,11 @@ export async function execute(
|
|||
this,
|
||||
'DELETE',
|
||||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}`,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
i,
|
||||
);
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
|
|
@ -53,7 +59,8 @@ export async function execute(
|
|||
returnData.push(...executionErrorData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
// A NodeError from the transport may be missing the itemIndex, add it
|
||||
throw stampItemIndexOnError(error, i);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import type {
|
|||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { stampItemIndexOnError } from '../../../../GenericFunctions';
|
||||
import { microsoftApiRequest, microsoftApiRequestAllItems } from '../../transport';
|
||||
import { workbookRLC } from '../common.descriptions';
|
||||
|
||||
|
|
@ -90,6 +91,7 @@ export async function execute(
|
|||
`/drive/items/${workbookId}/workbook/worksheets`,
|
||||
{},
|
||||
qs,
|
||||
i,
|
||||
);
|
||||
} else {
|
||||
qs.$top = this.getNodeParameter('limit', i);
|
||||
|
|
@ -99,6 +101,9 @@ export async function execute(
|
|||
`/drive/items/${workbookId}/workbook/worksheets`,
|
||||
{},
|
||||
qs,
|
||||
undefined,
|
||||
undefined,
|
||||
i,
|
||||
);
|
||||
responseData = responseData.value;
|
||||
}
|
||||
|
|
@ -117,7 +122,8 @@ export async function execute(
|
|||
returnData.push(...executionErrorData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
// A NodeError from the transport may be missing the itemIndex, add it
|
||||
throw stampItemIndexOnError(error, i);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import type {
|
|||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { stampItemIndexOnError } from '../../../../GenericFunctions';
|
||||
import type { ExcelResponse } from '../../helpers/interfaces';
|
||||
import { checkRange, prepareOutput } from '../../helpers/utils';
|
||||
import { microsoftApiRequest } from '../../transport';
|
||||
|
|
@ -159,6 +160,9 @@ export async function execute(
|
|||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`,
|
||||
{},
|
||||
qs,
|
||||
undefined,
|
||||
undefined,
|
||||
i,
|
||||
);
|
||||
} else {
|
||||
responseData = await microsoftApiRequest.call(
|
||||
|
|
@ -167,6 +171,9 @@ export async function execute(
|
|||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/usedRange`,
|
||||
{},
|
||||
qs,
|
||||
undefined,
|
||||
undefined,
|
||||
i,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -199,7 +206,8 @@ export async function execute(
|
|||
returnData.push(...executionErrorData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
// A NodeError from the transport may be missing the itemIndex, add it
|
||||
throw stampItemIndexOnError(error, i);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -252,6 +252,7 @@ export async function execute(
|
|||
qs.$select = options.fields;
|
||||
}
|
||||
|
||||
// Whole-batch operation: every param (including the SP target) is read at item 0.
|
||||
const workbookId = this.getNodeParameter('workbook', 0, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
|
@ -272,6 +273,11 @@ export async function execute(
|
|||
this,
|
||||
'PATCH',
|
||||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -288,6 +294,9 @@ export async function execute(
|
|||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/usedRange`,
|
||||
undefined,
|
||||
query,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
range = (worksheetData.address as string).split('!')[1];
|
||||
|
|
@ -305,6 +314,9 @@ export async function execute(
|
|||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`,
|
||||
{ values },
|
||||
qs,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
returnData.push(
|
||||
|
|
@ -361,6 +373,10 @@ export async function execute(
|
|||
'PATCH',
|
||||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`,
|
||||
{ values: updateSummary.updatedData },
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
const { updatedRows } = updateSummary;
|
||||
|
|
|
|||
|
|
@ -204,6 +204,7 @@ export async function execute(
|
|||
const nodeVersion = this.getNode().typeVersion;
|
||||
|
||||
try {
|
||||
// Whole-batch operation: every param (including the SP target) is read at item 0.
|
||||
const workbookId = this.getNodeParameter('workbook', 0, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
|
@ -224,6 +225,11 @@ export async function execute(
|
|||
this,
|
||||
'PATCH',
|
||||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -240,6 +246,9 @@ export async function execute(
|
|||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/usedRange`,
|
||||
undefined,
|
||||
query,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
range = (worksheetData.address as string).split('!')[1];
|
||||
|
|
@ -256,6 +265,10 @@ export async function execute(
|
|||
'PATCH',
|
||||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`,
|
||||
{ values },
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -351,6 +364,9 @@ export async function execute(
|
|||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/usedRange`,
|
||||
undefined,
|
||||
{ select: 'address' },
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
const usedRange = parseAddress(address as string);
|
||||
|
|
@ -366,6 +382,10 @@ export async function execute(
|
|||
'PATCH',
|
||||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`,
|
||||
{ values: updateSummary.updatedData },
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
const { updatedRows } = updateSummary;
|
||||
|
|
|
|||
|
|
@ -45,7 +45,8 @@ export const userTargetRLC: INodeProperties = {
|
|||
hint: 'The user principal name (UPN) or object ID of the user whose drive holds the workbooks',
|
||||
},
|
||||
],
|
||||
description: 'The user whose drive the Service Principal should act on',
|
||||
description:
|
||||
"The user whose drive the Service Principal should act on. Evaluated per input item — an expression can target a different user for each item. Operations that write all rows in one request (append, update, upsert) use the first item's target.",
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: ['microsoftEntraServicePrincipalApi'],
|
||||
|
|
@ -69,7 +70,8 @@ export const driveTargetRLC: INodeProperties = {
|
|||
hint: "The drive's own ID (looks like `b!…`), not a file or folder ID. Get it from `GET /users/{upn}/drive` (the `id` field).",
|
||||
},
|
||||
],
|
||||
description: 'The drive the Service Principal should act on',
|
||||
description:
|
||||
"The drive the Service Principal should act on. Evaluated per input item — an expression can target a different drive for each item. Operations that write all rows in one request (append, update, upsert) use the first item's target.",
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: ['microsoftEntraServicePrincipalApi'],
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ import { NodeOperationError } from 'n8n-workflow';
|
|||
|
||||
import { getExcelCredentialType, microsoftApiRequest } from '../transport';
|
||||
|
||||
// listSearch context throughout this file: the transport's trailing `0` is its
|
||||
// fallback read (getNodeParameter's 2nd arg here is a fallback, not an item index).
|
||||
|
||||
export async function searchWorkbooks(
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string,
|
||||
|
|
@ -39,6 +42,8 @@ export async function searchWorkbooks(
|
|||
undefined,
|
||||
undefined,
|
||||
paginationToken, // paginationToken contains the full URL
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
} else {
|
||||
response = await microsoftApiRequest.call(
|
||||
|
|
@ -50,6 +55,9 @@ export async function searchWorkbooks(
|
|||
select: 'id,name,webUrl',
|
||||
$top: 100,
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -98,6 +106,9 @@ export async function getWorksheetsList(
|
|||
{
|
||||
select: 'id,name',
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
@ -133,6 +144,10 @@ export async function getWorksheetTables(
|
|||
'GET',
|
||||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables`,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
const results: INodeListSearchItems[] = [];
|
||||
|
|
@ -149,6 +164,9 @@ export async function getWorksheetTables(
|
|||
{
|
||||
select: 'address',
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
const [sheetName, sheetRange] = address.split('!' as string);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ import type { IDataObject, ILoadOptionsFunctions, INodePropertyOptions } from 'n
|
|||
import { microsoftApiRequest } from '../transport';
|
||||
import { parseAddress } from '../helpers/utils';
|
||||
|
||||
// loadOptions context throughout this file: the transport's trailing `0` is its
|
||||
// fallback read (getNodeParameter's 2nd arg here is a fallback, not an item index).
|
||||
|
||||
export async function getWorksheetColumnRow(
|
||||
this: ILoadOptionsFunctions,
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
|
|
@ -24,6 +27,9 @@ export async function getWorksheetColumnRow(
|
|||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/usedRange`,
|
||||
undefined,
|
||||
{ select: 'values' },
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
columns = worksheetData.values[0] as string[];
|
||||
|
|
@ -36,6 +42,10 @@ export async function getWorksheetColumnRow(
|
|||
'PATCH',
|
||||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`,
|
||||
{ select: 'values' },
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
columns = worksheetData.values[0] as string[];
|
||||
|
|
@ -79,6 +89,10 @@ export async function getTableColumns(
|
|||
'GET',
|
||||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`,
|
||||
{},
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
return (response.value as IDataObject[]).map((column) => ({
|
||||
|
|
|
|||
|
|
@ -9,12 +9,14 @@ import {
|
|||
import type { Mock, Mocked } from 'vitest';
|
||||
import { mockDeep } from 'vitest-mock-extended';
|
||||
|
||||
import * as readRows from '../../actions/worksheet/readRows.operation';
|
||||
import { searchWorkbooks } from '../../methods/listSearch';
|
||||
import {
|
||||
driveEndpoint,
|
||||
getExcelCredentialType,
|
||||
getServicePrincipalResourceRoot,
|
||||
microsoftApiRequest,
|
||||
microsoftApiRequestAllItemsSkip,
|
||||
resolveScopeRoot,
|
||||
validateResourceTargetId,
|
||||
} from '../../transport/index';
|
||||
|
|
@ -58,7 +60,16 @@ describe('Microsoft Excel Transport', () => {
|
|||
graphApiBaseUrl: 'https://graph.microsoft.us',
|
||||
});
|
||||
|
||||
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/workbooks');
|
||||
await microsoftApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'GET',
|
||||
'/workbooks',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(mockRequestOAuth2).toHaveBeenCalledWith(
|
||||
'microsoftExcelOAuth2Api',
|
||||
|
|
@ -80,7 +91,16 @@ describe('Microsoft Excel Transport', () => {
|
|||
graphApiBaseUrl: '',
|
||||
});
|
||||
|
||||
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/workbooks');
|
||||
await microsoftApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'GET',
|
||||
'/workbooks',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(mockRequestOAuth2).toHaveBeenCalledWith(
|
||||
'microsoftExcelOAuth2Api',
|
||||
|
|
@ -101,7 +121,16 @@ describe('Microsoft Excel Transport', () => {
|
|||
},
|
||||
});
|
||||
|
||||
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/workbooks');
|
||||
await microsoftApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'GET',
|
||||
'/workbooks',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(mockRequestOAuth2).toHaveBeenCalledWith(
|
||||
'microsoftExcelOAuth2Api',
|
||||
|
|
@ -123,7 +152,16 @@ describe('Microsoft Excel Transport', () => {
|
|||
graphApiBaseUrl: 'https://graph.microsoft.com/',
|
||||
});
|
||||
|
||||
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/workbooks');
|
||||
await microsoftApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'GET',
|
||||
'/workbooks',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(mockRequestOAuth2).toHaveBeenCalledWith(
|
||||
'microsoftExcelOAuth2Api',
|
||||
|
|
@ -145,7 +183,16 @@ describe('Microsoft Excel Transport', () => {
|
|||
graphApiBaseUrl: 'https://graph.microsoft.com///',
|
||||
});
|
||||
|
||||
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/workbooks');
|
||||
await microsoftApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'GET',
|
||||
'/workbooks',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(mockRequestOAuth2).toHaveBeenCalledWith(
|
||||
'microsoftExcelOAuth2Api',
|
||||
|
|
@ -167,7 +214,16 @@ describe('Microsoft Excel Transport', () => {
|
|||
graphApiBaseUrl: 'https://graph.microsoft.us',
|
||||
});
|
||||
|
||||
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/workbooks');
|
||||
await microsoftApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'GET',
|
||||
'/workbooks',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(mockRequestOAuth2).toHaveBeenCalledWith(
|
||||
'microsoftExcelOAuth2Api',
|
||||
|
|
@ -189,7 +245,16 @@ describe('Microsoft Excel Transport', () => {
|
|||
graphApiBaseUrl: 'https://dod-graph.microsoft.us',
|
||||
});
|
||||
|
||||
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/workbooks');
|
||||
await microsoftApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'GET',
|
||||
'/workbooks',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(mockRequestOAuth2).toHaveBeenCalledWith(
|
||||
'microsoftExcelOAuth2Api',
|
||||
|
|
@ -211,7 +276,16 @@ describe('Microsoft Excel Transport', () => {
|
|||
graphApiBaseUrl: 'https://microsoftgraph.chinacloudapi.cn',
|
||||
});
|
||||
|
||||
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/workbooks');
|
||||
await microsoftApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'GET',
|
||||
'/workbooks',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(mockRequestOAuth2).toHaveBeenCalledWith(
|
||||
'microsoftExcelOAuth2Api',
|
||||
|
|
@ -235,7 +309,16 @@ describe('Microsoft Excel Transport', () => {
|
|||
it('should use microsoftExcelOAuth2Api when authentication is not set (backward compatibility)', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValue(undefined);
|
||||
|
||||
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/workbooks');
|
||||
await microsoftApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'GET',
|
||||
'/workbooks',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(mockExecuteFunctions.getCredentials).toHaveBeenCalledWith('microsoftExcelOAuth2Api');
|
||||
expect(mockRequestOAuth2).toHaveBeenCalledWith(
|
||||
|
|
@ -247,7 +330,16 @@ describe('Microsoft Excel Transport', () => {
|
|||
it('should use microsoftExcelOAuth2Api when explicitly selected', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValue('microsoftExcelOAuth2Api');
|
||||
|
||||
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/workbooks');
|
||||
await microsoftApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'GET',
|
||||
'/workbooks',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(mockExecuteFunctions.getCredentials).toHaveBeenCalledWith('microsoftExcelOAuth2Api');
|
||||
expect(mockRequestOAuth2).toHaveBeenCalledWith(
|
||||
|
|
@ -259,7 +351,16 @@ describe('Microsoft Excel Transport', () => {
|
|||
it('should use microsoftOAuth2Api when the generic credential is selected', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValue('microsoftOAuth2Api');
|
||||
|
||||
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/workbooks');
|
||||
await microsoftApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'GET',
|
||||
'/workbooks',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(mockExecuteFunctions.getCredentials).toHaveBeenCalledWith('microsoftOAuth2Api');
|
||||
expect(mockRequestOAuth2).toHaveBeenCalledWith('microsoftOAuth2Api', expect.anything());
|
||||
|
|
@ -268,7 +369,16 @@ describe('Microsoft Excel Transport', () => {
|
|||
it('should resolve the credential name from the authentication parameter at index 0', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValue('microsoftOAuth2Api');
|
||||
|
||||
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/workbooks');
|
||||
await microsoftApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'GET',
|
||||
'/workbooks',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(mockExecuteFunctions.getNodeParameter).toHaveBeenCalledWith('authentication', 0);
|
||||
});
|
||||
|
|
@ -276,7 +386,16 @@ describe('Microsoft Excel Transport', () => {
|
|||
it('should honor graphApiBaseUrl from the generic credential (sovereign cloud)', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValue('microsoftOAuth2Api');
|
||||
|
||||
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/workbooks');
|
||||
await microsoftApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'GET',
|
||||
'/workbooks',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(mockExecuteFunctions.getCredentials).toHaveBeenCalledWith('microsoftOAuth2Api');
|
||||
expect(mockRequestOAuth2).toHaveBeenCalledWith(
|
||||
|
|
@ -386,7 +505,16 @@ describe('Microsoft Excel Transport', () => {
|
|||
});
|
||||
|
||||
it('routes through requestWithAuthentication, never requestOAuth2', async () => {
|
||||
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', SCOPED_RESOURCE);
|
||||
await microsoftApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'GET',
|
||||
SCOPED_RESOURCE,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(mockExecuteFunctions.getCredentials).toHaveBeenCalledWith(
|
||||
'microsoftEntraServicePrincipalApi',
|
||||
|
|
@ -399,7 +527,16 @@ describe('Microsoft Excel Transport', () => {
|
|||
});
|
||||
|
||||
it('rebases /me onto /users/{id}/drive for the user target', async () => {
|
||||
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', SCOPED_RESOURCE);
|
||||
await microsoftApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'GET',
|
||||
SCOPED_RESOURCE,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
'microsoftEntraServicePrincipalApi',
|
||||
|
|
@ -421,7 +558,16 @@ describe('Microsoft Excel Transport', () => {
|
|||
(name in params ? params[name as string] : fallback) as never,
|
||||
);
|
||||
|
||||
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', SCOPED_RESOURCE);
|
||||
await microsoftApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'GET',
|
||||
SCOPED_RESOURCE,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
'microsoftEntraServicePrincipalApi',
|
||||
|
|
@ -434,7 +580,16 @@ describe('Microsoft Excel Transport', () => {
|
|||
it('rebases onto /drives/{id} for the drive target (no double /drive)', async () => {
|
||||
setSpParams({ resourceTarget: 'drive', driveTarget: { value: 'b!abc-123_XYZ' } });
|
||||
|
||||
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', SCOPED_RESOURCE);
|
||||
await microsoftApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'GET',
|
||||
SCOPED_RESOURCE,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
'microsoftEntraServicePrincipalApi',
|
||||
|
|
@ -449,7 +604,16 @@ describe('Microsoft Excel Transport', () => {
|
|||
graphApiBaseUrl: 'https://graph.microsoft.us',
|
||||
});
|
||||
|
||||
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', SCOPED_RESOURCE);
|
||||
await microsoftApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'GET',
|
||||
SCOPED_RESOURCE,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
'microsoftEntraServicePrincipalApi',
|
||||
|
|
@ -470,6 +634,8 @@ describe('Microsoft Excel Transport', () => {
|
|||
{},
|
||||
{},
|
||||
nextLink,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
|
|
@ -478,9 +644,99 @@ describe('Microsoft Excel Transport', () => {
|
|||
);
|
||||
});
|
||||
|
||||
// The target RLC accepts expressions, so execute call sites pass the loop index
|
||||
// and the transport must resolve the target at exactly that index.
|
||||
const setSpParamsPerItem = () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((name, itemIndex, fallback) => {
|
||||
if (name === 'authentication') return 'microsoftEntraServicePrincipalApi' as never;
|
||||
if (name === 'resourceTarget') return 'user' as never;
|
||||
if (name === 'userTarget')
|
||||
return { value: itemIndex === 1 ? 'john@contoso.com' : 'jane@contoso.com' } as never;
|
||||
return fallback as never;
|
||||
});
|
||||
};
|
||||
|
||||
it('resolves the target at the passed itemIndex', async () => {
|
||||
setSpParamsPerItem();
|
||||
|
||||
await microsoftApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'GET',
|
||||
SCOPED_RESOURCE,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
await microsoftApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'GET',
|
||||
SCOPED_RESOURCE,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
1,
|
||||
);
|
||||
|
||||
expect(mockRequestWithAuthentication).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'microsoftEntraServicePrincipalApi',
|
||||
expect.objectContaining({
|
||||
uri: `${baseUrl}/v1.0/users/jane%40contoso.com/drive/items/WB/workbook/worksheets/WS/usedRange`,
|
||||
}),
|
||||
);
|
||||
expect(mockRequestWithAuthentication).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'microsoftEntraServicePrincipalApi',
|
||||
expect.objectContaining({
|
||||
uri: `${baseUrl}/v1.0/users/john%40contoso.com/drive/items/WB/workbook/worksheets/WS/usedRange`,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the passed itemIndex root on every $skip page (AllItemsSkip)', async () => {
|
||||
setSpParamsPerItem();
|
||||
mockRequestWithAuthentication
|
||||
.mockResolvedValueOnce({ value: [{ id: 'row-1' }] })
|
||||
.mockResolvedValueOnce({ value: [] });
|
||||
|
||||
await microsoftApiRequestAllItemsSkip.call(
|
||||
mockExecuteFunctions,
|
||||
'value',
|
||||
'GET',
|
||||
SCOPED_RESOURCE,
|
||||
{},
|
||||
{},
|
||||
1,
|
||||
);
|
||||
|
||||
const johnUri = `${baseUrl}/v1.0/users/john%40contoso.com/drive/items/WB/workbook/worksheets/WS/usedRange`;
|
||||
expect(mockRequestWithAuthentication).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'microsoftEntraServicePrincipalApi',
|
||||
expect.objectContaining({ uri: johnUri }),
|
||||
);
|
||||
expect(mockRequestWithAuthentication).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'microsoftEntraServicePrincipalApi',
|
||||
expect.objectContaining({ uri: johnUri }),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws OperationalError when a scoped resource does not start with /drive', async () => {
|
||||
await expect(
|
||||
microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/workbooks'),
|
||||
microsoftApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'GET',
|
||||
'/workbooks',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
),
|
||||
).rejects.toThrow(OperationalError);
|
||||
expect(mockRequestWithAuthentication).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
@ -489,7 +745,16 @@ describe('Microsoft Excel Transport', () => {
|
|||
mockRequestWithAuthentication.mockRejectedValue(new Error('boom'));
|
||||
|
||||
await expect(
|
||||
microsoftApiRequest.call(mockExecuteFunctions, 'GET', SCOPED_RESOURCE),
|
||||
microsoftApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'GET',
|
||||
SCOPED_RESOURCE,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
),
|
||||
).rejects.toThrow(NodeApiError);
|
||||
});
|
||||
|
||||
|
|
@ -501,6 +766,10 @@ describe('Microsoft Excel Transport', () => {
|
|||
'PATCH',
|
||||
"/drive/items/WB/workbook/worksheets/WS/range(address='A1:A1')",
|
||||
body,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
|
|
@ -521,6 +790,10 @@ describe('Microsoft Excel Transport', () => {
|
|||
'POST',
|
||||
'/drive/items/WB/workbook/worksheets/add',
|
||||
body,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
|
|
@ -588,7 +861,7 @@ describe('Microsoft Excel Transport', () => {
|
|||
it('returns undefined for the OAuth2 credential', () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValue('microsoftExcelOAuth2Api');
|
||||
|
||||
expect(resolveScopeRoot.call(mockExecuteFunctions)).toBeUndefined();
|
||||
expect(resolveScopeRoot.call(mockExecuteFunctions, 0)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('resolves the user root for the Service Principal credential', () => {
|
||||
|
|
@ -601,7 +874,7 @@ describe('Microsoft Excel Transport', () => {
|
|||
(name) => params[name as string] as never,
|
||||
);
|
||||
|
||||
expect(resolveScopeRoot.call(mockExecuteFunctions)).toBe('/users/jane%40contoso.com');
|
||||
expect(resolveScopeRoot.call(mockExecuteFunctions, 0)).toBe('/users/jane%40contoso.com');
|
||||
});
|
||||
|
||||
it('falls back to the user target when resourceTarget is unpersisted', () => {
|
||||
|
|
@ -615,7 +888,43 @@ describe('Microsoft Excel Transport', () => {
|
|||
(name in params ? params[name as string] : fallback) as never,
|
||||
);
|
||||
|
||||
expect(resolveScopeRoot.call(mockExecuteFunctions)).toBe('/users/jane%40contoso.com');
|
||||
expect(resolveScopeRoot.call(mockExecuteFunctions, 0)).toBe('/users/jane%40contoso.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute error attribution', () => {
|
||||
it('stamps context.itemIndex on a NodeError from the failing item', async () => {
|
||||
const mockRequestWithAuthentication = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ values: [['row'], ['jane-row']] });
|
||||
mockExecuteFunctions.helpers.requestWithAuthentication = mockRequestWithAuthentication;
|
||||
(mockExecuteFunctions.helpers.constructExecutionMetaData as Mock).mockReturnValue([]);
|
||||
mockExecuteFunctions.continueOnFail.mockReturnValue(false);
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({ graphApiBaseUrl: '' });
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((name, itemIndex, fallback) => {
|
||||
const params: Record<string, unknown> = {
|
||||
authentication: 'microsoftEntraServicePrincipalApi',
|
||||
resourceTarget: 'user',
|
||||
workbook: 'WB',
|
||||
worksheet: 'WS',
|
||||
};
|
||||
if (name === 'userTarget')
|
||||
return { value: itemIndex === 1 ? 'not a user' : 'jane@contoso.com' } as never;
|
||||
return (name in params ? params[name as string] : fallback) as never;
|
||||
});
|
||||
|
||||
let caught: unknown;
|
||||
try {
|
||||
await readRows.execute.call(mockExecuteFunctions, [{ json: {} }, { json: {} }]);
|
||||
} catch (error) {
|
||||
caught = error;
|
||||
}
|
||||
|
||||
expect(caught).toBeInstanceOf(NodeOperationError);
|
||||
expect((caught as NodeOperationError).message).toBe('The target ID is not valid');
|
||||
expect((caught as NodeOperationError).context.itemIndex).toBe(1);
|
||||
// Item 0 resolved and requested normally before item 1 failed validation.
|
||||
expect(mockRequestWithAuthentication).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -86,12 +86,14 @@ export function driveEndpoint(root: string): string {
|
|||
|
||||
/**
|
||||
* App-only Graph scope root (`/users/{id}` or `/drives/{id}`), or `undefined` for OAuth2 (`/me`).
|
||||
* The `|| 'user'` and `''` fallbacks cover load-options, where the 2nd getNodeParameter arg is the
|
||||
* fallback (not an itemIndex) — an unpersisted target then coalesces to the "target ID required" error.
|
||||
* The target RLC accepts expressions and is resolved per item: execute call sites pass the loop's
|
||||
* item index; load-options call sites pass a literal 0 — there the 2nd getNodeParameter arg is the
|
||||
* fallback (not an itemIndex), which the `|| 'user'` and `''` fallbacks cover, so an unpersisted
|
||||
* target coalesces to the "target ID required" error.
|
||||
*/
|
||||
export function resolveScopeRoot(
|
||||
this: IExecuteFunctions | ILoadOptionsFunctions,
|
||||
itemIndex = 0,
|
||||
itemIndex: number,
|
||||
): string | undefined {
|
||||
if (getExcelCredentialType.call(this) !== 'microsoftEntraServicePrincipalApi') {
|
||||
return undefined;
|
||||
|
|
@ -105,14 +107,19 @@ export function resolveScopeRoot(
|
|||
return getServicePrincipalResourceRoot(target, id, this.getNode());
|
||||
}
|
||||
|
||||
// `itemIndex` is REQUIRED so the compiler enforces the per-item contract: execute
|
||||
// call sites pass the loop index (batch ops pass a literal 0, matching their item-0
|
||||
// param reads); loadOptions/listSearch call sites pass a literal 0, where
|
||||
// `getNodeParameter`'s 2nd arg is a fallback, not an index.
|
||||
export async function microsoftApiRequest(
|
||||
this: IExecuteFunctions | ILoadOptionsFunctions,
|
||||
method: IHttpRequestMethods,
|
||||
resource: string,
|
||||
body: any = {},
|
||||
qs: IDataObject = {},
|
||||
uri?: string,
|
||||
uri: string | undefined,
|
||||
headers: IDataObject = {},
|
||||
itemIndex: number,
|
||||
): Promise<any> {
|
||||
const credentialType = getExcelCredentialType.call(this);
|
||||
const isServicePrincipal = credentialType === 'microsoftEntraServicePrincipalApi';
|
||||
|
|
@ -124,9 +131,9 @@ export async function microsoftApiRequest(
|
|||
).replace(/\/+$/, '');
|
||||
|
||||
let uriToUse = uri || `${baseUrl}/v1.0/me${resource}`;
|
||||
// App-only has no `/me`; rebase page-1 onto the user/drive root. Absolute `@odata.nextLink` pages pass through.
|
||||
// App-only has no `/me`; rebase page-1 onto the item's user/drive root. Absolute `@odata.nextLink` pages pass through.
|
||||
if (!uri && isServicePrincipal) {
|
||||
const driveScopeRoot = resolveScopeRoot.call(this);
|
||||
const driveScopeRoot = resolveScopeRoot.call(this, itemIndex);
|
||||
if (driveScopeRoot) {
|
||||
// Programmer error, not user input: every Excel resource is `/drive`-rooted.
|
||||
if (!resource.startsWith('/drive')) {
|
||||
|
|
@ -159,6 +166,7 @@ export async function microsoftApiRequest(
|
|||
}
|
||||
return await this.helpers.requestOAuth2.call(this, credentialType, options);
|
||||
} catch (error) {
|
||||
// The operation's catch stamps the failing item's index.
|
||||
throw new NodeApiError(this.getNode(), error as JsonObject);
|
||||
}
|
||||
}
|
||||
|
|
@ -170,6 +178,7 @@ export async function microsoftApiRequestAllItems(
|
|||
endpoint: string,
|
||||
body: any = {},
|
||||
query: IDataObject = {},
|
||||
itemIndex: number,
|
||||
): Promise<any> {
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
|
|
@ -178,7 +187,16 @@ export async function microsoftApiRequestAllItems(
|
|||
query.$top = 100;
|
||||
|
||||
do {
|
||||
responseData = await microsoftApiRequest.call(this, method, endpoint, body, query, uri);
|
||||
responseData = await microsoftApiRequest.call(
|
||||
this,
|
||||
method,
|
||||
endpoint,
|
||||
body,
|
||||
query,
|
||||
uri,
|
||||
undefined,
|
||||
itemIndex,
|
||||
);
|
||||
uri = responseData['@odata.nextLink'];
|
||||
if (uri?.includes('$top')) {
|
||||
delete query.$top;
|
||||
|
|
@ -196,6 +214,7 @@ export async function microsoftApiRequestAllItemsSkip(
|
|||
endpoint: string,
|
||||
body: any = {},
|
||||
query: IDataObject = {},
|
||||
itemIndex: number,
|
||||
): Promise<any> {
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
|
|
@ -204,7 +223,17 @@ export async function microsoftApiRequestAllItemsSkip(
|
|||
query.$skip = 0;
|
||||
|
||||
do {
|
||||
responseData = await microsoftApiRequest.call(this, method, endpoint, body, query);
|
||||
// Every `$skip` page re-issues the scoped relative URL, re-resolved at the same index.
|
||||
responseData = await microsoftApiRequest.call(
|
||||
this,
|
||||
method,
|
||||
endpoint,
|
||||
body,
|
||||
query,
|
||||
undefined,
|
||||
undefined,
|
||||
itemIndex,
|
||||
);
|
||||
query.$skip += query.$top;
|
||||
returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]);
|
||||
} while (responseData.value.length !== 0);
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user