Skip to content

Commit 305f260

Browse files
mattgdnicknisi
authored andcommitted
Add tests and typing updates for pagination utils (#1252)
Adds tests and updates typing for pagination utils.
1 parent e5d5047 commit 305f260

File tree

8 files changed

+224
-35
lines changed

8 files changed

+224
-35
lines changed
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { List, PaginationOptions } from '../interfaces';
2+
import { AutoPaginatable } from './pagination';
3+
4+
type TestObject = {
5+
id: string;
6+
};
7+
8+
describe('AutoPaginatable', () => {
9+
let mockApiCall: jest.Mock;
10+
11+
beforeEach(() => {
12+
jest.useFakeTimers();
13+
mockApiCall = jest.fn();
14+
});
15+
16+
afterEach(() => {
17+
jest.useRealTimers();
18+
jest.clearAllMocks();
19+
});
20+
21+
it('returns initial data when limit is specified', async () => {
22+
const initialData: List<TestObject> = {
23+
object: 'list',
24+
data: [{ id: '1' }, { id: '2' }],
25+
listMetadata: { after: 'cursor1' },
26+
};
27+
28+
const paginatable = new AutoPaginatable<TestObject, PaginationOptions>(
29+
initialData,
30+
mockApiCall,
31+
{ limit: 2 },
32+
);
33+
34+
const result = await paginatable.autoPagination();
35+
36+
expect(result).toEqual(initialData.data);
37+
expect(mockApiCall).not.toHaveBeenCalled();
38+
});
39+
40+
it('paginates through all pages', async () => {
41+
const initialData: List<TestObject> = {
42+
object: 'list',
43+
data: [{ id: '1' }, { id: '2' }],
44+
listMetadata: { after: 'cursor1' },
45+
};
46+
47+
mockApiCall
48+
.mockResolvedValueOnce({
49+
object: 'list',
50+
data: [{ id: '3' }, { id: '4' }],
51+
listMetadata: { after: 'cursor2' },
52+
})
53+
.mockResolvedValueOnce({
54+
object: 'list',
55+
data: [{ id: '5' }, { id: '6' }],
56+
listMetadata: { after: null },
57+
});
58+
59+
const paginatable = new AutoPaginatable<TestObject, PaginationOptions>(
60+
initialData,
61+
mockApiCall,
62+
);
63+
64+
const resultPromise = paginatable.autoPagination();
65+
66+
// Fast-forward through setTimeout calls
67+
jest.advanceTimersByTimeAsync(250);
68+
69+
const result = await resultPromise;
70+
71+
expect(result).toEqual([
72+
{ id: '3' },
73+
{ id: '4' },
74+
{ id: '5' },
75+
{ id: '6' },
76+
]);
77+
78+
expect(mockApiCall).toHaveBeenCalledTimes(2);
79+
expect(mockApiCall).toHaveBeenNthCalledWith(1, {
80+
limit: 100,
81+
after: undefined,
82+
});
83+
expect(mockApiCall).toHaveBeenNthCalledWith(2, {
84+
limit: 100,
85+
after: 'cursor2',
86+
});
87+
});
88+
89+
it('respects rate limiting between requests', async () => {
90+
const initialData: List<TestObject> = {
91+
object: 'list',
92+
data: [{ id: '1' }],
93+
listMetadata: { after: 'cursor1' },
94+
};
95+
96+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
97+
98+
mockApiCall
99+
.mockResolvedValueOnce({
100+
object: 'list',
101+
data: [{ id: '2' }],
102+
listMetadata: { after: 'cursor2' },
103+
})
104+
.mockResolvedValueOnce({
105+
object: 'list',
106+
data: [{ id: '3' }],
107+
listMetadata: { after: null },
108+
});
109+
110+
const paginatable = new AutoPaginatable<TestObject, PaginationOptions>(
111+
initialData,
112+
mockApiCall,
113+
);
114+
115+
const resultPromise = paginatable.autoPagination();
116+
117+
jest.advanceTimersByTimeAsync(250);
118+
await resultPromise;
119+
120+
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 250);
121+
});
122+
123+
it('passes through additional options to API calls', async () => {
124+
const initialData: List<TestObject> = {
125+
object: 'list',
126+
data: [{ id: '1' }],
127+
listMetadata: { after: 'cursor1' },
128+
};
129+
130+
mockApiCall
131+
.mockResolvedValueOnce({
132+
object: 'list',
133+
data: [{ id: '2' }],
134+
listMetadata: { after: 'cursor2' },
135+
})
136+
.mockResolvedValueOnce({
137+
object: 'list',
138+
data: [{ id: '3' }],
139+
listMetadata: { after: null },
140+
});
141+
142+
const paginatable = new AutoPaginatable<
143+
TestObject,
144+
PaginationOptions & { status: 'active' }
145+
>(initialData, mockApiCall, { status: 'active' });
146+
147+
const resultPromise = paginatable.autoPagination();
148+
jest.advanceTimersByTimeAsync(1000);
149+
await resultPromise;
150+
151+
expect(mockApiCall).toHaveBeenNthCalledWith(1, {
152+
after: undefined,
153+
status: 'active',
154+
limit: 100,
155+
});
156+
expect(mockApiCall).toHaveBeenNthCalledWith(2, {
157+
after: 'cursor2',
158+
status: 'active',
159+
limit: 100,
160+
});
161+
});
162+
});

src/common/utils/pagination.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,31 @@
11
import { List, PaginationOptions } from '../interfaces';
22

3-
export class AutoPaginatable<T> {
3+
export class AutoPaginatable<
4+
ResourceType,
5+
ParametersType extends PaginationOptions,
6+
> {
47
readonly object = 'list' as const;
5-
readonly options: PaginationOptions;
8+
readonly options: ParametersType;
69

710
constructor(
8-
protected list: List<T>,
9-
private apiCall: (params: PaginationOptions) => Promise<List<T>>,
10-
options?: PaginationOptions,
11+
protected list: List<ResourceType>,
12+
private apiCall: (params: PaginationOptions) => Promise<List<ResourceType>>,
13+
options?: ParametersType,
1114
) {
12-
this.options = {
13-
...options,
14-
};
15+
this.options = options ?? ({} as ParametersType);
1516
}
1617

17-
get data(): T[] {
18+
get data(): ResourceType[] {
1819
return this.list.data;
1920
}
2021

2122
get listMetadata() {
2223
return this.list.listMetadata;
2324
}
2425

25-
private async *generatePages(params: PaginationOptions): AsyncGenerator<T[]> {
26+
private async *generatePages(
27+
params: PaginationOptions,
28+
): AsyncGenerator<ResourceType[]> {
2629
const result = await this.apiCall({
2730
...this.options,
2831
limit: 100,
@@ -42,12 +45,12 @@ export class AutoPaginatable<T> {
4245
* Automatically paginates over the list of results, returning the complete data set.
4346
* Returns the first result if `options.limit` is passed to the first request.
4447
*/
45-
async autoPagination(): Promise<T[]> {
48+
async autoPagination(): Promise<ResourceType[]> {
4649
if (this.options.limit) {
4750
return this.data;
4851
}
4952

50-
const results: T[] = [];
53+
const results: ResourceType[] = [];
5154

5255
for await (const page of this.generatePages({
5356
after: this.options.after,

src/directory-sync/directory-sync.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
ListDirectoriesOptions,
1212
ListDirectoryGroupsOptions,
1313
ListDirectoryUsersOptions,
14+
SerializedListDirectoriesOptions,
1415
} from './interfaces';
1516
import {
1617
deserializeDirectory,
@@ -25,7 +26,7 @@ export class DirectorySync {
2526

2627
async listDirectories(
2728
options?: ListDirectoriesOptions,
28-
): Promise<AutoPaginatable<Directory>> {
29+
): Promise<AutoPaginatable<Directory, SerializedListDirectoriesOptions>> {
2930
return new AutoPaginatable(
3031
await fetchAndDeserialize<DirectoryResponse, Directory>(
3132
this.workos,
@@ -58,7 +59,7 @@ export class DirectorySync {
5859

5960
async listGroups(
6061
options: ListDirectoryGroupsOptions,
61-
): Promise<AutoPaginatable<DirectoryGroup>> {
62+
): Promise<AutoPaginatable<DirectoryGroup, ListDirectoryGroupsOptions>> {
6263
return new AutoPaginatable(
6364
await fetchAndDeserialize<DirectoryGroupResponse, DirectoryGroup>(
6465
this.workos,
@@ -79,7 +80,12 @@ export class DirectorySync {
7980

8081
async listUsers<TCustomAttributes extends object = DefaultCustomAttributes>(
8182
options: ListDirectoryUsersOptions,
82-
): Promise<AutoPaginatable<DirectoryUserWithGroups<TCustomAttributes>>> {
83+
): Promise<
84+
AutoPaginatable<
85+
DirectoryUserWithGroups<TCustomAttributes>,
86+
ListDirectoryUsersOptions
87+
>
88+
> {
8389
return new AutoPaginatable(
8490
await fetchAndDeserialize<
8591
DirectoryUserWithGroupsResponse<TCustomAttributes>,

src/fga/fga.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ import {
2626
WarrantTokenResponse,
2727
BatchWriteResourcesOptions,
2828
BatchWriteResourcesResponse,
29+
SerializedListWarrantsOptions,
30+
SerializedListResourcesOptions,
31+
SerializedQueryOptions,
2932
} from './interfaces';
3033
import {
3134
deserializeBatchWriteResourcesResponse,
@@ -105,7 +108,7 @@ export class FGA {
105108

106109
async listResources(
107110
options?: ListResourcesOptions,
108-
): Promise<AutoPaginatable<Resource>> {
111+
): Promise<AutoPaginatable<Resource, SerializedListResourcesOptions>> {
109112
return new AutoPaginatable(
110113
await fetchAndDeserialize<ResourceResponse, Resource>(
111114
this.workos,
@@ -186,7 +189,7 @@ export class FGA {
186189
async listWarrants(
187190
options?: ListWarrantsOptions,
188191
requestOptions?: ListWarrantsRequestOptions,
189-
): Promise<AutoPaginatable<Warrant>> {
192+
): Promise<AutoPaginatable<Warrant, SerializedListWarrantsOptions>> {
190193
return new AutoPaginatable(
191194
await fetchAndDeserialize<WarrantResponse, Warrant>(
192195
this.workos,
@@ -210,8 +213,8 @@ export class FGA {
210213
async query(
211214
options: QueryOptions,
212215
requestOptions: QueryRequestOptions = {},
213-
): Promise<FgaPaginatable<QueryResult>> {
214-
return new FgaPaginatable<QueryResult>(
216+
): Promise<FgaPaginatable<QueryResult, SerializedQueryOptions>> {
217+
return new FgaPaginatable<QueryResult, SerializedQueryOptions>(
215218
await fetchAndDeserializeFGAList<QueryResultResponse, QueryResult>(
216219
this.workos,
217220
'/fga/v1/query',

src/fga/utils/fga-paginatable.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ import { FGAList } from '../interfaces/list.interface';
33
import { Warning } from '../interfaces/warning.interface';
44
import { PaginationOptions } from '../../common/interfaces';
55

6-
export class FgaPaginatable<T> extends AutoPaginatable<T> {
6+
export class FgaPaginatable<T, P extends PaginationOptions = PaginationOptions> extends AutoPaginatable<T, P> {
77
protected override list!: FGAList<T>;
88

99
constructor(
1010
list: FGAList<T>,
1111
apiCall: (params: PaginationOptions) => Promise<FGAList<T>>,
12-
options?: PaginationOptions,
12+
options?: P,
1313
) {
1414
super(list, apiCall, options);
1515
}

src/organizations/organizations.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export class Organizations {
3030

3131
async listOrganizations(
3232
options?: ListOrganizationsOptions,
33-
): Promise<AutoPaginatable<Organization>> {
33+
): Promise<AutoPaginatable<Organization, ListOrganizationsOptions>> {
3434
return new AutoPaginatable(
3535
await fetchAndDeserialize<OrganizationResponse, Organization>(
3636
this.workos,

src/sso/sso.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
ProfileAndTokenResponse,
1515
ProfileResponse,
1616
SSOAuthorizationURLOptions,
17+
SerializedListConnectionsOptions,
1718
} from './interfaces';
1819
import {
1920
deserializeConnection,
@@ -42,7 +43,7 @@ export class SSO {
4243

4344
async listConnections(
4445
options?: ListConnectionsOptions,
45-
): Promise<AutoPaginatable<Connection>> {
46+
): Promise<AutoPaginatable<Connection, SerializedListConnectionsOptions>> {
4647
return new AutoPaginatable(
4748
await fetchAndDeserialize<ConnectionResponse, Connection>(
4849
this.workos,

0 commit comments

Comments
 (0)