diff --git a/src/modules/database/base/tree.repository.ts b/src/modules/database/base/tree.repository.ts new file mode 100644 index 0000000..d145732 --- /dev/null +++ b/src/modules/database/base/tree.repository.ts @@ -0,0 +1,98 @@ +import { isNil, pick, unset } from 'lodash'; +import { + EntityManager, + EntityTarget, + FindOptionsUtils, + FindTreeOptions, + ObjectLiteral, + QueryRunner, + SelectQueryBuilder, + TreeRepository, + TreeRepositoryUtils, +} from 'typeorm'; + +import { OrderType, TreeChildrenResolve } from '../constants'; +import { OrderQueryType, QueryParams } from '../types'; +import { getOrderByQuery } from '../utils'; + +export abstract class BaseTreeRepository extends TreeRepository { + protected abstract _qbName: string; + + protected _childrenResolve?: TreeChildrenResolve; + + protected orderBy?: string | { name: string; order: `${OrderType}` }; + + // eslint-disable-next-line @typescript-eslint/no-useless-constructor + constructor(target: EntityTarget, manager: EntityManager, queryRunner?: QueryRunner) { + super(target, manager, queryRunner); + } + + get qbName() { + return this._qbName; + } + + get childrenResolve() { + return this._childrenResolve; + } + + buildBaseQB(qb?: SelectQueryBuilder): SelectQueryBuilder { + const queryBuilder = qb ?? this.createQueryBuilder(this.qbName); + return queryBuilder.leftJoinAndSelect(`${this.qbName}.parent`, 'parent'); + } + + addOrderByQuery(qb: SelectQueryBuilder, orderBy?: OrderQueryType) { + const orderByQuery = orderBy ?? this.orderBy; + return isNil(orderByQuery) ? qb : getOrderByQuery(qb, this.qbName, orderByQuery); + } + + async findTrees(options?: FindTreeOptions & QueryParams) { + const roots = await this.findRoots(options); + await Promise.all(root.map((root) => this.findDescendantsTree(root, options))); + return roots; + } + + async findDescendantsTree(entity: T, options?: FindTreeOptions & QueryParams) { + const { addQuery, orderBy, withTrashed, onlyTrashed } = options ?? {}; + let qb = this.buildBaseQB( + this.createDescendantsQueryBuilder(this.qbName, 'treeClosure', entity), + ); + qb = addQuery + ? await addQuery(this.addOrderByQuery(qb, orderBy)) + : this.addOrderByQuery(qb, orderBy); + if (withTrashed) { + qb.withDeleted(); + if (onlyTrashed) { + qb.where(`${this.qbName}.deletedAt IS NOT NULL`); + } + } + FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, pick(options, ['relations', 'depth'])); + const entities = await qb.getRawAndEntities(); + const relationMaps = TreeRepositoryUtils.createRelationMaps( + this.manager, + this.metadata, + this.qbName, + entities.raw, + ); + TreeRepositoryUtils.buildChildrenEntityTree( + this.metadata, + entity, + entities.entities, + relationMaps, + { depth: -1, ...pick(options, ['relations']) }, + ); + return entity; + } + + async toFlatTrees(trees: T[], depth = 0, parent: T | null = null) { + const data: Omit[] = []; + for (const item of trees) { + (item as any).depth = depth; + (item as any).parent = parent; + const { children } = item; + unset(item, 'children'); + data.push(item); + data.push(...(await this.toFlatTrees(children, depth + 1, item))); + } + return data as T[]; + } +} diff --git a/src/modules/database/constants.ts b/src/modules/database/constants.ts index 1ca733b..af1e344 100644 --- a/src/modules/database/constants.ts +++ b/src/modules/database/constants.ts @@ -13,3 +13,9 @@ export enum OrderType { ASC = 'ASC', DESC = 'DESC', } + +export enum TreeChildrenResolve { + DELETE = 'delete', + UP = 'up', + ROOT = 'root', +} diff --git a/src/modules/database/types.ts b/src/modules/database/types.ts index 10bb10d..7500941 100644 --- a/src/modules/database/types.ts +++ b/src/modules/database/types.ts @@ -28,3 +28,13 @@ export type OrderQueryType = | string | { name: string; order: `${OrderType}` } | Array; + +export interface QueryParams { + addQuery?: QueryHook; + + orderBy?: OrderQueryType; + + withTrashed?: boolean; + + onlyTrashed?: boolean; +}