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 { CatEntity } from './cat.entity'
|
||||||
|
import { CatHomePillowEntity } from './cat-home-pillow.entity'
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class CatHomeEntity {
|
export class CatHomeEntity {
|
||||||
@ -12,6 +13,9 @@ export class CatHomeEntity {
|
|||||||
@OneToOne(() => CatEntity, (cat) => cat.home)
|
@OneToOne(() => CatEntity, (cat) => cat.home)
|
||||||
cat: CatEntity
|
cat: CatEntity
|
||||||
|
|
||||||
|
@OneToMany(() => CatHomePillowEntity, (pillow) => pillow.home)
|
||||||
|
pillows: CatHomePillowEntity[]
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt: string
|
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 { Paginated, paginate, PaginateConfig, NO_PAGINATION } from './paginate'
|
||||||
import { PaginateQuery } from './decorator'
|
import { PaginateQuery } from './decorator'
|
||||||
import { HttpException } from '@nestjs/common'
|
import { HttpException } from '@nestjs/common'
|
||||||
import { CatEntity } from './__tests__/cat.entity'
|
import { CatEntity } from './__tests__/cat.entity'
|
||||||
import { CatToyEntity } from './__tests__/cat-toy.entity'
|
import { CatToyEntity } from './__tests__/cat-toy.entity'
|
||||||
import { CatHomeEntity } from './__tests__/cat-home.entity'
|
import { CatHomeEntity } from './__tests__/cat-home.entity'
|
||||||
|
import { CatHomePillowEntity } from './__tests__/cat-home-pillow.entity'
|
||||||
import { clone } from 'lodash'
|
import { clone } from 'lodash'
|
||||||
import {
|
import {
|
||||||
FilterComparator,
|
FilterComparator,
|
||||||
@ -21,9 +22,11 @@ describe('paginate', () => {
|
|||||||
let catRepo: Repository<CatEntity>
|
let catRepo: Repository<CatEntity>
|
||||||
let catToyRepo: Repository<CatToyEntity>
|
let catToyRepo: Repository<CatToyEntity>
|
||||||
let catHomeRepo: Repository<CatHomeEntity>
|
let catHomeRepo: Repository<CatHomeEntity>
|
||||||
|
let catHomePillowRepo: Repository<CatHomePillowEntity>
|
||||||
let cats: CatEntity[]
|
let cats: CatEntity[]
|
||||||
let catToys: CatToyEntity[]
|
let catToys: CatToyEntity[]
|
||||||
let catHomes: CatHomeEntity[]
|
let catHomes: CatHomeEntity[]
|
||||||
|
let catHomePillows: CatHomePillowEntity[]
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
dataSource = new DataSource({
|
dataSource = new DataSource({
|
||||||
@ -31,12 +34,13 @@ describe('paginate', () => {
|
|||||||
database: ':memory:',
|
database: ':memory:',
|
||||||
synchronize: true,
|
synchronize: true,
|
||||||
logging: false,
|
logging: false,
|
||||||
entities: [CatEntity, CatToyEntity, CatHomeEntity],
|
entities: [CatEntity, CatToyEntity, CatHomeEntity, CatHomePillowEntity],
|
||||||
})
|
})
|
||||||
await dataSource.initialize()
|
await dataSource.initialize()
|
||||||
catRepo = dataSource.getRepository(CatEntity)
|
catRepo = dataSource.getRepository(CatEntity)
|
||||||
catToyRepo = dataSource.getRepository(CatToyEntity)
|
catToyRepo = dataSource.getRepository(CatToyEntity)
|
||||||
catHomeRepo = dataSource.getRepository(CatHomeEntity)
|
catHomeRepo = dataSource.getRepository(CatHomeEntity)
|
||||||
|
catHomePillowRepo = dataSource.getRepository(CatHomePillowEntity)
|
||||||
|
|
||||||
cats = await catRepo.save([
|
cats = await catRepo.save([
|
||||||
catRepo.create({ name: 'Milo', color: 'brown', age: 6, size: { height: 25, width: 10, length: 40 } }),
|
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: 'Box', cat: cats[0] }),
|
||||||
catHomeRepo.create({ name: 'House', cat: cats[1] }),
|
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
|
// add friends to Milo
|
||||||
catRepo.save({ ...cats[0], friends: cats.slice(1) })
|
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')
|
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 () => {
|
it('should return result based on search term and searchBy columns', async () => {
|
||||||
const config: PaginateConfig<CatEntity> = {
|
const config: PaginateConfig<CatEntity> = {
|
||||||
sortableColumns: ['id', 'name', 'color'],
|
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 { PaginateQuery } from './decorator'
|
||||||
import { ServiceUnavailableException, Logger } from '@nestjs/common'
|
import { ServiceUnavailableException, Logger } from '@nestjs/common'
|
||||||
import { mapKeys } from 'lodash'
|
import { mapKeys } from 'lodash'
|
||||||
@ -43,7 +50,7 @@ export class Paginated<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PaginateConfig<T> {
|
export interface PaginateConfig<T> {
|
||||||
relations?: RelationColumn<T>[]
|
relations?: FindOptionsRelations<T> | RelationColumn<T>[]
|
||||||
sortableColumns: Column<T>[]
|
sortableColumns: Column<T>[]
|
||||||
nullSort?: 'first' | 'last'
|
nullSort?: 'first' | 'last'
|
||||||
searchableColumns?: Column<T>[]
|
searchableColumns?: Column<T>[]
|
||||||
@ -137,7 +144,7 @@ export async function paginate<T extends ObjectLiteral>(
|
|||||||
|
|
||||||
let [items, totalItems]: [T[], number] = [[], 0]
|
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) {
|
if (isPaginated) {
|
||||||
// Switch from take and skip to limit and offset
|
// 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)
|
queryBuilder.take(limit).skip((page - 1) * limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.relations?.length) {
|
if (config.relations) {
|
||||||
config.relations.forEach((relation) => {
|
// relations: ["relation"]
|
||||||
queryBuilder.leftJoinAndSelect(`${queryBuilder.alias}.${relation}`, `${queryBuilder.alias}_${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
|
let nullSort: 'NULLS LAST' | 'NULLS FIRST' | undefined = undefined
|
||||||
|
Loading…
Reference in New Issue
Block a user