Better splitting of the filter string to allow for filter values that contain a colon (#120)

This commit is contained in:
Alex Stansfield 2021-11-17 00:49:13 +07:00 committed by GitHub
parent 85f60194eb
commit 0089fffc79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 142 additions and 64 deletions

View File

@ -1,5 +1,13 @@
import { createConnection, Repository, Column, In } from 'typeorm' 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 { PaginateQuery } from './decorator'
import { Entity, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm' import { Entity, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm'
import { HttpException } from '@nestjs/common' import { HttpException } from '@nestjs/common'
@ -432,4 +440,48 @@ describe('paginate', () => {
expect(err).toBeInstanceOf(HttpException) 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)
})
}) })

View File

@ -66,6 +66,94 @@ export enum FilterOperator {
NOT = '$not', 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>( export async function paginate<T>(
query: PaginateQuery, query: PaginateQuery,
repo: Repository<T> | SelectQueryBuilder<T>, repo: Repository<T> | SelectQueryBuilder<T>,
@ -143,69 +231,7 @@ export async function paginate<T>(
} }
if (query.filter) { if (query.filter) {
const filter = {} const filter = parseFilter<T>(query, config)
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])
}
}
}
queryBuilder = queryBuilder.andWhere(filter) queryBuilder = queryBuilder.andWhere(filter)
} }