feat: add new feature to allowing nested relations (#499)

Should fix #452, #405
This commit is contained in:
David Sanchez 2023-02-23 02:58:47 -05:00 committed by GitHub
parent ab68a7b9c8
commit dabb9913c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 124 additions and 10 deletions

View File

@ -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
}

View File

@ -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

View File

@ -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<CatEntity>
let catToyRepo: Repository<CatToyEntity>
let catHomeRepo: Repository<CatHomeEntity>
let catHomePillowRepo: Repository<CatHomePillowEntity>
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<CatEntity> = {
relations: { home: { pillows: true } },
sortableColumns: ['id', 'name'],
searchableColumns: ['name'],
}
const query: PaginateQuery = {
path: '',
search: 'Garfield',
}
const result = await paginate<CatEntity>(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<CatEntity> = {
relations: <any>['homee'],
sortableColumns: ['id'],
}
const query: PaginateQuery = {
path: '',
}
try {
await paginate<CatEntity>(query, catRepo, config)
} catch (err) {
expect(err).toBeInstanceOf(TypeORMError)
}
})
it('should return result based on search term and searchBy columns', async () => {
const config: PaginateConfig<CatEntity> = {
sortableColumns: ['id', 'name', 'color'],

View File

@ -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<T> {
}
export interface PaginateConfig<T> {
relations?: RelationColumn<T>[]
relations?: FindOptionsRelations<T> | RelationColumn<T>[]
sortableColumns: Column<T>[]
nullSort?: 'first' | 'last'
searchableColumns?: Column<T>[]
@ -137,7 +144,7 @@ export async function paginate<T extends ObjectLiteral>(
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<T extends ObjectLiteral>(
queryBuilder.take(limit).skip((page - 1) * limit)
}
if (config.relations?.length) {
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<T> | RelationColumn<T>[],
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