fix: date column filter with iso dates

This commit is contained in:
ppetzold 2023-03-21 12:11:07 +01:00
parent ad488e31d7
commit 098216b3cc
5 changed files with 232 additions and 10 deletions

View File

@ -335,7 +335,7 @@ const config: PaginateConfig<CatEntity> = {
sortableColumns: ['id', 'name', 'home.pillows.color'],
searchableColumns: ['name', 'home.pillows.color'],
filterableColumns: {
'home.pillows.color': [FilterOperator.EQ]
'home.pillows.color': [FilterOperator.EQ],
},
}
@ -410,6 +410,8 @@ const config: PaginateConfig<CatEntity> = {
`?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=$lt:2022-12-20T10:00:00.000Z` where column `createdAt` is before iso date `2022-12-20T10:00:00.000Z`
`?filter.roles=$contains:moderator` where column `roles` is an array and contains the value `moderator`
`?filter.roles=$contains:moderator,admin` where column `roles` is an array and contains the values `moderator` and `admin`
@ -420,7 +422,7 @@ Multi filters are filters that can be applied to a single column with a comparat
### Examples
`?filter.id=$gt:3&filter.id=$lt:5` where column `id` is greater than `3` **and** less than `5`
`?filter.createdAt=$gt:2022-02-02&filter.createdAt=$lt:2022-02-10` where column `createdAt` is after `2022-02-02` **and** is before `2022-02-10`
`?filter.id=$contains:moderator&filter.id=$or:$contains:admin` where column `roles` is an array and contains `moderator` **or** `admin`

View File

@ -38,6 +38,9 @@ export class CatEntity {
@Column({ type: 'text' }) // We don't use enum type as it makes it easier when testing across different db drivers.
cutenessLevel: CutenessLevel
@Column({ nullable: true })
lastVetVisit: Date | null
@Column(() => SizeEmbed)
size: SizeEmbed

View File

@ -23,6 +23,7 @@ import {
extractVirtualProperty,
fixColumnAlias,
getPropertiesByColumnName,
isISODate,
} from './helper'
export enum FilterOperator {
@ -182,7 +183,7 @@ export function addWhereCondition<T>(qb: SelectQueryBuilder<T>, column: string,
})
}
export function getFilterTokens(raw?: string): FilterToken | null {
export function parseFilterToken(raw?: string): FilterToken | null {
if (raw === undefined || raw === null) {
return null
}
@ -241,7 +242,7 @@ export function parseFilter(
const input = query.filter[column]
const statements = !Array.isArray(input) ? [input] : input
for (const raw of statements) {
const token = getFilterTokens(raw)
const token = parseFilterToken(raw)
if (!token) {
continue
}
@ -270,12 +271,16 @@ export function parseFilter(
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(','))
params.findOperator = OperatorSymbolToFunction.get(token.operator)(
...token.value.split(',').map(fixValue)
)
break
case FilterOperator.IN:
case FilterOperator.CONTAINS: // <- IN and CONTAINS are identically handled.
case FilterOperator.CONTAINS:
params.findOperator = OperatorSymbolToFunction.get(token.operator)(token.value.split(','))
break
case FilterOperator.ILIKE:
@ -285,7 +290,7 @@ export function parseFilter(
params.findOperator = OperatorSymbolToFunction.get(token.operator)(`${token.value}%`)
break
default:
params.findOperator = OperatorSymbolToFunction.get(token.operator)(token.value)
params.findOperator = OperatorSymbolToFunction.get(token.operator)(fixValue(token.value))
}
filter[column] = [...(filter[column] || []), params]

View File

@ -155,3 +155,11 @@ export function getQueryUrlComponents(path: string): { queryOrigin: string; quer
}
return { queryOrigin, queryPath }
}
const isoDateRegExp = new RegExp(
/(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/
)
export function isISODate(str: string): boolean {
return isoDateRegExp.test(str)
}

View File

@ -8,7 +8,7 @@ import { CatHomeEntity } from './__tests__/cat-home.entity'
import { CatHomePillowEntity } from './__tests__/cat-home-pillow.entity'
import { clone } from 'lodash'
import {
getFilterTokens,
parseFilterToken,
FilterComparator,
FilterOperator,
FilterSuffix,
@ -17,6 +17,8 @@ import {
OperatorSymbolToFunction,
} from './filter'
const isoStringToDate = (isoString) => new Date(isoString)
describe('paginate', () => {
let dataSource: DataSource
let catRepo: Repository<CatEntity>
@ -44,7 +46,7 @@ describe('paginate', () => {
database: ':memory:',
}),
synchronize: true,
logging: true,
logging: false,
entities: [CatEntity, CatToyEntity, CatHomeEntity, CatHomePillowEntity],
})
await dataSource.initialize()
@ -59,6 +61,7 @@ describe('paginate', () => {
color: 'brown',
age: 6,
cutenessLevel: CutenessLevel.HIGH,
lastVetVisit: isoStringToDate('2022-12-19T10:00:00.000Z'),
size: { height: 25, width: 10, length: 40 },
}),
catRepo.create({
@ -66,6 +69,7 @@ describe('paginate', () => {
color: 'ginger',
age: 5,
cutenessLevel: CutenessLevel.MEDIUM,
lastVetVisit: isoStringToDate('2022-12-20T10:00:00.000Z'),
size: { height: 30, width: 15, length: 45 },
}),
catRepo.create({
@ -73,6 +77,7 @@ describe('paginate', () => {
color: 'black',
age: 4,
cutenessLevel: CutenessLevel.HIGH,
lastVetVisit: isoStringToDate('2022-12-21T10:00:00.000Z'),
size: { height: 25, width: 10, length: 50 },
}),
catRepo.create({
@ -80,6 +85,7 @@ describe('paginate', () => {
color: 'white',
age: 3,
cutenessLevel: CutenessLevel.LOW,
lastVetVisit: null,
size: { height: 35, width: 12, length: 40 },
}),
catRepo.create({
@ -87,6 +93,7 @@ describe('paginate', () => {
color: 'white',
age: null,
cutenessLevel: CutenessLevel.HIGH,
lastVetVisit: null,
size: { height: 10, width: 5, length: 15 },
}),
])
@ -1753,7 +1760,7 @@ describe('paginate', () => {
tokens: { comparator, operator: '$null', suffix: '$not', value: undefined },
},
])('should get filter tokens for "$string"', ({ string, tokens }) => {
expect(getFilterTokens(string)).toStrictEqual(tokens)
expect(parseFilterToken(string)).toStrictEqual(tokens)
})
}
@ -2248,4 +2255,201 @@ describe('paginate', () => {
expect(result.data).toStrictEqual([cats[0], cats[1], cats[2], cats[3], cats[4]])
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.id=$not:$in:1,2,5')
})
describe('should return result based on date column filter', () => {
it('with $not and $null operators', async () => {
const config: PaginateConfig<CatEntity> = {
sortableColumns: ['id'],
filterableColumns: {
lastVetVisit: [FilterSuffix.NOT, FilterOperator.NULL],
},
}
const query: PaginateQuery = {
path: '',
filter: {
lastVetVisit: '$not:$null',
},
}
const result = await paginate<CatEntity>(query, catRepo, config)
expect(result.meta.filter).toStrictEqual({
lastVetVisit: '$not:$null',
})
expect(result.data).toStrictEqual([cats[0], cats[1], cats[2]])
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.lastVetVisit=$not:$null')
})
it('with $lt operator', async () => {
const config: PaginateConfig<CatEntity> = {
sortableColumns: ['id'],
filterableColumns: {
lastVetVisit: [FilterOperator.LT],
},
}
const query: PaginateQuery = {
path: '',
filter: {
lastVetVisit: '$lt:2022-12-20T10:00:00.000Z',
},
}
const result = await paginate<CatEntity>(query, catRepo, config)
expect(result.meta.filter).toStrictEqual({
lastVetVisit: '$lt:2022-12-20T10:00:00.000Z',
})
expect(result.data).toStrictEqual([cats[0]])
expect(result.links.current).toBe(
'?page=1&limit=20&sortBy=id:ASC&filter.lastVetVisit=$lt:2022-12-20T10:00:00.000Z'
)
})
it('with $lte operator', async () => {
const config: PaginateConfig<CatEntity> = {
sortableColumns: ['id'],
filterableColumns: {
lastVetVisit: [FilterOperator.LTE],
},
}
const query: PaginateQuery = {
path: '',
filter: {
lastVetVisit: '$lte:2022-12-20T10:00:00.000Z',
},
}
const result = await paginate<CatEntity>(query, catRepo, config)
expect(result.meta.filter).toStrictEqual({
lastVetVisit: '$lte:2022-12-20T10:00:00.000Z',
})
expect(result.data).toStrictEqual([cats[0], cats[1]])
expect(result.links.current).toBe(
'?page=1&limit=20&sortBy=id:ASC&filter.lastVetVisit=$lte:2022-12-20T10:00:00.000Z'
)
})
it('with $btw operator', async () => {
const config: PaginateConfig<CatEntity> = {
sortableColumns: ['id'],
filterableColumns: {
lastVetVisit: [FilterOperator.BTW],
},
}
const query: PaginateQuery = {
path: '',
filter: {
lastVetVisit: '$btw:2022-12-20T08:00:00.000Z,2022-12-20T12:00:00.000Z',
},
}
const result = await paginate<CatEntity>(query, catRepo, config)
expect(result.meta.filter).toStrictEqual({
lastVetVisit: '$btw:2022-12-20T08:00:00.000Z,2022-12-20T12:00:00.000Z',
})
expect(result.data).toStrictEqual([cats[1]])
expect(result.links.current).toBe(
'?page=1&limit=20&sortBy=id:ASC&filter.lastVetVisit=$btw:2022-12-20T08:00:00.000Z,2022-12-20T12:00:00.000Z'
)
})
it('with $gte operator', async () => {
const config: PaginateConfig<CatEntity> = {
sortableColumns: ['id'],
filterableColumns: {
lastVetVisit: [FilterOperator.GTE],
},
}
const query: PaginateQuery = {
path: '',
filter: {
lastVetVisit: '$gte:2022-12-20T10:00:00.000Z',
},
}
const result = await paginate<CatEntity>(query, catRepo, config)
expect(result.meta.filter).toStrictEqual({
lastVetVisit: '$gte:2022-12-20T10:00:00.000Z',
})
expect(result.data).toStrictEqual([cats[1], cats[2]])
expect(result.links.current).toBe(
'?page=1&limit=20&sortBy=id:ASC&filter.lastVetVisit=$gte:2022-12-20T10:00:00.000Z'
)
})
it('with $gt operator', async () => {
const config: PaginateConfig<CatEntity> = {
sortableColumns: ['id'],
filterableColumns: {
lastVetVisit: [FilterOperator.GT],
},
}
const query: PaginateQuery = {
path: '',
filter: {
lastVetVisit: '$gt:2022-12-20T10:00:00.000Z',
},
}
const result = await paginate<CatEntity>(query, catRepo, config)
expect(result.meta.filter).toStrictEqual({
lastVetVisit: '$gt:2022-12-20T10:00:00.000Z',
})
expect(result.data).toStrictEqual([cats[2]])
expect(result.links.current).toBe(
'?page=1&limit=20&sortBy=id:ASC&filter.lastVetVisit=$gt:2022-12-20T10:00:00.000Z'
)
})
it('with $lt operator and date only', async () => {
{
const config: PaginateConfig<CatEntity> = {
sortableColumns: ['id'],
filterableColumns: {
lastVetVisit: [FilterOperator.LT],
},
}
const query: PaginateQuery = {
path: '',
filter: {
lastVetVisit: '$lt:2022-12-20',
},
}
const result = await paginate<CatEntity>(query, catRepo, config)
expect(result.meta.filter).toStrictEqual({
lastVetVisit: '$lt:2022-12-20',
})
expect(result.data).toStrictEqual([cats[0]])
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.lastVetVisit=$lt:2022-12-20')
}
{
const config: PaginateConfig<CatEntity> = {
sortableColumns: ['id'],
filterableColumns: {
lastVetVisit: [FilterOperator.LT],
},
}
const query: PaginateQuery = {
path: '',
filter: {
lastVetVisit: '$lt:2022-12-21',
},
}
const result = await paginate<CatEntity>(query, catRepo, config)
expect(result.meta.filter).toStrictEqual({
lastVetVisit: '$lt:2022-12-21',
})
expect(result.data).toStrictEqual([cats[0], cats[1]])
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.lastVetVisit=$lt:2022-12-21')
}
})
})
})