fix: many-to-many relations (#492)

This commit is contained in:
xMase 2023-02-15 09:05:52 +01:00 committed by GitHub
parent 02afb0a0d4
commit 53f18bd547
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 69 additions and 14 deletions

View File

@ -5,6 +5,8 @@ import {
DeleteDateColumn,
Entity,
JoinColumn,
JoinTable,
ManyToMany,
OneToMany,
OneToOne,
PrimaryGeneratedColumn,
@ -43,6 +45,10 @@ export class CatEntity {
@DeleteDateColumn({ nullable: true })
deletedAt?: string
@ManyToMany(() => CatEntity)
@JoinTable()
friends: CatEntity[]
@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

View File

@ -1,7 +1,13 @@
import { Brackets, FindOperator, SelectQueryBuilder } from 'typeorm'
import { WherePredicateOperator } from 'typeorm/query-builder/WhereClause'
import { PaginateQuery } from './decorator'
import { checkIsRelation, extractVirtualProperty, fixColumnAlias, getPropertiesByColumnName } from './helper'
import {
checkIsEmbedded,
checkIsRelation,
extractVirtualProperty,
fixColumnAlias,
getPropertiesByColumnName,
} from './helper'
import {
FilterComparator,
FilterOperator,
@ -95,9 +101,17 @@ export function addWhereCondition<T>(qb: SelectQueryBuilder<T>, column: string,
const columnProperties = getPropertiesByColumnName(column)
const { isVirtualProperty, query: virtualQuery } = extractVirtualProperty(qb, columnProperties)
const isRelation = checkIsRelation(qb, columnProperties.propertyPath)
const isEmbedded = checkIsEmbedded(qb, columnProperties.propertyPath)
filter[column].forEach((columnFilter: Filter, index: number) => {
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 parameters = fixQueryParam(alias, columnNamePerIteration, columnFilter, condition, {
[columnNamePerIteration]: columnFilter.findOperator.value,

View File

@ -68,12 +68,20 @@ export function checkIsRelation(qb: SelectQueryBuilder<unknown>, propertyPath: s
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
export function fixColumnAlias(
properties: ColumnProperties,
alias: string,
isRelation = false,
isVirtualProperty = false,
isEmbedded = false,
query?: ColumnMetadata['query']
): string {
if (isRelation) {
@ -82,11 +90,13 @@ export function fixColumnAlias(
} else if (isVirtualProperty && !query) {
return `${alias}_${properties.propertyPath}_${properties.propertyName}`
} else {
return `${alias}_${properties.propertyPath}.${properties.propertyName}` // include embeded property and relation property
return `${alias}_${properties.propertyPath}.${properties.propertyName}`
}
} else if (isVirtualProperty) {
return query ? `(${query(`${alias}`)})` : `${alias}_${properties.propertyName}`
} else if (isEmbedded) {
return `${alias}.${properties.propertyPath}.${properties.propertyName}`
} else {
return `${alias}.${properties.propertyName}` //
return `${alias}.${properties.propertyName}`
}
}

View File

@ -55,6 +55,9 @@ describe('paginate', () => {
catHomeRepo.create({ name: 'Box', cat: cats[0] }),
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 () => {
@ -462,9 +465,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(catToys[2])
delete toy.cat
const toy2 = clone(catToys[2])
const toy2 = clone(catToys[1])
delete toy2.cat
expect(result.data).toStrictEqual([Object.assign(clone(cats[0]), { toys: [toy2, toy] })])
@ -807,7 +810,7 @@ describe('paginate', () => {
delete copy.cat
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]]
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)
})
})

View File

@ -6,6 +6,7 @@ import { stringify } from 'querystring'
import { WherePredicateOperator } from 'typeorm/query-builder/WhereClause'
import {
checkIsRelation,
checkIsEmbedded,
Column,
extractVirtualProperty,
fixColumnAlias,
@ -142,8 +143,8 @@ export async function paginate<T extends ObjectLiteral>(
// 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)
// queryBuilder.limit(limit).offset((page - 1) * limit)
queryBuilder.take(limit).skip((page - 1) * limit)
}
if (config.relations?.length) {
@ -161,9 +162,8 @@ export async function paginate<T extends ObjectLiteral>(
const columnProperties = getPropertiesByColumnName(order[0])
const { isVirtualProperty } = extractVirtualProperty(queryBuilder, columnProperties)
const isRelation = checkIsRelation(queryBuilder, columnProperties.propertyPath)
const alias = fixColumnAlias(columnProperties, queryBuilder.alias, isRelation, isVirtualProperty)
const isEmbeded = checkIsEmbedded(queryBuilder, columnProperties.propertyPath)
const alias = fixColumnAlias(columnProperties, queryBuilder.alias, isRelation, isVirtualProperty, isEmbeded)
queryBuilder.addOrderBy(alias, order[1], nullSort)
}
@ -194,8 +194,15 @@ export async function paginate<T extends ObjectLiteral>(
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 isEmbeded = checkIsEmbedded(qb, property.propertyPath)
const alias = fixColumnAlias(
property,
qb.alias,
isRelation,
isVirtualProperty,
isEmbeded,
virtualQuery
)
const condition: WherePredicateOperator = {
operator: 'ilike',
parameters: [alias, `:${column}`],