mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-30 19:35:36 +08:00
fix(Microsoft Teams Node): Respect limit server-side in getAll operations (#34851)
This commit is contained in:
+101
@@ -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": "<div>First message</div>"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"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": []
|
||||
}
|
||||
+27
-1
@@ -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: '<div>First message</div>',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '1698130964683',
|
||||
messageType: 'message',
|
||||
body: {
|
||||
contentType: 'html',
|
||||
content: '<div>Second message</div>',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['getAll.workflow.json'],
|
||||
workflowFiles: ['getAll.workflow.json', 'getAll.limit.workflow.json'],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import type { IExecuteFunctions, INode } from 'n8n-workflow';
|
||||
|
||||
import { microsoftApiRequestAllItems } from '../../v2/transport';
|
||||
|
||||
describe('MicrosoftTeamsV2 transport, microsoftApiRequestAllItems', () => {
|
||||
const makeContext = (requestOAuth2: ReturnType<typeof vi.fn>) =>
|
||||
({
|
||||
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<typeof vi.fn>, index: number) =>
|
||||
requestOAuth2.mock.calls[index][1] as { qs: Record<string, unknown>; 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({});
|
||||
});
|
||||
});
|
||||
@@ -46,6 +46,7 @@ export async function microsoftApiRequestAllItems(
|
||||
|
||||
body: any = {},
|
||||
query: IDataObject = {},
|
||||
limit?: number,
|
||||
): Promise<any> {
|
||||
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<any> {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+9
-2
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,6 +268,7 @@ export async function microsoftApiRequestAllItems(
|
||||
endpoint: string,
|
||||
body: any = {},
|
||||
query: IDataObject = {},
|
||||
limit?: number,
|
||||
): Promise<any> {
|
||||
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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user