nestjs-paginate/src/filter.ts

238 lines
8.2 KiB
TypeScript
Raw Normal View History

import { Brackets, FindOperator, SelectQueryBuilder } from 'typeorm'
2023-01-30 11:17:14 +00:00
import { WherePredicateOperator } from 'typeorm/query-builder/WhereClause'
import { PaginateQuery } from './decorator'
2023-01-30 11:17:14 +00:00
import { checkIsRelation, extractVirtualProperty, fixColumnAlias, getPropertiesByColumnName } from './helper'
import {
FilterComparator,
FilterOperator,
FilterSuffix,
isComparator,
isOperator,
isSuffix,
OperatorSymbolToFunction,
} from './operator'
import { PaginateConfig } from './paginate'
2023-01-30 11:17:14 +00:00
type Filter = { comparator: FilterComparator; findOperator: FindOperator<string> }
type ColumnsFilters = { [columnName: string]: Filter[] }
2023-01-30 11:17:14 +00:00
export interface FilterToken {
comparator: FilterComparator
suffix?: FilterSuffix
operator: FilterOperator
value: string
2023-01-30 11:17:14 +00:00
}
// 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
export function fixQueryParam(
2023-01-30 11:17:14 +00:00
alias: string,
column: string,
filter: Filter,
condition: WherePredicateOperator,
parameters: { [key: string]: string }
): { [key: string]: string } {
const isNotOperator = (condition.operator as string) === 'not'
const conditionFixer = (
alias: string,
column: string,
filter: Filter,
operator: WherePredicateOperator['operator'],
parameters: { [key: string]: string }
): { condition_params: any; params: any } => {
let condition_params: any = undefined
let params = parameters
switch (operator) {
case 'between':
condition_params = [alias, `:${column}_from`, `:${column}_to`]
params = {
[column + '_from']: filter.findOperator.value[0],
[column + '_to']: filter.findOperator.value[1],
2023-01-30 11:17:14 +00:00
}
break
case 'in':
condition_params = [alias, `:...${column}`]
break
default:
condition_params = [alias, `:${column}`]
break
}
return { condition_params, params }
}
const { condition_params, params } = conditionFixer(
alias,
column,
filter,
isNotOperator ? condition['condition']['operator'] : condition.operator,
parameters
)
if (isNotOperator) {
condition['condition']['parameters'] = condition_params
} else {
condition.parameters = condition_params
}
return params
}
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) {
2023-01-30 11:17:14 +00:00
const columnProperties = getPropertiesByColumnName(column)
const { isVirtualProperty, query: virtualQuery } = extractVirtualProperty(qb, columnProperties)
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 condition = generatePredicateCondition(qb, column, columnFilter, alias, isVirtualProperty)
const parameters = fixQueryParam(alias, columnNamePerIteration, columnFilter, condition, {
[columnNamePerIteration]: columnFilter.findOperator.value,
})
if (columnFilter.comparator === FilterComparator.OR) {
qb.orWhere(qb['createWhereConditionExpression'](condition), parameters)
} else {
qb.andWhere(qb['createWhereConditionExpression'](condition), parameters)
}
2023-01-30 11:17:14 +00:00
})
}
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)
}
})
)
2023-01-30 11:17:14 +00:00
}