Skip to content

Commit

Permalink
feat: allow unpaginated request (ppetzold#406)
Browse files Browse the repository at this point in the history
  • Loading branch information
xMase authored Dec 13, 2022
1 parent 4346717 commit 7dd8b3f
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 20 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ lib/
coverage/
.DS_Store
npm-debug.log
.history
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ const paginateConfig: PaginateConfig<CatEntity> {
* Type: number
* Default: 100
* Description: The maximum amount of entities to return per page.
* Set it to 0, in conjunction with limit=0 on query param, to disable pagination.
*/
maxLimit: 20,

Expand Down
4 changes: 4 additions & 0 deletions src/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ type Join<K, P> = K extends string ? (P extends string ? `${K}${'' extends P ? '

type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...0[]]

// TODO: puts some comments here, in this ternary of doom
export type Column<T, D extends number = 2> = [D] extends [never]
? never
: T extends Record<string, any>
Expand All @@ -25,3 +26,6 @@ export type RelationColumn<T> = Extract<

export type Order<T> = [Column<T>, 'ASC' | 'DESC']
export type SortBy<T> = Order<T>[]

export const positiveNumberOrDefault = (value: number | undefined, defaultValue: number, minValue: 0 | 1 = 0) =>
value === undefined || value < minValue ? defaultValue : value
63 changes: 63 additions & 0 deletions src/paginate.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
isOperator,
getFilterTokens,
OperatorSymbolToFunction,
NO_PAGINATION,
} from './paginate'
import { PaginateQuery } from './decorator'
import { HttpException } from '@nestjs/common'
Expand Down Expand Up @@ -124,6 +125,68 @@ describe('paginate', () => {
expect(result.data).toStrictEqual(cats.slice(0, 1))
})

it('should default to limit maxLimit, if maxLimit is not 0', async () => {
const config: PaginateConfig<CatEntity> = {
sortableColumns: ['id'],
maxLimit: 1,
defaultLimit: 1,
}
const query: PaginateQuery = {
path: '',
limit: NO_PAGINATION,
}

const result = await paginate<CatEntity>(query, catRepo, config)
expect(result.data).toStrictEqual(cats.slice(0, 1))
})

it('should return all cats', async () => {
const config: PaginateConfig<CatEntity> = {
sortableColumns: ['id'],
maxLimit: NO_PAGINATION,
defaultLimit: 1,
}
const query: PaginateQuery = {
path: '',
limit: NO_PAGINATION,
}

const result = await paginate<CatEntity>(query, catRepo, config)

expect(result.data).toStrictEqual(cats)
})

it('should limit to defaultLimit, if limit is negative', async () => {
const config: PaginateConfig<CatEntity> = {
sortableColumns: ['id'],
maxLimit: NO_PAGINATION,
defaultLimit: 1,
}
const query: PaginateQuery = {
path: '',
limit: -1,
}

const result = await paginate<CatEntity>(query, catRepo, config)

expect(result.data).toStrictEqual(cats.slice(0, 1))
})

it('should default to limit defaultLimit, if maxLimit is 0', async () => {
const config: PaginateConfig<CatEntity> = {
sortableColumns: ['id'],
maxLimit: NO_PAGINATION,
defaultLimit: 1,
}
const query: PaginateQuery = {
path: '',
}

const result = await paginate<CatEntity>(query, catRepo, config)

expect(result.data).toStrictEqual(cats.slice(0, 1))
})

it('should default to limit maxLimit, if more than maxLimit is given', async () => {
const config: PaginateConfig<CatEntity> = {
sortableColumns: ['id'],
Expand Down
48 changes: 28 additions & 20 deletions src/paginate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { ServiceUnavailableException, Logger } from '@nestjs/common'
import { values, mapKeys } from 'lodash'
import { stringify } from 'querystring'
import { WherePredicateOperator } from 'typeorm/query-builder/WhereClause'
import { Column, Order, RelationColumn, SortBy } from './helper'
import { Column, Order, positiveNumberOrDefault, RelationColumn, SortBy } from './helper'

const logger: Logger = new Logger('nestjs-paginate')

Expand Down Expand Up @@ -171,16 +171,28 @@ function parseFilter<T>(query: PaginateQuery, config: PaginateConfig<T>) {
return filter
}

export const DEFAULT_MAX_LIMIT = 100
export const DEFAULT_LIMIT = 20
export const NO_PAGINATION = 0

export async function paginate<T extends ObjectLiteral>(
query: PaginateQuery,
repo: Repository<T> | SelectQueryBuilder<T>,
config: PaginateConfig<T>
): Promise<Paginated<T>> {
let page = query.page || 1
const limit = Math.min(query.limit || config.defaultLimit || 20, config.maxLimit || 100)
const page = positiveNumberOrDefault(query.page, 1, 1)

const defaultLimit = config.defaultLimit || DEFAULT_LIMIT
const maxLimit = positiveNumberOrDefault(config.maxLimit, DEFAULT_MAX_LIMIT)
const queryLimit = positiveNumberOrDefault(query.limit, defaultLimit)

const isPaginated = !(queryLimit === NO_PAGINATION && maxLimit === NO_PAGINATION)

const limit = isPaginated ? Math.min(queryLimit || defaultLimit, maxLimit || DEFAULT_MAX_LIMIT) : NO_PAGINATION

const sortBy = [] as SortBy<T>
const searchBy: Column<T>[] = []
let path
let path: string

const r = new RegExp('^(?:[a-z+]+:)?//', 'i')
let queryOrigin = ''
Expand Down Expand Up @@ -234,19 +246,12 @@ export async function paginate<T extends ObjectLiteral>(
}
}

if (page < 1) page = 1

let [items, totalItems]: [T[], number] = [[], 0]

let queryBuilder: SelectQueryBuilder<T>
const queryBuilder = repo instanceof Repository ? repo.createQueryBuilder('e') : repo

if (repo instanceof Repository) {
queryBuilder = repo
.createQueryBuilder('e')
.take(limit)
.skip((page - 1) * limit)
} else {
queryBuilder = repo.take(limit).skip((page - 1) * limit)
if (isPaginated) {
queryBuilder.take(limit).skip((page - 1) * limit)
}

if (config.relations?.length) {
Expand Down Expand Up @@ -360,10 +365,11 @@ export async function paginate<T extends ObjectLiteral>(
)
}

;[items, totalItems] = await queryBuilder.getManyAndCount()

let totalPages = totalItems / limit
if (totalItems % limit) totalPages = Math.ceil(totalPages)
if (isPaginated) {
;[items, totalItems] = await queryBuilder.getManyAndCount()
} else {
items = await queryBuilder.getMany()
}

const sortByQuery = sortBy.map((order) => `&sortBy=${order.join(':')}`).join('')
const searchQuery = query.search ? `&search=${query.search}` : ''
Expand All @@ -385,13 +391,15 @@ export async function paginate<T extends ObjectLiteral>(

const buildLink = (p: number): string => path + '?page=' + p + options

const totalPages = isPaginated ? Math.ceil(totalItems / limit) : 1

const results: Paginated<T> = {
data: items,
meta: {
itemsPerPage: limit,
itemsPerPage: isPaginated ? limit : items.length,
totalItems,
currentPage: page,
totalPages: totalPages,
totalPages,
sortBy,
search: query.search,
searchBy: query.search ? searchBy : undefined,
Expand Down

0 comments on commit 7dd8b3f

Please sign in to comment.