From 6f6b4da9a97b2ce560544f475cc9e8fc1ce7db1f Mon Sep 17 00:00:00 2001 From: Robin De Schepper Date: Sat, 9 Sep 2023 18:41:35 +0200 Subject: [PATCH] feat: support dot notation syntax for nested relations (#739) --- src/paginate.spec.ts | 35 +++++++++++++++++++++++++++- src/paginate.ts | 54 ++++++++++++++++++-------------------------- 2 files changed, 56 insertions(+), 33 deletions(-) diff --git a/src/paginate.spec.ts b/src/paginate.spec.ts index ed01df4..740d71d 100644 --- a/src/paginate.spec.ts +++ b/src/paginate.spec.ts @@ -710,7 +710,7 @@ describe('paginate', () => { expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&search=Garfield') }) - it('should load nested relations', async () => { + it('should load nested relations (object notation)', async () => { const config: PaginateConfig = { relations: { home: { pillows: true } }, sortableColumns: ['id', 'name'], @@ -743,6 +743,39 @@ describe('paginate', () => { expect(result.data[0].home.pillows).toStrictEqual(cat.home.pillows) }) + it('should load nested relations (array notation)', async () => { + const config: PaginateConfig = { + relations: ['home.pillows'], + sortableColumns: ['id', 'name'], + searchableColumns: ['name'], + } + const query: PaginateQuery = { + path: '', + search: 'Garfield', + } + + const result = await paginate(query, catRepo, config) + + const cat = clone(cats[1]) + const catHomesClone = clone(catHomes[1]) + const catHomePillowsClone3 = clone(catHomePillows[3]) + delete catHomePillowsClone3.home + const catHomePillowsClone4 = clone(catHomePillows[4]) + delete catHomePillowsClone4.home + const catHomePillowsClone5 = clone(catHomePillows[5]) + delete catHomePillowsClone5.home + + catHomesClone.countCat = cats.filter((cat) => cat.id === catHomesClone.cat.id).length + catHomesClone.pillows = [catHomePillowsClone3, catHomePillowsClone4, catHomePillowsClone5] + cat.home = catHomesClone + delete cat.home.cat + + expect(result.meta.search).toStrictEqual('Garfield') + expect(result.data).toStrictEqual([cat]) + expect(result.data[0].home).toBeDefined() + expect(result.data[0].home.pillows).toStrictEqual(cat.home.pillows) + }) + it('should throw an error when nonexistent relation loaded', async () => { const config: PaginateConfig = { relations: ['homee'], diff --git a/src/paginate.ts b/src/paginate.ts index d1586c1..c2984a2 100644 --- a/src/paginate.ts +++ b/src/paginate.ts @@ -6,6 +6,7 @@ import { FindOptionsRelations, ObjectLiteral, FindOptionsUtils, + FindOptionsRelationByString, } from 'typeorm' import { PaginateQuery } from './decorator' import { ServiceUnavailableException, Logger } from '@nestjs/common' @@ -28,6 +29,7 @@ import { getQueryUrlComponents, } from './helper' import { addFilter, FilterOperator, FilterSuffix } from './filter' +import { OrmUtils } from 'typeorm/util/OrmUtils' const logger: Logger = new Logger('nestjs-paginate') @@ -61,7 +63,7 @@ export enum PaginationType { } export interface PaginateConfig { - relations?: FindOptionsRelations | RelationColumn[] + relations?: FindOptionsRelations | RelationColumn[] | FindOptionsRelationByString sortableColumns: Column[] nullSort?: 'first' | 'last' searchableColumns?: Column[] @@ -123,41 +125,29 @@ export async function paginate( } if (config.relations) { - // relations: ["relation"] - if (Array.isArray(config.relations)) { - config.relations.forEach((relation) => { + const relations = Array.isArray(config.relations) + ? OrmUtils.propertyPathsToTruthyObject(config.relations) + : config.relations + const createQueryBuilderRelations = ( + prefix: string, + relations: FindOptionsRelations | RelationColumn[], + alias?: string + ) => { + Object.keys(relations).forEach((relationName) => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const relationSchema = relations![relationName]! + queryBuilder.leftJoinAndSelect( - `${queryBuilder.alias}.${relation}`, - `${queryBuilder.alias}_${relation}_rel` + `${alias ?? prefix}.${relationName}`, + `${alias ?? prefix}_${relationName}_rel` ) + + if (typeof relationSchema === 'object') { + createQueryBuilderRelations(relationName, relationSchema, `${alias ?? prefix}_${relationName}_rel`) + } }) - } else { - // relations: {relation:true} - const createQueryBuilderRelations = ( - prefix: string, - relations: FindOptionsRelations | RelationColumn[], - alias?: string - ) => { - Object.keys(relations).forEach((relationName) => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const relationSchema = relations![relationName]! - - queryBuilder.leftJoinAndSelect( - `${alias ?? prefix}.${relationName}`, - `${alias ?? prefix}_${relationName}_rel` - ) - - if (typeof relationSchema === 'object') { - createQueryBuilderRelations( - relationName, - relationSchema, - `${alias ?? prefix}_${relationName}_rel` - ) - } - }) - } - createQueryBuilderRelations(queryBuilder.alias, config.relations) } + createQueryBuilderRelations(queryBuilder.alias, relations) } let nullSort: 'NULLS LAST' | 'NULLS FIRST' | undefined = undefined