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'],
|
sortableColumns: ['id', 'name', 'home.pillows.color'],
|
||||||
searchableColumns: ['name', 'home.pillows.color'],
|
searchableColumns: ['name', 'home.pillows.color'],
|
||||||
filterableColumns: {
|
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=$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` 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`
|
`?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
|
### 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`
|
`?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.
|
@Column({ type: 'text' }) // We don't use enum type as it makes it easier when testing across different db drivers.
|
||||||
cutenessLevel: CutenessLevel
|
cutenessLevel: CutenessLevel
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
lastVetVisit: Date | null
|
||||||
|
|
||||||
@Column(() => SizeEmbed)
|
@Column(() => SizeEmbed)
|
||||||
size: SizeEmbed
|
size: SizeEmbed
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ import {
|
|||||||
extractVirtualProperty,
|
extractVirtualProperty,
|
||||||
fixColumnAlias,
|
fixColumnAlias,
|
||||||
getPropertiesByColumnName,
|
getPropertiesByColumnName,
|
||||||
|
isISODate,
|
||||||
} from './helper'
|
} from './helper'
|
||||||
|
|
||||||
export enum FilterOperator {
|
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) {
|
if (raw === undefined || raw === null) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -241,7 +242,7 @@ export function parseFilter(
|
|||||||
const input = query.filter[column]
|
const input = query.filter[column]
|
||||||
const statements = !Array.isArray(input) ? [input] : input
|
const statements = !Array.isArray(input) ? [input] : input
|
||||||
for (const raw of statements) {
|
for (const raw of statements) {
|
||||||
const token = getFilterTokens(raw)
|
const token = parseFilterToken(raw)
|
||||||
if (!token) {
|
if (!token) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -270,12 +271,16 @@ export function parseFilter(
|
|||||||
findOperator: undefined,
|
findOperator: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fixValue = (value: string) => (isISODate(value) ? new Date(value) : value)
|
||||||
|
|
||||||
switch (token.operator) {
|
switch (token.operator) {
|
||||||
case FilterOperator.BTW:
|
case FilterOperator.BTW:
|
||||||
params.findOperator = OperatorSymbolToFunction.get(token.operator)(...token.value.split(','))
|
params.findOperator = OperatorSymbolToFunction.get(token.operator)(
|
||||||
|
...token.value.split(',').map(fixValue)
|
||||||
|
)
|
||||||
break
|
break
|
||||||
case FilterOperator.IN:
|
case FilterOperator.IN:
|
||||||
case FilterOperator.CONTAINS: // <- IN and CONTAINS are identically handled.
|
case FilterOperator.CONTAINS:
|
||||||
params.findOperator = OperatorSymbolToFunction.get(token.operator)(token.value.split(','))
|
params.findOperator = OperatorSymbolToFunction.get(token.operator)(token.value.split(','))
|
||||||
break
|
break
|
||||||
case FilterOperator.ILIKE:
|
case FilterOperator.ILIKE:
|
||||||
@ -285,7 +290,7 @@ export function parseFilter(
|
|||||||
params.findOperator = OperatorSymbolToFunction.get(token.operator)(`${token.value}%`)
|
params.findOperator = OperatorSymbolToFunction.get(token.operator)(`${token.value}%`)
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
params.findOperator = OperatorSymbolToFunction.get(token.operator)(token.value)
|
params.findOperator = OperatorSymbolToFunction.get(token.operator)(fixValue(token.value))
|
||||||
}
|
}
|
||||||
|
|
||||||
filter[column] = [...(filter[column] || []), params]
|
filter[column] = [...(filter[column] || []), params]
|
||||||
|
@ -155,3 +155,11 @@ export function getQueryUrlComponents(path: string): { queryOrigin: string; quer
|
|||||||
}
|
}
|
||||||
return { queryOrigin, queryPath }
|
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 { CatHomePillowEntity } from './__tests__/cat-home-pillow.entity'
|
||||||
import { clone } from 'lodash'
|
import { clone } from 'lodash'
|
||||||
import {
|
import {
|
||||||
getFilterTokens,
|
parseFilterToken,
|
||||||
FilterComparator,
|
FilterComparator,
|
||||||
FilterOperator,
|
FilterOperator,
|
||||||
FilterSuffix,
|
FilterSuffix,
|
||||||
@ -17,6 +17,8 @@ import {
|
|||||||
OperatorSymbolToFunction,
|
OperatorSymbolToFunction,
|
||||||
} from './filter'
|
} from './filter'
|
||||||
|
|
||||||
|
const isoStringToDate = (isoString) => new Date(isoString)
|
||||||
|
|
||||||
describe('paginate', () => {
|
describe('paginate', () => {
|
||||||
let dataSource: DataSource
|
let dataSource: DataSource
|
||||||
let catRepo: Repository<CatEntity>
|
let catRepo: Repository<CatEntity>
|
||||||
@ -44,7 +46,7 @@ describe('paginate', () => {
|
|||||||
database: ':memory:',
|
database: ':memory:',
|
||||||
}),
|
}),
|
||||||
synchronize: true,
|
synchronize: true,
|
||||||
logging: true,
|
logging: false,
|
||||||
entities: [CatEntity, CatToyEntity, CatHomeEntity, CatHomePillowEntity],
|
entities: [CatEntity, CatToyEntity, CatHomeEntity, CatHomePillowEntity],
|
||||||
})
|
})
|
||||||
await dataSource.initialize()
|
await dataSource.initialize()
|
||||||
@ -59,6 +61,7 @@ describe('paginate', () => {
|
|||||||
color: 'brown',
|
color: 'brown',
|
||||||
age: 6,
|
age: 6,
|
||||||
cutenessLevel: CutenessLevel.HIGH,
|
cutenessLevel: CutenessLevel.HIGH,
|
||||||
|
lastVetVisit: isoStringToDate('2022-12-19T10:00:00.000Z'),
|
||||||
size: { height: 25, width: 10, length: 40 },
|
size: { height: 25, width: 10, length: 40 },
|
||||||
}),
|
}),
|
||||||
catRepo.create({
|
catRepo.create({
|
||||||
@ -66,6 +69,7 @@ describe('paginate', () => {
|
|||||||
color: 'ginger',
|
color: 'ginger',
|
||||||
age: 5,
|
age: 5,
|
||||||
cutenessLevel: CutenessLevel.MEDIUM,
|
cutenessLevel: CutenessLevel.MEDIUM,
|
||||||
|
lastVetVisit: isoStringToDate('2022-12-20T10:00:00.000Z'),
|
||||||
size: { height: 30, width: 15, length: 45 },
|
size: { height: 30, width: 15, length: 45 },
|
||||||
}),
|
}),
|
||||||
catRepo.create({
|
catRepo.create({
|
||||||
@ -73,6 +77,7 @@ describe('paginate', () => {
|
|||||||
color: 'black',
|
color: 'black',
|
||||||
age: 4,
|
age: 4,
|
||||||
cutenessLevel: CutenessLevel.HIGH,
|
cutenessLevel: CutenessLevel.HIGH,
|
||||||
|
lastVetVisit: isoStringToDate('2022-12-21T10:00:00.000Z'),
|
||||||
size: { height: 25, width: 10, length: 50 },
|
size: { height: 25, width: 10, length: 50 },
|
||||||
}),
|
}),
|
||||||
catRepo.create({
|
catRepo.create({
|
||||||
@ -80,6 +85,7 @@ describe('paginate', () => {
|
|||||||
color: 'white',
|
color: 'white',
|
||||||
age: 3,
|
age: 3,
|
||||||
cutenessLevel: CutenessLevel.LOW,
|
cutenessLevel: CutenessLevel.LOW,
|
||||||
|
lastVetVisit: null,
|
||||||
size: { height: 35, width: 12, length: 40 },
|
size: { height: 35, width: 12, length: 40 },
|
||||||
}),
|
}),
|
||||||
catRepo.create({
|
catRepo.create({
|
||||||
@ -87,6 +93,7 @@ describe('paginate', () => {
|
|||||||
color: 'white',
|
color: 'white',
|
||||||
age: null,
|
age: null,
|
||||||
cutenessLevel: CutenessLevel.HIGH,
|
cutenessLevel: CutenessLevel.HIGH,
|
||||||
|
lastVetVisit: null,
|
||||||
size: { height: 10, width: 5, length: 15 },
|
size: { height: 10, width: 5, length: 15 },
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
@ -1753,7 +1760,7 @@ describe('paginate', () => {
|
|||||||
tokens: { comparator, operator: '$null', suffix: '$not', value: undefined },
|
tokens: { comparator, operator: '$null', suffix: '$not', value: undefined },
|
||||||
},
|
},
|
||||||
])('should get filter tokens for "$string"', ({ string, tokens }) => {
|
])('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.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')
|
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