Skip to content

Commit

Permalink
Merge pull request #27 from lifeomic/pagination
Browse files Browse the repository at this point in the history
feat: add pagination helper
  • Loading branch information
swain authored Jun 6, 2022
2 parents 9af80ad + 64e4161 commit 13e6d64
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 0 deletions.
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,41 @@ console.log(response.data);
// ]
```

#### Pagination

The generated client provides a built-in helper for reading from paginated LifeOmic APIs:

```yaml
# Example endpoint
Name: listPaginatedPosts
Request:
type: object
properties:
filter: { type: string }
nextPageToken: { type: string }
pageSize: { type: string }
Response:
type: object
properties:
items:
$ref: '#/definitions/Post'
links:
type: object
properties:
self: { type: string }
next: { type: string }
```
```typescript
// Usage
const result = await client.paginate(client.listPaginatedPosts, {
filter: '...',
pageSize: '10',
});

result.length; // result is the fully-paginated list of posts
```

### OpenAPI Spec generation

Use the `generate-open-api-spec` command to generate an OpenAPI spec from a simple schema, which may be useful for interfacing with common OpenAPI tooling.
Expand Down
40 changes: 40 additions & 0 deletions src/generate-axios-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ const removePathParams = (url, params) =>
{}
);
const parseQueryParamsFromPagingLink = (link) => {
const params = new URLSearchParams(link.split("?")[1]);
return {
nextPageToken: params.get("nextPageToken"),
pageSize: params.get("pageSize"),
};
};
class Client {
constructor(client) {
this.client = client;
Expand All @@ -97,6 +106,26 @@ class Client {
url: substituteParams("/posts/:id", data),
});
}
async paginate(request, data, config) {
const result = [];
let nextPageParams = {};
do {
const response = await this[request.name](
{ ...nextPageParams, ...data },
config
);
result.push(...response.data.items);
nextPageParams = response.data.links.next
? parseQueryParamsFromPagingLink(response.data.links.next)
: {};
} while (!!nextPageParams.nextPageToken);
return result;
}
}
module.exports.Client = Client;
Expand Down Expand Up @@ -142,6 +171,17 @@ export declare class Client {
Endpoints["PUT /posts/:id"]["PathParams"],
config?: AxiosRequestConfig
): Promise<AxiosResponse<Endpoints["PUT /posts/:id"]["Response"]>>;
paginate<T extends { nextPageToken?: string; pageSize?: string }, Item>(
request: (
data: T,
config?: AxiosRequestConfig
) => Promise<
AxiosResponse<{ items: Item[]; links: { self: string; next?: string } }>
>,
data: T,
config?: AxiosRequestConfig
): Promise<Item[]>;
}
`,
},
Expand Down
40 changes: 40 additions & 0 deletions src/generate-axios-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ export declare class ${outputClass} {
): Promise<AxiosResponse<Endpoints['${endpoint}']['Response']>>`;
})
.join('\n\n')}
paginate<T extends { nextPageToken?: string; pageSize?: string }, Item>(
request: (
data: T,
config?: AxiosRequestConfig
) => Promise<
AxiosResponse<{ items: Item[]; links: { self: string; next?: string } }>
>,
data: T,
config?: AxiosRequestConfig
): Promise<Item[]>;
}`.trim(),
].join('\n');

Expand All @@ -60,6 +71,15 @@ const removePathParams = (url, params) =>
{}
);
const parseQueryParamsFromPagingLink = (link) => {
const params = new URLSearchParams(link.split('?')[1]);
return {
nextPageToken: params.get('nextPageToken'),
pageSize: params.get('pageSize')
};
};
class ${outputClass} {
constructor(client) {
Expand All @@ -83,6 +103,26 @@ class ${outputClass} {
`;
})
.join('\n\n')}
async paginate(request, data, config) {
const result = [];
let nextPageParams = {};
do {
const response = await this[request.name](
{ ...nextPageParams, ...data },
config
);
result.push(...response.data.items);
nextPageParams = response.data.links.next
? parseQueryParamsFromPagingLink(response.data.links.next)
: {};
} while (!!nextPageParams.nextPageToken);
return result;
}
}
module.exports.${outputClass} = ${outputClass};
Expand Down
102 changes: 102 additions & 0 deletions src/integration.axios.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,36 @@ const prepare = async () => {
},
},
},
'GET /posts/list': {
Name: 'listPosts',
Request: {
type: 'object',
additionalProperties: false,
properties: {
filter: { type: 'string' },
nextPageToken: { type: 'string' },
pageSize: { type: 'string' },
},
},
Response: {
type: 'object',
additionalProperties: false,
required: ['items', 'links'],
properties: {
items: {
type: 'array',
items: { type: 'string' },
},
links: {
type: 'object',
properties: {
self: { type: 'string' },
next: { type: 'string' },
},
},
},
},
},
},
};

Expand Down Expand Up @@ -112,4 +142,76 @@ describe('integration tests', () => {
},
});
});

test('pagination', async () => {
const { client, request } = await prepare();

request
.mockResolvedValueOnce({
data: {
items: ['first', 'second'],
links: {
self: 'blah-blah',
next: '/posts/list?nextPageToken=firstpagetoken&pageSize=10&randomProperty=blah',
},
},
})
.mockResolvedValueOnce({
data: {
items: ['third', 'fourth'],
links: {
self: 'blah-blah',
next: '/posts/list?nextPageToken=secondpagetoken&pageSize=10&randomProperty=blah',
},
},
})
.mockResolvedValueOnce({
data: {
items: ['fifth'],
links: {
self: 'blah-blah',
},
},
});

const result = await client.paginate(client.listPosts, {
filter: 'something',
});

expect(request).toHaveBeenCalledTimes(3);
expect(request).toHaveBeenNthCalledWith(1, {
method: 'GET',
params: {
filter: 'something',
},
url: '/posts/list',
});
// After first requests, inherits default page size
expect(request).toHaveBeenNthCalledWith(2, {
method: 'GET',
params: {
filter: 'something',
nextPageToken: 'firstpagetoken',
pageSize: '10',
},
url: '/posts/list',
});
expect(request).toHaveBeenNthCalledWith(3, {
method: 'GET',
params: {
filter: 'something',
nextPageToken: 'secondpagetoken',
pageSize: '10',
},
url: '/posts/list',
});

expect(result).toStrictEqual([
'first',
'second',
'third',
'fourth',
'fifth',
]);
});
});

0 comments on commit 13e6d64

Please sign in to comment.