-
-
Notifications
You must be signed in to change notification settings - Fork 105
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(api): Create a paginate method (#379)
- Loading branch information
Showing
2 changed files
with
168 additions
and
0 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import { paginate } from './paginate' | ||
|
||
describe('paginate', () => { | ||
it('should paginate without default query', () => { | ||
const totalCount = 100 | ||
const relativeUrl = '/items' | ||
const query = { page: 2, limit: 10 } | ||
|
||
const result = paginate(totalCount, relativeUrl, query) | ||
|
||
expect(result).toBeDefined() | ||
expect(result).toEqual( | ||
expect.objectContaining({ | ||
page: 2, | ||
perPage: 10, | ||
pageCount: 10, | ||
totalCount: 100 | ||
}) | ||
) | ||
expect(result.links.self).toEqual('/items?page=2&limit=10') | ||
expect(result.links.first).toEqual('/items?page=0&limit=10') | ||
expect(result.links.previous).toEqual('/items?page=1&limit=10') | ||
expect(result.links.next).toEqual('/items?page=3&limit=10') | ||
expect(result.links.last).toEqual('/items?page=9&limit=10') | ||
}) | ||
|
||
it('should paginate with default query', () => { | ||
const totalCount = 100 | ||
const relativeUrl = '/items' | ||
const query = { page: 5, limit: 10 } | ||
const defaultQuery = { pricing: 'pro', filter: 'admin' } | ||
|
||
const result = paginate(totalCount, relativeUrl, query, defaultQuery) | ||
|
||
expect(result).toBeDefined() | ||
expect(result).toEqual( | ||
expect.objectContaining({ | ||
page: 5, | ||
perPage: 10, | ||
pageCount: 10, | ||
totalCount: 100 | ||
}) | ||
) | ||
expect(result.links.self).toEqual( | ||
'/items?filter=admin&pricing=pro&page=5&limit=10' | ||
) | ||
expect(result.links.first).toEqual( | ||
'/items?filter=admin&pricing=pro&page=0&limit=10' | ||
) | ||
expect(result.links.previous).toEqual( | ||
'/items?filter=admin&pricing=pro&page=4&limit=10' | ||
) | ||
expect(result.links.next).toEqual( | ||
'/items?filter=admin&pricing=pro&page=6&limit=10' | ||
) | ||
expect(result.links.last).toEqual( | ||
'/items?filter=admin&pricing=pro&page=9&limit=10' | ||
) | ||
}) | ||
|
||
it('should paginate correctly edge cases where pervious or next is null', () => { | ||
const totalCount = 10 | ||
const relativeUrl = '/items' | ||
const query = { page: 0, limit: 10 } | ||
|
||
const result = paginate(totalCount, relativeUrl, query) | ||
|
||
expect(result).toBeDefined() | ||
expect(result).toEqual( | ||
expect.objectContaining({ | ||
page: 0, | ||
perPage: 10, | ||
pageCount: 1, | ||
totalCount: 10 | ||
}) | ||
) | ||
expect(result.links.self).toEqual('/items?page=0&limit=10') | ||
expect(result.links.first).toEqual('/items?page=0&limit=10') | ||
expect(result.links.previous).toBeNull() | ||
expect(result.links.next).toBeNull() | ||
expect(result.links.last).toEqual('/items?page=0&limit=10') | ||
}) | ||
|
||
it('should not be able to paginate when limit is 0 or undefined', () => { | ||
const totalCount = 10 | ||
const relativeUrl = '/items' | ||
const query = { page: 0, limit: 0 } | ||
|
||
expect(() => paginate(totalCount, relativeUrl, query)).toThrow( | ||
'Limit is required' | ||
) | ||
}) | ||
}) |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
export interface PaginatedMetadata { | ||
page: number | ||
perPage: number | ||
pageCount: number | ||
totalCount: number | ||
links: { | ||
self: string | ||
first: string | ||
previous: string | null | ||
next: string | null | ||
last: string | ||
} | ||
} | ||
|
||
interface QueryOptions { | ||
page: number | ||
limit: number | ||
sort?: string | ||
order?: string | ||
search?: string | ||
} | ||
|
||
//convert query object to query string to use in links | ||
const getQueryString = (query: QueryOptions) => { | ||
return Object.keys(query) | ||
.map((key) => `${key}=${query[key]}`) | ||
.join('&') | ||
} | ||
|
||
export const paginate = ( | ||
totalCount: number, | ||
relativeUrl: string, | ||
query: QueryOptions, | ||
defaultQuery?: Record<string, any> | ||
) => { | ||
//query.limit cannot be 0 or undefined | ||
if (!query.limit) throw new Error('Limit is required') | ||
let defaultQueryStr = '' | ||
if (defaultQuery) { | ||
//sorting entries to make sure the order is consistent and predictable during tests | ||
const sortedEntries = Object.entries(defaultQuery).sort(([keyA], [keyB]) => | ||
keyA.localeCompare(keyB) | ||
) | ||
//ignore keys with undefined values. Undefined values may occur when qury params are optional | ||
defaultQueryStr = sortedEntries.reduce((res, [key, value]) => { | ||
if (value !== undefined) { | ||
res += `${key}=${value}&` | ||
} | ||
return res | ||
}, '') | ||
} | ||
|
||
const metadata = {} as PaginatedMetadata | ||
metadata.page = query.page | ||
metadata.perPage = query.limit | ||
metadata.pageCount = Math.ceil(totalCount / query.limit) | ||
metadata.totalCount = totalCount | ||
|
||
//create links from relativeUrl , defalutQueryStr and query of type QueryOptions | ||
metadata.links = { | ||
self: `${relativeUrl}?${defaultQueryStr + getQueryString(query)}`, | ||
first: `${relativeUrl}?${defaultQueryStr + getQueryString({ ...query, page: 0 })}`, | ||
previous: | ||
query.page === 0 | ||
? null | ||
: `${relativeUrl}?${defaultQueryStr + getQueryString({ ...query, page: query.page - 1 })}`, | ||
next: | ||
query.page === metadata.pageCount - 1 | ||
? null | ||
: `${relativeUrl}?${defaultQueryStr + getQueryString({ ...query, page: query.page + 1 })}`, | ||
last: `${relativeUrl}?${defaultQueryStr + getQueryString({ ...query, page: metadata.pageCount - 1 })}` | ||
} | ||
|
||
return metadata | ||
} |