fix: many-to-many relations (#492)
This commit is contained in:
parent
02afb0a0d4
commit
53f18bd547
@ -5,6 +5,8 @@ import {
|
|||||||
DeleteDateColumn,
|
DeleteDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
JoinColumn,
|
JoinColumn,
|
||||||
|
JoinTable,
|
||||||
|
ManyToMany,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
OneToOne,
|
OneToOne,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
@ -43,6 +45,10 @@ export class CatEntity {
|
|||||||
@DeleteDateColumn({ nullable: true })
|
@DeleteDateColumn({ nullable: true })
|
||||||
deletedAt?: string
|
deletedAt?: string
|
||||||
|
|
||||||
|
@ManyToMany(() => CatEntity)
|
||||||
|
@JoinTable()
|
||||||
|
friends: CatEntity[]
|
||||||
|
|
||||||
@AfterLoad()
|
@AfterLoad()
|
||||||
// Fix due to typeorm bug that doesn't set entity to null
|
// 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
|
// when the reletated entity have only the virtual column property with a value different from null
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
import { Brackets, FindOperator, SelectQueryBuilder } from 'typeorm'
|
import { Brackets, FindOperator, SelectQueryBuilder } from 'typeorm'
|
||||||
import { WherePredicateOperator } from 'typeorm/query-builder/WhereClause'
|
import { WherePredicateOperator } from 'typeorm/query-builder/WhereClause'
|
||||||
import { PaginateQuery } from './decorator'
|
import { PaginateQuery } from './decorator'
|
||||||
import { checkIsRelation, extractVirtualProperty, fixColumnAlias, getPropertiesByColumnName } from './helper'
|
import {
|
||||||
|
checkIsEmbedded,
|
||||||
|
checkIsRelation,
|
||||||
|
extractVirtualProperty,
|
||||||
|
fixColumnAlias,
|
||||||
|
getPropertiesByColumnName,
|
||||||
|
} from './helper'
|
||||||
import {
|
import {
|
||||||
FilterComparator,
|
FilterComparator,
|
||||||
FilterOperator,
|
FilterOperator,
|
||||||
@ -95,9 +101,17 @@ export function addWhereCondition<T>(qb: SelectQueryBuilder<T>, column: string,
|
|||||||
const columnProperties = getPropertiesByColumnName(column)
|
const columnProperties = getPropertiesByColumnName(column)
|
||||||
const { isVirtualProperty, query: virtualQuery } = extractVirtualProperty(qb, columnProperties)
|
const { isVirtualProperty, query: virtualQuery } = extractVirtualProperty(qb, columnProperties)
|
||||||
const isRelation = checkIsRelation(qb, columnProperties.propertyPath)
|
const isRelation = checkIsRelation(qb, columnProperties.propertyPath)
|
||||||
|
const isEmbedded = checkIsEmbedded(qb, columnProperties.propertyPath)
|
||||||
filter[column].forEach((columnFilter: Filter, index: number) => {
|
filter[column].forEach((columnFilter: Filter, index: number) => {
|
||||||
const columnNamePerIteration = `${column}${index}`
|
const columnNamePerIteration = `${column}${index}`
|
||||||
const alias = fixColumnAlias(columnProperties, qb.alias, isRelation, isVirtualProperty, virtualQuery)
|
const alias = fixColumnAlias(
|
||||||
|
columnProperties,
|
||||||
|
qb.alias,
|
||||||
|
isRelation,
|
||||||
|
isVirtualProperty,
|
||||||
|
isEmbedded,
|
||||||
|
virtualQuery
|
||||||
|
)
|
||||||
const condition = generatePredicateCondition(qb, column, columnFilter, alias, isVirtualProperty)
|
const condition = generatePredicateCondition(qb, column, columnFilter, alias, isVirtualProperty)
|
||||||
const parameters = fixQueryParam(alias, columnNamePerIteration, columnFilter, condition, {
|
const parameters = fixQueryParam(alias, columnNamePerIteration, columnFilter, condition, {
|
||||||
[columnNamePerIteration]: columnFilter.findOperator.value,
|
[columnNamePerIteration]: columnFilter.findOperator.value,
|
||||||
|
@ -68,12 +68,20 @@ export function checkIsRelation(qb: SelectQueryBuilder<unknown>, propertyPath: s
|
|||||||
return !!qb?.expressionMap?.mainAlias?.metadata?.hasRelationWithPropertyPath(propertyPath)
|
return !!qb?.expressionMap?.mainAlias?.metadata?.hasRelationWithPropertyPath(propertyPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function checkIsEmbedded(qb: SelectQueryBuilder<unknown>, propertyPath: string): boolean {
|
||||||
|
if (!qb || !propertyPath) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !!qb?.expressionMap?.mainAlias?.metadata?.hasEmbeddedWithPropertyPath(propertyPath)
|
||||||
|
}
|
||||||
|
|
||||||
// This function is used to fix the column alias when using relation, embedded or virtual properties
|
// This function is used to fix the column alias when using relation, embedded or virtual properties
|
||||||
export function fixColumnAlias(
|
export function fixColumnAlias(
|
||||||
properties: ColumnProperties,
|
properties: ColumnProperties,
|
||||||
alias: string,
|
alias: string,
|
||||||
isRelation = false,
|
isRelation = false,
|
||||||
isVirtualProperty = false,
|
isVirtualProperty = false,
|
||||||
|
isEmbedded = false,
|
||||||
query?: ColumnMetadata['query']
|
query?: ColumnMetadata['query']
|
||||||
): string {
|
): string {
|
||||||
if (isRelation) {
|
if (isRelation) {
|
||||||
@ -82,11 +90,13 @@ export function fixColumnAlias(
|
|||||||
} else if (isVirtualProperty && !query) {
|
} else if (isVirtualProperty && !query) {
|
||||||
return `${alias}_${properties.propertyPath}_${properties.propertyName}`
|
return `${alias}_${properties.propertyPath}_${properties.propertyName}`
|
||||||
} else {
|
} else {
|
||||||
return `${alias}_${properties.propertyPath}.${properties.propertyName}` // include embeded property and relation property
|
return `${alias}_${properties.propertyPath}.${properties.propertyName}`
|
||||||
}
|
}
|
||||||
} else if (isVirtualProperty) {
|
} else if (isVirtualProperty) {
|
||||||
return query ? `(${query(`${alias}`)})` : `${alias}_${properties.propertyName}`
|
return query ? `(${query(`${alias}`)})` : `${alias}_${properties.propertyName}`
|
||||||
|
} else if (isEmbedded) {
|
||||||
|
return `${alias}.${properties.propertyPath}.${properties.propertyName}`
|
||||||
} else {
|
} else {
|
||||||
return `${alias}.${properties.propertyName}` //
|
return `${alias}.${properties.propertyName}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,6 +55,9 @@ describe('paginate', () => {
|
|||||||
catHomeRepo.create({ name: 'Box', cat: cats[0] }),
|
catHomeRepo.create({ name: 'Box', cat: cats[0] }),
|
||||||
catHomeRepo.create({ name: 'House', cat: cats[1] }),
|
catHomeRepo.create({ name: 'House', cat: cats[1] }),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// add friends to Milo
|
||||||
|
catRepo.save({ ...cats[0], friends: cats.slice(1) })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return an instance of Paginated', async () => {
|
it('should return an instance of Paginated', async () => {
|
||||||
@ -462,9 +465,9 @@ describe('paginate', () => {
|
|||||||
const result = await paginate<CatEntity>(query, catRepo, config)
|
const result = await paginate<CatEntity>(query, catRepo, config)
|
||||||
|
|
||||||
expect(result.meta.search).toStrictEqual('Mouse')
|
expect(result.meta.search).toStrictEqual('Mouse')
|
||||||
const toy = clone(catToys[1])
|
const toy = clone(catToys[2])
|
||||||
delete toy.cat
|
delete toy.cat
|
||||||
const toy2 = clone(catToys[2])
|
const toy2 = clone(catToys[1])
|
||||||
delete toy2.cat
|
delete toy2.cat
|
||||||
|
|
||||||
expect(result.data).toStrictEqual([Object.assign(clone(cats[0]), { toys: [toy2, toy] })])
|
expect(result.data).toStrictEqual([Object.assign(clone(cats[0]), { toys: [toy2, toy] })])
|
||||||
@ -807,7 +810,7 @@ describe('paginate', () => {
|
|||||||
delete copy.cat
|
delete copy.cat
|
||||||
return copy
|
return copy
|
||||||
})
|
})
|
||||||
copyCats[0].toys = [copyToys[0], copyToys[2], copyToys[1]]
|
copyCats[0].toys = [copyToys[0], copyToys[1], copyToys[2]]
|
||||||
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]]
|
||||||
@ -1872,4 +1875,19 @@ describe('paginate', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should return the right amount of results if a many to many relation is involved', async () => {
|
||||||
|
const config: PaginateConfig<CatEntity> = {
|
||||||
|
sortableColumns: ['id'],
|
||||||
|
defaultSortBy: [['id', 'ASC']],
|
||||||
|
relations: ['friends'],
|
||||||
|
}
|
||||||
|
const query: PaginateQuery = {
|
||||||
|
path: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await paginate<CatEntity>(query, catRepo, config)
|
||||||
|
|
||||||
|
expect(result.data.length).toBe(4)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -6,6 +6,7 @@ import { stringify } from 'querystring'
|
|||||||
import { WherePredicateOperator } from 'typeorm/query-builder/WhereClause'
|
import { WherePredicateOperator } from 'typeorm/query-builder/WhereClause'
|
||||||
import {
|
import {
|
||||||
checkIsRelation,
|
checkIsRelation,
|
||||||
|
checkIsEmbedded,
|
||||||
Column,
|
Column,
|
||||||
extractVirtualProperty,
|
extractVirtualProperty,
|
||||||
fixColumnAlias,
|
fixColumnAlias,
|
||||||
@ -142,8 +143,8 @@ export async function paginate<T extends ObjectLiteral>(
|
|||||||
// Switch from take and skip to limit and offset
|
// Switch from take and skip to limit and offset
|
||||||
// due to this problem https://github.com/typeorm/typeorm/issues/5670
|
// due to this problem https://github.com/typeorm/typeorm/issues/5670
|
||||||
// (anyway this creates more clean query without double dinstict)
|
// (anyway this creates more clean query without double dinstict)
|
||||||
queryBuilder.limit(limit).offset((page - 1) * limit)
|
// queryBuilder.limit(limit).offset((page - 1) * limit)
|
||||||
// queryBuilder.take(limit).skip((page - 1) * limit)
|
queryBuilder.take(limit).skip((page - 1) * limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.relations?.length) {
|
if (config.relations?.length) {
|
||||||
@ -161,9 +162,8 @@ export async function paginate<T extends ObjectLiteral>(
|
|||||||
const columnProperties = getPropertiesByColumnName(order[0])
|
const columnProperties = getPropertiesByColumnName(order[0])
|
||||||
const { isVirtualProperty } = extractVirtualProperty(queryBuilder, columnProperties)
|
const { isVirtualProperty } = extractVirtualProperty(queryBuilder, columnProperties)
|
||||||
const isRelation = checkIsRelation(queryBuilder, columnProperties.propertyPath)
|
const isRelation = checkIsRelation(queryBuilder, columnProperties.propertyPath)
|
||||||
|
const isEmbeded = checkIsEmbedded(queryBuilder, columnProperties.propertyPath)
|
||||||
const alias = fixColumnAlias(columnProperties, queryBuilder.alias, isRelation, isVirtualProperty)
|
const alias = fixColumnAlias(columnProperties, queryBuilder.alias, isRelation, isVirtualProperty, isEmbeded)
|
||||||
|
|
||||||
queryBuilder.addOrderBy(alias, order[1], nullSort)
|
queryBuilder.addOrderBy(alias, order[1], nullSort)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,8 +194,15 @@ export async function paginate<T extends ObjectLiteral>(
|
|||||||
const property = getPropertiesByColumnName(column)
|
const property = getPropertiesByColumnName(column)
|
||||||
const { isVirtualProperty, query: virtualQuery } = extractVirtualProperty(qb, property)
|
const { isVirtualProperty, query: virtualQuery } = extractVirtualProperty(qb, property)
|
||||||
const isRelation = checkIsRelation(qb, property.propertyPath)
|
const isRelation = checkIsRelation(qb, property.propertyPath)
|
||||||
|
const isEmbeded = checkIsEmbedded(qb, property.propertyPath)
|
||||||
const alias = fixColumnAlias(property, qb.alias, isRelation, isVirtualProperty, virtualQuery)
|
const alias = fixColumnAlias(
|
||||||
|
property,
|
||||||
|
qb.alias,
|
||||||
|
isRelation,
|
||||||
|
isVirtualProperty,
|
||||||
|
isEmbeded,
|
||||||
|
virtualQuery
|
||||||
|
)
|
||||||
const condition: WherePredicateOperator = {
|
const condition: WherePredicateOperator = {
|
||||||
operator: 'ilike',
|
operator: 'ilike',
|
||||||
parameters: [alias, `:${column}`],
|
parameters: [alias, `:${column}`],
|
||||||
|
Loading…
Reference in New Issue
Block a user