Better splitting of the filter string to allow for filter values that contain a colon (#120)
This commit is contained in:
parent
85f60194eb
commit
0089fffc79
@ -1,5 +1,13 @@
|
||||
import { createConnection, Repository, Column, In } from 'typeorm'
|
||||
import { Paginated, paginate, PaginateConfig, FilterOperator } from './paginate'
|
||||
import {
|
||||
Paginated,
|
||||
paginate,
|
||||
PaginateConfig,
|
||||
FilterOperator,
|
||||
isOperator,
|
||||
getOperatorFn,
|
||||
getFilterTokens,
|
||||
} from './paginate'
|
||||
import { PaginateQuery } from './decorator'
|
||||
import { Entity, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm'
|
||||
import { HttpException } from '@nestjs/common'
|
||||
@ -432,4 +440,48 @@ describe('paginate', () => {
|
||||
expect(err).toBeInstanceOf(HttpException)
|
||||
}
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ operator: '$eq', result: true },
|
||||
{ operator: '$gte', result: true },
|
||||
{ operator: '$gt', result: true },
|
||||
{ operator: '$in', result: true },
|
||||
{ operator: '$null', result: true },
|
||||
{ operator: '$lt', result: true },
|
||||
{ operator: '$lte', result: true },
|
||||
{ operator: '$not', result: true },
|
||||
{ operator: '$fake', result: false },
|
||||
])('should check operator "$operator" valid is $result', ({ operator, result }) => {
|
||||
expect(isOperator(operator)).toStrictEqual(result)
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ operator: '$eq', name: 'Equal' },
|
||||
{ operator: '$gt', name: 'MoreThan' },
|
||||
{ operator: '$gte', name: 'MoreThanOrEqual' },
|
||||
{ operator: '$in', name: 'In' },
|
||||
{ operator: '$null', name: 'IsNull' },
|
||||
{ operator: '$lt', name: 'LessThan' },
|
||||
{ operator: '$lte', name: 'LessThanOrEqual' },
|
||||
{ operator: '$not', name: 'Not' },
|
||||
])('should get operator function $name for "$operator"', ({ operator, name }) => {
|
||||
const func = getOperatorFn<CatEntity>(operator as FilterOperator)
|
||||
expect(func.name).toStrictEqual(name)
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ string: '$eq:value', tokens: [null, '$eq', 'value'] },
|
||||
{ string: '$eq:val:ue', tokens: [null, '$eq', 'val:ue'] },
|
||||
{ string: '$in:value1,value2,value3', tokens: [null, '$in', 'value1,value2,value3'] },
|
||||
{ string: 'value', tokens: [null, '$eq', 'value'] },
|
||||
{ string: 'val:ue', tokens: [null, '$eq', 'val:ue'] },
|
||||
{ string: '$not:value', tokens: [null, '$not', 'value'] },
|
||||
{ string: '$eq:$not:value', tokens: ['$eq', '$not', 'value'] },
|
||||
{ string: '$eq:$null', tokens: ['$eq', '$null'] },
|
||||
{ string: '$null', tokens: [null, '$null'] },
|
||||
{ string: '', tokens: [null, '$eq', ''] },
|
||||
{ string: '$eq:$not:$in:value', tokens: [] },
|
||||
])('should get filter tokens for "$string"', ({ string, tokens }) => {
|
||||
expect(getFilterTokens(string)).toStrictEqual(tokens)
|
||||
})
|
||||
})
|
||||
|
152
src/paginate.ts
152
src/paginate.ts
@ -66,6 +66,94 @@ export enum FilterOperator {
|
||||
NOT = '$not',
|
||||
}
|
||||
|
||||
export function isOperator(value: unknown): value is FilterOperator {
|
||||
return values(FilterOperator).includes(value as any)
|
||||
}
|
||||
|
||||
export function getOperatorFn<T>(op: FilterOperator): (...args: any[]) => FindOperator<T> {
|
||||
switch (op) {
|
||||
case FilterOperator.EQ:
|
||||
return Equal
|
||||
case FilterOperator.GT:
|
||||
return MoreThan
|
||||
case FilterOperator.GTE:
|
||||
return MoreThanOrEqual
|
||||
case FilterOperator.IN:
|
||||
return In
|
||||
case FilterOperator.NULL:
|
||||
return IsNull
|
||||
case FilterOperator.LT:
|
||||
return LessThan
|
||||
case FilterOperator.LTE:
|
||||
return LessThanOrEqual
|
||||
case FilterOperator.NOT:
|
||||
return Not
|
||||
}
|
||||
}
|
||||
|
||||
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>) {
|
||||
const filter = {}
|
||||
|
||||
for (const column of Object.keys(query.filter)) {
|
||||
if (!(column in config.filterableColumns)) {
|
||||
continue
|
||||
}
|
||||
const allowedOperators = config.filterableColumns[column as Column<T>]
|
||||
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)) {
|
||||
const args = op1 === FilterOperator.IN ? value.split(',') : value
|
||||
filter[column] = getOperatorFn<T>(op1)(args)
|
||||
}
|
||||
if (isOperator(op2)) {
|
||||
filter[column] = getOperatorFn<T>(op2)(filter[column])
|
||||
}
|
||||
}
|
||||
}
|
||||
return filter
|
||||
}
|
||||
|
||||
export async function paginate<T>(
|
||||
query: PaginateQuery,
|
||||
repo: Repository<T> | SelectQueryBuilder<T>,
|
||||
@ -143,69 +231,7 @@ export async function paginate<T>(
|
||||
}
|
||||
|
||||
if (query.filter) {
|
||||
const filter = {}
|
||||
function getOperatorFn(op: FilterOperator): (...args: any[]) => FindOperator<T> {
|
||||
switch (op) {
|
||||
case FilterOperator.EQ:
|
||||
return Equal
|
||||
case FilterOperator.GT:
|
||||
return MoreThan
|
||||
case FilterOperator.GTE:
|
||||
return MoreThanOrEqual
|
||||
case FilterOperator.IN:
|
||||
return In
|
||||
case FilterOperator.NULL:
|
||||
return IsNull
|
||||
case FilterOperator.LT:
|
||||
return LessThan
|
||||
case FilterOperator.LTE:
|
||||
return LessThanOrEqual
|
||||
case FilterOperator.NOT:
|
||||
return Not
|
||||
}
|
||||
}
|
||||
function isOperator(value: any): value is FilterOperator {
|
||||
return values(FilterOperator).includes(value)
|
||||
}
|
||||
for (const column of Object.keys(query.filter)) {
|
||||
if (!(column in config.filterableColumns)) {
|
||||
continue
|
||||
}
|
||||
const allowedOperators = config.filterableColumns[column as Column<T>]
|
||||
const input = query.filter[column]
|
||||
const statements = !Array.isArray(input) ? [input] : input
|
||||
for (const raw of statements) {
|
||||
const tokens = raw.split(':')
|
||||
if (tokens.length === 0 || tokens.length > 3) {
|
||||
continue
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
const [op2, op1, value] = tokens
|
||||
|
||||
if (!isOperator(op1) || !allowedOperators.includes(op1)) {
|
||||
continue
|
||||
}
|
||||
if (isOperator(op2) && !allowedOperators.includes(op2)) {
|
||||
continue
|
||||
}
|
||||
if (isOperator(op1)) {
|
||||
const args = op1 === FilterOperator.IN ? value.split(',') : value
|
||||
filter[column] = getOperatorFn(op1)(args)
|
||||
}
|
||||
if (isOperator(op2)) {
|
||||
filter[column] = getOperatorFn(op2)(filter[column])
|
||||
}
|
||||
}
|
||||
}
|
||||
const filter = parseFilter<T>(query, config)
|
||||
|
||||
queryBuilder = queryBuilder.andWhere(filter)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user