Skip to content

Instantly share code, notes, and snippets.

@merlosy
Last active November 11, 2022 15:41
Show Gist options
  • Save merlosy/1c333cf3e149ba182a7c0676a802bad8 to your computer and use it in GitHub Desktop.
Save merlosy/1c333cf3e149ba182a7c0676a802bad8 to your computer and use it in GitHub Desktop.
Paginated list in Typescript (from Content-Range header)

Pagination abstraction layer

  • Define Pagination class
  • Define PaginatedList<T> generic class
  • Utility functions for parsing http response
/**
* 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
};
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();
});
});
/**
* 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;
}
}
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