feat: multi filter with $and/$or comperator (#457)
This commit is contained in:
parent
38780469e0
commit
70c24b00ee
26
README.md
26
README.md
@ -301,7 +301,7 @@ const config: PaginateConfig<CatEntity> = {
|
|||||||
const result = await paginate<CatEntity>(query, catRepo, config)
|
const result = await paginate<CatEntity>(query, catRepo, config)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Filters
|
## Single Filters
|
||||||
|
|
||||||
Filter operators must be whitelisted per column in `PaginateConfig`.
|
Filter operators must be whitelisted per column in `PaginateConfig`.
|
||||||
|
|
||||||
@ -323,6 +323,30 @@ Filter operators must be whitelisted per column in `PaginateConfig`.
|
|||||||
|
|
||||||
`?filter.createdAt=$btw:2022-02-02,2022-02-10` where column `createdAt` is between the dates `2022-02-02` and `2022-02-10`
|
`?filter.createdAt=$btw:2022-02-02,2022-02-10` where column `createdAt` is between the dates `2022-02-02` and `2022-02-10`
|
||||||
|
|
||||||
|
## Multi Filters
|
||||||
|
|
||||||
|
Multi filters are filters that can be applied to a single column with a comparator. As for single filters, multi filters must be whitelisted per column in `PaginateConfig`.
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
`?filter.id=$gt:3&filter.id=$lt:5` where column `id` is greater than `3` **and** less than `5`
|
||||||
|
|
||||||
|
`?filter.id=$gt:3&filter.id=$or$lt:5` where column `id` is greater than `3` **or** less than `5`
|
||||||
|
|
||||||
|
`?filter.id=$gt:3&filter.id=$and$lt:5&filter.id=$or$eq:7` where column `id` is greater than `3` **and** less than `5` **or** equal to `7`
|
||||||
|
|
||||||
|
**Note:** the `and` operators are not required. The above example is equivalent to:
|
||||||
|
|
||||||
|
`?filter.id=$gt:3&filter.id=$lt:5&filter.id=$or$eq:7`
|
||||||
|
|
||||||
|
**Note:** the first comparator on the the first filter is ingnored becouse the filters are grouped by the column name and chained with an `and` to other filters.
|
||||||
|
|
||||||
|
`...&filter.id=5&filter.id=$or:7&filter.name=Milo&...`
|
||||||
|
|
||||||
|
is resolved to:
|
||||||
|
|
||||||
|
`WHERE ... AND (id = 5 OR id = 7) AND name = 'Milo' AND ...`
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
The package does not report error reasons in the response bodies. They are instead
|
The package does not report error reasons in the response bodies. They are instead
|
||||||
|
192
src/filter.ts
192
src/filter.ts
@ -1,25 +1,31 @@
|
|||||||
import { FindOperator, SelectQueryBuilder } from 'typeorm'
|
import { Brackets, FindOperator, SelectQueryBuilder } from 'typeorm'
|
||||||
import { WherePredicateOperator } from 'typeorm/query-builder/WhereClause'
|
import { WherePredicateOperator } from 'typeorm/query-builder/WhereClause'
|
||||||
|
import { PaginateQuery } from './decorator'
|
||||||
import { checkIsRelation, extractVirtualProperty, fixColumnAlias, getPropertiesByColumnName } from './helper'
|
import { checkIsRelation, extractVirtualProperty, fixColumnAlias, getPropertiesByColumnName } from './helper'
|
||||||
|
import {
|
||||||
|
FilterComparator,
|
||||||
|
FilterOperator,
|
||||||
|
FilterSuffix,
|
||||||
|
isComparator,
|
||||||
|
isOperator,
|
||||||
|
isSuffix,
|
||||||
|
OperatorSymbolToFunction,
|
||||||
|
} from './operator'
|
||||||
|
import { PaginateConfig } from './paginate'
|
||||||
|
|
||||||
export type Filter = { [columnName: string]: FindOperator<string> }
|
type Filter = { comparator: FilterComparator; findOperator: FindOperator<string> }
|
||||||
|
type ColumnsFilters = { [columnName: string]: Filter[] }
|
||||||
|
|
||||||
export function generatePredicateCondition(
|
export interface FilterToken {
|
||||||
qb: SelectQueryBuilder<unknown>,
|
comparator: FilterComparator
|
||||||
column: string,
|
suffix?: FilterSuffix
|
||||||
filter: Filter,
|
operator: FilterOperator
|
||||||
alias: string,
|
value: string
|
||||||
isVirtualProperty = false
|
|
||||||
): WherePredicateOperator {
|
|
||||||
return qb['getWherePredicateCondition'](
|
|
||||||
isVirtualProperty ? column : alias,
|
|
||||||
filter[column]
|
|
||||||
) as WherePredicateOperator
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function is used to fix the query parameters when using relation, embeded or virtual properties
|
// This function is used to fix the query parameters when using relation, embeded or virtual properties
|
||||||
// It will replace the column name with the alias name and return the new parameters
|
// It will replace the column name with the alias name and return the new parameters
|
||||||
function fixQueryParam(
|
export function fixQueryParam(
|
||||||
alias: string,
|
alias: string,
|
||||||
column: string,
|
column: string,
|
||||||
filter: Filter,
|
filter: Filter,
|
||||||
@ -41,8 +47,8 @@ function fixQueryParam(
|
|||||||
case 'between':
|
case 'between':
|
||||||
condition_params = [alias, `:${column}_from`, `:${column}_to`]
|
condition_params = [alias, `:${column}_from`, `:${column}_to`]
|
||||||
params = {
|
params = {
|
||||||
[column + '_from']: filter[column].value[0],
|
[column + '_from']: filter.findOperator.value[0],
|
||||||
[column + '_to']: filter[column].value[1],
|
[column + '_to']: filter.findOperator.value[1],
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 'in':
|
case 'in':
|
||||||
@ -72,14 +78,160 @@ function fixQueryParam(
|
|||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addWhereCondition(qb: SelectQueryBuilder<unknown>, column: string, filter: Filter) {
|
export function generatePredicateCondition(
|
||||||
|
qb: SelectQueryBuilder<unknown>,
|
||||||
|
column: string,
|
||||||
|
filter: Filter,
|
||||||
|
alias: string,
|
||||||
|
isVirtualProperty = false
|
||||||
|
): WherePredicateOperator {
|
||||||
|
return qb['getWherePredicateCondition'](
|
||||||
|
isVirtualProperty ? column : alias,
|
||||||
|
filter.findOperator
|
||||||
|
) as WherePredicateOperator
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addWhereCondition<T>(qb: SelectQueryBuilder<T>, column: string, filter: ColumnsFilters) {
|
||||||
const columnProperties = getPropertiesByColumnName(column)
|
const columnProperties = getPropertiesByColumnName(column)
|
||||||
const { isVirtualProperty, query: virtualQuery } = extractVirtualProperty(qb, columnProperties)
|
const { isVirtualProperty, query: virtualQuery } = extractVirtualProperty(qb, columnProperties)
|
||||||
const isRelation = checkIsRelation(qb, columnProperties.propertyPath)
|
const isRelation = checkIsRelation(qb, columnProperties.propertyPath)
|
||||||
|
filter[column].forEach((columnFilter: Filter, index: number) => {
|
||||||
|
const columnNamePerIteration = `${column}${index}`
|
||||||
const alias = fixColumnAlias(columnProperties, qb.alias, isRelation, isVirtualProperty, virtualQuery)
|
const alias = fixColumnAlias(columnProperties, qb.alias, isRelation, isVirtualProperty, virtualQuery)
|
||||||
const condition = generatePredicateCondition(qb, column, filter, alias, isVirtualProperty)
|
const condition = generatePredicateCondition(qb, column, columnFilter, alias, isVirtualProperty)
|
||||||
const parameters = fixQueryParam(alias, column, filter, condition, {
|
const parameters = fixQueryParam(alias, columnNamePerIteration, columnFilter, condition, {
|
||||||
[column]: filter[column].value,
|
[columnNamePerIteration]: columnFilter.findOperator.value,
|
||||||
})
|
})
|
||||||
|
if (columnFilter.comparator === FilterComparator.OR) {
|
||||||
|
qb.orWhere(qb['createWhereConditionExpression'](condition), parameters)
|
||||||
|
} else {
|
||||||
qb.andWhere(qb['createWhereConditionExpression'](condition), parameters)
|
qb.andWhere(qb['createWhereConditionExpression'](condition), parameters)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFilterTokens(raw?: string): FilterToken | null {
|
||||||
|
if (raw === undefined || raw === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const token: FilterToken = {
|
||||||
|
comparator: FilterComparator.AND,
|
||||||
|
suffix: undefined,
|
||||||
|
operator: FilterOperator.EQ,
|
||||||
|
value: raw,
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_OPERTATOR = 4 // max 4 operator es: $and:$not:$eq:$null
|
||||||
|
const OPERAND_SEPARATOR = ':'
|
||||||
|
|
||||||
|
const matches = raw.split(OPERAND_SEPARATOR)
|
||||||
|
const maxOperandCount = matches.length > MAX_OPERTATOR ? MAX_OPERTATOR : matches.length
|
||||||
|
const notValue: (FilterOperator | FilterSuffix | FilterComparator)[] = []
|
||||||
|
|
||||||
|
for (let i = 0; i < maxOperandCount; i++) {
|
||||||
|
const match = matches[i]
|
||||||
|
if (isComparator(match)) {
|
||||||
|
token.comparator = match
|
||||||
|
} else if (isSuffix(match)) {
|
||||||
|
token.suffix = match
|
||||||
|
} else if (isOperator(match)) {
|
||||||
|
token.operator = match
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
notValue.push(match)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notValue.length) {
|
||||||
|
token.value =
|
||||||
|
token.operator === FilterOperator.NULL
|
||||||
|
? undefined
|
||||||
|
: raw.replace(`${notValue.join(OPERAND_SEPARATOR)}${OPERAND_SEPARATOR}`, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseFilter<T>(
|
||||||
|
query: PaginateQuery,
|
||||||
|
filterableColumns?: PaginateConfig<T>['filterableColumns']
|
||||||
|
): ColumnsFilters {
|
||||||
|
const filter: ColumnsFilters = {}
|
||||||
|
if (!filterableColumns || !query.filter) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
for (const column of Object.keys(query.filter)) {
|
||||||
|
if (!(column in filterableColumns)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const allowedOperators = filterableColumns[column]
|
||||||
|
const input = query.filter[column]
|
||||||
|
const statements = !Array.isArray(input) ? [input] : input
|
||||||
|
for (const raw of statements) {
|
||||||
|
const token = getFilterTokens(raw)
|
||||||
|
if (
|
||||||
|
!token ||
|
||||||
|
!(
|
||||||
|
allowedOperators.includes(token.operator) ||
|
||||||
|
(token.suffix === FilterSuffix.NOT &&
|
||||||
|
allowedOperators.includes(token.suffix) &&
|
||||||
|
token.operator === FilterOperator.EQ) ||
|
||||||
|
(token.suffix &&
|
||||||
|
allowedOperators.includes(token.suffix) &&
|
||||||
|
allowedOperators.includes(token.operator))
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const params: (typeof filter)[0][0] = {
|
||||||
|
comparator: token.comparator,
|
||||||
|
findOperator: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (token.operator) {
|
||||||
|
case FilterOperator.BTW:
|
||||||
|
params.findOperator = OperatorSymbolToFunction.get(token.operator)(...token.value.split(','))
|
||||||
|
break
|
||||||
|
case FilterOperator.IN:
|
||||||
|
params.findOperator = OperatorSymbolToFunction.get(token.operator)(token.value.split(','))
|
||||||
|
break
|
||||||
|
case FilterOperator.ILIKE:
|
||||||
|
params.findOperator = OperatorSymbolToFunction.get(token.operator)(`%${token.value}%`)
|
||||||
|
break
|
||||||
|
case FilterOperator.SW:
|
||||||
|
params.findOperator = OperatorSymbolToFunction.get(token.operator)(`${token.value}%`)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
params.findOperator = OperatorSymbolToFunction.get(token.operator)(token.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
filter[column] = [...(filter[column] || []), params]
|
||||||
|
|
||||||
|
if (token.suffix) {
|
||||||
|
const lastFilterElement = filter[column].length - 1
|
||||||
|
filter[column][lastFilterElement].findOperator = OperatorSymbolToFunction.get(token.suffix)(
|
||||||
|
filter[column][lastFilterElement].findOperator
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addFilter<T>(
|
||||||
|
qb: SelectQueryBuilder<T>,
|
||||||
|
query: PaginateQuery,
|
||||||
|
filterableColumns?: PaginateConfig<T>['filterableColumns']
|
||||||
|
): SelectQueryBuilder<T> {
|
||||||
|
const filter = parseFilter(query, filterableColumns)
|
||||||
|
return qb.andWhere(
|
||||||
|
new Brackets((qb: SelectQueryBuilder<T>) => {
|
||||||
|
for (const column in filter) {
|
||||||
|
addWhereCondition(qb, column, filter)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -33,7 +33,7 @@ export type SortBy<T> = Order<T>[]
|
|||||||
export const positiveNumberOrDefault = (value: number | undefined, defaultValue: number, minValue: 0 | 1 = 0) =>
|
export const positiveNumberOrDefault = (value: number | undefined, defaultValue: number, minValue: 0 | 1 = 0) =>
|
||||||
value === undefined || value < minValue ? defaultValue : value
|
value === undefined || value < minValue ? defaultValue : value
|
||||||
|
|
||||||
type ColumnProperties = { propertyPath?: string; propertyName: string }
|
export type ColumnProperties = { propertyPath?: string; propertyName: string }
|
||||||
|
|
||||||
export function getPropertiesByColumnName(column: string): ColumnProperties {
|
export function getPropertiesByColumnName(column: string): ColumnProperties {
|
||||||
const propertyPath = column.split('.')
|
const propertyPath = column.split('.')
|
||||||
|
65
src/operator.ts
Normal file
65
src/operator.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
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<string>
|
||||||
|
>([
|
||||||
|
[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],
|
||||||
|
])
|
@ -1,20 +1,20 @@
|
|||||||
import { Repository, In, DataSource } from 'typeorm'
|
import { Repository, In, DataSource } from 'typeorm'
|
||||||
import {
|
import { Paginated, paginate, PaginateConfig, NO_PAGINATION } from './paginate'
|
||||||
Paginated,
|
|
||||||
paginate,
|
|
||||||
PaginateConfig,
|
|
||||||
FilterOperator,
|
|
||||||
isOperator,
|
|
||||||
getFilterTokens,
|
|
||||||
OperatorSymbolToFunction,
|
|
||||||
NO_PAGINATION,
|
|
||||||
} from './paginate'
|
|
||||||
import { PaginateQuery } from './decorator'
|
import { PaginateQuery } from './decorator'
|
||||||
import { HttpException } from '@nestjs/common'
|
import { HttpException } from '@nestjs/common'
|
||||||
import { CatEntity } from './__tests__/cat.entity'
|
import { CatEntity } from './__tests__/cat.entity'
|
||||||
import { CatToyEntity } from './__tests__/cat-toy.entity'
|
import { CatToyEntity } from './__tests__/cat-toy.entity'
|
||||||
import { CatHomeEntity } from './__tests__/cat-home.entity'
|
import { CatHomeEntity } from './__tests__/cat-home.entity'
|
||||||
import { clone } from 'lodash'
|
import { clone } from 'lodash'
|
||||||
|
import {
|
||||||
|
FilterComparator,
|
||||||
|
FilterOperator,
|
||||||
|
FilterSuffix,
|
||||||
|
isOperator,
|
||||||
|
isSuffix,
|
||||||
|
OperatorSymbolToFunction,
|
||||||
|
} from './operator'
|
||||||
|
import { getFilterTokens } from './filter'
|
||||||
|
|
||||||
describe('paginate', () => {
|
describe('paginate', () => {
|
||||||
let dataSource: DataSource
|
let dataSource: DataSource
|
||||||
@ -586,7 +586,7 @@ describe('paginate', () => {
|
|||||||
color: 'white',
|
color: 'white',
|
||||||
},
|
},
|
||||||
filterableColumns: {
|
filterableColumns: {
|
||||||
name: [FilterOperator.NOT],
|
name: [FilterSuffix.NOT],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const query: PaginateQuery = {
|
const query: PaginateQuery = {
|
||||||
@ -601,6 +601,7 @@ describe('paginate', () => {
|
|||||||
expect(result.meta.filter).toStrictEqual({
|
expect(result.meta.filter).toStrictEqual({
|
||||||
name: '$not:Leche',
|
name: '$not:Leche',
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(result.data).toStrictEqual([cats[3]])
|
expect(result.data).toStrictEqual([cats[3]])
|
||||||
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.name=$not:Leche')
|
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.name=$not:Leche')
|
||||||
})
|
})
|
||||||
@ -610,7 +611,7 @@ describe('paginate', () => {
|
|||||||
relations: ['cat'],
|
relations: ['cat'],
|
||||||
sortableColumns: ['id', 'name'],
|
sortableColumns: ['id', 'name'],
|
||||||
filterableColumns: {
|
filterableColumns: {
|
||||||
'cat.name': [FilterOperator.NOT],
|
'cat.name': [FilterSuffix.NOT],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const query: PaginateQuery = {
|
const query: PaginateQuery = {
|
||||||
@ -634,7 +635,7 @@ describe('paginate', () => {
|
|||||||
relations: ['toys'],
|
relations: ['toys'],
|
||||||
sortableColumns: ['id', 'name'],
|
sortableColumns: ['id', 'name'],
|
||||||
filterableColumns: {
|
filterableColumns: {
|
||||||
'toys.name': [FilterOperator.NOT],
|
'toys.name': [FilterSuffix.NOT],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const query: PaginateQuery = {
|
const query: PaginateQuery = {
|
||||||
@ -669,7 +670,7 @@ describe('paginate', () => {
|
|||||||
relations: ['cat'],
|
relations: ['cat'],
|
||||||
sortableColumns: ['id', 'name'],
|
sortableColumns: ['id', 'name'],
|
||||||
filterableColumns: {
|
filterableColumns: {
|
||||||
'cat.name': [FilterOperator.NOT],
|
'cat.name': [FilterSuffix.NOT],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const query: PaginateQuery = {
|
const query: PaginateQuery = {
|
||||||
@ -1025,7 +1026,7 @@ describe('paginate', () => {
|
|||||||
sortableColumns: ['id', 'name', 'size.height', 'size.length', 'size.width'],
|
sortableColumns: ['id', 'name', 'size.height', 'size.length', 'size.width'],
|
||||||
searchableColumns: ['size.height'],
|
searchableColumns: ['size.height'],
|
||||||
filterableColumns: {
|
filterableColumns: {
|
||||||
'size.height': [FilterOperator.NOT],
|
'size.height': [FilterSuffix.NOT],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const query: PaginateQuery = {
|
const query: PaginateQuery = {
|
||||||
@ -1046,7 +1047,7 @@ describe('paginate', () => {
|
|||||||
sortableColumns: ['id', 'name', 'size.height', 'size.length', 'size.width'],
|
sortableColumns: ['id', 'name', 'size.height', 'size.length', 'size.width'],
|
||||||
searchableColumns: ['size.height'],
|
searchableColumns: ['size.height'],
|
||||||
filterableColumns: {
|
filterableColumns: {
|
||||||
'size.height': [FilterOperator.NOT],
|
'size.height': [FilterSuffix.NOT],
|
||||||
},
|
},
|
||||||
relations: ['home'],
|
relations: ['home'],
|
||||||
}
|
}
|
||||||
@ -1078,7 +1079,7 @@ describe('paginate', () => {
|
|||||||
relations: ['cat'],
|
relations: ['cat'],
|
||||||
sortableColumns: ['id', 'name'],
|
sortableColumns: ['id', 'name'],
|
||||||
filterableColumns: {
|
filterableColumns: {
|
||||||
'cat.size.height': [FilterOperator.NOT],
|
'cat.size.height': [FilterSuffix.NOT],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const query: PaginateQuery = {
|
const query: PaginateQuery = {
|
||||||
@ -1108,7 +1109,7 @@ describe('paginate', () => {
|
|||||||
const query: PaginateQuery = {
|
const query: PaginateQuery = {
|
||||||
path: '',
|
path: '',
|
||||||
filter: {
|
filter: {
|
||||||
'toys.size.height': '$eq:1',
|
'toys.size.height': '1',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1120,10 +1121,10 @@ describe('paginate', () => {
|
|||||||
cat2.toys = [catToys3]
|
cat2.toys = [catToys3]
|
||||||
|
|
||||||
expect(result.meta.filter).toStrictEqual({
|
expect(result.meta.filter).toStrictEqual({
|
||||||
'toys.size.height': '$eq:1',
|
'toys.size.height': '1',
|
||||||
})
|
})
|
||||||
expect(result.data).toStrictEqual([cat2])
|
expect(result.data).toStrictEqual([cat2])
|
||||||
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.toys.size.height=$eq:1')
|
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.toys.size.height=1')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return result based on filter on embedded on one-to-one relation', async () => {
|
it('should return result based on filter on embedded on one-to-one relation', async () => {
|
||||||
@ -1226,7 +1227,7 @@ describe('paginate', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
filterableColumns: {
|
filterableColumns: {
|
||||||
name: [FilterOperator.NOT],
|
name: [FilterSuffix.NOT],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const query: PaginateQuery = {
|
const query: PaginateQuery = {
|
||||||
@ -1249,7 +1250,7 @@ describe('paginate', () => {
|
|||||||
const config: PaginateConfig<CatEntity> = {
|
const config: PaginateConfig<CatEntity> = {
|
||||||
sortableColumns: ['id'],
|
sortableColumns: ['id'],
|
||||||
filterableColumns: {
|
filterableColumns: {
|
||||||
name: [FilterOperator.NOT],
|
name: [FilterSuffix.NOT],
|
||||||
color: [FilterOperator.EQ],
|
color: [FilterOperator.EQ],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -1322,7 +1323,7 @@ describe('paginate', () => {
|
|||||||
sortableColumns: ['id'],
|
sortableColumns: ['id'],
|
||||||
searchableColumns: ['name', 'color'],
|
searchableColumns: ['name', 'color'],
|
||||||
filterableColumns: {
|
filterableColumns: {
|
||||||
id: [FilterOperator.NOT, FilterOperator.IN],
|
id: [FilterSuffix.NOT, FilterOperator.IN],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const query: PaginateQuery = {
|
const query: PaginateQuery = {
|
||||||
@ -1348,7 +1349,7 @@ describe('paginate', () => {
|
|||||||
color: In(['black', 'white']),
|
color: In(['black', 'white']),
|
||||||
},
|
},
|
||||||
filterableColumns: {
|
filterableColumns: {
|
||||||
id: [FilterOperator.NOT, FilterOperator.IN],
|
id: [FilterSuffix.NOT, FilterOperator.IN],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const query: PaginateQuery = {
|
const query: PaginateQuery = {
|
||||||
@ -1428,7 +1429,7 @@ describe('paginate', () => {
|
|||||||
const config: PaginateConfig<CatEntity> = {
|
const config: PaginateConfig<CatEntity> = {
|
||||||
sortableColumns: ['id'],
|
sortableColumns: ['id'],
|
||||||
filterableColumns: {
|
filterableColumns: {
|
||||||
age: [FilterOperator.NOT, FilterOperator.NULL],
|
age: [FilterSuffix.NOT, FilterOperator.NULL],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const query: PaginateQuery = {
|
const query: PaginateQuery = {
|
||||||
@ -1448,7 +1449,7 @@ describe('paginate', () => {
|
|||||||
const config: PaginateConfig<CatEntity> = {
|
const config: PaginateConfig<CatEntity> = {
|
||||||
sortableColumns: ['id'],
|
sortableColumns: ['id'],
|
||||||
filterableColumns: {
|
filterableColumns: {
|
||||||
'home.name': [FilterOperator.NOT, FilterOperator.NULL],
|
'home.name': [FilterSuffix.NOT, FilterOperator.NULL],
|
||||||
},
|
},
|
||||||
relations: ['home'],
|
relations: ['home'],
|
||||||
}
|
}
|
||||||
@ -1474,7 +1475,7 @@ describe('paginate', () => {
|
|||||||
const config: PaginateConfig<CatEntity> = {
|
const config: PaginateConfig<CatEntity> = {
|
||||||
sortableColumns: ['id'],
|
sortableColumns: ['id'],
|
||||||
filterableColumns: {
|
filterableColumns: {
|
||||||
name: [FilterOperator.NOT, FilterOperator.NULL],
|
name: [FilterSuffix.NOT, FilterOperator.NULL],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const query: PaginateQuery = {
|
const query: PaginateQuery = {
|
||||||
@ -1494,7 +1495,7 @@ describe('paginate', () => {
|
|||||||
const config: PaginateConfig<CatEntity> = {
|
const config: PaginateConfig<CatEntity> = {
|
||||||
sortableColumns: ['id'],
|
sortableColumns: ['id'],
|
||||||
filterableColumns: {
|
filterableColumns: {
|
||||||
age: [FilterOperator.NOT],
|
age: [FilterSuffix.NOT],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const query: PaginateQuery = {
|
const query: PaginateQuery = {
|
||||||
@ -1534,13 +1535,19 @@ describe('paginate', () => {
|
|||||||
{ operator: '$lt', result: true },
|
{ operator: '$lt', result: true },
|
||||||
{ operator: '$lte', result: true },
|
{ operator: '$lte', result: true },
|
||||||
{ operator: '$btw', result: true },
|
{ operator: '$btw', result: true },
|
||||||
{ operator: '$not', result: true },
|
|
||||||
{ operator: '$ilike', result: true },
|
{ operator: '$ilike', result: true },
|
||||||
{ operator: '$fake', result: false },
|
{ operator: '$fake', result: false },
|
||||||
])('should check operator "$operator" valid is $result', ({ operator, result }) => {
|
])('should check operator "$operator" valid is $result', ({ operator, result }) => {
|
||||||
expect(isOperator(operator)).toStrictEqual(result)
|
expect(isOperator(operator)).toStrictEqual(result)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it.each([{ suffix: '$not', result: true }])(
|
||||||
|
'should check suffix "$suffix" valid is $result',
|
||||||
|
({ suffix, result }) => {
|
||||||
|
expect(isSuffix(suffix)).toStrictEqual(result)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
{ operator: '$eq', name: 'Equal' },
|
{ operator: '$eq', name: 'Equal' },
|
||||||
{ operator: '$gt', name: 'MoreThan' },
|
{ operator: '$gt', name: 'MoreThan' },
|
||||||
@ -1557,23 +1564,172 @@ describe('paginate', () => {
|
|||||||
expect(func.name).toStrictEqual(name)
|
expect(func.name).toStrictEqual(name)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
for (const cc of [FilterComparator.AND, FilterComparator.OR, '']) {
|
||||||
|
const comparator = cc === '' ? FilterComparator.AND : cc
|
||||||
|
const cSrt = cc === '' ? cc : `${comparator}:`
|
||||||
it.each([
|
it.each([
|
||||||
{ string: '$ilike:value', tokens: [null, '$ilike', 'value'] },
|
{
|
||||||
{ string: '$eq:value', tokens: [null, '$eq', 'value'] },
|
string: cSrt + '$ilike:value',
|
||||||
{ string: '$eq:val:ue', tokens: [null, '$eq', 'val:ue'] },
|
tokens: { comparator, operator: '$ilike', suffix: undefined, value: 'value' },
|
||||||
{ string: '$in:value1,value2,value3', tokens: [null, '$in', 'value1,value2,value3'] },
|
},
|
||||||
{ string: '$not:$in:value1:a,value2:b,value3:c', tokens: ['$not', '$in', 'value1:a,value2:b,value3:c'] },
|
{ string: cSrt + '$eq:value', tokens: { comparator, operator: '$eq', suffix: undefined, value: 'value' } },
|
||||||
{ string: 'value', tokens: [null, '$eq', 'value'] },
|
{
|
||||||
{ string: 'val:ue', tokens: [null, '$eq', 'val:ue'] },
|
string: cSrt + '$eq:val:ue',
|
||||||
{ string: '$not:value', tokens: [null, '$not', 'value'] },
|
tokens: { comparator, operator: '$eq', suffix: undefined, value: 'val:ue' },
|
||||||
{ string: '$eq:$not:value', tokens: ['$eq', '$not', 'value'] },
|
},
|
||||||
{ string: '$eq:$null', tokens: ['$eq', '$null'] },
|
{
|
||||||
{ string: '$null', tokens: [null, '$null'] },
|
string: cSrt + '$in:value1,value2,value3',
|
||||||
{ string: '', tokens: [null, '$eq', ''] },
|
tokens: { comparator, operator: '$in', suffix: undefined, value: 'value1,value2,value3' },
|
||||||
{ string: '$eq:$not:$in:value', tokens: [] },
|
},
|
||||||
|
{
|
||||||
|
string: cSrt + '$not:$in:value1:a,value2:b,value3:c',
|
||||||
|
tokens: { comparator, operator: '$in', suffix: '$not', value: 'value1:a,value2:b,value3:c' },
|
||||||
|
},
|
||||||
|
{ string: cSrt + 'value', tokens: { comparator, operator: '$eq', suffix: undefined, value: 'value' } },
|
||||||
|
{ string: cSrt + 'val:ue', tokens: { comparator, operator: '$eq', suffix: undefined, value: 'val:ue' } },
|
||||||
|
{ string: cSrt + '$not:value', tokens: { comparator, operator: '$eq', suffix: '$not', value: 'value' } },
|
||||||
|
{
|
||||||
|
string: cSrt + '$eq:$not:value',
|
||||||
|
tokens: { comparator, operator: '$eq', suffix: '$not', value: 'value' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
string: cSrt + '$eq:$null',
|
||||||
|
tokens: { comparator, operator: '$null', suffix: undefined, value: undefined },
|
||||||
|
},
|
||||||
|
{ string: cSrt + '$null', tokens: { comparator, operator: '$null', suffix: undefined, value: undefined } },
|
||||||
|
{ string: cSrt + '', tokens: { comparator, operator: '$eq', suffix: undefined, value: '' } },
|
||||||
|
{
|
||||||
|
string: cSrt + '$eq:$not:$in:value',
|
||||||
|
tokens: { comparator, operator: '$in', suffix: '$not', value: 'value' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
string: cSrt + '$eq:$not:value:$in',
|
||||||
|
tokens: { comparator, operator: '$eq', suffix: '$not', value: 'value:$in' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
string: cSrt + '$eq:$not:$null:value:$in',
|
||||||
|
tokens: { comparator, operator: '$null', suffix: '$not', value: undefined },
|
||||||
|
},
|
||||||
])('should get filter tokens for "$string"', ({ string, tokens }) => {
|
])('should get filter tokens for "$string"', ({ string, tokens }) => {
|
||||||
expect(getFilterTokens(string)).toStrictEqual(tokens)
|
expect(getFilterTokens(string)).toStrictEqual(tokens)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should return result based on virtualcolumn filter', async () => {
|
||||||
|
const config: PaginateConfig<CatEntity> = {
|
||||||
|
sortableColumns: ['id'],
|
||||||
|
filterableColumns: {
|
||||||
|
'home.countCat': [FilterOperator.GT],
|
||||||
|
},
|
||||||
|
relations: ['home'],
|
||||||
|
}
|
||||||
|
const query: PaginateQuery = {
|
||||||
|
path: '',
|
||||||
|
filter: {
|
||||||
|
'home.countCat': '$gt:0',
|
||||||
|
},
|
||||||
|
sortBy: [['id', 'ASC']],
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await paginate<CatEntity>(query, catRepo, config)
|
||||||
|
const expectedResult = [0, 1].map((i) => {
|
||||||
|
const ret = Object.assign(clone(cats[i]), { home: Object.assign(clone(catHomes[i])) })
|
||||||
|
delete ret.home.cat
|
||||||
|
return ret
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.data).toStrictEqual(expectedResult)
|
||||||
|
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.home.countCat=$gt:0')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return result sorted by a virtualcolumn', async () => {
|
||||||
|
const config: PaginateConfig<CatEntity> = {
|
||||||
|
sortableColumns: ['home.countCat'],
|
||||||
|
relations: ['home'],
|
||||||
|
}
|
||||||
|
const query: PaginateQuery = {
|
||||||
|
path: '',
|
||||||
|
sortBy: [['home.countCat', 'ASC']],
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await paginate<CatEntity>(query, catRepo, config)
|
||||||
|
const expectedResult = [2, 3, 4, 0, 1].map((i) => {
|
||||||
|
const ret = clone(cats[i])
|
||||||
|
if (i == 0 || i == 1) {
|
||||||
|
ret.home = clone(catHomes[i])
|
||||||
|
ret.home.countCat = cats.filter((cat) => cat.id === ret.home.cat.id).length
|
||||||
|
delete ret.home.cat
|
||||||
|
} else {
|
||||||
|
ret.home = null
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.data).toStrictEqual(expectedResult)
|
||||||
|
expect(result.links.current).toBe('?page=1&limit=20&sortBy=home.countCat:ASC')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return result based on or between range filter', async () => {
|
||||||
|
const config: PaginateConfig<CatEntity> = {
|
||||||
|
sortableColumns: ['id'],
|
||||||
|
filterableColumns: {
|
||||||
|
age: [FilterOperator.BTW],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const query: PaginateQuery = {
|
||||||
|
path: '',
|
||||||
|
filter: {
|
||||||
|
age: ['$btw:4,5', '$or:$btw:5,6'],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const result = await paginate<CatEntity>(query, catRepo, config)
|
||||||
|
|
||||||
|
expect(result.data).toStrictEqual([cats[0], cats[1], cats[2]])
|
||||||
|
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.age=$btw:4,5&filter.age=$or:$btw:5,6')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return result based on or with all cats', async () => {
|
||||||
|
const config: PaginateConfig<CatEntity> = {
|
||||||
|
sortableColumns: ['id'],
|
||||||
|
filterableColumns: {
|
||||||
|
age: [FilterOperator.BTW],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const query: PaginateQuery = {
|
||||||
|
path: '',
|
||||||
|
filter: {
|
||||||
|
age: ['$null', '$or:$not:$eq:$null'],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const result = await paginate<CatEntity>(query, catRepo, config)
|
||||||
|
|
||||||
|
expect(result.data).toStrictEqual([...cats])
|
||||||
|
expect(result.links.current).toBe(
|
||||||
|
'?page=1&limit=20&sortBy=id:ASC&filter.age=$null&filter.age=$or:$not:$eq:$null'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return result sorted and filter by a virtualcolumn in main entity', async () => {
|
||||||
|
const config: PaginateConfig<CatHomeEntity> = {
|
||||||
|
sortableColumns: ['countCat'],
|
||||||
|
relations: ['cat'],
|
||||||
|
filterableColumns: {
|
||||||
|
countCat: [FilterOperator.GT],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const query: PaginateQuery = {
|
||||||
|
path: '',
|
||||||
|
filter: {
|
||||||
|
countCat: '$gt:0',
|
||||||
|
},
|
||||||
|
sortBy: [['countCat', 'ASC']],
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await paginate<CatHomeEntity>(query, catHomeRepo, config)
|
||||||
|
|
||||||
|
expect(result.data).toStrictEqual([catHomes[0], catHomes[1]])
|
||||||
|
expect(result.links.current).toBe('?page=1&limit=20&sortBy=countCat:ASC&filter.countCat=$gt:0')
|
||||||
|
})
|
||||||
|
|
||||||
it('should return result based on virtualcolumn filter', async () => {
|
it('should return result based on virtualcolumn filter', async () => {
|
||||||
const config: PaginateConfig<CatEntity> = {
|
const config: PaginateConfig<CatEntity> = {
|
||||||
|
151
src/paginate.ts
151
src/paginate.ts
@ -1,24 +1,7 @@
|
|||||||
import {
|
import { Repository, SelectQueryBuilder, Brackets, FindOptionsWhere, ObjectLiteral } from 'typeorm'
|
||||||
Repository,
|
|
||||||
SelectQueryBuilder,
|
|
||||||
FindOperator,
|
|
||||||
Equal,
|
|
||||||
MoreThan,
|
|
||||||
MoreThanOrEqual,
|
|
||||||
In,
|
|
||||||
IsNull,
|
|
||||||
LessThan,
|
|
||||||
LessThanOrEqual,
|
|
||||||
Not,
|
|
||||||
ILike,
|
|
||||||
Brackets,
|
|
||||||
Between,
|
|
||||||
FindOptionsWhere,
|
|
||||||
ObjectLiteral,
|
|
||||||
} from 'typeorm'
|
|
||||||
import { PaginateQuery } from './decorator'
|
import { PaginateQuery } from './decorator'
|
||||||
import { ServiceUnavailableException, Logger } from '@nestjs/common'
|
import { ServiceUnavailableException, Logger } from '@nestjs/common'
|
||||||
import { values, mapKeys } from 'lodash'
|
import { 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 {
|
import {
|
||||||
@ -32,7 +15,8 @@ import {
|
|||||||
RelationColumn,
|
RelationColumn,
|
||||||
SortBy,
|
SortBy,
|
||||||
} from './helper'
|
} from './helper'
|
||||||
import { addWhereCondition, Filter } from './filter'
|
import { FilterOperator, FilterSuffix } from './operator'
|
||||||
|
import { addFilter } from './filter'
|
||||||
|
|
||||||
const logger: Logger = new Logger('nestjs-paginate')
|
const logger: Logger = new Logger('nestjs-paginate')
|
||||||
|
|
||||||
@ -67,126 +51,14 @@ export interface PaginateConfig<T> {
|
|||||||
defaultSortBy?: SortBy<T>
|
defaultSortBy?: SortBy<T>
|
||||||
defaultLimit?: number
|
defaultLimit?: number
|
||||||
where?: FindOptionsWhere<T> | FindOptionsWhere<T>[]
|
where?: FindOptionsWhere<T> | FindOptionsWhere<T>[]
|
||||||
filterableColumns?: { [key in Column<T>]?: FilterOperator[] }
|
filterableColumns?: {
|
||||||
|
[key in Column<T>]?: (FilterOperator | FilterSuffix)[]
|
||||||
|
}
|
||||||
withDeleted?: boolean
|
withDeleted?: boolean
|
||||||
relativePath?: boolean
|
relativePath?: boolean
|
||||||
origin?: string
|
origin?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum FilterOperator {
|
|
||||||
EQ = '$eq',
|
|
||||||
GT = '$gt',
|
|
||||||
GTE = '$gte',
|
|
||||||
IN = '$in',
|
|
||||||
NULL = '$null',
|
|
||||||
LT = '$lt',
|
|
||||||
LTE = '$lte',
|
|
||||||
BTW = '$btw',
|
|
||||||
NOT = '$not',
|
|
||||||
ILIKE = '$ilike',
|
|
||||||
SW = '$sw',
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isOperator(value: unknown): value is FilterOperator {
|
|
||||||
return values(FilterOperator).includes(value as any)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const OperatorSymbolToFunction = new Map<FilterOperator, (...args: any[]) => FindOperator<string>>([
|
|
||||||
[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.NOT, Not],
|
|
||||||
[FilterOperator.ILIKE, ILike],
|
|
||||||
[FilterOperator.SW, ILike],
|
|
||||||
])
|
|
||||||
|
|
||||||
export function getFilterTokens(raw: string): string[] {
|
|
||||||
const tokens = []
|
|
||||||
const matches = raw.match(/(\$\w+):/g)
|
|
||||||
|
|
||||||
if (matches) {
|
|
||||||
const value = raw.replace(matches.join(''), '')
|
|
||||||
tokens.push(...matches.map((token) => token.substring(0, token.length - 1)), value)
|
|
||||||
} else {
|
|
||||||
tokens.push(raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tokens.length === 0 || tokens.length > 3) {
|
|
||||||
return []
|
|
||||||
} else if (tokens.length === 2) {
|
|
||||||
if (tokens[1] !== FilterOperator.NULL) {
|
|
||||||
tokens.unshift(null)
|
|
||||||
}
|
|
||||||
} else if (tokens.length === 1) {
|
|
||||||
if (tokens[0] === FilterOperator.NULL) {
|
|
||||||
tokens.unshift(null)
|
|
||||||
} else {
|
|
||||||
tokens.unshift(null, FilterOperator.EQ)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tokens
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseFilter<T>(query: PaginateQuery, config: PaginateConfig<T>): Filter {
|
|
||||||
const filter: Filter = {}
|
|
||||||
let filterableColumns = config.filterableColumns
|
|
||||||
if (filterableColumns === undefined) {
|
|
||||||
logger.debug("No 'filterableColumns' given, ignoring filters.")
|
|
||||||
filterableColumns = {}
|
|
||||||
}
|
|
||||||
for (const column of Object.keys(query.filter)) {
|
|
||||||
if (!(column in filterableColumns)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const allowedOperators = filterableColumns[column]
|
|
||||||
const input = query.filter[column]
|
|
||||||
const statements = !Array.isArray(input) ? [input] : input
|
|
||||||
for (const raw of statements) {
|
|
||||||
const tokens = getFilterTokens(raw)
|
|
||||||
if (tokens.length === 0) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const [op2, op1, value] = tokens
|
|
||||||
|
|
||||||
if (!isOperator(op1) || !allowedOperators.includes(op1)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (isOperator(op2) && !allowedOperators.includes(op2)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (isOperator(op1)) {
|
|
||||||
switch (op1) {
|
|
||||||
case FilterOperator.BTW:
|
|
||||||
filter[column] = OperatorSymbolToFunction.get(op1)(...value.split(','))
|
|
||||||
break
|
|
||||||
case FilterOperator.IN:
|
|
||||||
filter[column] = OperatorSymbolToFunction.get(op1)(value.split(','))
|
|
||||||
break
|
|
||||||
case FilterOperator.ILIKE:
|
|
||||||
filter[column] = OperatorSymbolToFunction.get(op1)(`%${value}%`)
|
|
||||||
break
|
|
||||||
case FilterOperator.SW:
|
|
||||||
filter[column] = OperatorSymbolToFunction.get(op1)(`${value}%`)
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
filter[column] = OperatorSymbolToFunction.get(op1)(value)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isOperator(op2)) {
|
|
||||||
filter[column] = OperatorSymbolToFunction.get(op2)(filter[column])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filter
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DEFAULT_MAX_LIMIT = 100
|
export const DEFAULT_MAX_LIMIT = 100
|
||||||
export const DEFAULT_LIMIT = 20
|
export const DEFAULT_LIMIT = 20
|
||||||
export const NO_PAGINATION = 0
|
export const NO_PAGINATION = 0
|
||||||
@ -342,14 +214,7 @@ export async function paginate<T extends ObjectLiteral>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (query.filter) {
|
if (query.filter) {
|
||||||
const filter = parseFilter(query, config)
|
addFilter(queryBuilder, query, config.filterableColumns)
|
||||||
queryBuilder.andWhere(
|
|
||||||
new Brackets((qb: SelectQueryBuilder<T>) => {
|
|
||||||
for (const column in filter) {
|
|
||||||
addWhereCondition(qb, column, filter)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPaginated) {
|
if (isPaginated) {
|
||||||
|
Loading…
Reference in New Issue
Block a user