From 89d316d4b93b29b3d96fa36373bae3eecd428711 Mon Sep 17 00:00:00 2001 From: Shireen Missi Date: Fri, 9 Aug 2024 15:17:47 +0100 Subject: [PATCH] Added custom pagination and addressed a review comment --- .../nodes-base/nodes/Okta/UserDescription.ts | 18 +++-- .../nodes-base/nodes/Okta/UserFunctions.ts | 39 +++++++++- .../nodes/Okta/test/UserFunctions.test.ts | 73 +++++++++++++++++++ 3 files changed, 120 insertions(+), 10 deletions(-) diff --git a/packages/nodes-base/nodes/Okta/UserDescription.ts b/packages/nodes-base/nodes/Okta/UserDescription.ts index 98886791474ce1..b10d580339a99d 100644 --- a/packages/nodes-base/nodes/Okta/UserDescription.ts +++ b/packages/nodes-base/nodes/Okta/UserDescription.ts @@ -1,5 +1,5 @@ import type { INodeProperties } from 'n8n-workflow'; -import { simplifyGetAllResponse, simplifyGetResponse } from './UserFunctions'; +import { getCursorPaginator, simplifyGetAllResponse, simplifyGetResponse } from './UserFunctions'; const BASE_API_URL = '/api/v1/users/'; export const userOperations: INodeProperties[] = [ { @@ -80,6 +80,7 @@ export const userOperations: INodeProperties[] = [ method: 'GET', url: BASE_API_URL, qs: { search: '={{$parameter["searchQuery"]}}' }, + returnFullResponse: true, }, output: { postReceive: [simplifyGetAllResponse], @@ -87,6 +88,9 @@ export const userOperations: INodeProperties[] = [ send: { paginate: true, }, + operations: { + pagination: getCursorPaginator(), + }, }, action: 'Get many users', }, @@ -552,16 +556,16 @@ export const userFields: INodeProperties[] = [ }, }, { - displayName: 'ID', - name: 'id', + displayName: 'By username', + name: 'login', type: 'string', - placeholder: 'e.g. 00u1abcd2345EfGHIjk6', + placeholder: '', }, { - displayName: 'By username', - name: 'email', + displayName: 'ID', + name: 'id', type: 'string', - placeholder: '', + placeholder: 'e.g. 00u1abcd2345EfGHIjk6', }, ], displayOptions: { diff --git a/packages/nodes-base/nodes/Okta/UserFunctions.ts b/packages/nodes-base/nodes/Okta/UserFunctions.ts index c803939483a21e..6def8ea296ae96 100644 --- a/packages/nodes-base/nodes/Okta/UserFunctions.ts +++ b/packages/nodes-base/nodes/Okta/UserFunctions.ts @@ -1,6 +1,8 @@ import type { + DeclarativeRestApiSettings, IDataObject, IExecuteFunctions, + IExecutePaginationFunctions, IExecuteSingleFunctions, IHookFunctions, IHttpRequestMethods, @@ -64,11 +66,11 @@ export async function getUsers( const responseData: OktaUser[] = await oktaApiRequest.call(this, 'GET', '/users/'); const filteredUsers = responseData.filter((user) => { if (!filter) return true; - const fullName = `${user.profile.firstName} ${user.profile.lastName}`.toLowerCase(); - return fullName.includes(filter.toLowerCase()); + const username = `${user.profile.login}`.toLowerCase(); + return username.includes(filter.toLowerCase()); }); const users: INodePropertyOptions[] = filteredUsers.map((user) => ({ - name: `${user.profile.firstName} ${user.profile.lastName}`, + name: `${user.profile.login}`, value: user.id, })); return { @@ -104,6 +106,7 @@ export async function simplifyGetAllResponse( if (!simplify) return ((items[0].json as unknown as IDataObject[]) ?? []).map((item: IDataObject) => ({ json: item, + headers: _response.headers, })) as INodeExecutionData[]; let simplifiedItems: INodeExecutionData[] = []; if (items[0].json) { @@ -112,6 +115,7 @@ export async function simplifyGetAllResponse( const simplifiedItem = simplifyOktaUser(item); return { json: simplifiedItem, + headers: _response.headers, }; }); } @@ -135,3 +139,32 @@ export async function simplifyGetResponse( }, ] as INodeExecutionData[]; } + +export const getCursorPaginator = () => { + return async function cursorPagination( + this: IExecutePaginationFunctions, + requestOptions: DeclarativeRestApiSettings.ResultOptions, + ): Promise { + if (!requestOptions.options.qs) { + requestOptions.options.qs = {}; + } + + let items: INodeExecutionData[] = []; + let responseData: INodeExecutionData[]; + let nextCursor: string | undefined = undefined; + const returnAll = this.getNodeParameter('returnAll', true) as boolean; + do { + requestOptions.options.qs.limit = 200; + requestOptions.options.qs.after = nextCursor; + responseData = await this.makeRoutingRequest(requestOptions); + if (responseData.length > 0) { + const headers = responseData[responseData.length - 1].headers; + const headersLink = (headers as IDataObject)?.link as string | undefined; + nextCursor = headersLink?.split('after=')[1]?.split('&')[0]?.split('>')[0]; + } + items = items.concat(responseData); + } while (returnAll && nextCursor); + + return items; + }; +}; diff --git a/packages/nodes-base/nodes/Okta/test/UserFunctions.test.ts b/packages/nodes-base/nodes/Okta/test/UserFunctions.test.ts index d3f24b7bd13121..fac6f36606815c 100644 --- a/packages/nodes-base/nodes/Okta/test/UserFunctions.test.ts +++ b/packages/nodes-base/nodes/Okta/test/UserFunctions.test.ts @@ -1,12 +1,15 @@ import type { + DeclarativeRestApiSettings, IDataObject, IExecuteFunctions, + IExecutePaginationFunctions, IExecuteSingleFunctions, ILoadOptionsFunctions, IN8nHttpFullResponse, INodeExecutionData, } from 'n8n-workflow'; import { + getCursorPaginator, getUsers, oktaApiRequest, simplifyGetAllResponse, @@ -300,3 +303,73 @@ describe('simplifyGetResponse', () => { expect(result).toEqual(expectedResult); }); }); +describe('getCursorPaginator', () => { + let mockContext: IExecutePaginationFunctions; + let mockRequestOptions: DeclarativeRestApiSettings.ResultOptions; + const baseUrl = 'https://api.example.com'; + + beforeEach(() => { + mockContext = { + getNodeParameter: jest.fn(), + makeRoutingRequest: jest.fn(), + } as unknown as IExecutePaginationFunctions; + + mockRequestOptions = { + options: { + qs: {}, + }, + } as DeclarativeRestApiSettings.ResultOptions; + }); + + it('should return all items when returnAll is true', async () => { + const mockResponseData: INodeExecutionData[] = [ + { json: { id: 1 }, headers: { link: `<${baseUrl}?after=cursor1>` } }, + { json: { id: 2 }, headers: { link: `<${baseUrl}?after=cursor2>` } }, + { json: { id: 3 }, headers: { link: `<${baseUrl}>` } }, + ]; + + (mockContext.getNodeParameter as jest.Mock).mockReturnValue(true); + (mockContext.makeRoutingRequest as jest.Mock) + .mockResolvedValueOnce([mockResponseData[0]]) + .mockResolvedValueOnce([mockResponseData[1]]) + .mockResolvedValueOnce([mockResponseData[2]]); + + const paginator = getCursorPaginator().bind(mockContext); + const result = await paginator(mockRequestOptions); + + expect(result).toEqual(mockResponseData); + expect(mockContext.getNodeParameter).toHaveBeenCalledWith('returnAll', true); + expect(mockContext.makeRoutingRequest).toHaveBeenCalledTimes(3); + }); + + it('should return items until nextCursor is undefined', async () => { + const mockResponseData: INodeExecutionData[] = [ + { json: { id: 1 }, headers: { link: `<${baseUrl}?after=cursor1>` } }, + { json: { id: 2 }, headers: { link: `<${baseUrl}>` } }, + ]; + + (mockContext.getNodeParameter as jest.Mock).mockReturnValue(true); + (mockContext.makeRoutingRequest as jest.Mock) + .mockResolvedValueOnce([mockResponseData[0]]) + .mockResolvedValueOnce([mockResponseData[1]]); + + const paginator = getCursorPaginator().bind(mockContext); + const result = await paginator(mockRequestOptions); + + expect(result).toEqual(mockResponseData); + expect(mockContext.getNodeParameter).toHaveBeenCalledWith('returnAll', true); + expect(mockContext.makeRoutingRequest).toHaveBeenCalledTimes(2); + }); + + it('should handle empty response data', async () => { + (mockContext.getNodeParameter as jest.Mock).mockReturnValue(true); + (mockContext.makeRoutingRequest as jest.Mock).mockResolvedValue([]); + + const paginator = getCursorPaginator().bind(mockContext); + const result = await paginator(mockRequestOptions); + + expect(result).toEqual([]); + expect(mockContext.getNodeParameter).toHaveBeenCalledWith('returnAll', true); + expect(mockContext.makeRoutingRequest).toHaveBeenCalledTimes(1); + }); +});