diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channelMessage/getAll.limit.workflow.json b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channelMessage/getAll.limit.workflow.json
new file mode 100644
index 00000000000..13785790831
--- /dev/null
+++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channelMessage/getAll.limit.workflow.json
@@ -0,0 +1,101 @@
+{
+ "name": "My workflow 36",
+ "nodes": [
+ {
+ "parameters": {},
+ "id": "6666-9999-88888",
+ "name": "When clicking \"Execute Workflow\"",
+ "type": "n8n-nodes-base.manualTrigger",
+ "typeVersion": 1,
+ "position": [880, 380]
+ },
+ {
+ "parameters": {
+ "resource": "channelMessage",
+ "operation": "getAll",
+ "teamId": {
+ "__rl": true,
+ "value": "1111-2222-3333",
+ "mode": "list",
+ "cachedResultName": "U.S. Sales"
+ },
+ "channelId": {
+ "__rl": true,
+ "value": "42:aaabbbccc.tacv2",
+ "mode": "list",
+ "cachedResultName": "Sales West",
+ "cachedResultUrl": "https://teams.microsoft.com/l/channel/threadId/Sales%20West?groupId=1111-2222-3333&tenantId=tenantId-111-222-333&allowXTenantAccess=False"
+ },
+ "returnAll": false,
+ "limit": 1
+ },
+ "id": "6666-5555-88888",
+ "name": "Microsoft Teams",
+ "type": "n8n-nodes-base.microsoftTeams",
+ "typeVersion": 2,
+ "position": [1100, 380],
+ "credentials": {
+ "microsoftTeamsOAuth2Api": {
+ "id": "6isd5ytvA0qV78eK",
+ "name": "Microsoft Teams account"
+ }
+ }
+ },
+ {
+ "parameters": {},
+ "id": "9d1a2e59-c71c-486c-b3ac-dec6adbc26b4",
+ "name": "No Operation, do nothing",
+ "type": "n8n-nodes-base.noOp",
+ "typeVersion": 1,
+ "position": [1400, 380]
+ }
+ ],
+ "pinData": {
+ "No Operation, do nothing": [
+ {
+ "json": {
+ "id": "1698130964682",
+ "messageType": "message",
+ "body": {
+ "contentType": "html",
+ "content": "
First message
"
+ }
+ }
+ }
+ ]
+ },
+ "connections": {
+ "When clicking \"Execute Workflow\"": {
+ "main": [
+ [
+ {
+ "node": "Microsoft Teams",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Microsoft Teams": {
+ "main": [
+ [
+ {
+ "node": "No Operation, do nothing",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ }
+ },
+ "active": false,
+ "settings": {
+ "executionOrder": "v1"
+ },
+ "versionId": "d89de79a-1819-4d29-b781-a1f3f00b4a2f",
+ "id": "i3NYGF0LXV4qDFVA",
+ "meta": {
+ "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363"
+ },
+ "tags": []
+}
diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channelMessage/getAll.test.ts b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channelMessage/getAll.test.ts
index 50feb0ab8ce..ec808f2a81c 100644
--- a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channelMessage/getAll.test.ts
+++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channelMessage/getAll.test.ts
@@ -64,8 +64,34 @@ describe('Test MicrosoftTeamsV2, channelMessage => getAll', () => {
],
});
+ nock('https://graph.microsoft.com')
+ .get('/beta/teams/1111-2222-3333/channels/42:aaabbbccc.tacv2/messages')
+ .query({ $top: 1 })
+ .reply(200, {
+ '@odata.nextLink':
+ 'https://graph.microsoft.com/beta/teams/1111-2222-3333/channels/42:aaabbbccc.tacv2/messages?$skiptoken=abc',
+ value: [
+ {
+ id: '1698130964682',
+ messageType: 'message',
+ body: {
+ contentType: 'html',
+ content: 'First message
',
+ },
+ },
+ {
+ id: '1698130964683',
+ messageType: 'message',
+ body: {
+ contentType: 'html',
+ content: 'Second message
',
+ },
+ },
+ ],
+ });
+
new NodeTestHarness().setupTests({
credentials,
- workflowFiles: ['getAll.workflow.json'],
+ workflowFiles: ['getAll.workflow.json', 'getAll.limit.workflow.json'],
});
});
diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/getAll.test.ts b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/getAll.test.ts
index 4b850a7abb1..7477057dab9 100644
--- a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/getAll.test.ts
+++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/getAll.test.ts
@@ -6,6 +6,7 @@ import { credentials } from '../../../credentials';
describe('Test MicrosoftTeamsV2, chatMessage => getAll', () => {
nock('https://graph.microsoft.com')
.get('/v1.0/chats/19:ebed9ad42c904d6c83adf0db360053ec@thread.v2/messages')
+ .query({ $top: 2 })
.reply(200, {
value: [
{
diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/servicePrincipal.test.ts b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/servicePrincipal.test.ts
index 3e6ae2d1560..2bde2c5f219 100644
--- a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/servicePrincipal.test.ts
+++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/servicePrincipal.test.ts
@@ -346,6 +346,8 @@ describe('Microsoft Teams V2 — Service Principal runtime guards', () => {
'GET',
'/beta/teams/1111-2222-3333/channels/19:abc@thread.tacv2/messages',
{},
+ { $top: 1 },
+ 1,
);
});
});
diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/transport.test.ts b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/transport.test.ts
new file mode 100644
index 00000000000..fb766168caa
--- /dev/null
+++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/transport.test.ts
@@ -0,0 +1,88 @@
+import type { IExecuteFunctions, INode } from 'n8n-workflow';
+
+import { microsoftApiRequestAllItems } from '../../v2/transport';
+
+describe('MicrosoftTeamsV2 transport, microsoftApiRequestAllItems', () => {
+ const makeContext = (requestOAuth2: ReturnType) =>
+ ({
+ getNodeParameter: vi.fn().mockReturnValue(undefined),
+ getCredentials: vi.fn().mockResolvedValue({}),
+ getNode: vi.fn().mockReturnValue({ name: 'Microsoft Teams' } as INode),
+ helpers: { requestOAuth2 },
+ }) as unknown as IExecuteFunctions;
+
+ const optionsOfCall = (requestOAuth2: ReturnType, index: number) =>
+ requestOAuth2.mock.calls[index][1] as { qs: Record; uri?: string };
+
+ it('forwards the query and stops before the next page once the limit is satisfied', async () => {
+ const requestOAuth2 = vi.fn().mockResolvedValue({
+ value: [{ id: '1' }, { id: '2' }, { id: '3' }],
+ '@odata.nextLink': 'https://graph.microsoft.com/next-page',
+ });
+ const ctx = makeContext(requestOAuth2);
+
+ const result = await microsoftApiRequestAllItems.call(
+ ctx,
+ 'value',
+ 'GET',
+ '/beta/teams/1/channels/2/messages',
+ {},
+ { $top: 2 },
+ 2,
+ );
+
+ expect(result).toEqual([{ id: '1' }, { id: '2' }]);
+ expect(requestOAuth2).toHaveBeenCalledTimes(1);
+ expect(optionsOfCall(requestOAuth2, 0).qs).toEqual({ $top: 2 });
+ });
+
+ it('paginates until the limit is reached without re-sending the query', async () => {
+ const page = (n: number) => Array.from({ length: n }, (_, i) => ({ id: String(i) }));
+ const requestOAuth2 = vi
+ .fn()
+ .mockResolvedValueOnce({
+ value: page(50),
+ '@odata.nextLink': 'https://graph.microsoft.com/next-page',
+ })
+ .mockResolvedValueOnce({ value: page(50) });
+ const ctx = makeContext(requestOAuth2);
+
+ const result = await microsoftApiRequestAllItems.call(
+ ctx,
+ 'value',
+ 'GET',
+ '/beta/teams/1/channels/2/messages',
+ {},
+ { $top: 100 },
+ 100,
+ );
+
+ expect(result).toHaveLength(100);
+ expect(requestOAuth2).toHaveBeenCalledTimes(2);
+ expect(optionsOfCall(requestOAuth2, 0).qs).toEqual({ $top: 100 });
+ expect(optionsOfCall(requestOAuth2, 1).qs).toEqual({});
+ expect(optionsOfCall(requestOAuth2, 1).uri).toBe('https://graph.microsoft.com/next-page');
+ });
+
+ it('walks every page when no limit is set', async () => {
+ const requestOAuth2 = vi
+ .fn()
+ .mockResolvedValueOnce({
+ value: [{ id: '1' }],
+ '@odata.nextLink': 'https://graph.microsoft.com/next-page',
+ })
+ .mockResolvedValueOnce({ value: [{ id: '2' }] });
+ const ctx = makeContext(requestOAuth2);
+
+ const result = await microsoftApiRequestAllItems.call(
+ ctx,
+ 'value',
+ 'GET',
+ '/v1.0/teams/1/channels',
+ );
+
+ expect(result).toEqual([{ id: '1' }, { id: '2' }]);
+ expect(requestOAuth2).toHaveBeenCalledTimes(2);
+ expect(optionsOfCall(requestOAuth2, 0).qs).toEqual({});
+ });
+});
diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v1/GenericFunctions.ts b/packages/nodes-base/nodes/Microsoft/Teams/v1/GenericFunctions.ts
index b4fe1631f1b..59d81584809 100644
--- a/packages/nodes-base/nodes/Microsoft/Teams/v1/GenericFunctions.ts
+++ b/packages/nodes-base/nodes/Microsoft/Teams/v1/GenericFunctions.ts
@@ -46,6 +46,7 @@ export async function microsoftApiRequestAllItems(
body: any = {},
query: IDataObject = {},
+ limit?: number,
): Promise {
const returnData: IDataObject[] = [];
@@ -53,12 +54,19 @@ export async function microsoftApiRequestAllItems(
let uri: string | undefined;
do {
- responseData = await microsoftApiRequest.call(this, method, endpoint, body, query, uri);
+ // `@odata.nextLink` already carries the query params; don't re-send them
+ responseData = await microsoftApiRequest.call(
+ this,
+ method,
+ endpoint,
+ body,
+ uri ? {} : query,
+ uri,
+ );
uri = responseData['@odata.nextLink'];
returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]);
- const limit = query.limit as number | undefined;
- if (limit && limit <= returnData.length) {
- return returnData;
+ if (limit && returnData.length >= limit) {
+ return returnData.slice(0, limit);
}
} while (responseData['@odata.nextLink'] !== undefined);
@@ -70,7 +78,6 @@ export async function microsoftApiRequestAllItemsSkip(
propertyName: string,
method: IHttpRequestMethods,
endpoint: string,
-
body: any = {},
query: IDataObject = {},
): Promise {
diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v1/MicrosoftTeamsV1.node.ts b/packages/nodes-base/nodes/Microsoft/Teams/v1/MicrosoftTeamsV1.node.ts
index a75646da434..2a6f5289276 100644
--- a/packages/nodes-base/nodes/Microsoft/Teams/v1/MicrosoftTeamsV1.node.ts
+++ b/packages/nodes-base/nodes/Microsoft/Teams/v1/MicrosoftTeamsV1.node.ts
@@ -275,7 +275,6 @@ export class MicrosoftTeamsV1 implements INodeType {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
const length = items.length;
- const qs: IDataObject = {};
let responseData;
const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0);
@@ -339,15 +338,17 @@ export class MicrosoftTeamsV1 implements INodeType {
`/v1.0/teams/${teamId}/channels`,
);
} else {
- qs.limit = this.getNodeParameter('limit', i);
+ const limit = this.getNodeParameter('limit', i);
+ // list-channels doesn't support $top; the limit still stops pagination early
responseData = await microsoftApiRequestAllItems.call(
this,
'value',
'GET',
`/v1.0/teams/${teamId}/channels`,
{},
+ {},
+ limit,
);
- responseData = responseData.splice(0, qs.limit);
}
}
//https://docs.microsoft.com/en-us/graph/api/channel-patch?view=graph-rest-beta&tabs=http
@@ -424,15 +425,16 @@ export class MicrosoftTeamsV1 implements INodeType {
`/beta/teams/${teamId}/channels/${channelId}/messages`,
);
} else {
- qs.limit = this.getNodeParameter('limit', i);
+ const limit = this.getNodeParameter('limit', i);
responseData = await microsoftApiRequestAllItems.call(
this,
'value',
'GET',
`/beta/teams/${teamId}/channels/${channelId}/messages`,
{},
+ { $top: limit },
+ limit,
);
- responseData = responseData.splice(0, qs.limit);
}
}
}
@@ -484,15 +486,16 @@ export class MicrosoftTeamsV1 implements INodeType {
`/v1.0/chats/${chatId}/messages`,
);
} else {
- qs.limit = this.getNodeParameter('limit', i);
+ const limit = this.getNodeParameter('limit', i);
responseData = await microsoftApiRequestAllItems.call(
this,
'value',
'GET',
`/v1.0/chats/${chatId}/messages`,
{},
+ { $top: limit },
+ limit,
);
- responseData = responseData.splice(0, qs.limit);
}
}
}
@@ -575,15 +578,18 @@ export class MicrosoftTeamsV1 implements INodeType {
`/v1.0/users/${memberId}/planner/tasks`,
);
} else {
- qs.limit = this.getNodeParameter('limit', i);
+ const limit = this.getNodeParameter('limit', i);
+ // Planner endpoints don't support OData query params like $top;
+ // the limit still stops pagination early
responseData = await microsoftApiRequestAllItems.call(
this,
'value',
'GET',
`/v1.0/users/${memberId}/planner/tasks`,
{},
+ {},
+ limit,
);
- responseData = responseData.splice(0, qs.limit);
}
} else {
//https://docs.microsoft.com/en-us/graph/api/plannerplan-list-tasks?view=graph-rest-1.0&tabs=http
@@ -596,15 +602,16 @@ export class MicrosoftTeamsV1 implements INodeType {
`/v1.0/planner/plans/${planId}/tasks`,
);
} else {
- qs.limit = this.getNodeParameter('limit', i);
+ const limit = this.getNodeParameter('limit', i);
responseData = await microsoftApiRequestAllItems.call(
this,
'value',
'GET',
`/v1.0/planner/plans/${planId}/tasks`,
{},
+ {},
+ limit,
);
- responseData = responseData.splice(0, qs.limit);
}
}
}
diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channel/getAll.operation.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channel/getAll.operation.ts
index 88321b9c453..22e1979f60f 100644
--- a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channel/getAll.operation.ts
+++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channel/getAll.operation.ts
@@ -27,7 +27,7 @@ export async function execute(this: IExecuteFunctions, i: number) {
return await microsoftApiRequestAllItems.call(this, 'value', 'GET', endpoint);
} else {
const limit = this.getNodeParameter('limit', i);
- const responseData = await microsoftApiRequestAllItems.call(this, 'value', 'GET', endpoint, {});
- return responseData.splice(0, limit);
+ // list-channels doesn't support $top; the limit still stops pagination early
+ return await microsoftApiRequestAllItems.call(this, 'value', 'GET', endpoint, {}, {}, limit);
}
}
diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channelMessage/getAll.operation.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channelMessage/getAll.operation.ts
index fd2bf2fdf14..790fb10e21d 100644
--- a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channelMessage/getAll.operation.ts
+++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channelMessage/getAll.operation.ts
@@ -36,7 +36,14 @@ export async function execute(this: IExecuteFunctions, i: number) {
return await microsoftApiRequestAllItems.call(this, 'value', 'GET', endpoint);
} else {
const limit = this.getNodeParameter('limit', i);
- const responseData = await microsoftApiRequestAllItems.call(this, 'value', 'GET', endpoint, {});
- return responseData.splice(0, limit);
+ return await microsoftApiRequestAllItems.call(
+ this,
+ 'value',
+ 'GET',
+ endpoint,
+ {},
+ { $top: limit },
+ limit,
+ );
}
}
diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMessage/getAll.operation.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMessage/getAll.operation.ts
index 6e2dbc68c2a..a33cb5489c5 100644
--- a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMessage/getAll.operation.ts
+++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMessage/getAll.operation.ts
@@ -39,13 +39,14 @@ export async function execute(this: IExecuteFunctions, i: number) {
);
} else {
const limit = this.getNodeParameter('limit', i);
- const responseData = await microsoftApiRequestAllItems.call(
+ return await microsoftApiRequestAllItems.call(
this,
'value',
'GET',
buildTeamsPath.call(this, ['/v1.0/chats/', { id: chatId }, '/messages']),
{},
+ { $top: limit },
+ limit,
);
- return responseData.splice(0, limit);
}
}
diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/task/getAll.operation.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/task/getAll.operation.ts
index 73de83805e5..7966d543a69 100644
--- a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/task/getAll.operation.ts
+++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/task/getAll.operation.ts
@@ -101,14 +101,17 @@ export async function execute(this: IExecuteFunctions, i: number) {
);
} else {
const limit = this.getNodeParameter('limit', i);
- const responseData = await microsoftApiRequestAllItems.call(
+ // Planner endpoints don't support OData query params like $top;
+ // the limit still stops pagination early
+ return await microsoftApiRequestAllItems.call(
this,
'value',
'GET',
`/v1.0/users/${memberId}/planner/tasks`,
{},
+ {},
+ limit,
);
- return responseData.splice(0, limit);
}
} else {
//https://docs.microsoft.com/en-us/graph/api/plannerplan-list-tasks?view=graph-rest-1.0&tabs=http
@@ -118,14 +121,7 @@ export async function execute(this: IExecuteFunctions, i: number) {
return await microsoftApiRequestAllItems.call(this, 'value', 'GET', endpoint);
} else {
const limit = this.getNodeParameter('limit', i);
- const responseData = await microsoftApiRequestAllItems.call(
- this,
- 'value',
- 'GET',
- endpoint,
- {},
- );
- return responseData.splice(0, limit);
+ return await microsoftApiRequestAllItems.call(this, 'value', 'GET', endpoint, {}, {}, limit);
}
}
}
diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/transport/index.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/transport/index.ts
index a58f730aad3..ca8fc3b18bf 100644
--- a/packages/nodes-base/nodes/Microsoft/Teams/v2/transport/index.ts
+++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/transport/index.ts
@@ -268,6 +268,7 @@ export async function microsoftApiRequestAllItems(
endpoint: string,
body: any = {},
query: IDataObject = {},
+ limit?: number,
): Promise {
const returnData: IDataObject[] = [];
@@ -275,12 +276,19 @@ export async function microsoftApiRequestAllItems(
let uri: string | undefined;
do {
- responseData = await microsoftApiRequest.call(this, method, endpoint, body, query, uri);
+ // `@odata.nextLink` already carries the query params; don't re-send them
+ responseData = await microsoftApiRequest.call(
+ this,
+ method,
+ endpoint,
+ body,
+ uri ? {} : query,
+ uri,
+ );
uri = responseData['@odata.nextLink'];
returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]);
- const limit = query.limit as number | undefined;
- if (limit && limit <= returnData.length) {
- return returnData;
+ if (limit && returnData.length >= limit) {
+ return returnData.slice(0, limit);
}
} while (responseData['@odata.nextLink'] !== undefined);