fix: date column filter with iso dates
This commit is contained in:
parent
ad488e31d7
commit
098216b3cc
@ -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`
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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]
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user