- Define
Pagination
class - Define
PaginatedList<T>
generic class - Utility functions for parsing http response
Last active
November 11, 2022 15:41
-
-
Save merlosy/1c333cf3e149ba182a7c0676a802bad8 to your computer and use it in GitHub Desktop.
Paginated list in Typescript (from Content-Range header)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* List of HTTP status codes | |
*/ | |
export const HTTP_STATUS = { | |
OK: 200, | |
/** | |
* The server has successfully fulfilled the request and that there is no additional content to send in the response payload body | |
* @see https://tools.ietf.org/html/rfc7231#section-6.3.5 | |
*/ | |
NoContent: 204, | |
/** | |
* The server is successfully fulfilling a range request for the target resource | |
* by transferring one or more parts of the selected representation that correspond to | |
* the satisfiable ranges found in the request's Range header field | |
* @see https://tools.ietf.org/html/rfc7233#section-4.1 | |
*/ | |
PartialContent: 206, | |
/** | |
* The request has not been applied because it lacks valid authentication credentials for the target resource. | |
* @see https://tools.ietf.org/html/rfc7235#section-3.1 | |
*/ | |
Unauthorized: 401, | |
/** | |
* The server understood the request but refuses to authorize it | |
* @see https://tools.ietf.org/html/rfc7231#section-6.5.3 | |
*/ | |
Forbidden: 403, | |
RangeNotSatisfiable: 416 | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { PaginatedList, Pagination } from './paginated-list.model'; | |
describe('PaginatedList', () => { | |
const arr = new Array(10).fill('a'); | |
it('should create a new instance', () => { | |
const pag = new Pagination(arr.length, 0, arr.length); | |
const list = new PaginatedList<string>(arr, pag); | |
expect(list.data).toEqual(arr); | |
expect(list.pagination.page).toEqual(0); | |
expect(list.pagination.perPage).toEqual(arr.length); | |
expect(list.pagination.total).toEqual(arr.length); | |
}); | |
it('should manage pagination', () => { | |
const pag = new Pagination(arr.length, 0, arr.length * 3); | |
const list = new PaginatedList<string>(arr, pag); | |
expect(list.data).toEqual(arr); | |
expect(list.pagination.getFirstIndex(1)).toEqual(arr.length); | |
expect(list.pagination.getLastIndex(1)).toEqual(2 * arr.length - 1); | |
expect(list.pagination.isEmpty()).toBeFalsy(); | |
}); | |
it('should manage shorter last page', () => { | |
// page 0: 10 / page 1: 10 / page 2: 5 | |
const pag = new Pagination(arr.length, 2, arr.length * 2.5); | |
const list = new PaginatedList<string>(arr, pag); | |
expect(list.data).toEqual(arr); | |
// page 1: 10 / page 2: 10 / page 3: 5 | |
expect(list.pagination.getFirstIndex(3)).toEqual(arr.length * 3); | |
expect(list.pagination.getLastIndex(3)).toEqual(2.5 * arr.length - 1); | |
}); | |
it('should check isLastPage', () => { | |
// page 0: 10 / page 1: 10 / page 2: 5 | |
const pag = new Pagination(arr.length, 2, arr.length * 2.5); | |
const list = new PaginatedList<string>(arr, pag); | |
expect(list.pagination.isLastPage(1)).toBeFalsy(); | |
expect(list.pagination.isLastPage(2)).toBeTruthy(); | |
expect(list.pagination.isLastPage(3)).toBeFalsy(); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Format a paginated list compliant with standards | |
*/ | |
export class PaginatedList<T> { | |
public pagination: Pagination; | |
public data: T[]; | |
/** Create a new PaginatedList object | |
* @param list list of elements | |
* @param pagination pagination related to the elements | |
*/ | |
constructor(list: T[], pagination: Pagination) { | |
this.data = list; | |
this.pagination = pagination; | |
} | |
} | |
export class Pagination { | |
private _perPage: number; | |
private _page: number; | |
private _total: number; | |
/** | |
* Create a pagination based on list indexes and total length | |
* @param {number} firstIndex first index of the page | |
* @param {number} lastIndex last index of the page | |
* @param {number} total total length of elements (all pages) | |
* @param {number} [pageSize] optional page size: forces a page size. This can be useful for last pages when they are not full. | |
*/ | |
public static fromPagination(firstIndex: number, lastIndex: number, total: number, pageSize?: number): Pagination { | |
const _perPage = pageSize || lastIndex - firstIndex + 1; | |
const _page = Math.floor((firstIndex + 1) / _perPage); | |
return new Pagination(_perPage, _page, total); | |
} | |
public static empty(): Pagination { | |
return new Pagination(0, -1, 0); | |
} | |
constructor(perPage: number, page: number, total: number) { | |
this._page = page; | |
this._perPage = perPage; | |
this._total = total; | |
} | |
/** | |
* number of elements displayed per page | |
*/ | |
get perPage() { | |
return this._perPage; | |
} | |
/** | |
* index of the current page, count start from 0 for the first page, -1 if there is no result | |
*/ | |
get page() { | |
return this._page; | |
} | |
/** | |
* total number of elements | |
*/ | |
get total() { | |
return this._total; | |
} | |
/** | |
* Calculate the first index for a given page | |
* @param page starts from 0 | |
*/ | |
public getFirstIndex(page: number): number { | |
return page * this.perPage; | |
} | |
/** | |
* Calculate the last index for a given page | |
* @param page starts from 0 | |
*/ | |
public getLastIndex(page: number): number { | |
return Math.min((page + 1) * this.perPage, this._total) - 1; | |
} | |
/** | |
* Whether a given page is the last one | |
* @param page starts from 0 | |
*/ | |
public isLastPage(page: number): boolean { | |
return this.getFirstIndex(page) <= this._total - 1 && this._total - 1 <= this.getLastIndex(page); | |
} | |
/** | |
* Whether the pagination is empty | |
*/ | |
public isEmpty(): boolean { | |
return this._perPage === 0; | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export function formatPaginatedResponse(response: AxiosResponse<T[]>, pageSize?: number): PaginatedList<T> { | |
if (response.status === HTTP_STATUS.OK) { | |
const data = response.data as T[]; | |
return new PaginatedList(data, new Pagination(data.length, 0, data.length)); | |
} else { | |
// status is 206 for Partial Content | |
return fromPaginated<T>(response, pageSize); | |
} | |
/** | |
* @param response The HTTP response | |
* @see https://tools.ietf.org/html/rfc7233#section-4.2 | |
*/ | |
export function fromPaginated<T>(response: AxiosResponse<T[]>, pageSize?: number): PaginatedList<T> { | |
const contentRange = response.headers.get('Content-Range'); | |
const list = response.data as T[]; | |
let pagination: Pagination; | |
if (contentRange) { | |
const [range, total] = contentRange.split(/\//); | |
if (total === '*') { | |
throw Error(`Total size is unknown`); | |
} | |
if (range === '*') { | |
pagination = Pagination.empty(); | |
} else { | |
const split = range.split(/\-/).map(i => parseInt(i, 10)); | |
const diff = split[1] - split[0] + 1; | |
pagination = Pagination.fromPagination(split[0], split[1], parseInt(total, 10), pageSize); | |
if (list.length !== diff) { | |
throw Error(`Inconsistent list size(${list.length}) instead of header(${diff})`); | |
} | |
} | |
} else { | |
throw new Error('Expected paginated result: "Content-Range" not found'); | |
} | |
return new PaginatedList(list, pagination); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment