345 lines
11 KiB
TypeScript
345 lines
11 KiB
TypeScript
import { values } from 'lodash'
|
|
import {
|
|
ArrayContains,
|
|
Between,
|
|
Brackets,
|
|
Equal,
|
|
FindOperator,
|
|
ILike,
|
|
In,
|
|
IsNull,
|
|
LessThan,
|
|
LessThanOrEqual,
|
|
MoreThan,
|
|
MoreThanOrEqual,
|
|
Not,
|
|
SelectQueryBuilder,
|
|
} from 'typeorm'
|
|
import { WherePredicateOperator } from 'typeorm/query-builder/WhereClause'
|
|
import { PaginateQuery } from './decorator'
|
|
import {
|
|
checkIsArray,
|
|
checkIsEmbedded,
|
|
checkIsRelation,
|
|
extractVirtualProperty,
|
|
fixColumnAlias,
|
|
getPropertiesByColumnName,
|
|
isISODate,
|
|
} from './helper'
|
|
|
|
export enum FilterOperator {
|
|
EQ = '$eq',
|
|
GT = '$gt',
|
|
GTE = '$gte',
|
|
IN = '$in',
|
|
NULL = '$null',
|
|
LT = '$lt',
|
|
LTE = '$lte',
|
|
BTW = '$btw',
|
|
ILIKE = '$ilike',
|
|
SW = '$sw',
|
|
CONTAINS = '$contains',
|
|
}
|
|
|
|
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],
|
|
[FilterOperator.CONTAINS, ArrayContains],
|
|
])
|
|
|
|
type Filter = { comparator: FilterComparator; findOperator: FindOperator<string> }
|
|
type ColumnsFilters = { [columnName: string]: Filter[] }
|
|
|
|
export interface FilterToken {
|
|
comparator: FilterComparator
|
|
suffix?: FilterSuffix
|
|
operator: FilterOperator
|
|
value: string
|
|
}
|
|
|
|
// 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(
|
|
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],
|
|
}
|
|
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) {
|
|
const columnProperties = getPropertiesByColumnName(column)
|
|
const { isVirtualProperty, query: virtualQuery } = extractVirtualProperty(qb, columnProperties)
|
|
const isRelation = checkIsRelation(qb, columnProperties.propertyPath)
|
|
const isEmbedded = checkIsEmbedded(qb, columnProperties.propertyPath)
|
|
const isArray = checkIsArray(qb, columnProperties.propertyName)
|
|
|
|
const alias = fixColumnAlias(columnProperties, qb.alias, isRelation, isVirtualProperty, isEmbedded, virtualQuery)
|
|
filter[column].forEach((columnFilter: Filter, index: number) => {
|
|
const columnNamePerIteration = `${columnProperties.column}${index}`
|
|
const condition = generatePredicateCondition(
|
|
qb,
|
|
columnProperties.column,
|
|
columnFilter,
|
|
alias,
|
|
isVirtualProperty
|
|
)
|
|
const parameters = fixQueryParam(alias, columnNamePerIteration, columnFilter, condition, {
|
|
[columnNamePerIteration]: columnFilter.findOperator.value,
|
|
})
|
|
if (isArray && condition.parameters?.length && !['not', 'isNull'].includes(condition.operator)) {
|
|
condition.parameters[0] = `cardinality(${condition.parameters[0]})`
|
|
}
|
|
if (columnFilter.comparator === FilterComparator.OR) {
|
|
qb.orWhere(qb['createWhereConditionExpression'](condition), parameters)
|
|
} else {
|
|
qb.andWhere(qb['createWhereConditionExpression'](condition), parameters)
|
|
}
|
|
})
|
|
}
|
|
|
|
export function parseFilterToken(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(
|
|
query: PaginateQuery,
|
|
filterableColumns?: { [column: string]: (FilterOperator | FilterSuffix)[] | true }
|
|
): 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 = parseFilterToken(raw)
|
|
if (!token) {
|
|
continue
|
|
}
|
|
if (allowedOperators === true) {
|
|
if (token.operator && !isOperator(token.operator)) {
|
|
continue
|
|
}
|
|
if (token.suffix && !isSuffix(token.suffix)) {
|
|
continue
|
|
}
|
|
} else {
|
|
if (
|
|
token.operator &&
|
|
token.operator !== FilterOperator.EQ &&
|
|
!allowedOperators.includes(token.operator)
|
|
) {
|
|
continue
|
|
}
|
|
if (token.suffix && !allowedOperators.includes(token.suffix)) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
const params: (typeof filter)[0][0] = {
|
|
comparator: token.comparator,
|
|
findOperator: undefined,
|
|
}
|
|
|
|
const fixValue = (value: string) => (isISODate(value) ? new Date(value) : value)
|
|
|
|
switch (token.operator) {
|
|
case FilterOperator.BTW:
|
|
params.findOperator = OperatorSymbolToFunction.get(token.operator)(
|
|
...token.value.split(',').map(fixValue)
|
|
)
|
|
break
|
|
case FilterOperator.IN:
|
|
case FilterOperator.CONTAINS:
|
|
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)(fixValue(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?: { [column: string]: (FilterOperator | FilterSuffix)[] | true }
|
|
): SelectQueryBuilder<T> {
|
|
const filter = parseFilter(query, filterableColumns)
|
|
|
|
const filterEntries = Object.entries(filter)
|
|
const orFilters = filterEntries.filter(([_, value]) => value.some((v) => v.comparator === '$or'))
|
|
const andFilters = filterEntries.filter(([_, value]) => value.some((v) => v.comparator !== '$or'))
|
|
|
|
qb.andWhere(
|
|
new Brackets((qb: SelectQueryBuilder<T>) => {
|
|
for (const [column] of orFilters) {
|
|
addWhereCondition(qb, column, filter)
|
|
}
|
|
})
|
|
)
|
|
|
|
qb.andWhere(
|
|
new Brackets((qb: SelectQueryBuilder<T>) => {
|
|
for (const [column] of andFilters) {
|
|
addWhereCondition(qb, column, filter)
|
|
}
|
|
})
|
|
)
|
|
|
|
return qb
|
|
}
|