feat: virtual column support (#434)
This commit is contained in:
parent
ff00785cd1
commit
77494531d3
@ -21,7 +21,7 @@
|
||||
"scripts": {
|
||||
"prebuild": "rimraf lib",
|
||||
"build": "tsc",
|
||||
"dev:yalc": "watch 'npm run build && yalc push' src",
|
||||
"dev:yalc": "nodemon --watch src --ext ts --exec 'npm run build && yalc push'",
|
||||
"format": "prettier --write \"src/**/*.ts\"",
|
||||
"format:ci": "prettier --list-different \"src/**/*.ts\"",
|
||||
"lint": "eslint -c .eslintrc.json --ext .ts --max-warnings 0 src",
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Column, CreateDateColumn, Entity, OneToOne, PrimaryGeneratedColumn } from 'typeorm'
|
||||
import { Column, CreateDateColumn, Entity, OneToOne, PrimaryGeneratedColumn, VirtualColumn } from 'typeorm'
|
||||
import { CatEntity } from './cat.entity'
|
||||
|
||||
@Entity()
|
||||
@ -14,4 +14,9 @@ export class CatHomeEntity {
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: string
|
||||
|
||||
@VirtualColumn({
|
||||
query: (alias) => `SELECT CAST(COUNT(*) AS INT) FROM "cat" WHERE "cat"."homeId" = ${alias}.id`,
|
||||
})
|
||||
countCat: number
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {
|
||||
AfterLoad,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
@ -12,7 +13,7 @@ import { CatToyEntity } from './cat-toy.entity'
|
||||
import { CatHomeEntity } from './cat-home.entity'
|
||||
import { SizeEmbed } from './size.embed'
|
||||
|
||||
@Entity()
|
||||
@Entity({ name: 'cat' })
|
||||
export class CatEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number
|
||||
@ -41,4 +42,13 @@ export class CatEntity {
|
||||
|
||||
@DeleteDateColumn({ nullable: true })
|
||||
deletedAt?: string
|
||||
|
||||
@AfterLoad()
|
||||
// Fix due to typeorm bug that doesn't set entity to null
|
||||
// when the reletated entity have only the virtual column property with a value different from null
|
||||
private afterLoad() {
|
||||
if (this.home && !this.home?.id) {
|
||||
this.home = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
85
src/filter.ts
Normal file
85
src/filter.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { FindOperator, SelectQueryBuilder } from 'typeorm'
|
||||
import { WherePredicateOperator } from 'typeorm/query-builder/WhereClause'
|
||||
import { checkIsRelation, extractVirtualProperty, fixColumnAlias, getPropertiesByColumnName } from './helper'
|
||||
|
||||
export type Filter = { [columnName: string]: FindOperator<string> }
|
||||
|
||||
export function generatePredicateCondition(
|
||||
qb: SelectQueryBuilder<unknown>,
|
||||
column: string,
|
||||
filter: Filter,
|
||||
alias: string,
|
||||
isVirtualProperty = false
|
||||
): WherePredicateOperator {
|
||||
return qb['getWherePredicateCondition'](
|
||||
isVirtualProperty ? column : alias,
|
||||
filter[column]
|
||||
) as WherePredicateOperator
|
||||
}
|
||||
|
||||
// This function is used to fix the query parameters when using relation, embeded or virtual properties
|
||||
// It will replace the column name with the alias name and return the new parameters
|
||||
function fixQueryParam(
|
||||
alias: string,
|
||||
column: string,
|
||||
filter: Filter,
|
||||
condition: WherePredicateOperator,
|
||||
parameters: { [key: string]: string }
|
||||
): { [key: string]: string } {
|
||||
const isNotOperator = (condition.operator as string) === 'not'
|
||||
|
||||
const conditionFixer = (
|
||||
alias: string,
|
||||
column: string,
|
||||
filter: Filter,
|
||||
operator: WherePredicateOperator['operator'],
|
||||
parameters: { [key: string]: string }
|
||||
): { condition_params: any; params: any } => {
|
||||
let condition_params: any = undefined
|
||||
let params = parameters
|
||||
switch (operator) {
|
||||
case 'between':
|
||||
condition_params = [alias, `:${column}_from`, `:${column}_to`]
|
||||
params = {
|
||||
[column + '_from']: filter[column].value[0],
|
||||
[column + '_to']: filter[column].value[1],
|
||||
}
|
||||
break
|
||||
case 'in':
|
||||
condition_params = [alias, `:...${column}`]
|
||||
break
|
||||
default:
|
||||
condition_params = [alias, `:${column}`]
|
||||
break
|
||||
}
|
||||
return { condition_params, params }
|
||||
}
|
||||
|
||||
const { condition_params, params } = conditionFixer(
|
||||
alias,
|
||||
column,
|
||||
filter,
|
||||
isNotOperator ? condition['condition']['operator'] : condition.operator,
|
||||
parameters
|
||||
)
|
||||
|
||||
if (isNotOperator) {
|
||||
condition['condition']['parameters'] = condition_params
|
||||
} else {
|
||||
condition.parameters = condition_params
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
export function addWhereCondition(qb: SelectQueryBuilder<unknown>, column: string, filter: Filter) {
|
||||
const columnProperties = getPropertiesByColumnName(column)
|
||||
const { isVirtualProperty, query: virtualQuery } = extractVirtualProperty(qb, columnProperties)
|
||||
const isRelation = checkIsRelation(qb, columnProperties.propertyPath)
|
||||
const alias = fixColumnAlias(columnProperties, qb.alias, isRelation, isVirtualProperty, virtualQuery)
|
||||
const condition = generatePredicateCondition(qb, column, filter, alias, isVirtualProperty)
|
||||
const parameters = fixQueryParam(alias, column, filter, condition, {
|
||||
[column]: filter[column].value,
|
||||
})
|
||||
qb.andWhere(qb['createWhereConditionExpression'](condition), parameters)
|
||||
}
|
@ -1,3 +1,6 @@
|
||||
import { SelectQueryBuilder } from 'typeorm'
|
||||
import { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata'
|
||||
|
||||
type Join<K, P> = K extends string ? (P extends string ? `${K}${'' extends P ? '' : '.'}${P}` : never) : never
|
||||
|
||||
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...0[]]
|
||||
@ -29,3 +32,61 @@ export type SortBy<T> = Order<T>[]
|
||||
|
||||
export const positiveNumberOrDefault = (value: number | undefined, defaultValue: number, minValue: 0 | 1 = 0) =>
|
||||
value === undefined || value < minValue ? defaultValue : value
|
||||
|
||||
type ColumnProperties = { propertyPath?: string; propertyName: string }
|
||||
|
||||
export function getPropertiesByColumnName(column: string): ColumnProperties {
|
||||
const propertyPath = column.split('.')
|
||||
return propertyPath.length > 1
|
||||
? {
|
||||
propertyPath: propertyPath[0],
|
||||
propertyName: propertyPath.slice(1).join('.'), // the join is in case of an embedded entity
|
||||
}
|
||||
: { propertyName: propertyPath[0] }
|
||||
}
|
||||
|
||||
export function extractVirtualProperty(
|
||||
qb: SelectQueryBuilder<unknown>,
|
||||
columnProperties: ColumnProperties
|
||||
): { isVirtualProperty: boolean; query?: ColumnMetadata['query'] } {
|
||||
const metadata = columnProperties.propertyPath
|
||||
? qb?.expressionMap?.mainAlias?.metadata?.findColumnWithPropertyPath(columnProperties.propertyPath)
|
||||
?.referencedColumn?.entityMetadata // on relation
|
||||
: qb?.expressionMap?.mainAlias?.metadata
|
||||
return (
|
||||
metadata?.columns?.find((column) => column.propertyName === columnProperties.propertyName) || {
|
||||
isVirtualProperty: false,
|
||||
query: undefined,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function checkIsRelation(qb: SelectQueryBuilder<unknown>, propertyPath: string): boolean {
|
||||
if (!qb || !propertyPath) {
|
||||
return false
|
||||
}
|
||||
return !!qb?.expressionMap?.mainAlias?.metadata?.hasRelationWithPropertyPath(propertyPath)
|
||||
}
|
||||
|
||||
// This function is used to fix the column alias when using relation, embedded or virtual properties
|
||||
export function fixColumnAlias(
|
||||
properties: ColumnProperties,
|
||||
alias: string,
|
||||
isRelation = false,
|
||||
isVirtualProperty = false,
|
||||
query?: ColumnMetadata['query']
|
||||
): string {
|
||||
if (isRelation) {
|
||||
if (isVirtualProperty && query) {
|
||||
return `(${query(`${alias}_${properties.propertyPath}`)})` // () is needed to avoid parameter conflict
|
||||
} else if (isVirtualProperty && !query) {
|
||||
return `${alias}_${properties.propertyPath}_${properties.propertyName}`
|
||||
} else {
|
||||
return `${alias}_${properties.propertyPath}.${properties.propertyName}` // include embeded property and relation property
|
||||
}
|
||||
} else if (isVirtualProperty) {
|
||||
return query ? `(${query(`${alias}`)})` : `${alias}_${properties.propertyName}`
|
||||
} else {
|
||||
return `${alias}.${properties.propertyName}` //
|
||||
}
|
||||
}
|
||||
|
@ -466,7 +466,8 @@ describe('paginate', () => {
|
||||
delete toy.cat
|
||||
const toy2 = clone(catToys[2])
|
||||
delete toy2.cat
|
||||
expect(result.data).toStrictEqual([Object.assign(clone(cats[0]), { toys: [toy, toy2] })])
|
||||
|
||||
expect(result.data).toStrictEqual([Object.assign(clone(cats[0]), { toys: [toy2, toy] })])
|
||||
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&search=Mouse')
|
||||
})
|
||||
|
||||
@ -482,7 +483,12 @@ describe('paginate', () => {
|
||||
|
||||
const result = await paginate<CatHomeEntity>(query, catHomeRepo, config)
|
||||
expect(result.meta.sortBy).toStrictEqual([['cat.id', 'DESC']])
|
||||
expect(result.data).toStrictEqual([catHomes[0], catHomes[1]].sort((a, b) => b.cat.id - a.cat.id))
|
||||
|
||||
const catHomesClone = clone([catHomes[0], catHomes[1]])
|
||||
catHomesClone[0].countCat = cats.filter((cat) => cat.id === catHomesClone[0].cat.id).length
|
||||
catHomesClone[1].countCat = cats.filter((cat) => cat.id === catHomesClone[1].cat.id).length
|
||||
|
||||
expect(result.data).toStrictEqual(catHomesClone.sort((a, b) => b.cat.id - a.cat.id))
|
||||
expect(result.links.current).toBe('?page=1&limit=20&sortBy=cat.id:DESC')
|
||||
})
|
||||
|
||||
@ -542,7 +548,11 @@ describe('paginate', () => {
|
||||
const result = await paginate<CatHomeEntity>(query, catHomeRepo, config)
|
||||
|
||||
expect(result.meta.search).toStrictEqual('Garfield')
|
||||
expect(result.data).toStrictEqual([catHomes[1]])
|
||||
|
||||
const catHomesClone = clone(catHomes[1])
|
||||
catHomesClone.countCat = cats.filter((cat) => cat.id === catHomesClone.cat.id).length
|
||||
|
||||
expect(result.data).toStrictEqual([catHomesClone])
|
||||
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&search=Garfield')
|
||||
})
|
||||
|
||||
@ -674,7 +684,11 @@ describe('paginate', () => {
|
||||
expect(result.meta.filter).toStrictEqual({
|
||||
'cat.name': '$not:Garfield',
|
||||
})
|
||||
expect(result.data).toStrictEqual([catHomes[0]])
|
||||
|
||||
const catHomesClone = clone(catHomes[0])
|
||||
catHomesClone.countCat = cats.filter((cat) => cat.id === catHomesClone.cat.id).length
|
||||
|
||||
expect(result.data).toStrictEqual([catHomesClone])
|
||||
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.cat.name=$not:Garfield')
|
||||
})
|
||||
|
||||
@ -698,7 +712,11 @@ describe('paginate', () => {
|
||||
expect(result.meta.filter).toStrictEqual({
|
||||
'cat.age': '$in:4,6',
|
||||
})
|
||||
expect(result.data).toStrictEqual([catHomes[0]])
|
||||
|
||||
const catHomesClone = clone(catHomes[0])
|
||||
catHomesClone.countCat = cats.filter((cat) => cat.id === catHomesClone.cat.id).length
|
||||
|
||||
expect(result.data).toStrictEqual([catHomesClone])
|
||||
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.cat.age=$in:4,6')
|
||||
})
|
||||
|
||||
@ -722,7 +740,11 @@ describe('paginate', () => {
|
||||
expect(result.meta.filter).toStrictEqual({
|
||||
'cat.age': '$btw:6,10',
|
||||
})
|
||||
expect(result.data).toStrictEqual([catHomes[0]])
|
||||
|
||||
const catHomesClone = clone(catHomes[0])
|
||||
catHomesClone.countCat = cats.filter((cat) => cat.id === catHomesClone.cat.id).length
|
||||
|
||||
expect(result.data).toStrictEqual([catHomesClone])
|
||||
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.cat.age=$btw:6,10')
|
||||
})
|
||||
|
||||
@ -771,9 +793,11 @@ describe('paginate', () => {
|
||||
|
||||
const copyHomes = catHomes.map((home: CatHomeEntity) => {
|
||||
const copy = clone(home)
|
||||
copy.countCat = cats.filter((cat) => cat.id === copy.cat.id).length
|
||||
delete copy.cat
|
||||
return copy
|
||||
})
|
||||
|
||||
copyCats[0].home = copyHomes[0]
|
||||
copyCats[1].home = copyHomes[1]
|
||||
|
||||
@ -782,7 +806,7 @@ describe('paginate', () => {
|
||||
delete copy.cat
|
||||
return copy
|
||||
})
|
||||
copyCats[0].toys = [copyToys[0], copyToys[1], copyToys[2]]
|
||||
copyCats[0].toys = [copyToys[0], copyToys[2], copyToys[1]]
|
||||
copyCats[1].toys = [copyToys[3]]
|
||||
|
||||
const orderedCats = [copyCats[3], copyCats[1], copyCats[2], copyCats[0], copyCats[4]]
|
||||
@ -869,7 +893,10 @@ describe('paginate', () => {
|
||||
}
|
||||
|
||||
const result = await paginate<CatHomeEntity>(query, catHomeRepo, config)
|
||||
const orderedHomes = [catHomes[1], catHomes[0]]
|
||||
const orderedHomes = clone([catHomes[1], catHomes[0]])
|
||||
|
||||
orderedHomes[0].countCat = cats.filter((cat) => cat.id === orderedHomes[0].cat.id).length
|
||||
orderedHomes[1].countCat = cats.filter((cat) => cat.id === orderedHomes[1].cat.id).length
|
||||
|
||||
expect(result.data).toStrictEqual(orderedHomes)
|
||||
expect(result.links.current).toBe('?page=1&limit=20&sortBy=cat.size.height:DESC')
|
||||
@ -969,8 +996,9 @@ describe('paginate', () => {
|
||||
}
|
||||
|
||||
const result = await paginate<CatHomeEntity>(query, catHomeRepo, config)
|
||||
|
||||
expect(result.data).toStrictEqual([catHomes[1]])
|
||||
const catHomeClone = clone(catHomes[1])
|
||||
catHomeClone.countCat = cats.filter((cat) => cat.id === catHomeClone.cat.id).length
|
||||
expect(result.data).toStrictEqual([catHomeClone])
|
||||
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&search=30')
|
||||
})
|
||||
|
||||
@ -1032,6 +1060,7 @@ describe('paginate', () => {
|
||||
const result = await paginate<CatEntity>(query, catRepo, config)
|
||||
|
||||
const home = clone(catHomes[1])
|
||||
home.countCat = cats.filter((cat) => cat.id === home.cat.id).length
|
||||
delete home.cat
|
||||
|
||||
const copyCats = [
|
||||
@ -1117,7 +1146,9 @@ describe('paginate', () => {
|
||||
expect(result.meta.filter).toStrictEqual({
|
||||
'cat.size.height': '$eq:30',
|
||||
})
|
||||
expect(result.data).toStrictEqual([catHomes[1]])
|
||||
const catClone = clone(catHomes[1])
|
||||
catClone.countCat = cats.filter((cat) => cat.size.height === 30 && cat.id == catClone.cat.id).length
|
||||
expect(result.data).toStrictEqual([catClone])
|
||||
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.cat.size.height=$eq:30')
|
||||
})
|
||||
|
||||
@ -1141,7 +1172,13 @@ describe('paginate', () => {
|
||||
expect(result.meta.filter).toStrictEqual({
|
||||
'cat.size.height': '$in:10,30,35',
|
||||
})
|
||||
expect(result.data).toStrictEqual([catHomes[1]])
|
||||
const catClone = clone(catHomes[1])
|
||||
catClone.countCat = cats.filter(
|
||||
(cat) =>
|
||||
(cat.size.height === 10 || cat.size.height === 30 || cat.size.height === 35) &&
|
||||
cat.id == catClone.cat.id
|
||||
).length
|
||||
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')
|
||||
})
|
||||
|
||||
@ -1165,7 +1202,15 @@ describe('paginate', () => {
|
||||
expect(result.meta.filter).toStrictEqual({
|
||||
'cat.size.height': '$btw:18,33',
|
||||
})
|
||||
expect(result.data).toStrictEqual([catHomes[0], catHomes[1]])
|
||||
|
||||
const catHomeClone = clone(catHomes)
|
||||
catHomeClone[0].countCat = cats.filter(
|
||||
(cat) => cat.size.height >= 18 && cat.size.height <= 33 && cat.id == catHomeClone[0].cat.id
|
||||
).length
|
||||
catHomeClone[1].countCat = cats.filter(
|
||||
(cat) => cat.size.height >= 18 && cat.size.height <= 33 && cat.id == catHomeClone[1].cat.id
|
||||
).length
|
||||
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')
|
||||
})
|
||||
|
||||
@ -1376,26 +1421,6 @@ describe('paginate', () => {
|
||||
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.age=$not:$null')
|
||||
})
|
||||
|
||||
it('should return result based on not null query', async () => {
|
||||
const config: PaginateConfig<CatEntity> = {
|
||||
sortableColumns: ['id'],
|
||||
filterableColumns: {
|
||||
age: [FilterOperator.NOT, FilterOperator.NULL],
|
||||
},
|
||||
}
|
||||
const query: PaginateQuery = {
|
||||
path: '',
|
||||
filter: {
|
||||
age: '$not:$null',
|
||||
},
|
||||
}
|
||||
|
||||
const result = await paginate<CatEntity>(query, catRepo, config)
|
||||
|
||||
expect(result.data).toStrictEqual([cats[0], cats[1], cats[2], cats[3]])
|
||||
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.age=$not:$null')
|
||||
})
|
||||
|
||||
it('should return result based on not null query on relation', async () => {
|
||||
const config: PaginateConfig<CatEntity> = {
|
||||
sortableColumns: ['id'],
|
||||
@ -1527,6 +1552,82 @@ describe('paginate', () => {
|
||||
expect(getFilterTokens(string)).toStrictEqual(tokens)
|
||||
})
|
||||
|
||||
it('should return result based on virtualcolumn filter', async () => {
|
||||
const config: PaginateConfig<CatEntity> = {
|
||||
sortableColumns: ['id'],
|
||||
filterableColumns: {
|
||||
'home.countCat': [FilterOperator.GT],
|
||||
},
|
||||
relations: ['home'],
|
||||
}
|
||||
const query: PaginateQuery = {
|
||||
path: '',
|
||||
filter: {
|
||||
'home.countCat': '$gt:0',
|
||||
},
|
||||
sortBy: [['id', 'ASC']],
|
||||
}
|
||||
|
||||
const result = await paginate<CatEntity>(query, catRepo, config)
|
||||
const expectedResult = [0, 1].map((i) => {
|
||||
const ret = Object.assign(clone(cats[i]), { home: Object.assign(clone(catHomes[i])) })
|
||||
delete ret.home.cat
|
||||
return ret
|
||||
})
|
||||
|
||||
expect(result.data).toStrictEqual(expectedResult)
|
||||
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.home.countCat=$gt:0')
|
||||
})
|
||||
|
||||
it('should return result sorted by a virtualcolumn', async () => {
|
||||
const config: PaginateConfig<CatEntity> = {
|
||||
sortableColumns: ['home.countCat'],
|
||||
relations: ['home'],
|
||||
}
|
||||
const query: PaginateQuery = {
|
||||
path: '',
|
||||
sortBy: [['home.countCat', 'ASC']],
|
||||
}
|
||||
|
||||
const result = await paginate<CatEntity>(query, catRepo, config)
|
||||
const expectedResult = [2, 3, 4, 0, 1].map((i) => {
|
||||
const ret = clone(cats[i])
|
||||
if (i == 0 || i == 1) {
|
||||
ret.home = clone(catHomes[i])
|
||||
ret.home.countCat = cats.filter((cat) => cat.id === ret.home.cat.id).length
|
||||
delete ret.home.cat
|
||||
} else {
|
||||
ret.home = null
|
||||
}
|
||||
return ret
|
||||
})
|
||||
|
||||
expect(result.data).toStrictEqual(expectedResult)
|
||||
expect(result.links.current).toBe('?page=1&limit=20&sortBy=home.countCat:ASC')
|
||||
})
|
||||
|
||||
it('should return result sorted and filter by a virtualcolumn in main entity', async () => {
|
||||
const config: PaginateConfig<CatHomeEntity> = {
|
||||
sortableColumns: ['countCat'],
|
||||
relations: ['cat'],
|
||||
filterableColumns: {
|
||||
countCat: [FilterOperator.GT],
|
||||
},
|
||||
}
|
||||
const query: PaginateQuery = {
|
||||
path: '',
|
||||
filter: {
|
||||
countCat: '$gt:0',
|
||||
},
|
||||
sortBy: [['countCat', 'ASC']],
|
||||
}
|
||||
|
||||
const result = await paginate<CatHomeEntity>(query, catHomeRepo, config)
|
||||
|
||||
expect(result.data).toStrictEqual([catHomes[0], catHomes[1]])
|
||||
expect(result.links.current).toBe('?page=1&limit=20&sortBy=countCat:ASC&filter.countCat=$gt:0')
|
||||
})
|
||||
|
||||
it('should return all items even if deleted', async () => {
|
||||
const config: PaginateConfig<CatEntity> = {
|
||||
sortableColumns: ['id'],
|
||||
|
101
src/paginate.ts
101
src/paginate.ts
@ -21,7 +21,18 @@ import { ServiceUnavailableException, Logger } from '@nestjs/common'
|
||||
import { values, mapKeys } from 'lodash'
|
||||
import { stringify } from 'querystring'
|
||||
import { WherePredicateOperator } from 'typeorm/query-builder/WhereClause'
|
||||
import { Column, Order, positiveNumberOrDefault, RelationColumn, SortBy } from './helper'
|
||||
import {
|
||||
checkIsRelation,
|
||||
Column,
|
||||
extractVirtualProperty,
|
||||
fixColumnAlias,
|
||||
getPropertiesByColumnName,
|
||||
Order,
|
||||
positiveNumberOrDefault,
|
||||
RelationColumn,
|
||||
SortBy,
|
||||
} from './helper'
|
||||
import { addWhereCondition, Filter } from './filter'
|
||||
|
||||
const logger: Logger = new Logger('nestjs-paginate')
|
||||
|
||||
@ -120,8 +131,8 @@ export function getFilterTokens(raw: string): string[] {
|
||||
return tokens
|
||||
}
|
||||
|
||||
function parseFilter<T>(query: PaginateQuery, config: PaginateConfig<T>) {
|
||||
const filter: { [columnName: string]: FindOperator<string> } = {}
|
||||
function parseFilter<T>(query: PaginateQuery, config: PaginateConfig<T>): Filter {
|
||||
const filter: Filter = {}
|
||||
let filterableColumns = config.filterableColumns
|
||||
if (filterableColumns === undefined) {
|
||||
logger.debug("No 'filterableColumns' given, ignoring filters.")
|
||||
@ -251,7 +262,11 @@ export async function paginate<T extends ObjectLiteral>(
|
||||
const queryBuilder = repo instanceof Repository ? repo.createQueryBuilder('e') : repo
|
||||
|
||||
if (isPaginated) {
|
||||
queryBuilder.take(limit).skip((page - 1) * limit)
|
||||
// Switch from take and skip to limit and offset
|
||||
// due to this problem https://github.com/typeorm/typeorm/issues/5670
|
||||
// (anyway this creates more clean query without double dinstict)
|
||||
queryBuilder.limit(limit).offset((page - 1) * limit)
|
||||
// queryBuilder.take(limit).skip((page - 1) * limit)
|
||||
}
|
||||
|
||||
if (config.relations?.length) {
|
||||
@ -266,11 +281,13 @@ export async function paginate<T extends ObjectLiteral>(
|
||||
}
|
||||
|
||||
for (const order of sortBy) {
|
||||
if (queryBuilder.expressionMap.mainAlias.metadata.hasRelationWithPropertyPath(order[0].split('.')[0])) {
|
||||
queryBuilder.addOrderBy(`${queryBuilder.alias}_${order[0]}`, order[1], nullSort)
|
||||
} else {
|
||||
queryBuilder.addOrderBy(`${queryBuilder.alias}.${order[0]}`, order[1], nullSort)
|
||||
}
|
||||
const columnProperties = getPropertiesByColumnName(order[0])
|
||||
const { isVirtualProperty } = extractVirtualProperty(queryBuilder, columnProperties)
|
||||
const isRelation = checkIsRelation(queryBuilder, columnProperties.propertyPath)
|
||||
|
||||
const alias = fixColumnAlias(columnProperties, queryBuilder.alias, isRelation, isVirtualProperty)
|
||||
|
||||
queryBuilder.addOrderBy(alias, order[1], nullSort)
|
||||
}
|
||||
|
||||
if (config.select?.length > 0) {
|
||||
@ -297,25 +314,23 @@ export async function paginate<T extends ObjectLiteral>(
|
||||
queryBuilder.andWhere(
|
||||
new Brackets((qb: SelectQueryBuilder<T>) => {
|
||||
for (const column of searchBy) {
|
||||
const propertyPath = (column as string).split('.')
|
||||
const hasRelation =
|
||||
propertyPath.length > 1 &&
|
||||
queryBuilder.expressionMap.mainAlias.metadata.hasRelationWithPropertyPath(propertyPath[0])
|
||||
const property = getPropertiesByColumnName(column)
|
||||
const { isVirtualProperty, query: virtualQuery } = extractVirtualProperty(qb, property)
|
||||
const isRelation = checkIsRelation(qb, property.propertyPath)
|
||||
|
||||
const alias = fixColumnAlias(property, qb.alias, isRelation, isVirtualProperty, virtualQuery)
|
||||
const condition: WherePredicateOperator = {
|
||||
operator: 'ilike',
|
||||
parameters: [alias, `:${column}`],
|
||||
}
|
||||
|
||||
if (['postgres', 'cockroachdb'].includes(queryBuilder.connection.options.type)) {
|
||||
const alias = hasRelation ? `"${qb.alias}"_` : `"${qb.alias}".`
|
||||
let columns = ''
|
||||
|
||||
for (const property of propertyPath) {
|
||||
columns += `"${property}".`
|
||||
}
|
||||
const aliasColumn = alias + columns.substring(0, columns.length - 1)
|
||||
|
||||
qb.orWhere(`${aliasColumn}::text ILIKE(:search)`, { search: `%${query.search}%` })
|
||||
} else {
|
||||
const aliasColumn = hasRelation ? `${qb.alias}_${column}` : `${qb.alias}.${column}`
|
||||
qb.orWhere(`UPPER(${aliasColumn}) LIKE UPPER(:search)`, { search: `%${query.search}%` })
|
||||
condition.parameters[0] += '::text'
|
||||
}
|
||||
|
||||
qb.orWhere(qb['createWhereConditionExpression'](condition), {
|
||||
[column]: `%${query.search}%`,
|
||||
})
|
||||
}
|
||||
})
|
||||
)
|
||||
@ -326,41 +341,7 @@ export async function paginate<T extends ObjectLiteral>(
|
||||
queryBuilder.andWhere(
|
||||
new Brackets((qb: SelectQueryBuilder<T>) => {
|
||||
for (const column in filter) {
|
||||
const propertyPath = (column as string).split('.')
|
||||
if (propertyPath.length > 1) {
|
||||
let parameters = { [column]: filter[column].value }
|
||||
// TODO: refactor below
|
||||
const isRelation = queryBuilder.expressionMap.mainAlias.metadata.hasRelationWithPropertyPath(
|
||||
propertyPath[0]
|
||||
)
|
||||
const alias = isRelation ? `${qb.alias}_${column}` : `${qb.alias}.${column}`
|
||||
|
||||
const condition = qb['getWherePredicateCondition'](
|
||||
alias,
|
||||
filter[column]
|
||||
) as WherePredicateOperator
|
||||
|
||||
switch (condition.operator) {
|
||||
case 'between':
|
||||
condition.parameters = [alias, `:${column}_from`, `:${column}_to`]
|
||||
parameters = {
|
||||
[column + '_from']: filter[column].value[0],
|
||||
[column + '_to']: filter[column].value[1],
|
||||
}
|
||||
break
|
||||
case 'in':
|
||||
condition.parameters = [alias, `:...${column}`]
|
||||
break
|
||||
default:
|
||||
condition.parameters = [alias, `:${column}`]
|
||||
break
|
||||
}
|
||||
qb.andWhere(qb['createWhereConditionExpression'](condition), parameters)
|
||||
} else {
|
||||
qb.andWhere({
|
||||
[column]: filter[column],
|
||||
})
|
||||
}
|
||||
addWhereCondition(qb, column, filter)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user