Skip to content

Commit 92ad9fc

Browse files
authored
feat(typescript): Parse pagination headers in API responses #SCD-665 (#984)
1 parent 9959a81 commit 92ad9fc

File tree

2 files changed

+90
-9
lines changed

2 files changed

+90
-9
lines changed

clients/typescript/__tests__/BasicApiTest.ts

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@ const fs = require("fs");
66

77
global.FormData = FormData;
88

9-
const getMockFetch = (jsonResponse, textResponse) => jest.fn(() => Promise.resolve({
9+
const getMockFetch = (jsonResponse, textResponse, headers) => jest.fn(() => Promise.resolve({
1010
json: () => Promise.resolve(jsonResponse),
1111
text: () => Promise.resolve(textResponse),
1212
blob: () => Promise.resolve(textResponse),
1313
status: 200,
14-
ok: true
14+
ok: true,
15+
headers: {
16+
get: (name: string) => headers[name.toLowerCase()]
17+
}
1518
})) as jest.Mock;
1619

1720
describe('LocalesApi', () => {
@@ -21,7 +24,7 @@ describe('LocalesApi', () => {
2124

2225
describe('localesCreate', () => {
2326
beforeEach(() => {
24-
mockFetch = getMockFetch({}, 'foo');
27+
mockFetch = getMockFetch({}, 'foo', {});
2528
configuration = new Configuration(
2629
{
2730
apiKey: `token PHRASE_TOKEN`,
@@ -46,7 +49,7 @@ describe('LocalesApi', () => {
4649

4750
describe('localesList', () => {
4851
beforeEach(() => {
49-
mockFetch = getMockFetch([{id: 'locale_id_1'}], 'foo');
52+
mockFetch = getMockFetch([{id: 'locale_id_1'}], 'foo', {});
5053
configuration = new Configuration(
5154
{
5255
apiKey: `token PHRASE_TOKEN`,
@@ -59,8 +62,43 @@ describe('LocalesApi', () => {
5962
test('lists locales', async () => {
6063
const projectId = 'my-project-id';
6164

62-
await api.localesList({projectId}).then((response) => {
63-
expect(response[0].id).toBe('locale_id_1');
65+
await api.localesList({projectId}).then((data) => {
66+
expect(data[0].id).toBe('locale_id_1');
67+
});
68+
69+
expect(mockFetch.mock.calls.length).toBe(1);
70+
expect(mockFetch.mock.calls[0][0]).toBe(`https://api.phrase.com/v2/projects/${projectId}/locales`);
71+
});
72+
});
73+
74+
describe('localesListRaw', () => {
75+
beforeEach(() => {
76+
mockFetch = getMockFetch([{id: 'locale_id_1'}], 'foo', {
77+
pagination: '{"total_count":59,"total_pages_count":3,"current_page":1,"current_per_page":25,"previous_page":null,"next_page":2}',
78+
link: '<https://api.phrase.com/v2/projects/my-project-id/locales?page=1>; rel=first, <https://api.phrase.com/v2/projects/my-project-id/locales?page=3>; rel=last, <https://api.phrase.com/v2/projects/my-project-id/locales?page=2>; rel=next'
79+
});
80+
configuration = new Configuration(
81+
{
82+
apiKey: `token PHRASE_TOKEN`,
83+
fetchApi: mockFetch
84+
}
85+
);
86+
api = new LocalesApi(configuration);
87+
});
88+
89+
test('lists locales and checks pagination', async () => {
90+
const projectId = 'my-project-id';
91+
92+
await api.localesListRaw({projectId}).then((response) => {
93+
expect(response.isPaginated).toBe(true);
94+
expect(response.hasNextPage).toBe(true);
95+
expect(response.nextPageUrl).toBe('https://api.phrase.com/v2/projects/my-project-id/locales?page=2');
96+
expect(response.totalCount).toBe(59);
97+
expect(response.totalPages).toBe(3);
98+
expect(response.nextPage).toBe(2);
99+
response.value().then((data) => {
100+
expect(data[0].id).toBe('locale_id_1');
101+
});
64102
});
65103

66104
expect(mockFetch.mock.calls.length).toBe(1);
@@ -76,7 +114,7 @@ describe('UploadsApi', () => {
76114

77115
describe('uploadCreate', () => {
78116
beforeEach(() => {
79-
mockFetch = getMockFetch({id: "upload_id"}, 'foo');
117+
mockFetch = getMockFetch({id: "upload_id"}, 'foo', {});
80118
configuration = new Configuration(
81119
{
82120
apiKey: `token PHRASE_TOKEN`,

openapi-generator/templates/typescript-fetch/runtime.mustache

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,14 +288,57 @@ export interface Middleware {
288288
export interface ApiResponse<T> {
289289
raw: Response;
290290
value(): Promise<T>;
291+
isPaginated?: boolean;
292+
hasNextPage?: boolean;
293+
nextPage?: number;
294+
nextPageUrl?: string;
295+
totalCount?: number;
296+
totalPages?: number;
291297
}
292298

293299
export interface ResponseTransformer<T> {
294300
(json: any): T;
295301
}
296302

297-
export class JSONApiResponse<T> {
298-
constructor(public raw: Response, private transformer: ResponseTransformer<T> = (jsonValue: any) => jsonValue) {}
303+
export class JSONApiResponse<T> implements ApiResponse<T> {
304+
public isPaginated: boolean;
305+
public hasNextPage: boolean;
306+
public nextPage?: number;
307+
public nextPageUrl?: string;
308+
public totalCount?: number;
309+
public totalPages?: number;
310+
311+
constructor(public raw: Response, private transformer: ResponseTransformer<T> = (jsonValue: any) => jsonValue) {
312+
const link = raw.headers.get('Link') || raw.headers.get('link');
313+
const pagination = raw.headers.get('Pagination') || raw.headers.get('pagination');
314+
this.isPaginated = !!link;
315+
this.hasNextPage = false;
316+
if (link) {
317+
const match = link.match(/<([^>]+)>;\s*rel=next/);
318+
if (match) {
319+
this.hasNextPage = true;
320+
this.nextPageUrl = match[1];
321+
}
322+
}
323+
if (pagination) {
324+
try {
325+
const paginationObj = JSON.parse(pagination);
326+
if (paginationObj) {
327+
if (typeof paginationObj['total_count'] === 'number') {
328+
this.totalCount = paginationObj['total_count'];
329+
}
330+
if (typeof paginationObj['total_pages_count'] === 'number') {
331+
this.totalPages = paginationObj['total_pages_count'];
332+
}
333+
if (typeof paginationObj['next_page'] === 'number') {
334+
this.nextPage = paginationObj['next_page'];
335+
}
336+
}
337+
} catch (e) {
338+
// ignore invalid pagination header
339+
}
340+
}
341+
}
299342

300343
async value(): Promise<T> {
301344
return this.transformer(await this.raw.json());

0 commit comments

Comments
 (0)