diff --git a/src/modules/content/repositories/category.repository.ts b/src/modules/content/repositories/category.repository.ts new file mode 100644 index 0000000..55b1c1c --- /dev/null +++ b/src/modules/content/repositories/category.repository.ts @@ -0,0 +1,114 @@ +import { pick, unset } from 'lodash'; +import { FindOptionsUtils, FindTreeOptions, TreeRepository, TreeRepositoryUtils } from 'typeorm'; + +import { CategoryEntity } from '@/modules/content/entities/CategoryEntity'; +import { CustomRepository } from '@/modules/database/decorators/repository.decorator'; + +@CustomRepository(CategoryEntity) +export class CategoryRepository extends TreeRepository { + buildBaseQB() { + return this.createQueryBuilder('category').leftJoinAndSelect('category.parent', 'parent'); + } + + async findTrees(options?: FindTreeOptions) { + const roots = await this.findRoots(options); + await Promise.all(roots.map((root) => this.findDescendantsTree(root, options))); + return roots; + } + + findRoots(options?: FindTreeOptions): Promise { + const escape = (val: string) => this.manager.connection.driver.escape(val); + const joinColumn = this.metadata.treeParentRelation!.joinColumns[0]; + const parentPropertyName = joinColumn.givenDatabaseName || joinColumn.databaseName; + const qb = this.buildBaseQB().orderBy('category.customOrder', 'ASC'); + FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options); + return qb.where(`${escape('category')}.${escape(parentPropertyName)} IS NULL`).getMany(); + } + + findDescendants(entity: CategoryEntity, options?: FindTreeOptions): Promise { + const qb = this.createDescendantsQueryBuilder('category', 'treeClosure', entity); + FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options); + qb.orderBy('category.customOrder', 'ASC'); + return qb.getMany(); + } + + findAncestors(entity: CategoryEntity, options?: FindTreeOptions): Promise { + const qb = this.createAncestorsQueryBuilder('category', 'treeClosure', entity); + FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options); + qb.orderBy('category.customOrder', 'ASC'); + return qb.getMany(); + } + + async findDescendantsTree(entity: CategoryEntity, options?: FindTreeOptions) { + const qb = this.createDescendantsQueryBuilder('category', 'treeClosure', entity) + .leftJoinAndSelect('category.parent', 'parent') + .orderBy('category.customOrder', 'ASC'); + FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, pick(options, ['relations', 'depth'])); + const entities = await qb.getRawAndEntities(); + const relationMaps = TreeRepositoryUtils.createRelationMaps( + this.manager, + this.metadata, + 'category', + entities.raw, + ); + TreeRepositoryUtils.buildChildrenEntityTree( + this.metadata, + entity, + entities.entities, + relationMaps, + { depth: -1, ...pick(options, ['relations']) }, + ); + return entity; + } + + async findAncestorsTree( + entity: CategoryEntity, + options?: FindTreeOptions, + ): Promise { + const qb = this.createAncestorsQueryBuilder('category', 'treeClosure', entity) + .leftJoinAndSelect('category.parent', 'parent') + .orderBy('category.customOrder', 'ASC'); + FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options); + const entities = await qb.getRawAndEntities(); + const relationMaps = TreeRepositoryUtils.createRelationMaps( + this.manager, + this.metadata, + 'category', + entities.raw, + ); + TreeRepositoryUtils.buildParentEntityTree( + this.metadata, + entity, + entities.entities, + relationMaps, + ); + return entity; + } + + async countDescendants(entity: CategoryEntity): Promise { + const qb = this.createDescendantsQueryBuilder('category', 'treeClosure', entity); + return qb.getCount(); + } + + async countAncestors(entity: CategoryEntity) { + const qb = this.createAncestorsQueryBuilder('category', 'treeClosure', entity); + return qb.getCount(); + } + + async toFlatTrees( + trees: CategoryEntity[], + depth = 0, + parent: CategoryEntity | null = null, + ): Promise { + const data: Omit[] = []; + for (const item of trees) { + item.depth = depth; + item.parent = parent; + const { children } = item; + unset(item, 'children'); + data.push(item); + data.push(...(await this.toFlatTrees(children, depth + 1, item))); + } + return data as CategoryEntity[]; + } +} diff --git a/src/modules/content/repositories/comment.repository.ts b/src/modules/content/repositories/comment.repository.ts new file mode 100644 index 0000000..4e1acac --- /dev/null +++ b/src/modules/content/repositories/comment.repository.ts @@ -0,0 +1,95 @@ +import { pick, unset } from 'lodash'; +import { + FindOptionsUtils, + FindTreeOptions, + SelectQueryBuilder, + TreeRepository, + TreeRepositoryUtils, +} from 'typeorm'; + +import { CommentEntity } from '@/modules/content/entities/comment.entity'; +import { CustomRepository } from '@/modules/database/decorators/repository.decorator'; + +type FindCommentTreeOptions = FindTreeOptions & { + addQuery?: (query: SelectQueryBuilder) => SelectQueryBuilder; +}; +@CustomRepository(CommentEntity) +export class CommentRepository extends TreeRepository { + buildBaseQB(qb: SelectQueryBuilder): SelectQueryBuilder { + return qb + .leftJoinAndSelect(`comment.parent`, 'parent') + .leftJoinAndSelect(`comment.post`, `post`) + .orderBy('comment.createdAt', 'DESC'); + } + + async findTrees(options: FindCommentTreeOptions): Promise { + options.relations = ['parent', 'children']; + const roots = await this.findRoots(options); + await Promise.all(roots.map((root) => this.findDescendantsTree(root, options))); + return roots; + } + + findRoots(options?: FindCommentTreeOptions): Promise { + const { addQuery, ...rest } = options; + const escape = (val: string) => this.manager.connection.driver.escape(val); + const joinColumn = this.metadata.treeParentRelation!.joinColumns[0]; + const parentPropertyName = joinColumn.givenDatabaseName || joinColumn.databaseName; + + let qb = this.buildBaseQB(this.createQueryBuilder('comment')); + FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, rest); + qb.where(`${escape('comment')}.${escape(parentPropertyName)} IS NULL`); + qb = addQuery ? addQuery(qb) : qb; + return qb.getMany(); + } + + createDtsQueryBuilder( + closureTable: string, + entity: CommentEntity, + options: FindCommentTreeOptions = {}, + ): SelectQueryBuilder { + const { addQuery } = options; + const qb = this.buildBaseQB( + super.createDescendantsQueryBuilder('comment', closureTable, entity), + ); + return addQuery ? addQuery(qb) : qb; + } + + async findDescendantsTree( + entity: CommentEntity, + options: FindCommentTreeOptions = {}, + ): Promise { + const qb: SelectQueryBuilder = this.createDtsQueryBuilder( + 'treeClosure', + entity, + options, + ); + FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, pick(options, ['relations', 'depth'])); + const entities = await qb.getRawAndEntities(); + const relationMaps = TreeRepositoryUtils.createRelationMaps( + this.manager, + this.metadata, + 'comment', + entities.raw, + ); + TreeRepositoryUtils.buildChildrenEntityTree( + this.metadata, + entity, + entities.entities, + relationMaps, + { depth: -1, ...pick(options, ['relations']) }, + ); + return entity; + } + + async toFlatTrees(trees: CommentEntity[], depth = 0): Promise { + const data: Omit[] = []; + for (const item of trees) { + item.depth = depth; + const { children } = item; + unset(item, 'children'); + data.push(item); + data.push(...(await this.toFlatTrees(children, depth + 1))); + } + return data as CommentEntity[]; + } +}