fix: implemented a proper mechanism for using where condition in paginate options (#759)
This commit is contained in:
parent
e62feb869f
commit
a992f340c5
@ -1,6 +1,7 @@
|
||||
import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'
|
||||
import { CatEntity } from './cat.entity'
|
||||
import { SizeEmbed } from './size.embed'
|
||||
import { ToyShopEntity } from './toy-shop.entity'
|
||||
|
||||
@Entity()
|
||||
export class CatToyEntity {
|
||||
@ -13,6 +14,10 @@ export class CatToyEntity {
|
||||
@Column(() => SizeEmbed)
|
||||
size: SizeEmbed
|
||||
|
||||
@ManyToOne(() => ToyShopEntity, { nullable: true })
|
||||
@JoinColumn()
|
||||
shop?: ToyShopEntity
|
||||
|
||||
@ManyToOne(() => CatEntity, (cat) => cat.toys)
|
||||
@JoinColumn()
|
||||
cat: CatEntity
|
||||
|
13
src/__tests__/toy-shop-address.entity.ts
Normal file
13
src/__tests__/toy-shop-address.entity.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'
|
||||
|
||||
@Entity()
|
||||
export class ToyShopAddressEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number
|
||||
|
||||
@Column()
|
||||
address: string
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: string
|
||||
}
|
18
src/__tests__/toy-shop.entity.ts
Normal file
18
src/__tests__/toy-shop.entity.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Column, CreateDateColumn, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn } from 'typeorm'
|
||||
import { ToyShopAddressEntity } from './toy-shop-address.entity'
|
||||
|
||||
@Entity()
|
||||
export class ToyShopEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number
|
||||
|
||||
@Column()
|
||||
shopName: string
|
||||
|
||||
@OneToOne(() => ToyShopAddressEntity, { nullable: true })
|
||||
@JoinColumn()
|
||||
address: ToyShopAddressEntity
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: string
|
||||
}
|
@ -140,8 +140,14 @@ export function fixColumnAlias(
|
||||
return `(${query(`${alias}_${properties.propertyPath}_rel`)})` // () is needed to avoid parameter conflict
|
||||
} else if ((isVirtualProperty && !query) || properties.isNested) {
|
||||
if (properties.propertyName.includes('.')) {
|
||||
const [nestedRel, nestedCol] = properties.propertyName.split('.')
|
||||
return `${alias}_${properties.propertyPath}_rel_${nestedRel}_rel.${nestedCol}`
|
||||
const propertyPath = properties.propertyName.split('.')
|
||||
const nestedRelations = propertyPath
|
||||
.slice(0, -1)
|
||||
.map((v) => `${v}_rel`)
|
||||
.join('_')
|
||||
const nestedCol = propertyPath[propertyPath.length - 1]
|
||||
|
||||
return `${alias}_${properties.propertyPath}_rel_${nestedRelations}.${nestedCol}`
|
||||
} else {
|
||||
return `${alias}_${properties.propertyPath}_rel_${properties.propertyName}`
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Repository, In, DataSource, TypeORMError } from 'typeorm'
|
||||
import { Repository, In, DataSource, TypeORMError, Like } from 'typeorm'
|
||||
import { Paginated, paginate, PaginateConfig, NO_PAGINATION } from './paginate'
|
||||
import { PaginateQuery } from './decorator'
|
||||
import { HttpException } from '@nestjs/common'
|
||||
@ -16,6 +16,8 @@ import {
|
||||
isSuffix,
|
||||
OperatorSymbolToFunction,
|
||||
} from './filter'
|
||||
import { ToyShopEntity } from './__tests__/toy-shop.entity'
|
||||
import { ToyShopAddressEntity } from './__tests__/toy-shop-address.entity'
|
||||
|
||||
const isoStringToDate = (isoString) => new Date(isoString)
|
||||
|
||||
@ -23,10 +25,15 @@ describe('paginate', () => {
|
||||
let dataSource: DataSource
|
||||
let catRepo: Repository<CatEntity>
|
||||
let catToyRepo: Repository<CatToyEntity>
|
||||
let toyShopRepo: Repository<ToyShopEntity>
|
||||
let toyShopAddressRepository: Repository<ToyShopAddressEntity>
|
||||
let catHomeRepo: Repository<CatHomeEntity>
|
||||
let catHomePillowRepo: Repository<CatHomePillowEntity>
|
||||
let cats: CatEntity[]
|
||||
let catToys: CatToyEntity[]
|
||||
let catToysWithoutShop: CatToyEntity[]
|
||||
let toyShopsAddresses: ToyShopAddressEntity[]
|
||||
let toysShops: ToyShopEntity[]
|
||||
let catHomes: CatHomeEntity[]
|
||||
let catHomePillows: CatHomePillowEntity[]
|
||||
|
||||
@ -47,13 +54,22 @@ describe('paginate', () => {
|
||||
}),
|
||||
synchronize: true,
|
||||
logging: ['error'],
|
||||
entities: [CatEntity, CatToyEntity, CatHomeEntity, CatHomePillowEntity],
|
||||
entities: [
|
||||
CatEntity,
|
||||
CatToyEntity,
|
||||
ToyShopAddressEntity,
|
||||
CatHomeEntity,
|
||||
CatHomePillowEntity,
|
||||
ToyShopEntity,
|
||||
],
|
||||
})
|
||||
await dataSource.initialize()
|
||||
catRepo = dataSource.getRepository(CatEntity)
|
||||
catToyRepo = dataSource.getRepository(CatToyEntity)
|
||||
catHomeRepo = dataSource.getRepository(CatHomeEntity)
|
||||
catHomePillowRepo = dataSource.getRepository(CatHomePillowEntity)
|
||||
toyShopRepo = dataSource.getRepository(ToyShopEntity)
|
||||
toyShopAddressRepository = dataSource.getRepository(ToyShopAddressEntity)
|
||||
|
||||
cats = await catRepo.save([
|
||||
catRepo.create({
|
||||
@ -97,12 +113,41 @@ describe('paginate', () => {
|
||||
size: { height: 10, width: 5, length: 15 },
|
||||
}),
|
||||
])
|
||||
|
||||
toyShopsAddresses = await toyShopAddressRepository.save([
|
||||
toyShopAddressRepository.create({ address: '123 Main St' }),
|
||||
])
|
||||
|
||||
toysShops = await toyShopRepo.save([
|
||||
toyShopRepo.create({ shopName: 'Best Toys', address: toyShopsAddresses[0] }),
|
||||
toyShopRepo.create({ shopName: 'Lovely Toys' }),
|
||||
])
|
||||
|
||||
catToys = await catToyRepo.save([
|
||||
catToyRepo.create({ name: 'Fuzzy Thing', cat: cats[0], size: { height: 10, width: 10, length: 10 } }),
|
||||
catToyRepo.create({ name: 'Stuffed Mouse', cat: cats[0], size: { height: 5, width: 5, length: 12 } }),
|
||||
catToyRepo.create({ name: 'Mouse', cat: cats[0], size: { height: 6, width: 4, length: 13 } }),
|
||||
catToyRepo.create({
|
||||
name: 'Stuffed Mouse',
|
||||
shop: toysShops[0],
|
||||
cat: cats[0],
|
||||
size: { height: 5, width: 5, length: 12 },
|
||||
}),
|
||||
catToyRepo.create({
|
||||
name: 'Mouse',
|
||||
shop: toysShops[1],
|
||||
cat: cats[0],
|
||||
size: { height: 6, width: 4, length: 13 },
|
||||
}),
|
||||
catToyRepo.create({ name: 'String', cat: cats[1], size: { height: 1, width: 1, length: 50 } }),
|
||||
])
|
||||
|
||||
catToysWithoutShop = catToys.map(({ shop: _, ...other }) => {
|
||||
const newInstance = new CatToyEntity()
|
||||
for (const otherKey in other) {
|
||||
newInstance[otherKey] = other[otherKey]
|
||||
}
|
||||
return newInstance
|
||||
})
|
||||
|
||||
catHomes = await catHomeRepo.save([
|
||||
catHomeRepo.create({ name: 'Box', cat: cats[0] }),
|
||||
catHomeRepo.create({ name: 'House', cat: cats[1] }),
|
||||
@ -594,7 +639,7 @@ describe('paginate', () => {
|
||||
const result = await paginate<CatToyEntity>(query, catToyRepo, config)
|
||||
|
||||
expect(result.meta.search).toStrictEqual('Milo')
|
||||
expect(result.data).toStrictEqual([catToys[0], catToys[1], catToys[2]])
|
||||
expect(result.data).toStrictEqual([catToysWithoutShop[0], catToysWithoutShop[1], catToysWithoutShop[2]])
|
||||
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&search=Milo')
|
||||
})
|
||||
|
||||
@ -616,9 +661,9 @@ describe('paginate', () => {
|
||||
const result = await paginate<CatEntity>(query, catRepo, config)
|
||||
|
||||
expect(result.meta.search).toStrictEqual('Mouse')
|
||||
const toy = clone(catToys[1])
|
||||
const toy = clone(catToysWithoutShop[1])
|
||||
delete toy.cat
|
||||
const toy2 = clone(catToys[2])
|
||||
const toy2 = clone(catToysWithoutShop[2])
|
||||
delete toy2.cat
|
||||
|
||||
expect(result.data).toStrictEqual([Object.assign(clone(cats[0]), { toys: [toy2, toy] })])
|
||||
@ -661,13 +706,15 @@ describe('paginate', () => {
|
||||
const result = await paginate<CatToyEntity>(query, catToyRepo, config)
|
||||
|
||||
expect(result.meta.search).toStrictEqual('Milo')
|
||||
expect(result.data).toStrictEqual([catToys[0], catToys[1], catToys[2]].sort((a, b) => b.cat.id - a.cat.id))
|
||||
expect(result.data).toStrictEqual(
|
||||
[catToysWithoutShop[0], catToysWithoutShop[1], catToysWithoutShop[2]].sort((a, b) => b.cat.id - a.cat.id)
|
||||
)
|
||||
expect(result.links.current).toBe('?page=1&limit=20&sortBy=cat.id:DESC&search=Milo')
|
||||
})
|
||||
|
||||
it('should return result based on sort on one-to-many relation', async () => {
|
||||
const config: PaginateConfig<CatEntity> = {
|
||||
relations: ['toys'],
|
||||
relations: ['toys', 'toys.shop', 'toys.shop.address'],
|
||||
sortableColumns: ['id', 'name', 'toys.id'],
|
||||
searchableColumns: ['name', 'toys.name'],
|
||||
}
|
||||
@ -684,6 +731,9 @@ describe('paginate', () => {
|
||||
delete toy1.cat
|
||||
const toy2 = clone(catToys[2])
|
||||
delete toy2.cat
|
||||
|
||||
delete result.data[0].toys[0].shop.address
|
||||
|
||||
expect(result.data).toStrictEqual([Object.assign(clone(cats[0]), { toys: [toy2, toy1] })])
|
||||
expect(result.links.current).toBe('?page=1&limit=20&sortBy=toys.id:DESC&search=Mouse')
|
||||
})
|
||||
@ -865,6 +915,103 @@ describe('paginate', () => {
|
||||
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC')
|
||||
})
|
||||
|
||||
it('should return valid data filtering by not id field many-to-one', async () => {
|
||||
const config: PaginateConfig<CatToyEntity> = {
|
||||
sortableColumns: ['id', 'name'],
|
||||
relations: ['cat'],
|
||||
where: {
|
||||
cat: {
|
||||
name: cats[0].name,
|
||||
},
|
||||
},
|
||||
}
|
||||
const query: PaginateQuery = {
|
||||
path: '',
|
||||
}
|
||||
|
||||
const result = await paginate<CatToyEntity>(query, catToyRepo, config)
|
||||
|
||||
expect(result.meta.totalItems).toBe(3)
|
||||
result.data.forEach((toy) => {
|
||||
expect(toy.cat.id).toBe(cats[0].id)
|
||||
})
|
||||
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC')
|
||||
})
|
||||
|
||||
it('should return result based on where one-to-many relation', async () => {
|
||||
const config: PaginateConfig<CatEntity> = {
|
||||
relations: ['toys'],
|
||||
sortableColumns: ['id', 'name'],
|
||||
where: {
|
||||
toys: {
|
||||
name: 'Stuffed Mouse',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const query: PaginateQuery = {
|
||||
path: '',
|
||||
}
|
||||
|
||||
const result = await paginate<CatEntity>(query, catRepo, config)
|
||||
|
||||
expect(result.data.length).toBe(1)
|
||||
expect(result.data[0].toys.length).toBe(1)
|
||||
expect(result.data[0].toys[0].name).toBe('Stuffed Mouse')
|
||||
})
|
||||
|
||||
it('should return all cats with a toys from the lovely shop', async () => {
|
||||
const config: PaginateConfig<CatEntity> = {
|
||||
relations: ['toys', 'toys.shop'],
|
||||
sortableColumns: ['id', 'name'],
|
||||
where: {
|
||||
toys: {
|
||||
shop: {
|
||||
shopName: 'Lovely Toys',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const query: PaginateQuery = {
|
||||
path: '',
|
||||
}
|
||||
|
||||
const result = await paginate<CatEntity>(query, catRepo, config)
|
||||
|
||||
expect(result.data.length).toBe(1)
|
||||
expect(result.data[0].toys.length).toBe(1)
|
||||
expect(result.data[0].toys[0].shop.id).toStrictEqual(toysShops[1].id)
|
||||
expect(result.data[0].toys[0].name).toBe('Mouse')
|
||||
})
|
||||
|
||||
it('should return all cats from shop where street name like 123', async () => {
|
||||
const config: PaginateConfig<CatEntity> = {
|
||||
relations: ['toys', 'toys.shop', 'toys.shop.address'],
|
||||
sortableColumns: ['id', 'name'],
|
||||
where: {
|
||||
toys: {
|
||||
shop: {
|
||||
address: {
|
||||
address: Like('%123%'),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const query: PaginateQuery = {
|
||||
path: '',
|
||||
}
|
||||
|
||||
const result = await paginate<CatEntity>(query, catRepo, config)
|
||||
|
||||
expect(result.data.length).toBe(1)
|
||||
expect(result.data[0].toys.length).toBe(1)
|
||||
expect(result.data[0].toys[0].shop).toStrictEqual(toysShops[0])
|
||||
expect(result.data[0].toys[0].name).toBe('Stuffed Mouse')
|
||||
})
|
||||
|
||||
it('should return result based on filter on many-to-one relation', async () => {
|
||||
const config: PaginateConfig<CatToyEntity> = {
|
||||
relations: ['cat'],
|
||||
@ -908,9 +1055,9 @@ describe('paginate', () => {
|
||||
|
||||
const cat1 = clone(cats[0])
|
||||
const cat2 = clone(cats[1])
|
||||
const catToys1 = clone(catToys[0])
|
||||
const catToys2 = clone(catToys[2])
|
||||
const catToys3 = clone(catToys[3])
|
||||
const catToys1 = clone(catToysWithoutShop[0])
|
||||
const catToys2 = clone(catToysWithoutShop[2])
|
||||
const catToys3 = clone(catToysWithoutShop[3])
|
||||
delete catToys1.cat
|
||||
delete catToys2.cat
|
||||
delete catToys3.cat
|
||||
@ -1062,7 +1209,7 @@ describe('paginate', () => {
|
||||
copyCats[0].home = copyHomes[0]
|
||||
copyCats[1].home = copyHomes[1]
|
||||
|
||||
const copyToys = catToys.map((toy: CatToyEntity) => {
|
||||
const copyToys = catToysWithoutShop.map((toy: CatToyEntity) => {
|
||||
const copy = clone(toy)
|
||||
delete copy.cat
|
||||
return copy
|
||||
@ -1095,16 +1242,16 @@ describe('paginate', () => {
|
||||
|
||||
const result = await paginate<CatEntity>(query, catRepo, config)
|
||||
|
||||
const toy0 = clone(catToys[0])
|
||||
const toy0 = clone(catToysWithoutShop[0])
|
||||
delete toy0.cat
|
||||
|
||||
const toy1 = clone(catToys[1])
|
||||
const toy1 = clone(catToysWithoutShop[1])
|
||||
delete toy1.cat
|
||||
|
||||
const toy2 = clone(catToys[2])
|
||||
const toy2 = clone(catToysWithoutShop[2])
|
||||
delete toy2.cat
|
||||
|
||||
const toy3 = clone(catToys[3])
|
||||
const toy3 = clone(catToysWithoutShop[3])
|
||||
delete toy3.cat
|
||||
|
||||
const orderedCats = [
|
||||
@ -1136,7 +1283,7 @@ describe('paginate', () => {
|
||||
}
|
||||
|
||||
const result = await paginate<CatToyEntity>(query, catToyRepo, config)
|
||||
const orderedToys = [catToys[3], catToys[0], catToys[2], catToys[1]]
|
||||
const orderedToys = [catToysWithoutShop[3], catToysWithoutShop[0], catToysWithoutShop[2], catToysWithoutShop[1]]
|
||||
|
||||
expect(result.data).toStrictEqual(orderedToys)
|
||||
expect(result.links.current).toBe(
|
||||
@ -1279,7 +1426,12 @@ describe('paginate', () => {
|
||||
|
||||
const result = await paginate<CatToyEntity>(query, catToyRepo, config)
|
||||
expect(result.meta.search).toStrictEqual('1')
|
||||
expect(result.data).toStrictEqual([catToys[3], catToys[0], catToys[1], catToys[2]])
|
||||
expect(result.data).toStrictEqual([
|
||||
catToysWithoutShop[3],
|
||||
catToysWithoutShop[0],
|
||||
catToysWithoutShop[1],
|
||||
catToysWithoutShop[2],
|
||||
])
|
||||
expect(result.links.current).toBe('?page=1&limit=20&sortBy=cat.(size.height):DESC&search=1')
|
||||
})
|
||||
|
||||
|
@ -1,32 +1,33 @@
|
||||
import {
|
||||
Brackets,
|
||||
FindOperator,
|
||||
FindOptionsRelationByString,
|
||||
FindOptionsRelations,
|
||||
FindOptionsUtils,
|
||||
FindOptionsWhere,
|
||||
ObjectLiteral,
|
||||
Repository,
|
||||
SelectQueryBuilder,
|
||||
Brackets,
|
||||
FindOptionsWhere,
|
||||
FindOptionsRelations,
|
||||
ObjectLiteral,
|
||||
FindOptionsUtils,
|
||||
FindOptionsRelationByString,
|
||||
} from 'typeorm'
|
||||
import { PaginateQuery } from './decorator'
|
||||
import { ServiceUnavailableException, Logger } from '@nestjs/common'
|
||||
import { Logger, ServiceUnavailableException } from '@nestjs/common'
|
||||
import { mapKeys } from 'lodash'
|
||||
import { stringify } from 'querystring'
|
||||
import { WherePredicateOperator } from 'typeorm/query-builder/WhereClause'
|
||||
import {
|
||||
checkIsRelation,
|
||||
checkIsEmbedded,
|
||||
checkIsRelation,
|
||||
Column,
|
||||
extractVirtualProperty,
|
||||
fixColumnAlias,
|
||||
getPropertiesByColumnName,
|
||||
getQueryUrlComponents,
|
||||
includesAllPrimaryKeyColumns,
|
||||
isEntityKey,
|
||||
Order,
|
||||
positiveNumberOrDefault,
|
||||
RelationColumn,
|
||||
SortBy,
|
||||
includesAllPrimaryKeyColumns,
|
||||
isEntityKey,
|
||||
getQueryUrlComponents,
|
||||
} from './helper'
|
||||
import { addFilter, FilterOperator, FilterSuffix } from './filter'
|
||||
import { OrmUtils } from 'typeorm/util/OrmUtils'
|
||||
@ -46,7 +47,9 @@ export class Paginated<T> {
|
||||
searchBy: Column<T>[]
|
||||
search: string
|
||||
select: string[]
|
||||
filter?: { [column: string]: string | string[] }
|
||||
filter?: {
|
||||
[column: string]: string | string[]
|
||||
}
|
||||
}
|
||||
links: {
|
||||
first?: string
|
||||
@ -86,6 +89,47 @@ export const DEFAULT_MAX_LIMIT = 100
|
||||
export const DEFAULT_LIMIT = 20
|
||||
export const NO_PAGINATION = 0
|
||||
|
||||
function generateWhereStatement<T>(
|
||||
queryBuilder: SelectQueryBuilder<T>,
|
||||
obj: FindOptionsWhere<T> | FindOptionsWhere<T>[]
|
||||
) {
|
||||
const toTransform = Array.isArray(obj) ? obj : [obj]
|
||||
return toTransform.map((item) => flattenWhereAndTransform(queryBuilder, item).join(' AND ')).join(' OR ')
|
||||
}
|
||||
|
||||
function flattenWhereAndTransform<T>(
|
||||
queryBuilder: SelectQueryBuilder<T>,
|
||||
obj: FindOptionsWhere<T>,
|
||||
separator = '.',
|
||||
parentKey = ''
|
||||
) {
|
||||
return Object.entries(obj).flatMap(([key, value]) => {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
const joinedKey = parentKey ? `${parentKey}${separator}${key}` : key
|
||||
|
||||
if (typeof value === 'object' && value !== null && !(value instanceof FindOperator)) {
|
||||
return flattenWhereAndTransform(queryBuilder, value as FindOptionsWhere<T>, separator, joinedKey)
|
||||
} else {
|
||||
const property = getPropertiesByColumnName(joinedKey)
|
||||
const { isVirtualProperty, query: virtualQuery } = extractVirtualProperty(queryBuilder, property)
|
||||
const isRelation = checkIsRelation(queryBuilder, property.propertyPath)
|
||||
const isEmbedded = checkIsEmbedded(queryBuilder, property.propertyPath)
|
||||
const alias = fixColumnAlias(
|
||||
property,
|
||||
queryBuilder.alias,
|
||||
isRelation,
|
||||
isVirtualProperty,
|
||||
isEmbedded,
|
||||
virtualQuery
|
||||
)
|
||||
return queryBuilder['createWhereConditionExpression'](
|
||||
queryBuilder['getWherePredicateCondition'](alias, value)
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function paginate<T extends ObjectLiteral>(
|
||||
query: PaginateQuery,
|
||||
repo: Repository<T> | SelectQueryBuilder<T>,
|
||||
@ -199,8 +243,9 @@ export async function paginate<T extends ObjectLiteral>(
|
||||
queryBuilder.select(cols)
|
||||
}
|
||||
|
||||
if (config.where) {
|
||||
queryBuilder.andWhere(new Brackets((qb) => qb.andWhere(config.where)))
|
||||
if (config.where && repo instanceof Repository) {
|
||||
const baseWhereStr = generateWhereStatement(queryBuilder, config.where)
|
||||
queryBuilder.andWhere(`(${baseWhereStr})`)
|
||||
}
|
||||
|
||||
if (config.withDeleted) {
|
||||
|
Loading…
Reference in New Issue
Block a user