diff --git a/src/index.ts b/src/index.ts index 4bd2038..258bdfe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,2 @@ export * from './decorator' export * from './paginate' -export { FilterOperator, FilterComparator } from './operator' diff --git a/src/operator.ts b/src/operator.ts deleted file mode 100644 index fc827fd..0000000 --- a/src/operator.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { values } from 'lodash' -import { - Equal, - FindOperator, - In, - MoreThan, - MoreThanOrEqual, - IsNull, - LessThan, - LessThanOrEqual, - Between, - ILike, - Not, -} from 'typeorm' - -export enum FilterOperator { - EQ = '$eq', - GT = '$gt', - GTE = '$gte', - IN = '$in', - NULL = '$null', - LT = '$lt', - LTE = '$lte', - BTW = '$btw', - ILIKE = '$ilike', - SW = '$sw', -} - -export function isOperator(value: unknown): value is FilterOperator { - return values(FilterOperator).includes(value as any) -} - -export enum FilterSuffix { - NOT = '$not', -} - -export function isSuffix(value: unknown): value is FilterSuffix { - return values(FilterSuffix).includes(value as any) -} - -export enum FilterComparator { - AND = '$and', - OR = '$or', -} - -export function isComparator(value: unknown): value is FilterComparator { - return values(FilterComparator).includes(value as any) -} - -export const OperatorSymbolToFunction = new Map< - FilterOperator | FilterSuffix, - (...args: any[]) => FindOperator ->([ - [FilterOperator.EQ, Equal], - [FilterOperator.GT, MoreThan], - [FilterOperator.GTE, MoreThanOrEqual], - [FilterOperator.IN, In], - [FilterOperator.NULL, IsNull], - [FilterOperator.LT, LessThan], - [FilterOperator.LTE, LessThanOrEqual], - [FilterOperator.BTW, Between], - [FilterOperator.ILIKE, ILike], - [FilterSuffix.NOT, Not], - [FilterOperator.SW, ILike], -]) diff --git a/src/filter.ts b/src/paginate/filter.ts similarity index 83% rename from src/filter.ts rename to src/paginate/filter.ts index a24acdf..662234e 100644 --- a/src/filter.ts +++ b/src/paginate/filter.ts @@ -1,6 +1,21 @@ -import { Brackets, FindOperator, SelectQueryBuilder } from 'typeorm' +import { values } from 'lodash' +import { + Brackets, + Equal, + FindOperator, + In, + MoreThan, + MoreThanOrEqual, + IsNull, + LessThan, + LessThanOrEqual, + Between, + ILike, + Not, + SelectQueryBuilder, +} from 'typeorm' import { WherePredicateOperator } from 'typeorm/query-builder/WhereClause' -import { PaginateQuery } from './decorator' +import { PaginateQuery } from '../decorator' import { checkIsEmbedded, checkIsRelation, @@ -8,16 +23,57 @@ import { fixColumnAlias, getPropertiesByColumnName, } from './helper' -import { - FilterComparator, - FilterOperator, - FilterSuffix, - isComparator, - isOperator, - isSuffix, - OperatorSymbolToFunction, -} from './operator' -import { PaginateConfig } from './paginate' + +export enum FilterOperator { + EQ = '$eq', + GT = '$gt', + GTE = '$gte', + IN = '$in', + NULL = '$null', + LT = '$lt', + LTE = '$lte', + BTW = '$btw', + ILIKE = '$ilike', + SW = '$sw', +} + +export function isOperator(value: unknown): value is FilterOperator { + return values(FilterOperator).includes(value as any) +} + +export enum FilterSuffix { + NOT = '$not', +} + +export function isSuffix(value: unknown): value is FilterSuffix { + return values(FilterSuffix).includes(value as any) +} + +export enum FilterComparator { + AND = '$and', + OR = '$or', +} + +export function isComparator(value: unknown): value is FilterComparator { + return values(FilterComparator).includes(value as any) +} + +export const OperatorSymbolToFunction = new Map< + FilterOperator | FilterSuffix, + (...args: any[]) => FindOperator +>([ + [FilterOperator.EQ, Equal], + [FilterOperator.GT, MoreThan], + [FilterOperator.GTE, MoreThanOrEqual], + [FilterOperator.IN, In], + [FilterOperator.NULL, IsNull], + [FilterOperator.LT, LessThan], + [FilterOperator.LTE, LessThanOrEqual], + [FilterOperator.BTW, Between], + [FilterOperator.ILIKE, ILike], + [FilterSuffix.NOT, Not], + [FilterOperator.SW, ILike], +]) type Filter = { comparator: FilterComparator; findOperator: FindOperator } type ColumnsFilters = { [columnName: string]: Filter[] } @@ -168,7 +224,7 @@ export function getFilterTokens(raw?: string): FilterToken | null { export function parseFilter( query: PaginateQuery, - filterableColumns?: PaginateConfig['filterableColumns'] + filterableColumns?: { [column: string]: (FilterOperator | FilterSuffix)[] } ): ColumnsFilters { const filter: ColumnsFilters = {} if (!filterableColumns || !query.filter) { @@ -237,7 +293,7 @@ export function parseFilter( export function addFilter( qb: SelectQueryBuilder, query: PaginateQuery, - filterableColumns?: PaginateConfig['filterableColumns'] + filterableColumns?: { [column: string]: (FilterOperator | FilterSuffix)[] } ): SelectQueryBuilder { const filter = parseFilter(query, filterableColumns) return qb.andWhere( diff --git a/src/helper.ts b/src/paginate/helper.ts similarity index 97% rename from src/helper.ts rename to src/paginate/helper.ts index ae19f81..0f0c294 100644 --- a/src/helper.ts +++ b/src/paginate/helper.ts @@ -34,6 +34,10 @@ export type RelationColumn = Extract< export type Order = [Column, 'ASC' | 'DESC'] export type SortBy = Order[] +export function isEntityKey(entityColumns: Column[], column: string): column is Column { + return !!entityColumns.find((c) => c === column) +} + 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/index.spec.ts similarity index 99% rename from src/paginate.spec.ts rename to src/paginate/index.spec.ts index de4c273..4deb8b5 100644 --- a/src/paginate.spec.ts +++ b/src/paginate/index.spec.ts @@ -1,21 +1,21 @@ import { Repository, In, DataSource, TypeORMError } from 'typeorm' -import { Paginated, paginate, PaginateConfig, NO_PAGINATION } from './paginate' -import { PaginateQuery } from './decorator' +import { Paginated, paginate, PaginateConfig, NO_PAGINATION } from '.' +import { PaginateQuery } from '../decorator' import { HttpException } from '@nestjs/common' -import { CatEntity } from './__tests__/cat.entity' -import { CatToyEntity } from './__tests__/cat-toy.entity' -import { CatHomeEntity } from './__tests__/cat-home.entity' -import { CatHomePillowEntity } from './__tests__/cat-home-pillow.entity' +import { CatEntity } from '../__tests__/cat.entity' +import { CatToyEntity } from '../__tests__/cat-toy.entity' +import { CatHomeEntity } from '../__tests__/cat-home.entity' +import { CatHomePillowEntity } from '../__tests__/cat-home-pillow.entity' import { clone } from 'lodash' import { + getFilterTokens, FilterComparator, FilterOperator, FilterSuffix, isOperator, isSuffix, OperatorSymbolToFunction, -} from './operator' -import { getFilterTokens } from './filter' +} from './filter' describe('paginate', () => { let dataSource: DataSource diff --git a/src/paginate.ts b/src/paginate/index.ts similarity index 95% rename from src/paginate.ts rename to src/paginate/index.ts index 2f3565e..139e7cf 100644 --- a/src/paginate.ts +++ b/src/paginate/index.ts @@ -7,7 +7,7 @@ import { ObjectLiteral, FindOptionsUtils, } from 'typeorm' -import { PaginateQuery } from './decorator' +import { PaginateQuery } from '../decorator' import { ServiceUnavailableException, Logger } from '@nestjs/common' import { mapKeys } from 'lodash' import { stringify } from 'querystring' @@ -25,12 +25,14 @@ import { SortBy, hasColumnWithPropertyPath, includesAllPrimaryKeyColumns, + isEntityKey, } from './helper' -import { FilterOperator, FilterSuffix } from './operator' -import { addFilter } from './filter' +import { addFilter, FilterOperator, FilterSuffix } from './filter' const logger: Logger = new Logger('nestjs-paginate') +export { FilterOperator, FilterSuffix } + export class Paginated { data: T[] meta: { @@ -121,10 +123,6 @@ export async function paginate( path = queryOrigin + queryPath } - function isEntityKey(entityColumns: Column[], column: string): column is Column { - return !!entityColumns.find((c) => c === column) - } - if (config.sortableColumns.length < 1) { logger.debug("Missing required 'sortableColumns' config.") throw new ServiceUnavailableException() @@ -142,18 +140,6 @@ export async function paginate( sortBy.push(...(config.defaultSortBy || [[config.sortableColumns[0], 'ASC']])) } - if (config.searchableColumns) { - if (query.searchBy) { - for (const column of query.searchBy) { - if (isEntityKey(config.searchableColumns, column)) { - searchBy.push(column) - } - } - } else { - searchBy.push(...config.searchableColumns) - } - } - let [items, totalItems]: [T[], number] = [[], 0] const queryBuilder = repo instanceof Repository ? repo.createQueryBuilder('__root') : repo @@ -165,10 +151,12 @@ export async function paginate( } if (isPaginated) { - // Switch from take and skip to limit and offset - // due to this problem https://github.com/typeorm/typeorm/issues/5670 - // (anyway this creates more clean query without double distinct) - // queryBuilder.limit(limit).offset((page - 1) * limit) + // Allow user to choose between limit/offset and take/skip. + // However, using limit/offset can return unexpected results. + // For more information see: + // [#477](https://github.com/ppetzold/nestjs-paginate/issues/477) + // [#4742](https://github.com/typeorm/typeorm/issues/4742) + // [#5670](https://github.com/typeorm/typeorm/issues/5670) if (paginationType === PaginationType.LIMIT_AND_OFFSET) { queryBuilder.limit(limit).offset((page - 1) * limit) } else { @@ -248,6 +236,18 @@ export async function paginate( queryBuilder.withDeleted() } + if (config.searchableColumns) { + if (query.searchBy) { + for (const column of query.searchBy) { + if (isEntityKey(config.searchableColumns, column)) { + searchBy.push(column) + } + } + } else { + searchBy.push(...config.searchableColumns) + } + } + if (query.search && searchBy.length) { queryBuilder.andWhere( new Brackets((qb: SelectQueryBuilder) => {