feat: allow unpaginated request (#406)
This commit is contained in:
parent
2a3dc9573c
commit
0cd2c6afa1
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@ lib/
|
|||||||
coverage/
|
coverage/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
|
.history
|
||||||
|
@ -200,6 +200,7 @@ const paginateConfig: PaginateConfig<CatEntity> {
|
|||||||
* Type: number
|
* Type: number
|
||||||
* Default: 100
|
* Default: 100
|
||||||
* Description: The maximum amount of entities to return per page.
|
* 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,
|
maxLimit: 20,
|
||||||
|
|
||||||
|
@ -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[]]
|
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]
|
export type Column<T, D extends number = 2> = [D] extends [never]
|
||||||
? never
|
? never
|
||||||
: T extends Record<string, any>
|
: T extends Record<string, any>
|
||||||
@ -25,3 +26,6 @@ export type RelationColumn<T> = Extract<
|
|||||||
|
|
||||||
export type Order<T> = [Column<T>, 'ASC' | 'DESC']
|
export type Order<T> = [Column<T>, 'ASC' | 'DESC']
|
||||||
export type SortBy<T> = Order<T>[]
|
export type SortBy<T> = Order<T>[]
|
||||||
|
|
||||||
|
export const positiveNumberOrDefault = (value: number | undefined, defaultValue: number, minValue: 0 | 1 = 0) =>
|
||||||
|
value === undefined || value < minValue ? defaultValue : value
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
isOperator,
|
isOperator,
|
||||||
getFilterTokens,
|
getFilterTokens,
|
||||||
OperatorSymbolToFunction,
|
OperatorSymbolToFunction,
|
||||||
|
NO_PAGINATION,
|
||||||
} from './paginate'
|
} from './paginate'
|
||||||
import { PaginateQuery } from './decorator'
|
import { PaginateQuery } from './decorator'
|
||||||
import { HttpException } from '@nestjs/common'
|
import { HttpException } from '@nestjs/common'
|
||||||
@ -124,6 +125,68 @@ describe('paginate', () => {
|
|||||||
expect(result.data).toStrictEqual(cats.slice(0, 1))
|
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 () => {
|
it('should default to limit maxLimit, if more than maxLimit is given', async () => {
|
||||||
const config: PaginateConfig<CatEntity> = {
|
const config: PaginateConfig<CatEntity> = {
|
||||||
sortableColumns: ['id'],
|
sortableColumns: ['id'],
|
||||||
|
@ -21,7 +21,7 @@ import { ServiceUnavailableException, Logger } from '@nestjs/common'
|
|||||||
import { values, mapKeys } from 'lodash'
|
import { values, mapKeys } from 'lodash'
|
||||||
import { stringify } from 'querystring'
|
import { stringify } from 'querystring'
|
||||||
import { WherePredicateOperator } from 'typeorm/query-builder/WhereClause'
|
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')
|
const logger: Logger = new Logger('nestjs-paginate')
|
||||||
|
|
||||||
@ -171,16 +171,28 @@ function parseFilter<T>(query: PaginateQuery, config: PaginateConfig<T>) {
|
|||||||
return filter
|
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>(
|
export async function paginate<T extends ObjectLiteral>(
|
||||||
query: PaginateQuery,
|
query: PaginateQuery,
|
||||||
repo: Repository<T> | SelectQueryBuilder<T>,
|
repo: Repository<T> | SelectQueryBuilder<T>,
|
||||||
config: PaginateConfig<T>
|
config: PaginateConfig<T>
|
||||||
): Promise<Paginated<T>> {
|
): Promise<Paginated<T>> {
|
||||||
let page = query.page || 1
|
const page = positiveNumberOrDefault(query.page, 1, 1)
|
||||||
const limit = Math.min(query.limit || config.defaultLimit || 20, config.maxLimit || 100)
|
|
||||||
|
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 sortBy = [] as SortBy<T>
|
||||||
const searchBy: Column<T>[] = []
|
const searchBy: Column<T>[] = []
|
||||||
let path
|
let path: string
|
||||||
|
|
||||||
const r = new RegExp('^(?:[a-z+]+:)?//', 'i')
|
const r = new RegExp('^(?:[a-z+]+:)?//', 'i')
|
||||||
let queryOrigin = ''
|
let queryOrigin = ''
|
||||||
@ -234,19 +246,12 @@ export async function paginate<T extends ObjectLiteral>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (page < 1) page = 1
|
|
||||||
|
|
||||||
let [items, totalItems]: [T[], number] = [[], 0]
|
let [items, totalItems]: [T[], number] = [[], 0]
|
||||||
|
|
||||||
let queryBuilder: SelectQueryBuilder<T>
|
const queryBuilder = repo instanceof Repository ? repo.createQueryBuilder('e') : repo
|
||||||
|
|
||||||
if (repo instanceof Repository) {
|
if (isPaginated) {
|
||||||
queryBuilder = repo
|
queryBuilder.take(limit).skip((page - 1) * limit)
|
||||||
.createQueryBuilder('e')
|
|
||||||
.take(limit)
|
|
||||||
.skip((page - 1) * limit)
|
|
||||||
} else {
|
|
||||||
queryBuilder = repo.take(limit).skip((page - 1) * limit)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.relations?.length) {
|
if (config.relations?.length) {
|
||||||
@ -360,10 +365,11 @@ export async function paginate<T extends ObjectLiteral>(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isPaginated) {
|
||||||
;[items, totalItems] = await queryBuilder.getManyAndCount()
|
;[items, totalItems] = await queryBuilder.getManyAndCount()
|
||||||
|
} else {
|
||||||
let totalPages = totalItems / limit
|
items = await queryBuilder.getMany()
|
||||||
if (totalItems % limit) totalPages = Math.ceil(totalPages)
|
}
|
||||||
|
|
||||||
const sortByQuery = sortBy.map((order) => `&sortBy=${order.join(':')}`).join('')
|
const sortByQuery = sortBy.map((order) => `&sortBy=${order.join(':')}`).join('')
|
||||||
const searchQuery = query.search ? `&search=${query.search}` : ''
|
const searchQuery = query.search ? `&search=${query.search}` : ''
|
||||||
@ -385,13 +391,15 @@ export async function paginate<T extends ObjectLiteral>(
|
|||||||
|
|
||||||
const buildLink = (p: number): string => path + '?page=' + p + options
|
const buildLink = (p: number): string => path + '?page=' + p + options
|
||||||
|
|
||||||
|
const totalPages = isPaginated ? Math.ceil(totalItems / limit) : 1
|
||||||
|
|
||||||
const results: Paginated<T> = {
|
const results: Paginated<T> = {
|
||||||
data: items,
|
data: items,
|
||||||
meta: {
|
meta: {
|
||||||
itemsPerPage: limit,
|
itemsPerPage: isPaginated ? limit : items.length,
|
||||||
totalItems,
|
totalItems,
|
||||||
currentPage: page,
|
currentPage: page,
|
||||||
totalPages: totalPages,
|
totalPages,
|
||||||
sortBy,
|
sortBy,
|
||||||
search: query.search,
|
search: query.search,
|
||||||
searchBy: query.search ? searchBy : undefined,
|
searchBy: query.search ? searchBy : undefined,
|
||||||
|
Loading…
Reference in New Issue
Block a user