diff --git a/.gitignore b/.gitignore index 73da952e..6d767397 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ lib/ coverage/ .DS_Store npm-debug.log +.history diff --git a/README.md b/README.md index 260b0ff5..41d79659 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,7 @@ const paginateConfig: PaginateConfig { * 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, diff --git a/src/helper.ts b/src/helper.ts index 63244520..9ad70afe 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -2,6 +2,7 @@ type Join = 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 = [D] extends [never] ? never : T extends Record @@ -25,3 +26,6 @@ export type RelationColumn = Extract< export type Order = [Column, 'ASC' | 'DESC'] export type SortBy = Order[] + +export const positiveNumberOrDefault = (value: number | undefined, defaultValue: number, minValue: 0 | 1 = 0) => + value === undefined || value < minValue ? defaultValue : value diff --git a/src/paginate.spec.ts b/src/paginate.spec.ts index 435a077b..b452414e 100644 --- a/src/paginate.spec.ts +++ b/src/paginate.spec.ts @@ -7,6 +7,7 @@ import { isOperator, getFilterTokens, OperatorSymbolToFunction, + NO_PAGINATION, } from './paginate' import { PaginateQuery } from './decorator' import { HttpException } from '@nestjs/common' @@ -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 = { + sortableColumns: ['id'], + maxLimit: 1, + defaultLimit: 1, + } + const query: PaginateQuery = { + path: '', + limit: NO_PAGINATION, + } + + const result = await paginate(query, catRepo, config) + expect(result.data).toStrictEqual(cats.slice(0, 1)) + }) + + it('should return all cats', async () => { + const config: PaginateConfig = { + sortableColumns: ['id'], + maxLimit: NO_PAGINATION, + defaultLimit: 1, + } + const query: PaginateQuery = { + path: '', + limit: NO_PAGINATION, + } + + const result = await paginate(query, catRepo, config) + + expect(result.data).toStrictEqual(cats) + }) + + it('should limit to defaultLimit, if limit is negative', async () => { + const config: PaginateConfig = { + sortableColumns: ['id'], + maxLimit: NO_PAGINATION, + defaultLimit: 1, + } + const query: PaginateQuery = { + path: '', + limit: -1, + } + + const result = await paginate(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 = { + sortableColumns: ['id'], + maxLimit: NO_PAGINATION, + defaultLimit: 1, + } + const query: PaginateQuery = { + path: '', + } + + const result = await paginate(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 = { sortableColumns: ['id'], diff --git a/src/paginate.ts b/src/paginate.ts index f18d8185..e13ed030 100644 --- a/src/paginate.ts +++ b/src/paginate.ts @@ -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') @@ -171,16 +171,28 @@ function parseFilter(query: PaginateQuery, config: PaginateConfig) { return filter } +export const DEFAULT_MAX_LIMIT = 100 +export const DEFAULT_LIMIT = 20 +export const NO_PAGINATION = 0 + export async function paginate( query: PaginateQuery, repo: Repository | SelectQueryBuilder, config: PaginateConfig ): Promise> { - 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 const searchBy: Column[] = [] - let path + let path: string const r = new RegExp('^(?:[a-z+]+:)?//', 'i') let queryOrigin = '' @@ -234,19 +246,12 @@ export async function paginate( } } - if (page < 1) page = 1 - let [items, totalItems]: [T[], number] = [[], 0] - let queryBuilder: SelectQueryBuilder + 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) { @@ -360,10 +365,11 @@ export async function paginate( ) } - ;[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}` : '' @@ -385,13 +391,15 @@ export async function paginate( const buildLink = (p: number): string => path + '?page=' + p + options + const totalPages = isPaginated ? Math.ceil(totalItems / limit) : 1 + const results: Paginated = { 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,