Skip to content

Commit

Permalink
feat(api): Create a paginate method (#379)
Browse files Browse the repository at this point in the history
  • Loading branch information
muntaxir4 authored and rajdip-b committed Jul 29, 2024
1 parent a64e48c commit 09576f1
Show file tree
Hide file tree
Showing 2 changed files with 168 additions and 0 deletions.
93 changes: 93 additions & 0 deletions apps/api/src/common/paginate.spec.ts
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'
)
})
})
75 changes: 75 additions & 0 deletions apps/api/src/common/paginate.ts
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
}

0 comments on commit 09576f1

Please sign in to comment.