feat: nested search and filter (#509)

BREAKING CHANGE: Embedded fields have to wrapped in brackets e.g. `toys.(size.height)`.
This commit is contained in:
David Sanchez 2023-03-14 19:12:30 +01:00 committed by ppetzold
parent 5a72a53b2f
commit 339249ce24
5 changed files with 167 additions and 59 deletions

View File

@ -15,7 +15,7 @@ Pagination and filtering helper method for TypeORM repositories or query builder
- Search across columns - Search across columns
- Select columns - Select columns
- Filter using operators (`$eq`, `$not`, `$null`, `$in`, `$gt`, `$gte`, `$lt`, `$lte`, `$btw`, `$ilike`, `$sw`) - Filter using operators (`$eq`, `$not`, `$null`, `$in`, `$gt`, `$gte`, `$lt`, `$lte`, `$btw`, `$ilike`, `$sw`)
- Include relations - Include relations and nested relations
- Virtual column support - Virtual column support
## Installation ## Installation
@ -351,6 +351,31 @@ const config: PaginateConfig<CatEntity> = {
const result = await paginate<CatEntity>(query, catRepo, config) const result = await paginate<CatEntity>(query, catRepo, config)
``` ```
## Usage with nested Relations
Similar as with relations, you can specify nested relations for sorting, filtering, search and relations:
### Example
#### Endpoint
```url
http://localhost:3000/cats?filter.home.pillows.color=$eq:ping,String
```
#### Code
```typescript
const config: PaginateConfig<CatEntity> = {
relations: { home: { pillows: true } },
sortableColumns: ['id', 'name', 'home.pillows.color'],
searchableColumns: ['name', 'home.pillows.color'],
filterableColumns: { 'home.pillows.color': [FilterOperator.EQ] },
}
const result = await paginate<CatEntity>(query, catRepo, config)
```
## Usage of pagination type ## Usage of pagination type
You can use either `limit`/`offset` or `take`/`skip` to return paginated results. You can use either `limit`/`offset` or `take`/`skip` to return paginated results.

View File

@ -104,8 +104,14 @@ export function addWhereCondition<T>(qb: SelectQueryBuilder<T>, column: string,
const isEmbedded = checkIsEmbedded(qb, columnProperties.propertyPath) const isEmbedded = checkIsEmbedded(qb, columnProperties.propertyPath)
const alias = fixColumnAlias(columnProperties, qb.alias, isRelation, isVirtualProperty, isEmbedded, virtualQuery) const alias = fixColumnAlias(columnProperties, qb.alias, isRelation, isVirtualProperty, isEmbedded, virtualQuery)
filter[column].forEach((columnFilter: Filter, index: number) => { filter[column].forEach((columnFilter: Filter, index: number) => {
const columnNamePerIteration = `${column}${index}` const columnNamePerIteration = `${columnProperties.column}${index}`
const condition = generatePredicateCondition(qb, column, columnFilter, alias, isVirtualProperty) const condition = generatePredicateCondition(
qb,
columnProperties.column,
columnFilter,
alias,
isVirtualProperty
)
const parameters = fixQueryParam(alias, columnNamePerIteration, columnFilter, condition, { const parameters = fixQueryParam(alias, columnNamePerIteration, columnFilter, condition, {
[columnNamePerIteration]: columnFilter.findOperator.value, [columnNamePerIteration]: columnFilter.findOperator.value,
}) })

View File

@ -1,7 +1,11 @@
import { SelectQueryBuilder } from 'typeorm' import { SelectQueryBuilder } from 'typeorm'
import { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata' import { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata'
type Join<K, P> = K extends string ? (P extends string ? `${K}${'' extends P ? '' : '.'}${P}` : never) : never type Join<K, P> = K extends string
? P extends string
? `${K}${'' extends P ? '' : '.'}${P | `(${P}` | `${P})`}`
: never
: never
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...0[]] type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...0[]]
@ -33,16 +37,30 @@ export type SortBy<T> = Order<T>[]
export const positiveNumberOrDefault = (value: number | undefined, defaultValue: number, minValue: 0 | 1 = 0) => export const positiveNumberOrDefault = (value: number | undefined, defaultValue: number, minValue: 0 | 1 = 0) =>
value === undefined || value < minValue ? defaultValue : value value === undefined || value < minValue ? defaultValue : value
export type ColumnProperties = { propertyPath?: string; propertyName: string } export type ColumnProperties = { propertyPath?: string; propertyName: string; isEmbedded: boolean; column: string }
export function getPropertiesByColumnName(column: string): ColumnProperties { export function getPropertiesByColumnName(column: string): ColumnProperties {
const propertyPath = column.split('.') const propertyPath = column.split('.')
return propertyPath.length > 1 if (propertyPath.length > 1) {
? { const propertyNamePath = propertyPath.slice(1)
propertyPath: propertyPath[0], let isEmbedded = false,
propertyName: propertyPath.slice(1).join('.'), // the join is in case of an embedded entity propertyName = propertyNamePath.join('.')
}
: { propertyName: propertyPath[0] } if (!propertyName.startsWith('(') && propertyNamePath.length > 1) {
isEmbedded = true
}
propertyName = propertyName.replace('(', '').replace(')', '')
return {
propertyPath: propertyPath[0],
propertyName, // the join is in case of an embedded entity
isEmbedded,
column: `${propertyPath[0]}.${propertyName}`,
}
} else {
return { propertyName: propertyPath[0], isEmbedded: false, column: propertyPath[0] }
}
} }
export function extractVirtualProperty( export function extractVirtualProperty(
@ -106,7 +124,7 @@ export function fixColumnAlias(
if (isRelation) { if (isRelation) {
if (isVirtualProperty && query) { if (isVirtualProperty && query) {
return `(${query(`${alias}_${properties.propertyPath}`)})` // () is needed to avoid parameter conflict return `(${query(`${alias}_${properties.propertyPath}`)})` // () is needed to avoid parameter conflict
} else if (isVirtualProperty && !query) { } else if ((isVirtualProperty && !query) || properties.isEmbedded) {
return `${alias}_${properties.propertyPath}_${properties.propertyName}` return `${alias}_${properties.propertyPath}_${properties.propertyName}`
} else { } else {
return `${alias}_${properties.propertyPath}.${properties.propertyName}` return `${alias}_${properties.propertyPath}.${properties.propertyName}`

View File

@ -882,7 +882,7 @@ describe('paginate', () => {
it('should return result based on sort on embedded entity on one-to-many relation', async () => { it('should return result based on sort on embedded entity on one-to-many relation', async () => {
const config: PaginateConfig<CatEntity> = { const config: PaginateConfig<CatEntity> = {
sortableColumns: ['id', 'name', 'toys.size.height', 'toys.size.length', 'toys.size.width'], sortableColumns: ['id', 'name', 'toys.(size.height)', 'toys.(size.length)', 'toys.(size.width)'],
searchableColumns: ['name'], searchableColumns: ['name'],
relations: ['toys'], relations: ['toys'],
} }
@ -890,8 +890,8 @@ describe('paginate', () => {
path: '', path: '',
sortBy: [ sortBy: [
['id', 'DESC'], ['id', 'DESC'],
['toys.size.height', 'ASC'], ['toys.(size.height)', 'ASC'],
['toys.size.length', 'ASC'], ['toys.(size.length)', 'ASC'],
], ],
} }
@ -918,21 +918,21 @@ describe('paginate', () => {
] ]
expect(result.data).toStrictEqual(orderedCats) expect(result.data).toStrictEqual(orderedCats)
expect(result.links.current).toBe( expect(result.links.current).toBe(
'?page=1&limit=20&sortBy=id:DESC&sortBy=toys.size.height:ASC&sortBy=toys.size.length:ASC' '?page=1&limit=20&sortBy=id:DESC&sortBy=toys.(size.height):ASC&sortBy=toys.(size.length):ASC'
) )
}) })
it('should return result based on sort on embedded entity on many-to-one relation', async () => { it('should return result based on sort on embedded entity on many-to-one relation', async () => {
const config: PaginateConfig<CatToyEntity> = { const config: PaginateConfig<CatToyEntity> = {
sortableColumns: ['id', 'name', 'cat.size.height', 'cat.size.length', 'cat.size.width'], sortableColumns: ['id', 'name', 'cat.(size.height)', 'cat.(size.length)', 'cat.(size.width)'],
searchableColumns: ['name'], searchableColumns: ['name'],
relations: ['cat'], relations: ['cat'],
} }
const query: PaginateQuery = { const query: PaginateQuery = {
path: '', path: '',
sortBy: [ sortBy: [
['cat.size.height', 'DESC'], ['cat.(size.height)', 'DESC'],
['cat.size.length', 'DESC'], ['cat.(size.length)', 'DESC'],
['name', 'ASC'], ['name', 'ASC'],
], ],
} }
@ -942,19 +942,19 @@ describe('paginate', () => {
expect(result.data).toStrictEqual(orderedToys) expect(result.data).toStrictEqual(orderedToys)
expect(result.links.current).toBe( expect(result.links.current).toBe(
'?page=1&limit=20&sortBy=cat.size.height:DESC&sortBy=cat.size.length:DESC&sortBy=name:ASC' '?page=1&limit=20&sortBy=cat.(size.height):DESC&sortBy=cat.(size.length):DESC&sortBy=name:ASC'
) )
}) })
it('should return result based on sort on embedded entity on one-to-one relation', async () => { it('should return result based on sort on embedded entity on one-to-one relation', async () => {
const config: PaginateConfig<CatHomeEntity> = { const config: PaginateConfig<CatHomeEntity> = {
sortableColumns: ['id', 'name', 'cat.size.height', 'cat.size.length', 'cat.size.width'], sortableColumns: ['id', 'name', 'cat.(size.height)', 'cat.(size.length)', 'cat.(size.width)'],
searchableColumns: ['name'], searchableColumns: ['name'],
relations: ['cat'], relations: ['cat'],
} }
const query: PaginateQuery = { const query: PaginateQuery = {
path: '', path: '',
sortBy: [['cat.size.height', 'DESC']], sortBy: [['cat.(size.height)', 'DESC']],
} }
const result = await paginate<CatHomeEntity>(query, catHomeRepo, config) const result = await paginate<CatHomeEntity>(query, catHomeRepo, config)
@ -964,7 +964,7 @@ describe('paginate', () => {
orderedHomes[1].countCat = cats.filter((cat) => cat.id === orderedHomes[1].cat.id).length orderedHomes[1].countCat = cats.filter((cat) => cat.id === orderedHomes[1].cat.id).length
expect(result.data).toStrictEqual(orderedHomes) expect(result.data).toStrictEqual(orderedHomes)
expect(result.links.current).toBe('?page=1&limit=20&sortBy=cat.size.height:DESC') expect(result.links.current).toBe('?page=1&limit=20&sortBy=cat.(size.height):DESC')
}) })
it('should return result based on search on embedded entity', async () => { it('should return result based on search on embedded entity', async () => {
@ -1008,8 +1008,8 @@ describe('paginate', () => {
it('should return result based on search term on embedded entity on many-to-one relation', async () => { it('should return result based on search term on embedded entity on many-to-one relation', async () => {
const config: PaginateConfig<CatToyEntity> = { const config: PaginateConfig<CatToyEntity> = {
sortableColumns: ['id', 'name', 'cat.size.height', 'cat.size.length', 'cat.size.width'], sortableColumns: ['id', 'name', 'cat.(size.height)', 'cat.(size.length)', 'cat.(size.width)'],
searchableColumns: ['cat.size.height'], searchableColumns: ['cat.(size.height)'],
relations: ['cat'], relations: ['cat'],
} }
const query: PaginateQuery = { const query: PaginateQuery = {
@ -1025,8 +1025,8 @@ describe('paginate', () => {
it('should return result based on search term on embedded entity on one-to-many relation', async () => { it('should return result based on search term on embedded entity on one-to-many relation', async () => {
const config: PaginateConfig<CatEntity> = { const config: PaginateConfig<CatEntity> = {
sortableColumns: ['id', 'name', 'toys.size.height', 'toys.size.length', 'toys.size.width'], sortableColumns: ['id', 'name', 'toys.(size.height)', 'toys.(size.length)', 'toys.(size.width)'],
searchableColumns: ['toys.size.height'], searchableColumns: ['toys.(size.height)'],
relations: ['toys'], relations: ['toys'],
} }
const query: PaginateQuery = { const query: PaginateQuery = {
@ -1051,8 +1051,8 @@ describe('paginate', () => {
it('should return result based on search term on embedded entity on one-to-one relation', async () => { it('should return result based on search term on embedded entity on one-to-one relation', async () => {
const config: PaginateConfig<CatHomeEntity> = { const config: PaginateConfig<CatHomeEntity> = {
sortableColumns: ['id', 'name', 'cat.size.height', 'cat.size.length', 'cat.size.width'], sortableColumns: ['id', 'name', 'cat.(size.height)', 'cat.(size.length)', 'cat.(size.width)'],
searchableColumns: ['cat.size.height'], searchableColumns: ['cat.(size.height)'],
relations: ['cat'], relations: ['cat'],
} }
const query: PaginateQuery = { const query: PaginateQuery = {
@ -1069,20 +1069,20 @@ describe('paginate', () => {
it('should return result based on sort and search on embedded many-to-one relation', async () => { it('should return result based on sort and search on embedded many-to-one relation', async () => {
const config: PaginateConfig<CatToyEntity> = { const config: PaginateConfig<CatToyEntity> = {
sortableColumns: ['id', 'name', 'cat.size.height', 'cat.size.length', 'cat.size.width'], sortableColumns: ['id', 'name', 'cat.(size.height)', 'cat.(size.length)', 'cat.(size.width)'],
searchableColumns: ['cat.size.width'], searchableColumns: ['cat.(size.width)'],
relations: ['cat'], relations: ['cat'],
} }
const query: PaginateQuery = { const query: PaginateQuery = {
path: '', path: '',
search: '1', search: '1',
sortBy: [['cat.size.height', 'DESC']], sortBy: [['cat.(size.height)', 'DESC']],
} }
const result = await paginate<CatToyEntity>(query, catToyRepo, config) const result = await paginate<CatToyEntity>(query, catToyRepo, config)
expect(result.meta.search).toStrictEqual('1') expect(result.meta.search).toStrictEqual('1')
expect(result.data).toStrictEqual([catToys[3], catToys[0], catToys[1], catToys[2]]) expect(result.data).toStrictEqual([catToys[3], catToys[0], catToys[1], catToys[2]])
expect(result.links.current).toBe('?page=1&limit=20&sortBy=cat.size.height:DESC&search=1') expect(result.links.current).toBe('?page=1&limit=20&sortBy=cat.(size.height):DESC&search=1')
}) })
it('should return result based on filter on embedded entity', async () => { it('should return result based on filter on embedded entity', async () => {
@ -1143,23 +1143,23 @@ describe('paginate', () => {
relations: ['cat'], relations: ['cat'],
sortableColumns: ['id', 'name'], sortableColumns: ['id', 'name'],
filterableColumns: { filterableColumns: {
'cat.size.height': [FilterSuffix.NOT], 'cat.(size.height)': [FilterSuffix.NOT],
}, },
} }
const query: PaginateQuery = { const query: PaginateQuery = {
path: '', path: '',
filter: { filter: {
'cat.size.height': '$not:25', 'cat.(size.height)': '$not:25',
}, },
} }
const result = await paginate<CatToyEntity>(query, catToyRepo, config) const result = await paginate<CatToyEntity>(query, catToyRepo, config)
expect(result.meta.filter).toStrictEqual({ expect(result.meta.filter).toStrictEqual({
'cat.size.height': '$not:25', 'cat.(size.height)': '$not:25',
}) })
expect(result.data).toStrictEqual([catToys[3]]) expect(result.data).toStrictEqual([catToys[3]])
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.cat.size.height=$not:25') expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.cat.(size.height)=$not:25')
}) })
it('should return result based on filter on embedded on one-to-many relation', async () => { it('should return result based on filter on embedded on one-to-many relation', async () => {
@ -1167,13 +1167,13 @@ describe('paginate', () => {
relations: ['toys'], relations: ['toys'],
sortableColumns: ['id', 'name'], sortableColumns: ['id', 'name'],
filterableColumns: { filterableColumns: {
'toys.size.height': [FilterOperator.EQ], 'toys.(size.height)': [FilterOperator.EQ],
}, },
} }
const query: PaginateQuery = { const query: PaginateQuery = {
path: '', path: '',
filter: { filter: {
'toys.size.height': '1', 'toys.(size.height)': '1',
}, },
} }
@ -1185,10 +1185,10 @@ describe('paginate', () => {
cat2.toys = [catToys3] cat2.toys = [catToys3]
expect(result.meta.filter).toStrictEqual({ expect(result.meta.filter).toStrictEqual({
'toys.size.height': '1', 'toys.(size.height)': '1',
}) })
expect(result.data).toStrictEqual([cat2]) expect(result.data).toStrictEqual([cat2])
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.toys.size.height=1') expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.toys.(size.height)=1')
}) })
it('should return result based on filter on embedded on one-to-one relation', async () => { it('should return result based on filter on embedded on one-to-one relation', async () => {
@ -1196,25 +1196,25 @@ describe('paginate', () => {
relations: ['cat'], relations: ['cat'],
sortableColumns: ['id', 'name'], sortableColumns: ['id', 'name'],
filterableColumns: { filterableColumns: {
'cat.size.height': [FilterOperator.EQ], 'cat.(size.height)': [FilterOperator.EQ],
}, },
} }
const query: PaginateQuery = { const query: PaginateQuery = {
path: '', path: '',
filter: { filter: {
'cat.size.height': '$eq:30', 'cat.(size.height)': '$eq:30',
}, },
} }
const result = await paginate<CatHomeEntity>(query, catHomeRepo, config) const result = await paginate<CatHomeEntity>(query, catHomeRepo, config)
expect(result.meta.filter).toStrictEqual({ expect(result.meta.filter).toStrictEqual({
'cat.size.height': '$eq:30', 'cat.(size.height)': '$eq:30',
}) })
const catClone = clone(catHomes[1]) const catClone = clone(catHomes[1])
catClone.countCat = cats.filter((cat) => cat.size.height === 30 && cat.id == catClone.cat.id).length catClone.countCat = cats.filter((cat) => cat.size.height === 30 && cat.id == catClone.cat.id).length
expect(result.data).toStrictEqual([catClone]) expect(result.data).toStrictEqual([catClone])
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.cat.size.height=$eq:30') expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.cat.(size.height)=$eq:30')
}) })
it('should return result based on $in filter on embedded on one-to-one relation', async () => { it('should return result based on $in filter on embedded on one-to-one relation', async () => {
@ -1222,20 +1222,20 @@ describe('paginate', () => {
relations: ['cat'], relations: ['cat'],
sortableColumns: ['id', 'name'], sortableColumns: ['id', 'name'],
filterableColumns: { filterableColumns: {
'cat.size.height': [FilterOperator.IN], 'cat.(size.height)': [FilterOperator.IN],
}, },
} }
const query: PaginateQuery = { const query: PaginateQuery = {
path: '', path: '',
filter: { filter: {
'cat.size.height': '$in:10,30,35', 'cat.(size.height)': '$in:10,30,35',
}, },
} }
const result = await paginate<CatHomeEntity>(query, catHomeRepo, config) const result = await paginate<CatHomeEntity>(query, catHomeRepo, config)
expect(result.meta.filter).toStrictEqual({ expect(result.meta.filter).toStrictEqual({
'cat.size.height': '$in:10,30,35', 'cat.(size.height)': '$in:10,30,35',
}) })
const catClone = clone(catHomes[1]) const catClone = clone(catHomes[1])
catClone.countCat = cats.filter( catClone.countCat = cats.filter(
@ -1244,7 +1244,7 @@ describe('paginate', () => {
cat.id == catClone.cat.id cat.id == catClone.cat.id
).length ).length
expect(result.data).toStrictEqual([catClone]) expect(result.data).toStrictEqual([catClone])
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.cat.size.height=$in:10,30,35') expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.cat.(size.height)=$in:10,30,35')
}) })
it('should return result based on $btw filter on embedded on one-to-one relation', async () => { it('should return result based on $btw filter on embedded on one-to-one relation', async () => {
@ -1252,20 +1252,20 @@ describe('paginate', () => {
relations: ['cat'], relations: ['cat'],
sortableColumns: ['id', 'name'], sortableColumns: ['id', 'name'],
filterableColumns: { filterableColumns: {
'cat.size.height': [FilterOperator.BTW], 'cat.(size.height)': [FilterOperator.BTW],
}, },
} }
const query: PaginateQuery = { const query: PaginateQuery = {
path: '', path: '',
filter: { filter: {
'cat.size.height': '$btw:18,33', 'cat.(size.height)': '$btw:18,33',
}, },
} }
const result = await paginate<CatHomeEntity>(query, catHomeRepo, config) const result = await paginate<CatHomeEntity>(query, catHomeRepo, config)
expect(result.meta.filter).toStrictEqual({ expect(result.meta.filter).toStrictEqual({
'cat.size.height': '$btw:18,33', 'cat.(size.height)': '$btw:18,33',
}) })
const catHomeClone = clone(catHomes) const catHomeClone = clone(catHomes)
@ -1276,7 +1276,7 @@ describe('paginate', () => {
(cat) => cat.size.height >= 18 && cat.size.height <= 33 && cat.id == catHomeClone[1].cat.id (cat) => cat.size.height >= 18 && cat.size.height <= 33 && cat.id == catHomeClone[1].cat.id
).length ).length
expect(result.data).toStrictEqual([catHomeClone[0], catHomeClone[1]]) expect(result.data).toStrictEqual([catHomeClone[0], catHomeClone[1]])
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.cat.size.height=$btw:18,33') expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.cat.(size.height)=$btw:18,33')
}) })
it('should return result based on where array and filter', async () => { it('should return result based on where array and filter', async () => {
@ -1954,12 +1954,12 @@ describe('paginate', () => {
it('should return selected columns', async () => { it('should return selected columns', async () => {
const config: PaginateConfig<CatEntity> = { const config: PaginateConfig<CatEntity> = {
sortableColumns: ['id', 'name'], sortableColumns: ['id', 'name'],
select: ['id', 'name', 'toys.name', 'toys.size.height', 'toys.size.length'], select: ['id', 'name', 'toys.name', 'toys.(size.height)', 'toys.(size.length)'],
relations: ['toys'], relations: ['toys'],
} }
const query: PaginateQuery = { const query: PaginateQuery = {
path: '', path: '',
select: ['id', 'toys.size.height'], select: ['id', 'toys.(size.height)'],
} }
const result = await paginate<CatEntity>(query, catRepo, config) const result = await paginate<CatEntity>(query, catRepo, config)
@ -2015,4 +2015,62 @@ describe('paginate', () => {
expect(result.data[0].toys).toHaveLength(1) expect(result.data[0].toys).toHaveLength(1)
}) })
it('should search nested relations', async () => {
const config: PaginateConfig<CatEntity> = {
relations: { home: { pillows: true } },
sortableColumns: ['id', 'name'],
searchableColumns: ['name', 'home.pillows.color'],
}
const query: PaginateQuery = {
path: '',
search: 'pink',
}
const result = await paginate<CatEntity>(query, catRepo, config)
const cat = clone(cats[1])
const catHomesClone = clone(catHomes[1])
const catHomePillowsClone = clone(catHomePillows[3])
delete catHomePillowsClone.home
catHomesClone.countCat = cats.filter((cat) => cat.id === catHomesClone.cat.id).length
catHomesClone.pillows = [catHomePillowsClone]
cat.home = catHomesClone
delete cat.home.cat
expect(result.meta.search).toStrictEqual('pink')
expect(result.data).toStrictEqual([cat])
expect(result.data[0].home).toBeDefined()
expect(result.data[0].home.pillows).toStrictEqual(cat.home.pillows)
})
it('should filter nested relations', async () => {
const config: PaginateConfig<CatEntity> = {
relations: { home: { pillows: true } },
sortableColumns: ['id', 'name'],
filterableColumns: { 'home.pillows.color': [FilterOperator.EQ] },
}
const query: PaginateQuery = {
path: '',
filter: { 'home.pillows.color': 'pink' },
}
const result = await paginate<CatEntity>(query, catRepo, config)
const cat = clone(cats[1])
const catHomesClone = clone(catHomes[1])
const catHomePillowsClone = clone(catHomePillows[3])
delete catHomePillowsClone.home
catHomesClone.countCat = cats.filter((cat) => cat.id === catHomesClone.cat.id).length
catHomesClone.pillows = [catHomePillowsClone]
cat.home = catHomesClone
delete cat.home.cat
expect(result.meta.filter['home.pillows.color']).toStrictEqual('pink')
expect(result.data).toStrictEqual([cat])
expect(result.data[0].home).toBeDefined()
expect(result.data[0].home.pillows).toStrictEqual(cat.home.pillows)
})
}) })

View File

@ -62,13 +62,13 @@ export interface PaginateConfig<T> {
sortableColumns: Column<T>[] sortableColumns: Column<T>[]
nullSort?: 'first' | 'last' nullSort?: 'first' | 'last'
searchableColumns?: Column<T>[] searchableColumns?: Column<T>[]
select?: Column<T>[] select?: Column<T>[] | string[]
maxLimit?: number maxLimit?: number
defaultSortBy?: SortBy<T> defaultSortBy?: SortBy<T>
defaultLimit?: number defaultLimit?: number
where?: FindOptionsWhere<T> | FindOptionsWhere<T>[] where?: FindOptionsWhere<T> | FindOptionsWhere<T>[]
filterableColumns?: { filterableColumns?: {
[key in Column<T>]?: (FilterOperator | FilterSuffix)[] [key in Column<T> | string]?: (FilterOperator | FilterSuffix)[]
} }
loadEagerRelations?: boolean loadEagerRelations?: boolean
withDeleted?: boolean withDeleted?: boolean
@ -264,9 +264,10 @@ export async function paginate<T extends ObjectLiteral>(
isEmbeded, isEmbeded,
virtualQuery virtualQuery
) )
const condition: WherePredicateOperator = { const condition: WherePredicateOperator = {
operator: 'ilike', operator: 'ilike',
parameters: [alias, `:${column}`], parameters: [alias, `:${property.column}`],
} }
if (['postgres', 'cockroachdb'].includes(queryBuilder.connection.options.type)) { if (['postgres', 'cockroachdb'].includes(queryBuilder.connection.options.type)) {
@ -274,7 +275,7 @@ export async function paginate<T extends ObjectLiteral>(
} }
qb.orWhere(qb['createWhereConditionExpression'](condition), { qb.orWhere(qb['createWhereConditionExpression'](condition), {
[column]: `%${query.search}%`, [property.column]: `%${query.search}%`,
}) })
} }
}) })