Skip to content

Commit

Permalink
Added custom pagination and addressed a review comment
Browse files Browse the repository at this point in the history
  • Loading branch information
ShireenMissi committed Aug 9, 2024
1 parent 6e713b6 commit 89d316d
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 10 deletions.
18 changes: 11 additions & 7 deletions packages/nodes-base/nodes/Okta/UserDescription.ts
Original file line number Diff line number Diff line change
@@ -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[] = [
{
Expand Down Expand Up @@ -80,13 +80,17 @@ export const userOperations: INodeProperties[] = [
method: 'GET',
url: BASE_API_URL,
qs: { search: '={{$parameter["searchQuery"]}}' },
returnFullResponse: true,
},
output: {
postReceive: [simplifyGetAllResponse],
},
send: {
paginate: true,
},
operations: {
pagination: getCursorPaginator(),
},
},
action: 'Get many users',
},
Expand Down Expand Up @@ -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: {
Expand Down
39 changes: 36 additions & 3 deletions packages/nodes-base/nodes/Okta/UserFunctions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type {
DeclarativeRestApiSettings,
IDataObject,
IExecuteFunctions,
IExecutePaginationFunctions,
IExecuteSingleFunctions,
IHookFunctions,
IHttpRequestMethods,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -112,6 +115,7 @@ export async function simplifyGetAllResponse(
const simplifiedItem = simplifyOktaUser(item);
return {
json: simplifiedItem,
headers: _response.headers,
};
});
}
Expand All @@ -135,3 +139,32 @@ export async function simplifyGetResponse(
},
] as INodeExecutionData[];
}

export const getCursorPaginator = () => {
return async function cursorPagination(
this: IExecutePaginationFunctions,
requestOptions: DeclarativeRestApiSettings.ResultOptions,
): Promise<INodeExecutionData[]> {
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;
};
};
73 changes: 73 additions & 0 deletions packages/nodes-base/nodes/Okta/test/UserFunctions.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import type {
DeclarativeRestApiSettings,
IDataObject,
IExecuteFunctions,
IExecutePaginationFunctions,
IExecuteSingleFunctions,
ILoadOptionsFunctions,
IN8nHttpFullResponse,
INodeExecutionData,
} from 'n8n-workflow';
import {
getCursorPaginator,
getUsers,
oktaApiRequest,
simplifyGetAllResponse,
Expand Down Expand Up @@ -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);
});
});

0 comments on commit 89d316d

Please sign in to comment.