feat: virtual column support (#434)
This commit is contained in:
parent
ff00785cd1
commit
77494531d3
@ -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",
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
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 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}` //
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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'],
|
||||||
|
@ -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],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user