feat: add new feature to allowing nested relations (#499)
Should fix #452, #405
This commit is contained in:
parent
ab68a7b9c8
commit
dabb9913c7
17
src/__tests__/cat-home-pillow.entity.ts
Normal file
17
src/__tests__/cat-home-pillow.entity.ts
Normal 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
|
||||
}
|
@ -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
|
||||
|
||||
|
@ -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'],
|
||||
|
@ -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) {
|
||||
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<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
|
||||
|
Loading…
Reference in New Issue
Block a user