From dabb9913c7e76b2213c9c4a3a99b533d4c8c7a33 Mon Sep 17 00:00:00 2001 From: David Sanchez Date: Thu, 23 Feb 2023 02:58:47 -0500 Subject: [PATCH] feat: add new feature to allowing nested relations (#499) Should fix #452, #405 --- src/__tests__/cat-home-pillow.entity.ts | 17 +++++++ src/__tests__/cat-home.entity.ts | 6 ++- src/paginate.spec.ts | 65 ++++++++++++++++++++++++- src/paginate.ts | 46 ++++++++++++++--- 4 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 src/__tests__/cat-home-pillow.entity.ts diff --git a/src/__tests__/cat-home-pillow.entity.ts b/src/__tests__/cat-home-pillow.entity.ts new file mode 100644 index 0000000..ff94080 --- /dev/null +++ b/src/__tests__/cat-home-pillow.entity.ts @@ -0,0 +1,17 @@ +import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm' +import { CatHomeEntity } from './cat-home.entity' + +@Entity() +export class CatHomePillowEntity { + @PrimaryGeneratedColumn() + id: number + + @ManyToOne(() => CatHomeEntity, (home) => home.pillows) + home: CatHomeEntity + + @Column() + color: string + + @CreateDateColumn() + createdAt: string +} diff --git a/src/__tests__/cat-home.entity.ts b/src/__tests__/cat-home.entity.ts index 32939dd..b6db6d8 100644 --- a/src/__tests__/cat-home.entity.ts +++ b/src/__tests__/cat-home.entity.ts @@ -1,5 +1,6 @@ -import { Column, CreateDateColumn, Entity, OneToOne, PrimaryGeneratedColumn, VirtualColumn } from 'typeorm' +import { Column, CreateDateColumn, Entity, OneToMany, OneToOne, PrimaryGeneratedColumn, VirtualColumn } from 'typeorm' import { CatEntity } from './cat.entity' +import { CatHomePillowEntity } from './cat-home-pillow.entity' @Entity() export class CatHomeEntity { @@ -12,6 +13,9 @@ export class CatHomeEntity { @OneToOne(() => CatEntity, (cat) => cat.home) cat: CatEntity + @OneToMany(() => CatHomePillowEntity, (pillow) => pillow.home) + pillows: CatHomePillowEntity[] + @CreateDateColumn() createdAt: string diff --git a/src/paginate.spec.ts b/src/paginate.spec.ts index ab8d661..644d2a2 100644 --- a/src/paginate.spec.ts +++ b/src/paginate.spec.ts @@ -1,10 +1,11 @@ -import { Repository, In, DataSource } from 'typeorm' +import { Repository, In, DataSource, TypeORMError } from 'typeorm' import { Paginated, paginate, PaginateConfig, NO_PAGINATION } from './paginate' import { PaginateQuery } from './decorator' import { HttpException } from '@nestjs/common' import { CatEntity } from './__tests__/cat.entity' import { CatToyEntity } from './__tests__/cat-toy.entity' import { CatHomeEntity } from './__tests__/cat-home.entity' +import { CatHomePillowEntity } from './__tests__/cat-home-pillow.entity' import { clone } from 'lodash' import { FilterComparator, @@ -21,9 +22,11 @@ describe('paginate', () => { let catRepo: Repository let catToyRepo: Repository let catHomeRepo: Repository + let catHomePillowRepo: Repository let cats: CatEntity[] let catToys: CatToyEntity[] let catHomes: CatHomeEntity[] + let catHomePillows: CatHomePillowEntity[] beforeAll(async () => { dataSource = new DataSource({ @@ -31,12 +34,13 @@ describe('paginate', () => { database: ':memory:', synchronize: true, logging: false, - entities: [CatEntity, CatToyEntity, CatHomeEntity], + entities: [CatEntity, CatToyEntity, CatHomeEntity, CatHomePillowEntity], }) await dataSource.initialize() catRepo = dataSource.getRepository(CatEntity) catToyRepo = dataSource.getRepository(CatToyEntity) catHomeRepo = dataSource.getRepository(CatHomeEntity) + catHomePillowRepo = dataSource.getRepository(CatHomePillowEntity) cats = await catRepo.save([ catRepo.create({ name: 'Milo', color: 'brown', age: 6, size: { height: 25, width: 10, length: 40 } }), @@ -55,6 +59,14 @@ describe('paginate', () => { catHomeRepo.create({ name: 'Box', cat: cats[0] }), catHomeRepo.create({ name: 'House', cat: cats[1] }), ]) + catHomePillows = await catHomePillowRepo.save([ + catHomePillowRepo.create({ color: 'red', home: catHomes[0] }), + catHomePillowRepo.create({ color: 'yellow', home: catHomes[0] }), + catHomePillowRepo.create({ color: 'blue', home: catHomes[0] }), + catHomePillowRepo.create({ color: 'pink', home: catHomes[1] }), + catHomePillowRepo.create({ color: 'purple', home: catHomes[1] }), + catHomePillowRepo.create({ color: 'teal', home: catHomes[1] }), + ]) // add friends to Milo catRepo.save({ ...cats[0], friends: cats.slice(1) }) @@ -559,6 +571,55 @@ describe('paginate', () => { expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&search=Garfield') }) + it('should load nested relations', async () => { + const config: PaginateConfig = { + relations: { home: { pillows: true } }, + 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'], + sortableColumns: ['id'], + } + const query: PaginateQuery = { + path: '', + } + + try { + await paginate(query, catRepo, config) + } catch (err) { + expect(err).toBeInstanceOf(TypeORMError) + } + }) + it('should return result based on search term and searchBy columns', async () => { const config: PaginateConfig = { sortableColumns: ['id', 'name', 'color'], diff --git a/src/paginate.ts b/src/paginate.ts index 89c77c1..2dd022e 100644 --- a/src/paginate.ts +++ b/src/paginate.ts @@ -1,4 +1,11 @@ -import { Repository, SelectQueryBuilder, Brackets, FindOptionsWhere, ObjectLiteral } from 'typeorm' +import { + Repository, + SelectQueryBuilder, + Brackets, + FindOptionsWhere, + FindOptionsRelations, + ObjectLiteral, +} from 'typeorm' import { PaginateQuery } from './decorator' import { ServiceUnavailableException, Logger } from '@nestjs/common' import { mapKeys } from 'lodash' @@ -43,7 +50,7 @@ export class Paginated { } export interface PaginateConfig { - relations?: RelationColumn[] + relations?: FindOptionsRelations | RelationColumn[] sortableColumns: Column[] nullSort?: 'first' | 'last' searchableColumns?: Column[] @@ -137,7 +144,7 @@ export async function paginate( let [items, totalItems]: [T[], number] = [[], 0] - const queryBuilder = repo instanceof Repository ? repo.createQueryBuilder('e') : repo + const queryBuilder = repo instanceof Repository ? repo.createQueryBuilder('__root') : repo if (isPaginated) { // Switch from take and skip to limit and offset @@ -147,10 +154,35 @@ export async function paginate( queryBuilder.take(limit).skip((page - 1) * limit) } - if (config.relations?.length) { - config.relations.forEach((relation) => { - queryBuilder.leftJoinAndSelect(`${queryBuilder.alias}.${relation}`, `${queryBuilder.alias}_${relation}`) - }) + if (config.relations) { + // relations: ["relation"] + if (Array.isArray(config.relations)) { + config.relations.forEach((relation) => { + queryBuilder.leftJoinAndSelect(`${queryBuilder.alias}.${relation}`, `${queryBuilder.alias}_${relation}`) + }) + } 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}` + ) + + if (typeof relationSchema === 'object') { + createQueryBuilderRelations(relationName, relationSchema, `${prefix}_${relationName}`) + } + }) + } + createQueryBuilderRelations(queryBuilder.alias, config.relations) + } } let nullSort: 'NULLS LAST' | 'NULLS FIRST' | undefined = undefined