From 77494531d39f93f4f305172824070280ad8a57ac Mon Sep 17 00:00:00 2001 From: xMase <11925311+xMase@users.noreply.github.com> Date: Mon, 30 Jan 2023 12:17:14 +0100 Subject: [PATCH] feat: virtual column support (#434) --- package.json | 2 +- src/__tests__/cat-home.entity.ts | 7 +- src/__tests__/cat.entity.ts | 12 ++- src/filter.ts | 85 ++++++++++++++++ src/helper.ts | 61 +++++++++++ src/paginate.spec.ts | 167 +++++++++++++++++++++++++------ src/paginate.ts | 101 ++++++++----------- 7 files changed, 339 insertions(+), 96 deletions(-) create mode 100644 src/filter.ts diff --git a/package.json b/package.json index 9c30e18..8f9ab40 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/__tests__/cat-home.entity.ts b/src/__tests__/cat-home.entity.ts index 933326e..32939dd 100644 --- a/src/__tests__/cat-home.entity.ts +++ b/src/__tests__/cat-home.entity.ts @@ -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 } diff --git a/src/__tests__/cat.entity.ts b/src/__tests__/cat.entity.ts index 67afb7a..c4afbf6 100644 --- a/src/__tests__/cat.entity.ts +++ b/src/__tests__/cat.entity.ts @@ -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 + } + } } diff --git a/src/filter.ts b/src/filter.ts new file mode 100644 index 0000000..1bb1d0b --- /dev/null +++ b/src/filter.ts @@ -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 } + +export function generatePredicateCondition( + qb: SelectQueryBuilder, + 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, 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) +} diff --git a/src/helper.ts b/src/helper.ts index 9ad70af..d10ed41 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -1,3 +1,6 @@ +import { SelectQueryBuilder } from 'typeorm' +import { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata' + type Join = 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 = Order[] 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, + 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, 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}` // + } +} diff --git a/src/paginate.spec.ts b/src/paginate.spec.ts index e8a5095..ad49212 100644 --- a/src/paginate.spec.ts +++ b/src/paginate.spec.ts @@ -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(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(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(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(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(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 = { - sortableColumns: ['id'], - filterableColumns: { - age: [FilterOperator.NOT, FilterOperator.NULL], - }, - } - const query: PaginateQuery = { - path: '', - filter: { - age: '$not:$null', - }, - } - - const result = await paginate(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 = { 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 = { + 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(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 = { + sortableColumns: ['home.countCat'], + relations: ['home'], + } + const query: PaginateQuery = { + path: '', + sortBy: [['home.countCat', 'ASC']], + } + + const result = await paginate(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 = { + sortableColumns: ['countCat'], + relations: ['cat'], + filterableColumns: { + countCat: [FilterOperator.GT], + }, + } + const query: PaginateQuery = { + path: '', + filter: { + countCat: '$gt:0', + }, + sortBy: [['countCat', 'ASC']], + } + + const result = await paginate(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 = { sortableColumns: ['id'], diff --git a/src/paginate.ts b/src/paginate.ts index 4d3debf..7464ca8 100644 --- a/src/paginate.ts +++ b/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(query: PaginateQuery, config: PaginateConfig) { - const filter: { [columnName: string]: FindOperator } = {} +function parseFilter(query: PaginateQuery, config: PaginateConfig): 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( 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( } 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( queryBuilder.andWhere( new Brackets((qb: SelectQueryBuilder) => { 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( queryBuilder.andWhere( new Brackets((qb: SelectQueryBuilder) => { 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) } }) )