feat: virtual column support (#434)

This commit is contained in:
xMase 2023-01-30 12:17:14 +01:00 committed by GitHub
parent ff00785cd1
commit 77494531d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 339 additions and 96 deletions

View File

@ -21,7 +21,7 @@
"scripts": { "scripts": {
"prebuild": "rimraf lib", "prebuild": "rimraf lib",
"build": "tsc", "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": "prettier --write \"src/**/*.ts\"",
"format:ci": "prettier --list-different \"src/**/*.ts\"", "format:ci": "prettier --list-different \"src/**/*.ts\"",
"lint": "eslint -c .eslintrc.json --ext .ts --max-warnings 0 src", "lint": "eslint -c .eslintrc.json --ext .ts --max-warnings 0 src",

View File

@ -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' import { CatEntity } from './cat.entity'
@Entity() @Entity()
@ -14,4 +14,9 @@ export class CatHomeEntity {
@CreateDateColumn() @CreateDateColumn()
createdAt: string createdAt: string
@VirtualColumn({
query: (alias) => `SELECT CAST(COUNT(*) AS INT) FROM "cat" WHERE "cat"."homeId" = ${alias}.id`,
})
countCat: number
} }

View File

@ -1,4 +1,5 @@
import { import {
AfterLoad,
Column, Column,
CreateDateColumn, CreateDateColumn,
DeleteDateColumn, DeleteDateColumn,
@ -12,7 +13,7 @@ import { CatToyEntity } from './cat-toy.entity'
import { CatHomeEntity } from './cat-home.entity' import { CatHomeEntity } from './cat-home.entity'
import { SizeEmbed } from './size.embed' import { SizeEmbed } from './size.embed'
@Entity() @Entity({ name: 'cat' })
export class CatEntity { export class CatEntity {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number id: number
@ -41,4 +42,13 @@ export class CatEntity {
@DeleteDateColumn({ nullable: true }) @DeleteDateColumn({ nullable: true })
deletedAt?: string 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
View 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)
}

View File

@ -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 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[]] 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) => export const positiveNumberOrDefault = (value: number | undefined, defaultValue: number, minValue: 0 | 1 = 0) =>
value === undefined || value < minValue ? defaultValue : value 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}` //
}
}

View File

@ -466,7 +466,8 @@ describe('paginate', () => {
delete toy.cat delete toy.cat
const toy2 = clone(catToys[2]) const toy2 = clone(catToys[2])
delete toy2.cat 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') 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) const result = await paginate<CatHomeEntity>(query, catHomeRepo, config)
expect(result.meta.sortBy).toStrictEqual([['cat.id', 'DESC']]) 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') 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) const result = await paginate<CatHomeEntity>(query, catHomeRepo, config)
expect(result.meta.search).toStrictEqual('Garfield') 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') 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({ expect(result.meta.filter).toStrictEqual({
'cat.name': '$not:Garfield', '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') 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({ expect(result.meta.filter).toStrictEqual({
'cat.age': '$in:4,6', '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') 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({ expect(result.meta.filter).toStrictEqual({
'cat.age': '$btw:6,10', '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') 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 copyHomes = catHomes.map((home: CatHomeEntity) => {
const copy = clone(home) const copy = clone(home)
copy.countCat = cats.filter((cat) => cat.id === copy.cat.id).length
delete copy.cat delete copy.cat
return copy return copy
}) })
copyCats[0].home = copyHomes[0] copyCats[0].home = copyHomes[0]
copyCats[1].home = copyHomes[1] copyCats[1].home = copyHomes[1]
@ -782,7 +806,7 @@ describe('paginate', () => {
delete copy.cat delete copy.cat
return copy 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]] copyCats[1].toys = [copyToys[3]]
const orderedCats = [copyCats[3], copyCats[1], copyCats[2], copyCats[0], copyCats[4]] 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 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.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')
@ -969,8 +996,9 @@ describe('paginate', () => {
} }
const result = await paginate<CatHomeEntity>(query, catHomeRepo, config) const result = await paginate<CatHomeEntity>(query, catHomeRepo, config)
const catHomeClone = clone(catHomes[1])
expect(result.data).toStrictEqual([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') 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 result = await paginate<CatEntity>(query, catRepo, config)
const home = clone(catHomes[1]) const home = clone(catHomes[1])
home.countCat = cats.filter((cat) => cat.id === home.cat.id).length
delete home.cat delete home.cat
const copyCats = [ const copyCats = [
@ -1117,7 +1146,9 @@ describe('paginate', () => {
expect(result.meta.filter).toStrictEqual({ expect(result.meta.filter).toStrictEqual({
'cat.size.height': '$eq:30', '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') 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({ expect(result.meta.filter).toStrictEqual({
'cat.size.height': '$in:10,30,35', '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') 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({ expect(result.meta.filter).toStrictEqual({
'cat.size.height': '$btw:18,33', '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') 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') 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 () => { it('should return result based on not null query on relation', async () => {
const config: PaginateConfig<CatEntity> = { const config: PaginateConfig<CatEntity> = {
sortableColumns: ['id'], sortableColumns: ['id'],
@ -1527,6 +1552,82 @@ describe('paginate', () => {
expect(getFilterTokens(string)).toStrictEqual(tokens) 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 () => { it('should return all items even if deleted', async () => {
const config: PaginateConfig<CatEntity> = { const config: PaginateConfig<CatEntity> = {
sortableColumns: ['id'], sortableColumns: ['id'],

View File

@ -21,7 +21,18 @@ import { ServiceUnavailableException, Logger } from '@nestjs/common'
import { values, mapKeys } from 'lodash' import { values, mapKeys } from 'lodash'
import { stringify } from 'querystring' import { stringify } from 'querystring'
import { WherePredicateOperator } from 'typeorm/query-builder/WhereClause' 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') const logger: Logger = new Logger('nestjs-paginate')
@ -120,8 +131,8 @@ export function getFilterTokens(raw: string): string[] {
return tokens return tokens
} }
function parseFilter<T>(query: PaginateQuery, config: PaginateConfig<T>) { function parseFilter<T>(query: PaginateQuery, config: PaginateConfig<T>): Filter {
const filter: { [columnName: string]: FindOperator<string> } = {} const filter: Filter = {}
let filterableColumns = config.filterableColumns let filterableColumns = config.filterableColumns
if (filterableColumns === undefined) { if (filterableColumns === undefined) {
logger.debug("No 'filterableColumns' given, ignoring filters.") 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 const queryBuilder = repo instanceof Repository ? repo.createQueryBuilder('e') : repo
if (isPaginated) { 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) { if (config.relations?.length) {
@ -266,11 +281,13 @@ export async function paginate<T extends ObjectLiteral>(
} }
for (const order of sortBy) { for (const order of sortBy) {
if (queryBuilder.expressionMap.mainAlias.metadata.hasRelationWithPropertyPath(order[0].split('.')[0])) { const columnProperties = getPropertiesByColumnName(order[0])
queryBuilder.addOrderBy(`${queryBuilder.alias}_${order[0]}`, order[1], nullSort) const { isVirtualProperty } = extractVirtualProperty(queryBuilder, columnProperties)
} else { const isRelation = checkIsRelation(queryBuilder, columnProperties.propertyPath)
queryBuilder.addOrderBy(`${queryBuilder.alias}.${order[0]}`, order[1], nullSort)
} const alias = fixColumnAlias(columnProperties, queryBuilder.alias, isRelation, isVirtualProperty)
queryBuilder.addOrderBy(alias, order[1], nullSort)
} }
if (config.select?.length > 0) { if (config.select?.length > 0) {
@ -297,25 +314,23 @@ export async function paginate<T extends ObjectLiteral>(
queryBuilder.andWhere( queryBuilder.andWhere(
new Brackets((qb: SelectQueryBuilder<T>) => { new Brackets((qb: SelectQueryBuilder<T>) => {
for (const column of searchBy) { for (const column of searchBy) {
const propertyPath = (column as string).split('.') const property = getPropertiesByColumnName(column)
const hasRelation = const { isVirtualProperty, query: virtualQuery } = extractVirtualProperty(qb, property)
propertyPath.length > 1 && const isRelation = checkIsRelation(qb, property.propertyPath)
queryBuilder.expressionMap.mainAlias.metadata.hasRelationWithPropertyPath(propertyPath[0])
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)) { if (['postgres', 'cockroachdb'].includes(queryBuilder.connection.options.type)) {
const alias = hasRelation ? `"${qb.alias}"_` : `"${qb.alias}".` condition.parameters[0] += '::text'
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}%` }) qb.orWhere(qb['createWhereConditionExpression'](condition), {
} else { [column]: `%${query.search}%`,
const aliasColumn = hasRelation ? `${qb.alias}_${column}` : `${qb.alias}.${column}` })
qb.orWhere(`UPPER(${aliasColumn}) LIKE UPPER(:search)`, { search: `%${query.search}%` })
}
} }
}) })
) )
@ -326,41 +341,7 @@ export async function paginate<T extends ObjectLiteral>(
queryBuilder.andWhere( queryBuilder.andWhere(
new Brackets((qb: SelectQueryBuilder<T>) => { new Brackets((qb: SelectQueryBuilder<T>) => {
for (const column in filter) { for (const column in filter) {
const propertyPath = (column as string).split('.') addWhereCondition(qb, column, filter)
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],
})
}
} }
}) })
) )