diff --git a/src/__tests__/cat.entity.ts b/src/__tests__/cat.entity.ts index c4afbf6..6ba7373 100644 --- a/src/__tests__/cat.entity.ts +++ b/src/__tests__/cat.entity.ts @@ -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 diff --git a/src/filter.ts b/src/filter.ts index e7e2c8b..6038a07 100644 --- a/src/filter.ts +++ b/src/filter.ts @@ -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(qb: SelectQueryBuilder, 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, diff --git a/src/helper.ts b/src/helper.ts index 2a6c13e..8b507a9 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -68,12 +68,20 @@ export function checkIsRelation(qb: SelectQueryBuilder, propertyPath: s return !!qb?.expressionMap?.mainAlias?.metadata?.hasRelationWithPropertyPath(propertyPath) } +export function checkIsEmbedded(qb: SelectQueryBuilder, 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}` } } diff --git a/src/paginate.spec.ts b/src/paginate.spec.ts index 84f8a5b..cd623f1 100644 --- a/src/paginate.spec.ts +++ b/src/paginate.spec.ts @@ -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(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 = { + sortableColumns: ['id'], + defaultSortBy: [['id', 'ASC']], + relations: ['friends'], + } + const query: PaginateQuery = { + path: '', + } + + const result = await paginate(query, catRepo, config) + + expect(result.data.length).toBe(4) + }) }) diff --git a/src/paginate.ts b/src/paginate.ts index d0928a5..d9d773c 100644 --- a/src/paginate.ts +++ b/src/paginate.ts @@ -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( // 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( 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( 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}`],