parent
							
								
									b37dfa8103
								
							
						
					
					
						commit
						4ec73cc0e7
					
				@ -32,6 +32,7 @@ export class ContentModule {
 | 
			
		||||
                    repositories.CategoryRepository,
 | 
			
		||||
                    services.CategoryService,
 | 
			
		||||
                    repositories.TagRepository,
 | 
			
		||||
                    repositories.CommentRepository,
 | 
			
		||||
                    { token: services.SearchService, optional: true },
 | 
			
		||||
                ],
 | 
			
		||||
                useFactory(
 | 
			
		||||
@ -39,6 +40,7 @@ export class ContentModule {
 | 
			
		||||
                    categoryRepository: repositories.CategoryRepository,
 | 
			
		||||
                    categoryService: services.CategoryService,
 | 
			
		||||
                    tagRepository: repositories.TagRepository,
 | 
			
		||||
                    commentRepository: repositories.CommentRepository,
 | 
			
		||||
                    searchService: services.SearchService,
 | 
			
		||||
                ) {
 | 
			
		||||
                    return new PostService(
 | 
			
		||||
@ -46,6 +48,7 @@ export class ContentModule {
 | 
			
		||||
                        categoryRepository,
 | 
			
		||||
                        categoryService,
 | 
			
		||||
                        tagRepository,
 | 
			
		||||
                        commentRepository,
 | 
			
		||||
                        searchService,
 | 
			
		||||
                        config.searchType,
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,9 @@
 | 
			
		||||
import { Exclude, Expose } from 'class-transformer';
 | 
			
		||||
import { Exclude, Expose, Type } from 'class-transformer';
 | 
			
		||||
import {
 | 
			
		||||
    BaseEntity,
 | 
			
		||||
    Column,
 | 
			
		||||
    CreateDateColumn,
 | 
			
		||||
    DeleteDateColumn,
 | 
			
		||||
    Entity,
 | 
			
		||||
    Index,
 | 
			
		||||
    ManyToOne,
 | 
			
		||||
@ -46,10 +47,17 @@ export class CommentEntity extends BaseEntity {
 | 
			
		||||
    depth = 0;
 | 
			
		||||
 | 
			
		||||
    @Expose({ groups: ['comment-detail', 'comment-list'] })
 | 
			
		||||
    @TreeParent({ onDelete: 'NO ACTION' })
 | 
			
		||||
    @TreeParent({ onDelete: 'CASCADE' })
 | 
			
		||||
    parent: Relation<CommentEntity> | null;
 | 
			
		||||
 | 
			
		||||
    @Expose({ groups: ['comment-tree'] })
 | 
			
		||||
    @TreeChildren({ cascade: true })
 | 
			
		||||
    children: Relation<CommentEntity[]>;
 | 
			
		||||
 | 
			
		||||
    @Expose()
 | 
			
		||||
    @Type(() => Date)
 | 
			
		||||
    @DeleteDateColumn({
 | 
			
		||||
        comment: '删除时间',
 | 
			
		||||
    })
 | 
			
		||||
    deletedAt: Date;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -102,9 +102,7 @@ export class PostEntity extends BaseEntity {
 | 
			
		||||
    category: Relation<CategoryEntity>;
 | 
			
		||||
 | 
			
		||||
    @Expose()
 | 
			
		||||
    @ManyToMany(() => TagEntity, (tag) => tag.posts, {
 | 
			
		||||
        cascade: true,
 | 
			
		||||
    })
 | 
			
		||||
    @ManyToMany(() => TagEntity, (tag) => tag.posts)
 | 
			
		||||
    @JoinTable()
 | 
			
		||||
    tags: Relation<TagEntity>[];
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,165 +1,13 @@
 | 
			
		||||
import { pick, unset } from 'lodash';
 | 
			
		||||
import { FindOptionsUtils, FindTreeOptions, TreeRepository } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
import { CategoryEntity } from '@/modules/content/entities';
 | 
			
		||||
import { BaseTreeRepository } from '@/modules/database/base';
 | 
			
		||||
import { OrderType, TreeChildrenResolve } from '@/modules/database/constants';
 | 
			
		||||
import { CustomRepository } from '@/modules/database/decorators';
 | 
			
		||||
 | 
			
		||||
@CustomRepository(CategoryEntity)
 | 
			
		||||
export class CategoryRepository extends TreeRepository<CategoryEntity> {
 | 
			
		||||
    /**
 | 
			
		||||
     * 构建基础查询器
 | 
			
		||||
     */
 | 
			
		||||
    buildBaseQB() {
 | 
			
		||||
        return this.createQueryBuilder('category').leftJoinAndSelect('category.parent', 'parent');
 | 
			
		||||
    }
 | 
			
		||||
export class CategoryRepository extends BaseTreeRepository<CategoryEntity> {
 | 
			
		||||
    protected _qbName = 'category';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 树形结构查询
 | 
			
		||||
     * @param options
 | 
			
		||||
     */
 | 
			
		||||
    async findTrees(
 | 
			
		||||
        options?: FindTreeOptions & {
 | 
			
		||||
            onlyTrashed?: boolean;
 | 
			
		||||
            withTrashed?: boolean;
 | 
			
		||||
        },
 | 
			
		||||
    ) {
 | 
			
		||||
        const roots = await this.findRoots(options);
 | 
			
		||||
        await Promise.all(roots.map((root) => this.findDescendantsTree(root, options)));
 | 
			
		||||
        return roots;
 | 
			
		||||
    }
 | 
			
		||||
    protected orderBy = { name: 'customOrder', order: OrderType.ASC };
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 查询顶级分类
 | 
			
		||||
     * @param options
 | 
			
		||||
     */
 | 
			
		||||
    findRoots(
 | 
			
		||||
        options?: FindTreeOptions & {
 | 
			
		||||
            onlyTrashed?: boolean;
 | 
			
		||||
            withTrashed?: boolean;
 | 
			
		||||
        },
 | 
			
		||||
    ) {
 | 
			
		||||
        const escapeAlias = (alias: string) => this.manager.connection.driver.escape(alias);
 | 
			
		||||
        const escapeColumn = (column: string) => this.manager.connection.driver.escape(column);
 | 
			
		||||
 | 
			
		||||
        const joinColumn = this.metadata.treeParentRelation!.joinColumns[0];
 | 
			
		||||
        const parentPropertyName = joinColumn.givenDatabaseName || joinColumn.databaseName;
 | 
			
		||||
        const qb = this.buildBaseQB().orderBy('category.customOrder', 'ASC');
 | 
			
		||||
        qb.where(`${escapeAlias('category')}.${escapeColumn(parentPropertyName)} IS NULL`);
 | 
			
		||||
 | 
			
		||||
        FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, pick(options, ['relations', 'depth']));
 | 
			
		||||
 | 
			
		||||
        if (options?.withTrashed) {
 | 
			
		||||
            qb.withDeleted();
 | 
			
		||||
            if (options?.onlyTrashed) qb.where(`category.deletedAt IS NOT NULL`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return qb.getMany();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 查询后代分类
 | 
			
		||||
     * @param entity
 | 
			
		||||
     * @param options
 | 
			
		||||
     */
 | 
			
		||||
    findDescendants(
 | 
			
		||||
        entity: CategoryEntity,
 | 
			
		||||
        options?: FindTreeOptions & {
 | 
			
		||||
            onlyTrashed?: boolean;
 | 
			
		||||
            withTrashed?: boolean;
 | 
			
		||||
        },
 | 
			
		||||
    ) {
 | 
			
		||||
        const qb = this.createDescendantsQueryBuilder('category', 'treeClosure', entity);
 | 
			
		||||
        FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options);
 | 
			
		||||
        qb.orderBy('category.customOrder', 'ASC');
 | 
			
		||||
 | 
			
		||||
        if (options?.withTrashed) {
 | 
			
		||||
            qb.withDeleted();
 | 
			
		||||
            if (options?.onlyTrashed) qb.where(`category.deletedAt IS NOT NULL`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return qb.getMany();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 查询祖先分类
 | 
			
		||||
     * @param entity
 | 
			
		||||
     * @param options
 | 
			
		||||
     */
 | 
			
		||||
    findAncestors(
 | 
			
		||||
        entity: CategoryEntity,
 | 
			
		||||
        options?: FindTreeOptions & {
 | 
			
		||||
            onlyTrashed?: boolean;
 | 
			
		||||
            withTrashed?: boolean;
 | 
			
		||||
        },
 | 
			
		||||
    ) {
 | 
			
		||||
        const qb = this.createDescendantsQueryBuilder('category', 'treeClosure', entity);
 | 
			
		||||
        FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options);
 | 
			
		||||
        qb.orderBy('category.customOrder', 'ASC');
 | 
			
		||||
 | 
			
		||||
        if (options?.withTrashed) {
 | 
			
		||||
            qb.withDeleted();
 | 
			
		||||
            if (options?.onlyTrashed) qb.where(`category.deletedAt IS NOT NULL`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return qb.getMany();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 打平并展开树
 | 
			
		||||
     * @param trees
 | 
			
		||||
     * @param depth
 | 
			
		||||
     * @param parent
 | 
			
		||||
     */
 | 
			
		||||
    async toFlatTrees(trees: CategoryEntity[], depth = 0, parent: CategoryEntity | null = null) {
 | 
			
		||||
        const data: Omit<CategoryEntity, 'children'>[] = [];
 | 
			
		||||
 | 
			
		||||
        for (const tree of trees) {
 | 
			
		||||
            tree.depth = depth;
 | 
			
		||||
            tree.parent = parent;
 | 
			
		||||
            const { children } = tree;
 | 
			
		||||
            unset(tree, 'children');
 | 
			
		||||
            data.push(tree);
 | 
			
		||||
            data.push(...(await this.toFlatTrees(children, depth + 1, tree)));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return data as CategoryEntity[];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 统计后代元素数量
 | 
			
		||||
     * @param entity
 | 
			
		||||
     * @param options
 | 
			
		||||
     */
 | 
			
		||||
    async countDescendants(
 | 
			
		||||
        entity: CategoryEntity,
 | 
			
		||||
        options?: { withTrashed?: boolean; onlyTrashed?: boolean },
 | 
			
		||||
    ) {
 | 
			
		||||
        const qb = this.createDescendantsQueryBuilder('category', 'treeClosure', entity);
 | 
			
		||||
 | 
			
		||||
        if (options?.withTrashed) {
 | 
			
		||||
            qb.withDeleted();
 | 
			
		||||
            if (options?.onlyTrashed) qb.where(`category.deleteAt IS NOT NULL`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return qb.getCount();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 统计祖先元素数量
 | 
			
		||||
     * @param entity
 | 
			
		||||
     * @param options
 | 
			
		||||
     */
 | 
			
		||||
    async countAncestors(
 | 
			
		||||
        entity: CategoryEntity,
 | 
			
		||||
        options?: { withTrashed?: boolean; onlyTrashed?: boolean },
 | 
			
		||||
    ) {
 | 
			
		||||
        const qb = this.createAncestorsQueryBuilder('category', 'treeClosure', entity);
 | 
			
		||||
 | 
			
		||||
        if (options?.withTrashed) {
 | 
			
		||||
            qb.withDeleted();
 | 
			
		||||
            if (options?.onlyTrashed) qb.where(`category.deletedAt IS NOT NULL`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return qb.getCount();
 | 
			
		||||
    }
 | 
			
		||||
    protected _childrenResolve = TreeChildrenResolve.UP;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,137 +1,29 @@
 | 
			
		||||
import { pick, unset } from 'lodash';
 | 
			
		||||
import {
 | 
			
		||||
    FindOptionsUtils,
 | 
			
		||||
    FindTreeOptions,
 | 
			
		||||
    SelectQueryBuilder,
 | 
			
		||||
    TreeRepository,
 | 
			
		||||
    TreeRepositoryUtils,
 | 
			
		||||
} from 'typeorm';
 | 
			
		||||
import { isNil } from 'lodash';
 | 
			
		||||
import { FindTreeOptions, SelectQueryBuilder } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
import { CommentEntity } from '@/modules/content/entities';
 | 
			
		||||
import { BaseTreeRepository } from '@/modules/database/base';
 | 
			
		||||
import { CustomRepository } from '@/modules/database/decorators';
 | 
			
		||||
 | 
			
		||||
type FindCommentTreeOptions = FindTreeOptions & {
 | 
			
		||||
    addQuery?: (query: SelectQueryBuilder<CommentEntity>) => SelectQueryBuilder<CommentEntity>;
 | 
			
		||||
};
 | 
			
		||||
import { QueryParams } from '@/modules/database/types';
 | 
			
		||||
 | 
			
		||||
@CustomRepository(CommentEntity)
 | 
			
		||||
export class CommentRepository extends TreeRepository<CommentEntity> {
 | 
			
		||||
    /**
 | 
			
		||||
     * 构建基础查询器
 | 
			
		||||
     */
 | 
			
		||||
export class CommentRepository extends BaseTreeRepository<CommentEntity> {
 | 
			
		||||
    protected _qbName = 'comment';
 | 
			
		||||
 | 
			
		||||
    protected orderBy = 'createdAt';
 | 
			
		||||
 | 
			
		||||
    buildBaseQB(qb: SelectQueryBuilder<CommentEntity>): SelectQueryBuilder<CommentEntity> {
 | 
			
		||||
        return qb
 | 
			
		||||
            .leftJoinAndSelect(`comment.parent`, 'parent')
 | 
			
		||||
            .leftJoinAndSelect(`comment.post`, 'post')
 | 
			
		||||
            .orderBy('comment.createdAt', 'DESC');
 | 
			
		||||
        return super.buildBaseQB(qb).leftJoinAndSelect(`${this.qbName}.post`, 'post');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 查询树
 | 
			
		||||
     * @param options
 | 
			
		||||
     */
 | 
			
		||||
    async findTrees(options: FindCommentTreeOptions = {}) {
 | 
			
		||||
        options.relations = ['parent', 'children'];
 | 
			
		||||
 | 
			
		||||
        const roots = await this.findRoots(options);
 | 
			
		||||
        await Promise.all(roots.map((root) => this.findDescendantsTree(root, options)));
 | 
			
		||||
 | 
			
		||||
        return roots;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 查询顶级评论
 | 
			
		||||
     * @param options
 | 
			
		||||
     */
 | 
			
		||||
    findRoots(options: FindCommentTreeOptions = {}) {
 | 
			
		||||
        const { addQuery, ...rest } = options;
 | 
			
		||||
        const escapeAlias = (alias: string) => this.manager.connection.driver.escape(alias);
 | 
			
		||||
        const escapeColumn = (column: string) => this.manager.connection.driver.escape(column);
 | 
			
		||||
 | 
			
		||||
        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(`${escapeAlias('comment')}.${escapeColumn(parentPropertyName)} IS NULL`);
 | 
			
		||||
        qb = addQuery ? addQuery(qb) : qb;
 | 
			
		||||
 | 
			
		||||
        return qb.getMany();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 创建后代查询器
 | 
			
		||||
     * @param closureTableAlias
 | 
			
		||||
     * @param entity
 | 
			
		||||
     * @param options
 | 
			
		||||
     */
 | 
			
		||||
    createDtsQueryBuilder(
 | 
			
		||||
        closureTableAlias: string,
 | 
			
		||||
        entity: CommentEntity,
 | 
			
		||||
        options: FindCommentTreeOptions = {},
 | 
			
		||||
    ): SelectQueryBuilder<CommentEntity> {
 | 
			
		||||
        const { addQuery } = options;
 | 
			
		||||
        const qb = this.buildBaseQB(
 | 
			
		||||
            super.createDescendantsQueryBuilder('comment', closureTableAlias, entity),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return addQuery ? addQuery(qb) : qb;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 查询后代树
 | 
			
		||||
     * @param entity
 | 
			
		||||
     * @param options
 | 
			
		||||
     */
 | 
			
		||||
    async findDescendantsTree(
 | 
			
		||||
        entity: CommentEntity,
 | 
			
		||||
        options: FindCommentTreeOptions = {},
 | 
			
		||||
    ): Promise<CommentEntity> {
 | 
			
		||||
        const qb: SelectQueryBuilder<CommentEntity> = 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']),
 | 
			
		||||
    async findTrees(
 | 
			
		||||
        options: FindTreeOptions & QueryParams<CommentEntity> & { post?: string } = {},
 | 
			
		||||
    ): Promise<CommentEntity[]> {
 | 
			
		||||
        return super.findTrees({
 | 
			
		||||
            ...options,
 | 
			
		||||
            addQuery: async (qb) => {
 | 
			
		||||
                return isNil(options.post) ? qb : qb.where('post.id = :id', { id: options.post });
 | 
			
		||||
            },
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return entity;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 打平并展开树
 | 
			
		||||
     * @param trees
 | 
			
		||||
     * @param depth
 | 
			
		||||
     */
 | 
			
		||||
    async toFlatTrees(trees: CommentEntity[], depth = 0) {
 | 
			
		||||
        const data: Omit<CommentEntity, 'children'>[] = [];
 | 
			
		||||
 | 
			
		||||
        for (const tree of trees) {
 | 
			
		||||
            tree.depth = depth;
 | 
			
		||||
            const { children } = tree;
 | 
			
		||||
            unset(tree, 'children');
 | 
			
		||||
            data.push(tree);
 | 
			
		||||
            data.push(...(await this.toFlatTrees(children, depth + 1)));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return data as CommentEntity[];
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,11 @@
 | 
			
		||||
import { Repository } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
import { CommentEntity, PostEntity } from '@/modules/content/entities';
 | 
			
		||||
import { BaseRepository } from '@/modules/database/base';
 | 
			
		||||
import { CustomRepository } from '@/modules/database/decorators';
 | 
			
		||||
 | 
			
		||||
@CustomRepository(PostEntity)
 | 
			
		||||
export class PostRepository extends Repository<PostEntity> {
 | 
			
		||||
export class PostRepository extends BaseRepository<PostEntity> {
 | 
			
		||||
    protected _qbName = 'post';
 | 
			
		||||
 | 
			
		||||
    buildBaseQB() {
 | 
			
		||||
        // 在查询之前先查询出评论数量在添加到commentCount字段上
 | 
			
		||||
        return this.createQueryBuilder('post')
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,11 @@
 | 
			
		||||
import { Repository } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
import { PostEntity, TagEntity } from '@/modules/content/entities';
 | 
			
		||||
import { BaseRepository } from '@/modules/database/base';
 | 
			
		||||
import { CustomRepository } from '@/modules/database/decorators';
 | 
			
		||||
 | 
			
		||||
@CustomRepository(TagEntity)
 | 
			
		||||
export class TagRepository extends Repository<TagEntity> {
 | 
			
		||||
export class TagRepository extends BaseRepository<TagEntity> {
 | 
			
		||||
    protected _qbName = 'tag';
 | 
			
		||||
 | 
			
		||||
    buildBaseQB() {
 | 
			
		||||
        return this.createQueryBuilder('tag')
 | 
			
		||||
            .leftJoinAndSelect('tag.posts', 'posts')
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
 | 
			
		||||
import { isNil, omit } from 'lodash';
 | 
			
		||||
import { EntityNotFoundError, In } from 'typeorm';
 | 
			
		||||
import { EntityNotFoundError } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    CreateCategoryDto,
 | 
			
		||||
@ -11,6 +11,7 @@ import {
 | 
			
		||||
} from '@/modules/content/dtos';
 | 
			
		||||
import { CategoryEntity } from '@/modules/content/entities';
 | 
			
		||||
import { CategoryRepository } from '@/modules/content/repositories';
 | 
			
		||||
import { BaseService } from '@/modules/database/base';
 | 
			
		||||
import { SelectTrashMode } from '@/modules/database/constants';
 | 
			
		||||
import { treePaginate } from '@/modules/database/helpers';
 | 
			
		||||
 | 
			
		||||
@ -18,15 +19,18 @@ import { treePaginate } from '@/modules/database/helpers';
 | 
			
		||||
 * 分类数据操作
 | 
			
		||||
 */
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class CategoryService {
 | 
			
		||||
    constructor(protected repository: CategoryRepository) {}
 | 
			
		||||
export class CategoryService extends BaseService<CategoryEntity, CategoryRepository> {
 | 
			
		||||
    protected enableTrash = true;
 | 
			
		||||
 | 
			
		||||
    constructor(protected repository: CategoryRepository) {
 | 
			
		||||
        super(repository);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 查询分类树
 | 
			
		||||
     */
 | 
			
		||||
    async findTrees(options: QueryCategoryTreeDto) {
 | 
			
		||||
        const { trashed = SelectTrashMode.NONE } = options;
 | 
			
		||||
 | 
			
		||||
        return this.repository.findTrees({
 | 
			
		||||
            withTrashed: trashed === SelectTrashMode.ALL || trashed === SelectTrashMode.ONLY,
 | 
			
		||||
            onlyTrashed: trashed === SelectTrashMode.ONLY,
 | 
			
		||||
@ -39,13 +43,11 @@ export class CategoryService {
 | 
			
		||||
     */
 | 
			
		||||
    async paginate(options: QueryCategoryDto) {
 | 
			
		||||
        const { trashed = SelectTrashMode.NONE } = options;
 | 
			
		||||
 | 
			
		||||
        const tree = await this.repository.findTrees({
 | 
			
		||||
            withTrashed: trashed === SelectTrashMode.ALL || trashed === SelectTrashMode.ONLY,
 | 
			
		||||
            onlyTrashed: trashed === SelectTrashMode.ONLY,
 | 
			
		||||
        });
 | 
			
		||||
        const data = await this.repository.toFlatTrees(tree);
 | 
			
		||||
 | 
			
		||||
        return treePaginate(options, data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -69,7 +71,6 @@ export class CategoryService {
 | 
			
		||||
            ...data,
 | 
			
		||||
            parent: await this.getParent(undefined, data.parent),
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return this.detail(item.id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -79,58 +80,24 @@ export class CategoryService {
 | 
			
		||||
     */
 | 
			
		||||
    async update(data: UpdateCategoryDto) {
 | 
			
		||||
        await this.repository.update(data.id, omit(data, ['id', 'parent']));
 | 
			
		||||
        const item = await this.detail(data.id);
 | 
			
		||||
        await this.detail(data.id);
 | 
			
		||||
        const item = await this.repository.findOneOrFail({
 | 
			
		||||
            where: { id: data.id },
 | 
			
		||||
            relations: ['parent'],
 | 
			
		||||
        });
 | 
			
		||||
        const parent = await this.getParent(item.parent?.id, data.parent);
 | 
			
		||||
        const sholdUpdateParent =
 | 
			
		||||
        const shouldUpdateParent =
 | 
			
		||||
            (!isNil(item.parent) && !isNil(parent) && item.parent.id !== parent.id) ||
 | 
			
		||||
            (isNil(item.parent) && !isNil(parent)) ||
 | 
			
		||||
            (!isNil(item.parent) && isNil(parent));
 | 
			
		||||
 | 
			
		||||
        // 父分类单独更新
 | 
			
		||||
        if (parent !== undefined && sholdUpdateParent) {
 | 
			
		||||
        if (parent !== undefined && shouldUpdateParent) {
 | 
			
		||||
            item.parent = parent;
 | 
			
		||||
            await this.repository.save(item, { reload: true });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return item;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 删除分类
 | 
			
		||||
     * @param id
 | 
			
		||||
     */
 | 
			
		||||
    async delete(ids: string[], trash?: boolean) {
 | 
			
		||||
        const items = await this.repository.find({
 | 
			
		||||
            where: { id: In(ids) },
 | 
			
		||||
            withDeleted: true,
 | 
			
		||||
            relations: ['parent', 'children'],
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // 把子分类提升一级
 | 
			
		||||
        for (const item of items) {
 | 
			
		||||
            if (!isNil(item.children) && item.children.length > 0) {
 | 
			
		||||
                const nchildren = [...item.children].map((c) => {
 | 
			
		||||
                    c.parent = item.parent;
 | 
			
		||||
                    return c;
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                await this.repository.save(nchildren);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (trash) {
 | 
			
		||||
            const directs = items.filter((item) => !isNil(item.deletedAt));
 | 
			
		||||
            const sorts = items.filter((item) => isNil(item.deletedAt));
 | 
			
		||||
 | 
			
		||||
            return [
 | 
			
		||||
                ...(await this.repository.remove(directs)),
 | 
			
		||||
                ...(await this.repository.softRemove(sorts)),
 | 
			
		||||
            ];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.repository.remove(items);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取请求传入的父分类
 | 
			
		||||
     * @param current 当前分类的ID
 | 
			
		||||
@ -150,22 +117,4 @@ export class CategoryService {
 | 
			
		||||
        }
 | 
			
		||||
        return parent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 恢复分类
 | 
			
		||||
     * @param ids
 | 
			
		||||
     */
 | 
			
		||||
    async restore(ids: string[]) {
 | 
			
		||||
        const items = await this.repository.find({
 | 
			
		||||
            where: { id: In(ids) } as any,
 | 
			
		||||
            withDeleted: true,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const trasheds = items.filter((item) => !isNil(item)).map((item) => item.id);
 | 
			
		||||
        if (trasheds.length < 1) return [];
 | 
			
		||||
        await this.repository.restore(trasheds);
 | 
			
		||||
        const qb = this.repository.buildBaseQB();
 | 
			
		||||
        qb.andWhereInIds(trasheds);
 | 
			
		||||
        return qb.getMany();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -2,22 +2,24 @@ import { ForbiddenException, Injectable } from '@nestjs/common';
 | 
			
		||||
 | 
			
		||||
import { isNil } from 'lodash';
 | 
			
		||||
 | 
			
		||||
import { EntityNotFoundError, In, SelectQueryBuilder } from 'typeorm';
 | 
			
		||||
import { EntityNotFoundError, SelectQueryBuilder } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
import { CreateCommentDto, QueryCommentDto, QueryCommentTreeDto } from '@/modules/content/dtos';
 | 
			
		||||
import { CommentEntity } from '@/modules/content/entities';
 | 
			
		||||
import { CommentRepository, PostRepository } from '@/modules/content/repositories';
 | 
			
		||||
import { treePaginate } from '@/modules/database/helpers';
 | 
			
		||||
import { BaseService } from '@/modules/database/base';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 评论数据操作
 | 
			
		||||
 */
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class CommentService {
 | 
			
		||||
export class CommentService extends BaseService<CommentEntity, CommentRepository> {
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected repository: CommentRepository,
 | 
			
		||||
        protected postRepository: PostRepository,
 | 
			
		||||
    ) {}
 | 
			
		||||
    ) {
 | 
			
		||||
        super(repository);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 直接查询评论树
 | 
			
		||||
@ -25,7 +27,7 @@ export class CommentService {
 | 
			
		||||
     */
 | 
			
		||||
    async findTrees(options: QueryCommentTreeDto = {}) {
 | 
			
		||||
        return this.repository.findTrees({
 | 
			
		||||
            addQuery: (qb) => {
 | 
			
		||||
            addQuery: async (qb) => {
 | 
			
		||||
                return isNil(options.post) ? qb : qb.where('post.id = :id', { id: options.post });
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
@ -35,28 +37,17 @@ export class CommentService {
 | 
			
		||||
     * 查找一篇文章的评论并分页
 | 
			
		||||
     * @param dto
 | 
			
		||||
     */
 | 
			
		||||
    async paginate(dto: QueryCommentDto) {
 | 
			
		||||
        const { post, ...query } = dto;
 | 
			
		||||
    async paginate(options: QueryCommentDto) {
 | 
			
		||||
        const { post } = options;
 | 
			
		||||
        const addQuery = (qb: SelectQueryBuilder<CommentEntity>) => {
 | 
			
		||||
            const condition: Record<string, string> = {};
 | 
			
		||||
            if (!isNil(post)) condition.post = post;
 | 
			
		||||
            return Object.keys(condition).length > 0 ? qb.andWhere(condition) : qb;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const data = await this.repository.findRoots({ addQuery });
 | 
			
		||||
 | 
			
		||||
        let comments: CommentEntity[] = [];
 | 
			
		||||
 | 
			
		||||
        for (let i = 0; i < data.length; i++) {
 | 
			
		||||
            const c = data[i];
 | 
			
		||||
            comments.push(
 | 
			
		||||
                await this.repository.findDescendantsTree(c, {
 | 
			
		||||
                    addQuery,
 | 
			
		||||
                }),
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        comments = await this.repository.toFlatTrees(comments);
 | 
			
		||||
        return treePaginate(query, comments);
 | 
			
		||||
        return super.paginate({
 | 
			
		||||
            ...options,
 | 
			
		||||
            addQuery,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -66,32 +57,20 @@ export class CommentService {
 | 
			
		||||
     */
 | 
			
		||||
    async create(data: CreateCommentDto) {
 | 
			
		||||
        const parent = await this.getParent(undefined, data.parent);
 | 
			
		||||
 | 
			
		||||
        if (!isNil(parent) && parent.post.id !== data.post) {
 | 
			
		||||
            throw new ForbiddenException('Parent comment and child comment must belong same post!');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const item = await this.repository.save({
 | 
			
		||||
            ...data,
 | 
			
		||||
            parent,
 | 
			
		||||
            post: await this.getPost(data.post),
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return this.repository.findOneOrFail({ where: { id: item.id } });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 删除评论
 | 
			
		||||
     * @param ids
 | 
			
		||||
     */
 | 
			
		||||
    async delete(ids: string[]) {
 | 
			
		||||
        const comments = await this.repository.find({ where: { id: In(ids) } });
 | 
			
		||||
        return this.repository.remove(comments);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取评论所属文章实例
 | 
			
		||||
     * @parem id
 | 
			
		||||
     * @param id
 | 
			
		||||
     */
 | 
			
		||||
    protected async getPost(id: string) {
 | 
			
		||||
        return !isNil(id) ? this.postRepository.findOneOrFail({ where: { id } }) : id;
 | 
			
		||||
@ -100,24 +79,21 @@ export class CommentService {
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取请求传入的父分类
 | 
			
		||||
     * @param current 当前分类的ID
 | 
			
		||||
     * @param parentId
 | 
			
		||||
     * @param id
 | 
			
		||||
     */
 | 
			
		||||
    protected async getParent(current?: string, parentId?: string) {
 | 
			
		||||
        if (current === parentId) return undefined;
 | 
			
		||||
    protected async getParent(current?: string, id?: string) {
 | 
			
		||||
        if (current === id) return undefined;
 | 
			
		||||
        let parent: CommentEntity | undefined;
 | 
			
		||||
        if (parentId !== undefined) {
 | 
			
		||||
            if (parentId === null) return null;
 | 
			
		||||
        if (id !== undefined) {
 | 
			
		||||
            if (id === null) return null;
 | 
			
		||||
            parent = await this.repository.findOne({
 | 
			
		||||
                relations: ['parent', 'post'],
 | 
			
		||||
                where: { id: parentId },
 | 
			
		||||
                where: { id },
 | 
			
		||||
            });
 | 
			
		||||
            if (!parent)
 | 
			
		||||
                throw new EntityNotFoundError(
 | 
			
		||||
                    CommentEntity,
 | 
			
		||||
                    `Parent comment ${parentId} not exists !`,
 | 
			
		||||
                );
 | 
			
		||||
            if (!parent) {
 | 
			
		||||
                throw new EntityNotFoundError(CommentEntity, `Parent comment ${id} not exists!`);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return parent;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -7,11 +7,17 @@ import { EntityNotFoundError, In, IsNull, Not, SelectQueryBuilder } from 'typeor
 | 
			
		||||
import { PostOrderType } from '@/modules/content/constants';
 | 
			
		||||
import { CreatePostDto, QueryPostDto, UpdatePostDto } from '@/modules/content/dtos';
 | 
			
		||||
import { PostEntity } from '@/modules/content/entities';
 | 
			
		||||
import { CategoryRepository, PostRepository, TagRepository } from '@/modules/content/repositories';
 | 
			
		||||
import {
 | 
			
		||||
    CategoryRepository,
 | 
			
		||||
    CommentRepository,
 | 
			
		||||
    PostRepository,
 | 
			
		||||
    TagRepository,
 | 
			
		||||
} from '@/modules/content/repositories';
 | 
			
		||||
 | 
			
		||||
import { CategoryService } from '@/modules/content/services/category.service';
 | 
			
		||||
import { SearchService } from '@/modules/content/services/search.service';
 | 
			
		||||
import { SearchType } from '@/modules/content/types';
 | 
			
		||||
import { BaseService } from '@/modules/database/base';
 | 
			
		||||
import { SelectTrashMode } from '@/modules/database/constants';
 | 
			
		||||
import { paginate } from '@/modules/database/helpers';
 | 
			
		||||
import { QueryHook } from '@/modules/database/types';
 | 
			
		||||
@ -20,16 +26,24 @@ type FindParams = {
 | 
			
		||||
    [key in keyof Omit<QueryPostDto, 'limit' | 'page'>]: QueryPostDto[key];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 文章数据操作
 | 
			
		||||
 */
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class PostService {
 | 
			
		||||
export class PostService extends BaseService<PostEntity, PostRepository, FindParams> {
 | 
			
		||||
    protected enableTrash = true;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected repository: PostRepository,
 | 
			
		||||
        protected categoryRepository: CategoryRepository,
 | 
			
		||||
        protected categoryService: CategoryService,
 | 
			
		||||
        protected tagRepository: TagRepository,
 | 
			
		||||
        protected commentRepository: CommentRepository,
 | 
			
		||||
        protected searchService?: SearchService,
 | 
			
		||||
        protected search_type: SearchType = 'against',
 | 
			
		||||
    ) {}
 | 
			
		||||
    ) {
 | 
			
		||||
        super(repository);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取分页数据
 | 
			
		||||
@ -41,7 +55,7 @@ export class PostService {
 | 
			
		||||
            return this.searchService.search(
 | 
			
		||||
                options.search,
 | 
			
		||||
                pick(options, ['trashed', 'page', 'limit']),
 | 
			
		||||
            );
 | 
			
		||||
            ) as any;
 | 
			
		||||
        }
 | 
			
		||||
        const qb = await this.buildListQuery(this.repository.buildBaseQB(), options, callback);
 | 
			
		||||
        return paginate(qb, options);
 | 
			
		||||
@ -80,9 +94,7 @@ export class PostService {
 | 
			
		||||
                : [],
 | 
			
		||||
        };
 | 
			
		||||
        const item = await this.repository.save(createPostDto);
 | 
			
		||||
 | 
			
		||||
        if (!isNil(this.searchService)) await this.searchService.create(item);
 | 
			
		||||
 | 
			
		||||
        return this.detail(item.id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -92,16 +104,14 @@ export class PostService {
 | 
			
		||||
     */
 | 
			
		||||
    async update(data: UpdatePostDto) {
 | 
			
		||||
        const post = await this.detail(data.id);
 | 
			
		||||
 | 
			
		||||
        if (data.category !== undefined) {
 | 
			
		||||
            // 更新分类
 | 
			
		||||
            const category = isNil(data.category)
 | 
			
		||||
                ? null
 | 
			
		||||
                : await this.categoryRepository.findOneByOrFail({ id: data.category });
 | 
			
		||||
            post.category = category;
 | 
			
		||||
            this.repository.save(post, { reload: true });
 | 
			
		||||
            await this.repository.save(post);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (isArray(data.tags)) {
 | 
			
		||||
            // 更新文章关联标签
 | 
			
		||||
            await this.repository
 | 
			
		||||
@ -110,11 +120,9 @@ export class PostService {
 | 
			
		||||
                .of(post)
 | 
			
		||||
                .addAndRemove(data.tags, post.tags ?? []);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await this.repository.update(data.id, omit(data, ['id', 'tags', 'category']));
 | 
			
		||||
        const result = await this.detail(data.id);
 | 
			
		||||
        if (!isNil(this.searchService)) await this.searchService.update([post]);
 | 
			
		||||
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -126,25 +134,27 @@ export class PostService {
 | 
			
		||||
        const items = await this.repository.find({
 | 
			
		||||
            where: { id: In(ids) },
 | 
			
		||||
            withDeleted: true,
 | 
			
		||||
            relations: ['category', 'comments', 'tags'],
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        let result: PostEntity[] = [];
 | 
			
		||||
        if (trash) {
 | 
			
		||||
            // 对已软删除的数据再次删除时直接通过remove方法从数据库中清除
 | 
			
		||||
            const directs = items.filter((item) => !isNil(item.deletedAt));
 | 
			
		||||
            // 需要记录直接删除的ids,因为remove(directs)会使directs的id为undefined,再次删除meili数据会有问题!!!
 | 
			
		||||
            const directIds = directs.map(({ id }) => id);
 | 
			
		||||
            const softs = items.filter((item) => isNil(item.deletedAt));
 | 
			
		||||
            result = [
 | 
			
		||||
                ...(await this.repository.remove(directs)),
 | 
			
		||||
                ...(await this.repository.softRemove(softs)),
 | 
			
		||||
            ];
 | 
			
		||||
            if (!isNil(this.searchService)) {
 | 
			
		||||
                await this.searchService.delete(directs.map(({ id }) => id));
 | 
			
		||||
                await this.searchService.delete(directIds);
 | 
			
		||||
                await this.searchService.update(softs);
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            result = await this.repository.remove(items);
 | 
			
		||||
            if (!isNil(this.searchService)) {
 | 
			
		||||
                await this.searchService.delete(result.map(({ id }) => id));
 | 
			
		||||
                await this.searchService.delete(ids);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return result;
 | 
			
		||||
@ -163,6 +173,14 @@ export class PostService {
 | 
			
		||||
        const trasheds = items.filter((item) => !isNil(item)).map((item) => item.id);
 | 
			
		||||
        if (trasheds.length < 1) return [];
 | 
			
		||||
        await this.repository.restore(trasheds);
 | 
			
		||||
 | 
			
		||||
        for (const post of trasheds) {
 | 
			
		||||
            await this.commentRepository.restore({
 | 
			
		||||
                post: { id: post },
 | 
			
		||||
                deletedAt: Not(IsNull()),
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const qb = await this.buildListQuery(this.repository.buildBaseQB(), {}, async (qbuilder) =>
 | 
			
		||||
            qbuilder.andWhereInIds(trasheds),
 | 
			
		||||
        );
 | 
			
		||||
@ -181,7 +199,7 @@ export class PostService {
 | 
			
		||||
        callback?: QueryHook<PostEntity>,
 | 
			
		||||
    ) {
 | 
			
		||||
        const { category, tag, orderBy, isPublished, trashed = SelectTrashMode.NONE } = options;
 | 
			
		||||
 | 
			
		||||
        // 是否查询回收站
 | 
			
		||||
        if (trashed === SelectTrashMode.ALL || trashed === SelectTrashMode.ONLY) {
 | 
			
		||||
            qb.withDeleted();
 | 
			
		||||
            if (trashed === SelectTrashMode.ONLY) qb.where(`post.deletedAt is not null`);
 | 
			
		||||
@ -210,8 +228,12 @@ export class PostService {
 | 
			
		||||
            qb.andWhere('title LIKE :search', { search: `%${search}%` })
 | 
			
		||||
                .orWhere('body LIKE :search', { search: `%${search}%` })
 | 
			
		||||
                .orWhere('summary LIKE :search', { search: `%${search}%` })
 | 
			
		||||
                .orWhere('category.name LIKE :search', { search: `%${search}%` })
 | 
			
		||||
                .orWhere('tags.name LIKE :search', { search: `%${search}%` });
 | 
			
		||||
                .orWhere('category.name LIKE :search', {
 | 
			
		||||
                    search: `%${search}%`,
 | 
			
		||||
                })
 | 
			
		||||
                .orWhere('tags.name LIKE :search', {
 | 
			
		||||
                    search: `%${search}%`,
 | 
			
		||||
                });
 | 
			
		||||
        } else if (this.search_type === 'against') {
 | 
			
		||||
            qb.andWhere('MATCH(title) AGAINST (:search IN BOOLEAN MODE)', {
 | 
			
		||||
                search: `${search}*`,
 | 
			
		||||
@ -259,7 +281,7 @@ export class PostService {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 查询出分类及后代分类下的所有文章的Query构建
 | 
			
		||||
     * 查询出分类及其后代分类下的所有文章的Query构建
 | 
			
		||||
     * @param id
 | 
			
		||||
     * @param qb
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
@ -23,41 +23,48 @@ async function getPostData(
 | 
			
		||||
    cmtRepo: CommentRepository,
 | 
			
		||||
    post: PostEntity,
 | 
			
		||||
) {
 | 
			
		||||
    const categories = [
 | 
			
		||||
        ...(await catRepo.findAncestors(post.category)).map((item) => {
 | 
			
		||||
            return {
 | 
			
		||||
    let categories: { id: string; name: string }[] = [];
 | 
			
		||||
    if (post.category) {
 | 
			
		||||
        categories = [
 | 
			
		||||
            ...(await catRepo.findAncestors(post.category)).map((item) => ({
 | 
			
		||||
                id: item.id,
 | 
			
		||||
                name: item.name,
 | 
			
		||||
            };
 | 
			
		||||
        }),
 | 
			
		||||
        { id: post.category.id, name: post.category.name },
 | 
			
		||||
    ];
 | 
			
		||||
            })),
 | 
			
		||||
            { id: post.category.id, name: post.category.name },
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const comments = (
 | 
			
		||||
    let comments: { id: string; body: string }[] = [];
 | 
			
		||||
 | 
			
		||||
    comments = (
 | 
			
		||||
        await cmtRepo.find({
 | 
			
		||||
            relations: ['post'],
 | 
			
		||||
            where: { post: { id: post.id } },
 | 
			
		||||
        })
 | 
			
		||||
    ).map((item) => ({ id: item.id, body: item.body }));
 | 
			
		||||
 | 
			
		||||
    return [
 | 
			
		||||
        {
 | 
			
		||||
            ...pick(instanceToPlain(post), [
 | 
			
		||||
                'id',
 | 
			
		||||
                'title',
 | 
			
		||||
                'body',
 | 
			
		||||
                'summary',
 | 
			
		||||
                'commentCount',
 | 
			
		||||
                'deletedAt',
 | 
			
		||||
                'publishedAt',
 | 
			
		||||
                'createdAt',
 | 
			
		||||
                'updatedAt',
 | 
			
		||||
            ]),
 | 
			
		||||
            categories,
 | 
			
		||||
            tags: post.tags.map((item) => ({ id: item.id, name: item.name })),
 | 
			
		||||
            comments,
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
    let tags: { id: string; name: string }[] = [];
 | 
			
		||||
    if (post.tags) {
 | 
			
		||||
        tags = post.tags.map((item) => ({ id: item.id, name: item.name }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        ...pick(instanceToPlain(post), [
 | 
			
		||||
            'id',
 | 
			
		||||
            'title',
 | 
			
		||||
            'body',
 | 
			
		||||
            'summary',
 | 
			
		||||
            'commentCount',
 | 
			
		||||
            'deletedAt',
 | 
			
		||||
            'publishedAt',
 | 
			
		||||
            'createdAt',
 | 
			
		||||
            'updatedAt',
 | 
			
		||||
        ]),
 | 
			
		||||
        body: post.body,
 | 
			
		||||
        categories,
 | 
			
		||||
        tags,
 | 
			
		||||
        comments,
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
@ -101,7 +108,6 @@ export class SearchService {
 | 
			
		||||
            sort: ['updatedAt:desc', 'commentCount:desc'],
 | 
			
		||||
            filter,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            items: result.hits,
 | 
			
		||||
            currentPage: result.page,
 | 
			
		||||
@ -115,19 +121,18 @@ export class SearchService {
 | 
			
		||||
    async create(post: PostEntity) {
 | 
			
		||||
        return this.client
 | 
			
		||||
            .index(this.index)
 | 
			
		||||
            .addDocuments(await getPostData(this.categoryRepository, this.commentRepository, post));
 | 
			
		||||
            .addDocuments([
 | 
			
		||||
                await getPostData(this.categoryRepository, this.commentRepository, post),
 | 
			
		||||
            ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async update(posts: PostEntity[]) {
 | 
			
		||||
        return this.client
 | 
			
		||||
            .index(this.index)
 | 
			
		||||
            .updateDocuments(
 | 
			
		||||
                await Promise.all(
 | 
			
		||||
                    posts.map((post) =>
 | 
			
		||||
                        getPostData(this.categoryRepository, this.commentRepository, post),
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
            );
 | 
			
		||||
        const payload = await Promise.all(
 | 
			
		||||
            posts?.map((post) =>
 | 
			
		||||
                getPostData(this.categoryRepository, this.commentRepository, post),
 | 
			
		||||
            ),
 | 
			
		||||
        );
 | 
			
		||||
        return this.client.index(this.index).updateDocuments(payload);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async delete(ids: string[]) {
 | 
			
		||||
 | 
			
		||||
@ -1,46 +1,21 @@
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
 | 
			
		||||
import { isNil, omit } from 'lodash';
 | 
			
		||||
import { omit } from 'lodash';
 | 
			
		||||
 | 
			
		||||
import { In, SelectQueryBuilder } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
import { CreateTagDto, QueryTagsDto, UpdateTagDto } from '@/modules/content/dtos';
 | 
			
		||||
import { CreateTagDto, UpdateTagDto } from '@/modules/content/dtos';
 | 
			
		||||
import { TagEntity } from '@/modules/content/entities';
 | 
			
		||||
import { TagRepository } from '@/modules/content/repositories';
 | 
			
		||||
import { SelectTrashMode } from '@/modules/database/constants';
 | 
			
		||||
import { paginate } from '@/modules/database/helpers';
 | 
			
		||||
import { QueryHook } from '@/modules/database/types';
 | 
			
		||||
 | 
			
		||||
type FindParams = {
 | 
			
		||||
    [key in keyof Omit<QueryTagsDto, 'limit' | 'page'>]: QueryTagsDto[key];
 | 
			
		||||
};
 | 
			
		||||
import { BaseService } from '@/modules/database/base';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 标签数据操作
 | 
			
		||||
 */
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class TagService {
 | 
			
		||||
    constructor(protected repository: TagRepository) {}
 | 
			
		||||
export class TagService extends BaseService<TagEntity, TagRepository> {
 | 
			
		||||
    protected enableTrash = true;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取标签数据
 | 
			
		||||
     * @param options 分页选项
 | 
			
		||||
     * @param callback 添加额外的查询
 | 
			
		||||
     */
 | 
			
		||||
    async paginate(options: QueryTagsDto) {
 | 
			
		||||
        const qb = await this.buildListQuery(this.repository.buildBaseQB(), options);
 | 
			
		||||
        return paginate(qb, options);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 查询单个标签信息
 | 
			
		||||
     * @param id
 | 
			
		||||
     * @param callback 添加额外的查询
 | 
			
		||||
     */
 | 
			
		||||
    async detail(id: string) {
 | 
			
		||||
        const qb = this.repository.buildBaseQB();
 | 
			
		||||
        qb.where(`tag.id = :id`, { id });
 | 
			
		||||
        return qb.getOneOrFail();
 | 
			
		||||
    constructor(protected repository: TagRepository) {
 | 
			
		||||
        super(repository);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -60,67 +35,4 @@ export class TagService {
 | 
			
		||||
        await this.repository.update(data.id, omit(data, ['id']));
 | 
			
		||||
        return this.detail(data.id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 删除标签
 | 
			
		||||
     * @param ids
 | 
			
		||||
     */
 | 
			
		||||
    async delete(ids: string[], trash: boolean) {
 | 
			
		||||
        const items = await this.repository.find({
 | 
			
		||||
            where: { id: In(ids) } as any,
 | 
			
		||||
            withDeleted: true,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (trash) {
 | 
			
		||||
            const directs = items.filter((item) => !isNil(item.deletedAt));
 | 
			
		||||
            const sorts = items.filter((item) => isNil(item.deletedAt));
 | 
			
		||||
 | 
			
		||||
            return [
 | 
			
		||||
                ...(await this.repository.remove(directs)),
 | 
			
		||||
                ...(await this.repository.softRemove(sorts)),
 | 
			
		||||
            ];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.repository.remove(items);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 软删除标签
 | 
			
		||||
     * @param ids
 | 
			
		||||
     */
 | 
			
		||||
    async restore(ids: string[]) {
 | 
			
		||||
        const items = await this.repository.find({
 | 
			
		||||
            where: { id: In(ids) } as any,
 | 
			
		||||
            withDeleted: true,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // 过滤掉不在回收站的标签
 | 
			
		||||
        const trashed = items.filter((item) => !isNil(item.deletedAt)).map((item) => item.id);
 | 
			
		||||
        if (trashed.length < 1) return trashed;
 | 
			
		||||
        await this.repository.restore(trashed);
 | 
			
		||||
        const qb = this.repository.buildBaseQB().where({ id: In(trashed) });
 | 
			
		||||
        return qb.getMany();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 构建标签列表查询器(需要查到软删除)
 | 
			
		||||
     * @param qb
 | 
			
		||||
     * @param options
 | 
			
		||||
     * @param callback
 | 
			
		||||
     */
 | 
			
		||||
    protected async buildListQuery(
 | 
			
		||||
        qb: SelectQueryBuilder<TagEntity>,
 | 
			
		||||
        options: FindParams,
 | 
			
		||||
        callback?: QueryHook<TagEntity>,
 | 
			
		||||
    ) {
 | 
			
		||||
        const { trashed } = options;
 | 
			
		||||
 | 
			
		||||
        if (trashed === SelectTrashMode.ALL || trashed === SelectTrashMode.ONLY) {
 | 
			
		||||
            qb.withDeleted();
 | 
			
		||||
            if (trashed === SelectTrashMode.ONLY) qb.where(`tag.deletedAt IS NOT NULL`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (callback) return callback(qb);
 | 
			
		||||
        return qb;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,17 +1,23 @@
 | 
			
		||||
import { Optional } from '@nestjs/common';
 | 
			
		||||
import { DataSource, EventSubscriber } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
import { PostBodyType } from '@/modules/content/constants';
 | 
			
		||||
import { PostEntity } from '@/modules/content/entities';
 | 
			
		||||
import { PostRepository } from '@/modules/content/repositories';
 | 
			
		||||
import { SanitizeService } from '@/modules/content/services/sanitize.service';
 | 
			
		||||
import { BaseSubscriber } from '@/modules/database/base';
 | 
			
		||||
 | 
			
		||||
@EventSubscriber()
 | 
			
		||||
export class PostSubscriber {
 | 
			
		||||
export class PostSubscriber extends BaseSubscriber<PostEntity> {
 | 
			
		||||
    protected entity = PostEntity;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected dataSource: DataSource,
 | 
			
		||||
        protected sanitizeService: SanitizeService,
 | 
			
		||||
        protected postRepository: PostRepository,
 | 
			
		||||
    ) {}
 | 
			
		||||
        @Optional() protected sanitizeService?: SanitizeService,
 | 
			
		||||
    ) {
 | 
			
		||||
        super(dataSource);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    listenTo() {
 | 
			
		||||
        return PostEntity;
 | 
			
		||||
@ -19,6 +25,7 @@ export class PostSubscriber {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 加载文章数据的处理
 | 
			
		||||
     * @param entity
 | 
			
		||||
     */
 | 
			
		||||
    async afterLoad(entity: PostEntity) {
 | 
			
		||||
        if (entity.type === PostBodyType.HTML) {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								src/modules/database/base/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/modules/database/base/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
			
		||||
export * from './repository';
 | 
			
		||||
export * from './service';
 | 
			
		||||
export * from './subcriber';
 | 
			
		||||
export * from './tree.repository';
 | 
			
		||||
							
								
								
									
										46
									
								
								src/modules/database/base/repository.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/modules/database/base/repository.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,46 @@
 | 
			
		||||
import { isNil } from 'lodash';
 | 
			
		||||
 | 
			
		||||
import { ObjectLiteral, Repository, SelectQueryBuilder } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
import { OrderType } from '@/modules/database/constants';
 | 
			
		||||
import { getOrderByQuery } from '@/modules/database/helpers';
 | 
			
		||||
import { OrderQueryType } from '@/modules/database/types';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 基础存储类
 | 
			
		||||
 */
 | 
			
		||||
export abstract class BaseRepository<E extends ObjectLiteral> extends Repository<E> {
 | 
			
		||||
    /**
 | 
			
		||||
     * 构建查询时默认的模型对应的查询名称
 | 
			
		||||
     */
 | 
			
		||||
    protected abstract _qbName: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 返回查询器名称
 | 
			
		||||
     */
 | 
			
		||||
    get qbName() {
 | 
			
		||||
        return this._qbName;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 构建基础查询器
 | 
			
		||||
     */
 | 
			
		||||
    buildBaseQB(): SelectQueryBuilder<E> {
 | 
			
		||||
        return this.createQueryBuilder(this.qbName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 默认排序规则,可以通过每个方法的orderBy选项进行覆盖
 | 
			
		||||
     */
 | 
			
		||||
    protected orderBy?: string | { name: string; order: `${OrderType}` };
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 生成排序的QueryBuilder
 | 
			
		||||
     * @param qb
 | 
			
		||||
     * @param orderBy
 | 
			
		||||
     */
 | 
			
		||||
    addOrderByQuery(qb: SelectQueryBuilder<E>, orderBy?: OrderQueryType) {
 | 
			
		||||
        const orderByQuery = orderBy ?? this.orderBy;
 | 
			
		||||
        return !isNil(orderByQuery) ? getOrderByQuery(qb, this.qbName, orderByQuery) : qb;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										215
									
								
								src/modules/database/base/service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										215
									
								
								src/modules/database/base/service.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,215 @@
 | 
			
		||||
import { ForbiddenException, NotFoundException } from '@nestjs/common';
 | 
			
		||||
import { isNil } from 'lodash';
 | 
			
		||||
import { In, ObjectLiteral, SelectQueryBuilder } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
import { SelectTrashMode, TreeChildrenResolve } from '../constants';
 | 
			
		||||
import { paginate, treePaginate } from '../helpers';
 | 
			
		||||
import { PaginateOptions, PaginateReturn, QueryHook, ServiceListQueryOption } from '../types';
 | 
			
		||||
 | 
			
		||||
import { BaseRepository } from './repository';
 | 
			
		||||
import { BaseTreeRepository } from './tree.repository';
 | 
			
		||||
/**
 | 
			
		||||
 *  CRUD操作服务
 | 
			
		||||
 */
 | 
			
		||||
export abstract class BaseService<
 | 
			
		||||
    E extends ObjectLiteral,
 | 
			
		||||
    R extends BaseRepository<E> | BaseTreeRepository<E>,
 | 
			
		||||
    P extends ServiceListQueryOption<E> = ServiceListQueryOption<E>,
 | 
			
		||||
> {
 | 
			
		||||
    /**
 | 
			
		||||
     * 服务默认存储类
 | 
			
		||||
     */
 | 
			
		||||
    protected repository: R;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 是否开启软删除功能
 | 
			
		||||
     */
 | 
			
		||||
    protected enableTrash = true;
 | 
			
		||||
 | 
			
		||||
    constructor(repository: R) {
 | 
			
		||||
        this.repository = repository;
 | 
			
		||||
        if (
 | 
			
		||||
            !(
 | 
			
		||||
                this.repository instanceof BaseRepository ||
 | 
			
		||||
                this.repository instanceof BaseTreeRepository
 | 
			
		||||
            )
 | 
			
		||||
        ) {
 | 
			
		||||
            throw new Error(
 | 
			
		||||
                'Repository must instance of BaseRepository or BaseTreeRepository in DataService!',
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取数据列表
 | 
			
		||||
     * @param params 查询参数
 | 
			
		||||
     * @param callback 回调查询
 | 
			
		||||
     */
 | 
			
		||||
    async list(options?: P, callback?: QueryHook<E>): Promise<E[]> {
 | 
			
		||||
        const { trashed: isTrashed = false } = options ?? {};
 | 
			
		||||
        const trashed = isTrashed || SelectTrashMode.NONE;
 | 
			
		||||
        if (this.repository instanceof BaseTreeRepository) {
 | 
			
		||||
            const withTrashed =
 | 
			
		||||
                this.enableTrash &&
 | 
			
		||||
                (trashed === SelectTrashMode.ALL || trashed === SelectTrashMode.ONLY);
 | 
			
		||||
            const onlyTrashed = this.enableTrash && trashed === SelectTrashMode.ONLY;
 | 
			
		||||
            const tree = await this.repository.findTrees({
 | 
			
		||||
                ...options,
 | 
			
		||||
                withTrashed,
 | 
			
		||||
                onlyTrashed,
 | 
			
		||||
            });
 | 
			
		||||
            return this.repository.toFlatTrees(tree);
 | 
			
		||||
        }
 | 
			
		||||
        const qb = await this.buildListQB(this.repository.buildBaseQB(), options, callback);
 | 
			
		||||
        return qb.getMany();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取分页数据
 | 
			
		||||
     * @param options 分页选项
 | 
			
		||||
     * @param callback 回调查询
 | 
			
		||||
     */
 | 
			
		||||
    async paginate(
 | 
			
		||||
        options?: PaginateOptions & P,
 | 
			
		||||
        callback?: QueryHook<E>,
 | 
			
		||||
    ): Promise<PaginateReturn<E>> {
 | 
			
		||||
        const queryOptions = (options ?? {}) as P;
 | 
			
		||||
        if (this.repository instanceof BaseTreeRepository) {
 | 
			
		||||
            const data = await this.list(queryOptions, callback);
 | 
			
		||||
            return treePaginate(options, data) as PaginateReturn<E>;
 | 
			
		||||
        }
 | 
			
		||||
        const qb = await this.buildListQB(this.repository.buildBaseQB(), queryOptions, callback);
 | 
			
		||||
        return paginate(qb, options);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取数据详情
 | 
			
		||||
     * @param id
 | 
			
		||||
     * @param trashed 查询时是否包含已软删除的数据
 | 
			
		||||
     * @param callback 回调查询
 | 
			
		||||
     */
 | 
			
		||||
    async detail(id: string, callback?: QueryHook<E>): Promise<E> {
 | 
			
		||||
        const qb = await this.buildItemQB(id, this.repository.buildBaseQB(), callback);
 | 
			
		||||
        const item = await qb.getOne();
 | 
			
		||||
        if (!item) throw new NotFoundException(`${this.repository.qbName} ${id} not exists!`);
 | 
			
		||||
        return item;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 创建数据,如果子类没有实现则抛出404
 | 
			
		||||
     * @param data 请求数据
 | 
			
		||||
     * @param others 其它参数
 | 
			
		||||
     */
 | 
			
		||||
    create(data: any, ...others: any[]): Promise<E> {
 | 
			
		||||
        throw new ForbiddenException(`Can not to create ${this.repository.qbName}!`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 更新数据,如果子类没有实现则抛出404
 | 
			
		||||
     * @param data 请求数据
 | 
			
		||||
     * @param others 其它参数
 | 
			
		||||
     */
 | 
			
		||||
    update(data: any, ...others: any[]): Promise<E> {
 | 
			
		||||
        throw new ForbiddenException(`Can not to update ${this.repository.qbName}!`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 批量删除数据
 | 
			
		||||
     * @param data 需要删除的id列表
 | 
			
		||||
     * @param trash 是否只扔到回收站,如果为true则软删除
 | 
			
		||||
     */
 | 
			
		||||
    async delete(ids: string[], trash?: boolean) {
 | 
			
		||||
        let items: E[] = [];
 | 
			
		||||
        if (this.repository instanceof BaseTreeRepository) {
 | 
			
		||||
            items = await this.repository.find({
 | 
			
		||||
                where: { id: In(ids) as any },
 | 
			
		||||
                withDeleted: this.enableTrash ? true : undefined,
 | 
			
		||||
                relations: ['parent', 'children'],
 | 
			
		||||
            });
 | 
			
		||||
            if (this.repository.childrenResolve === TreeChildrenResolve.UP) {
 | 
			
		||||
                for (const item of items) {
 | 
			
		||||
                    if (isNil(item.children) || item.children.length <= 0) continue;
 | 
			
		||||
                    const nchildren = [...item.children].map((c) => {
 | 
			
		||||
                        c.parent = item.parent;
 | 
			
		||||
                        return item;
 | 
			
		||||
                    });
 | 
			
		||||
                    await this.repository.save(nchildren);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            items = await this.repository.find({
 | 
			
		||||
                where: { id: In(ids) as any },
 | 
			
		||||
                withDeleted: this.enableTrash ? true : undefined,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        if (this.enableTrash && trash) {
 | 
			
		||||
            const directs = items.filter((item) => !isNil(item.deletedAt));
 | 
			
		||||
            const softs = items.filter((item) => isNil(item.deletedAt));
 | 
			
		||||
            return [
 | 
			
		||||
                ...(await this.repository.remove(directs)),
 | 
			
		||||
                ...(await this.repository.softRemove(softs)),
 | 
			
		||||
            ];
 | 
			
		||||
        }
 | 
			
		||||
        return this.repository.remove(items);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 批量恢复回收站中的数据
 | 
			
		||||
     * @param data 需要恢复的id列表
 | 
			
		||||
     */
 | 
			
		||||
    async restore(ids: string[]) {
 | 
			
		||||
        if (!this.enableTrash) {
 | 
			
		||||
            throw new ForbiddenException(
 | 
			
		||||
                `Can not to retore ${this.repository.qbName},because trash not enabled!`,
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        const items = await this.repository.find({
 | 
			
		||||
            where: { id: In(ids) as any },
 | 
			
		||||
            withDeleted: true,
 | 
			
		||||
        });
 | 
			
		||||
        const trasheds = items.filter((item) => !isNil(item));
 | 
			
		||||
        if (trasheds.length < 0) return [];
 | 
			
		||||
        await this.repository.restore(trasheds.map((item) => item.id));
 | 
			
		||||
        const qb = await this.buildListQB(
 | 
			
		||||
            this.repository.buildBaseQB(),
 | 
			
		||||
            undefined,
 | 
			
		||||
            async (builder) => builder.andWhereInIds(trasheds),
 | 
			
		||||
        );
 | 
			
		||||
        return qb.getMany();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取查询单个项目的QueryBuilder
 | 
			
		||||
     * @param id 查询数据的ID
 | 
			
		||||
     * @param qb querybuilder实例
 | 
			
		||||
     * @param callback 查询回调
 | 
			
		||||
     */
 | 
			
		||||
    protected async buildItemQB(id: string, qb: SelectQueryBuilder<E>, callback?: QueryHook<E>) {
 | 
			
		||||
        qb.where(`${this.repository.qbName}.id = :id`, { id });
 | 
			
		||||
        if (callback) return callback(qb);
 | 
			
		||||
        return qb;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取查询数据列表的 QueryBuilder
 | 
			
		||||
     * @param qb querybuilder实例
 | 
			
		||||
     * @param options 查询选项
 | 
			
		||||
     * @param callback 查询回调
 | 
			
		||||
     */
 | 
			
		||||
    protected async buildListQB(qb: SelectQueryBuilder<E>, options?: P, callback?: QueryHook<E>) {
 | 
			
		||||
        const { trashed } = options ?? {};
 | 
			
		||||
        const queryName = this.repository.qbName;
 | 
			
		||||
        // 是否查询回收站
 | 
			
		||||
        if (
 | 
			
		||||
            this.enableTrash &&
 | 
			
		||||
            (trashed === SelectTrashMode.ALL || trashed === SelectTrashMode.ONLY)
 | 
			
		||||
        ) {
 | 
			
		||||
            qb.withDeleted();
 | 
			
		||||
            if (trashed === SelectTrashMode.ONLY) {
 | 
			
		||||
                qb.where(`${queryName}.deletedAt is not null`);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (callback) return callback(qb);
 | 
			
		||||
        return qb;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										91
									
								
								src/modules/database/base/subcriber.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								src/modules/database/base/subcriber.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,91 @@
 | 
			
		||||
import { Optional } from '@nestjs/common';
 | 
			
		||||
import { isNil } from 'lodash';
 | 
			
		||||
import {
 | 
			
		||||
    DataSource,
 | 
			
		||||
    EntitySubscriberInterface,
 | 
			
		||||
    EntityTarget,
 | 
			
		||||
    EventSubscriber,
 | 
			
		||||
    InsertEvent,
 | 
			
		||||
    ObjectLiteral,
 | 
			
		||||
    ObjectType,
 | 
			
		||||
    RecoverEvent,
 | 
			
		||||
    RemoveEvent,
 | 
			
		||||
    SoftRemoveEvent,
 | 
			
		||||
    TransactionCommitEvent,
 | 
			
		||||
    TransactionRollbackEvent,
 | 
			
		||||
    TransactionStartEvent,
 | 
			
		||||
    UpdateEvent,
 | 
			
		||||
} from 'typeorm';
 | 
			
		||||
 | 
			
		||||
import { getCustomRepository } from '@/modules/database/helpers';
 | 
			
		||||
import { RepositoryType } from '@/modules/database/types';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 基础模型观察者
 | 
			
		||||
 */
 | 
			
		||||
type SubscriberEvent<E extends ObjectLiteral> =
 | 
			
		||||
    | InsertEvent<E>
 | 
			
		||||
    | UpdateEvent<E>
 | 
			
		||||
    | SoftRemoveEvent<E>
 | 
			
		||||
    | RemoveEvent<E>
 | 
			
		||||
    | RecoverEvent<E>
 | 
			
		||||
    | TransactionStartEvent
 | 
			
		||||
    | TransactionCommitEvent
 | 
			
		||||
    | TransactionRollbackEvent;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 基础模型观察者
 | 
			
		||||
 */
 | 
			
		||||
@EventSubscriber()
 | 
			
		||||
export abstract class BaseSubscriber<E extends ObjectLiteral>
 | 
			
		||||
    implements EntitySubscriberInterface<E>
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * 监听的模型
 | 
			
		||||
     */
 | 
			
		||||
    protected abstract entity: ObjectType<E>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 构造函数
 | 
			
		||||
     * @param dataSource 数据连接池
 | 
			
		||||
     */
 | 
			
		||||
    constructor(@Optional() protected dataSource?: DataSource) {
 | 
			
		||||
        if (!isNil(this.dataSource)) this.dataSource.subscribers.push(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected getDataSource(event: SubscriberEvent<E>) {
 | 
			
		||||
        return this.dataSource ?? event.connection;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected getManage(event: SubscriberEvent<E>) {
 | 
			
		||||
        return this.dataSource ? this.dataSource.manager : event.manager;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    listenTo() {
 | 
			
		||||
        return this.entity;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async afterLoad(entity: any) {
 | 
			
		||||
        // 是否启用树形
 | 
			
		||||
        if ('parent' in entity && isNil(entity.depth)) entity.depth = 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected getRepositoy<
 | 
			
		||||
        C extends ClassType<T>,
 | 
			
		||||
        T extends RepositoryType<E>,
 | 
			
		||||
        A extends EntityTarget<ObjectLiteral>,
 | 
			
		||||
    >(event: SubscriberEvent<E>, repository?: C, entity?: A) {
 | 
			
		||||
        return isNil(repository)
 | 
			
		||||
            ? this.getDataSource(event).getRepository(entity ?? this.entity)
 | 
			
		||||
            : getCustomRepository<T, E>(this.getDataSource(event), repository);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 判断某个字段是否被更新
 | 
			
		||||
     * @param cloumn
 | 
			
		||||
     * @param event
 | 
			
		||||
     */
 | 
			
		||||
    protected isUpdated(cloumn: keyof E, event: UpdateEvent<E>) {
 | 
			
		||||
        return !!event.updatedColumns.find((item) => item.propertyName === cloumn);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										276
									
								
								src/modules/database/base/tree.repository.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										276
									
								
								src/modules/database/base/tree.repository.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,276 @@
 | 
			
		||||
import { isNil, pick, unset } from 'lodash';
 | 
			
		||||
import {
 | 
			
		||||
    EntityManager,
 | 
			
		||||
    EntityTarget,
 | 
			
		||||
    FindOptionsUtils,
 | 
			
		||||
    FindTreeOptions,
 | 
			
		||||
    ObjectLiteral,
 | 
			
		||||
    QueryRunner,
 | 
			
		||||
    SelectQueryBuilder,
 | 
			
		||||
    TreeRepository,
 | 
			
		||||
    TreeRepositoryUtils,
 | 
			
		||||
} from 'typeorm';
 | 
			
		||||
 | 
			
		||||
import { OrderType, TreeChildrenResolve } from '../constants';
 | 
			
		||||
import { getOrderByQuery } from '../helpers';
 | 
			
		||||
import { OrderQueryType, QueryParams } from '../types';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 基础树形存储类
 | 
			
		||||
 */
 | 
			
		||||
export class BaseTreeRepository<E extends ObjectLiteral> extends TreeRepository<E> {
 | 
			
		||||
    /**
 | 
			
		||||
     * 查询器名称
 | 
			
		||||
     */
 | 
			
		||||
    protected _qbName = 'treeEntity';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 删除父分类后是否提升子分类的等级
 | 
			
		||||
     */
 | 
			
		||||
    protected _childrenResolve?: TreeChildrenResolve;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 默认排序规则,可以通过每个方法的orderBy选项进行覆盖
 | 
			
		||||
     */
 | 
			
		||||
    protected orderBy?: string | { name: string; order: `${OrderType}` };
 | 
			
		||||
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-useless-constructor
 | 
			
		||||
    constructor(target: EntityTarget<E>, manager: EntityManager, queryRunner?: QueryRunner) {
 | 
			
		||||
        super(target, manager, queryRunner);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 返回查询器名称
 | 
			
		||||
     */
 | 
			
		||||
    get qbName() {
 | 
			
		||||
        return this._qbName;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 返回子分类的等级
 | 
			
		||||
     */
 | 
			
		||||
    get childrenResolve() {
 | 
			
		||||
        return this._childrenResolve;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 构建基础查询器
 | 
			
		||||
     */
 | 
			
		||||
    buildBaseQB(qb?: SelectQueryBuilder<E>): SelectQueryBuilder<E> {
 | 
			
		||||
        const queryBuilder = qb ?? this.createQueryBuilder(this.qbName);
 | 
			
		||||
        return queryBuilder.leftJoinAndSelect(`${this.qbName}.parent`, 'parent');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 生成排序的QueryBuilder
 | 
			
		||||
     * @param qb
 | 
			
		||||
     * @param orderBy
 | 
			
		||||
     */
 | 
			
		||||
    addOrderByQuery(qb: SelectQueryBuilder<E>, orderBy?: OrderQueryType) {
 | 
			
		||||
        const orderByQuery = orderBy ?? this.orderBy;
 | 
			
		||||
        return !isNil(orderByQuery) ? getOrderByQuery(qb, this.qbName, orderByQuery) : qb;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 查询树形分类
 | 
			
		||||
     * @param options
 | 
			
		||||
     */
 | 
			
		||||
    async findTrees(options?: FindTreeOptions & QueryParams<E>) {
 | 
			
		||||
        const roots = await this.findRoots(options);
 | 
			
		||||
        await Promise.all(roots.map((root) => this.findDescendantsTree(root, options)));
 | 
			
		||||
        return roots;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 查询顶级分类
 | 
			
		||||
     * @param options
 | 
			
		||||
     */
 | 
			
		||||
    async findRoots(options?: FindTreeOptions & QueryParams<E>) {
 | 
			
		||||
        const { addQuery, orderBy, withTrashed, onlyTrashed } = options ?? {};
 | 
			
		||||
        const escapeAlias = (alias: string) => this.manager.connection.driver.escape(alias);
 | 
			
		||||
        const escapeColumn = (column: string) => this.manager.connection.driver.escape(column);
 | 
			
		||||
 | 
			
		||||
        const joinColumn = this.metadata.treeParentRelation!.joinColumns[0];
 | 
			
		||||
        const parentPropertyName = joinColumn.givenDatabaseName || joinColumn.databaseName;
 | 
			
		||||
 | 
			
		||||
        let qb = this.addOrderByQuery(this.buildBaseQB(), orderBy);
 | 
			
		||||
        qb.where(`${escapeAlias(this.qbName)}.${escapeColumn(parentPropertyName)} IS NULL`);
 | 
			
		||||
        FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, pick(options, ['relations', 'depth']));
 | 
			
		||||
        qb = addQuery ? await addQuery(qb) : qb;
 | 
			
		||||
        if (withTrashed) {
 | 
			
		||||
            qb.withDeleted();
 | 
			
		||||
            if (onlyTrashed) qb.where(`${this.qbName}.deletedAt IS NOT NULL`);
 | 
			
		||||
        }
 | 
			
		||||
        return qb.getMany();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 查询后代树
 | 
			
		||||
     * @param entity
 | 
			
		||||
     * @param options
 | 
			
		||||
     */
 | 
			
		||||
    async findDescendantsTree(entity: E, options?: FindTreeOptions & QueryParams<E>) {
 | 
			
		||||
        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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 查询祖先树
 | 
			
		||||
     * @param entity
 | 
			
		||||
     * @param options
 | 
			
		||||
     */
 | 
			
		||||
    async findAncestorsTree(entity: E, options?: FindTreeOptions & QueryParams<E>) {
 | 
			
		||||
        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, options);
 | 
			
		||||
 | 
			
		||||
        const entities = await qb.getRawAndEntities();
 | 
			
		||||
        const relationMaps = TreeRepositoryUtils.createRelationMaps(
 | 
			
		||||
            this.manager,
 | 
			
		||||
            this.metadata,
 | 
			
		||||
            'treeEntity',
 | 
			
		||||
            entities.raw,
 | 
			
		||||
        );
 | 
			
		||||
        TreeRepositoryUtils.buildParentEntityTree(
 | 
			
		||||
            this.metadata,
 | 
			
		||||
            entity,
 | 
			
		||||
            entities.entities,
 | 
			
		||||
            relationMaps,
 | 
			
		||||
        );
 | 
			
		||||
        return entity;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 查询后代元素
 | 
			
		||||
     * @param entity
 | 
			
		||||
     * @param options
 | 
			
		||||
     */
 | 
			
		||||
    async findDescendants(entity: E, options?: FindTreeOptions & QueryParams<E>) {
 | 
			
		||||
        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, options);
 | 
			
		||||
        return qb.getMany();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 查询祖先元素
 | 
			
		||||
     * @param entity
 | 
			
		||||
     * @param options
 | 
			
		||||
     */
 | 
			
		||||
    async findAncestors(entity: E, options?: FindTreeOptions & QueryParams<E>) {
 | 
			
		||||
        const { addQuery, orderBy, withTrashed, onlyTrashed } = options ?? {};
 | 
			
		||||
        let qb = this.buildBaseQB(
 | 
			
		||||
            this.createAncestorsQueryBuilder(this.qbName, 'treeClosure', entity),
 | 
			
		||||
        );
 | 
			
		||||
        FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options);
 | 
			
		||||
        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`);
 | 
			
		||||
        }
 | 
			
		||||
        return qb.getMany();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 统计后代元素数量
 | 
			
		||||
     * @param entity
 | 
			
		||||
     * @param options
 | 
			
		||||
     */
 | 
			
		||||
    async countDescendants(entity: E, options?: FindTreeOptions & QueryParams<E>) {
 | 
			
		||||
        const { addQuery, orderBy, withTrashed, onlyTrashed } = options ?? {};
 | 
			
		||||
        let qb = 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`);
 | 
			
		||||
        }
 | 
			
		||||
        return qb.getCount();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 统计祖先元素数量
 | 
			
		||||
     * @param entity
 | 
			
		||||
     * @param options
 | 
			
		||||
     */
 | 
			
		||||
    async countAncestors(entity: E, options?: FindTreeOptions & QueryParams<E>) {
 | 
			
		||||
        const { addQuery, orderBy, withTrashed, onlyTrashed } = options ?? {};
 | 
			
		||||
        let qb = this.createAncestorsQueryBuilder(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`);
 | 
			
		||||
        }
 | 
			
		||||
        return qb.getCount();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 打平并展开树
 | 
			
		||||
     * @param trees
 | 
			
		||||
     * @param level
 | 
			
		||||
     */
 | 
			
		||||
    async toFlatTrees(trees: E[], depth = 0, parent: E | null = null): Promise<E[]> {
 | 
			
		||||
        const data: Omit<E, 'children'>[] = [];
 | 
			
		||||
        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 E[];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -11,3 +11,20 @@ export enum SelectTrashMode {
 | 
			
		||||
    ONLY = 'only',
 | 
			
		||||
    NONE = 'none',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 排序方式
 | 
			
		||||
 */
 | 
			
		||||
export enum OrderType {
 | 
			
		||||
    ASC = 'ASC',
 | 
			
		||||
    DESC = 'DESC',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 树形模型在删除父级后子级的处理方式
 | 
			
		||||
 */
 | 
			
		||||
export enum TreeChildrenResolve {
 | 
			
		||||
    DELETE = 'delete',
 | 
			
		||||
    UP = 'up',
 | 
			
		||||
    ROOT = 'root',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,8 @@
 | 
			
		||||
import { isNil } from 'lodash';
 | 
			
		||||
import { ObjectLiteral, SelectQueryBuilder } from 'typeorm';
 | 
			
		||||
import { DataSource, ObjectLiteral, ObjectType, Repository, SelectQueryBuilder } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
import { PaginateOptions, PaginateReturn } from '@/modules/database/types';
 | 
			
		||||
import { CUSTOM_REPOSITORY_METADATA } from '@/modules/database/constants';
 | 
			
		||||
import { OrderQueryType, PaginateOptions, PaginateReturn } from '@/modules/database/types';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 分页函数
 | 
			
		||||
@ -71,3 +72,43 @@ export const treePaginate = <E extends ObjectLiteral>(
 | 
			
		||||
        items,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 为查询添加排序,默认排序规则为DESC
 | 
			
		||||
 * @param qb 原查询
 | 
			
		||||
 * @param alias 别名
 | 
			
		||||
 * @param orderBy 查询排序
 | 
			
		||||
 */
 | 
			
		||||
export const getOrderByQuery = <E extends ObjectLiteral>(
 | 
			
		||||
    qb: SelectQueryBuilder<E>,
 | 
			
		||||
    alias: string,
 | 
			
		||||
    orderBy?: OrderQueryType,
 | 
			
		||||
) => {
 | 
			
		||||
    if (isNil(orderBy)) return qb;
 | 
			
		||||
    if (typeof orderBy === 'string') return qb.orderBy(`${alias}.${orderBy}`, 'DESC');
 | 
			
		||||
    if (Array.isArray(orderBy)) {
 | 
			
		||||
        for (const item of orderBy) {
 | 
			
		||||
            typeof item === 'string'
 | 
			
		||||
                ? qb.addOrderBy(`${alias}.${item}`, 'DESC')
 | 
			
		||||
                : qb.addOrderBy(`${alias}.${item.name}`, item.order);
 | 
			
		||||
        }
 | 
			
		||||
        return qb;
 | 
			
		||||
    }
 | 
			
		||||
    return qb.orderBy(`${alias}.${(orderBy as any).name}`, (orderBy as any).order);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 获取自定义Repository的实例
 | 
			
		||||
 * @param dataSource 数据连接池
 | 
			
		||||
 * @param Repo repository类
 | 
			
		||||
 */
 | 
			
		||||
export const getCustomRepository = <T extends Repository<E>, E extends ObjectLiteral>(
 | 
			
		||||
    dataSource: DataSource,
 | 
			
		||||
    Repo: ClassType<T>,
 | 
			
		||||
): T => {
 | 
			
		||||
    if (isNil(Repo)) return null;
 | 
			
		||||
    const entity = Reflect.getMetadata(CUSTOM_REPOSITORY_METADATA, Repo);
 | 
			
		||||
    if (!entity) return null;
 | 
			
		||||
    const base = dataSource.getRepository<ObjectType<any>>(entity);
 | 
			
		||||
    return new Repo(base.target, base.manager, base.queryRunner) as T;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,13 @@
 | 
			
		||||
import { ObjectLiteral, SelectQueryBuilder } from 'typeorm';
 | 
			
		||||
import {
 | 
			
		||||
    FindTreeOptions,
 | 
			
		||||
    ObjectLiteral,
 | 
			
		||||
    Repository,
 | 
			
		||||
    SelectQueryBuilder,
 | 
			
		||||
    TreeRepository,
 | 
			
		||||
} from 'typeorm';
 | 
			
		||||
 | 
			
		||||
import { BaseRepository, BaseTreeRepository } from '@/modules/database/base';
 | 
			
		||||
import { OrderType, SelectTrashMode } from '@/modules/database/constants';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 一个查询钩子,用于修改或增强给定的查询构建器。
 | 
			
		||||
@ -68,3 +77,57 @@ export interface PaginateReturn<E extends ObjectLiteral> {
 | 
			
		||||
     */
 | 
			
		||||
    meta: PaginateMeta;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 排序类型,{字段名称: 排序方法}
 | 
			
		||||
 * 如果多个值则传入数组即可
 | 
			
		||||
 * 排序方法不设置,默认DESC
 | 
			
		||||
 */
 | 
			
		||||
export type OrderQueryType =
 | 
			
		||||
    | string
 | 
			
		||||
    | { name: string; order: `${OrderType}` }
 | 
			
		||||
    | Array<{ name: string; order: `${OrderType}` } | string>;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 数据列表查询类型
 | 
			
		||||
 */
 | 
			
		||||
export interface QueryParams<E extends ObjectLiteral> {
 | 
			
		||||
    addQuery?: QueryHook<E>;
 | 
			
		||||
    orderBy?: OrderQueryType;
 | 
			
		||||
    withTrashed?: boolean;
 | 
			
		||||
    onlyTrashed?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 服务类数据列表查询类型
 | 
			
		||||
 */
 | 
			
		||||
export type ServiceListQueryOption<E extends ObjectLiteral> =
 | 
			
		||||
    | ServiceListQueryOptionWithTrashed<E>
 | 
			
		||||
    | ServiceListQueryOptionNotWithTrashed<E>;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 带有软删除的服务类数据列表查询类型
 | 
			
		||||
 */
 | 
			
		||||
type ServiceListQueryOptionWithTrashed<E extends ObjectLiteral> = Omit<
 | 
			
		||||
    FindTreeOptions & QueryParams<E>,
 | 
			
		||||
    'withTrashed'
 | 
			
		||||
> & {
 | 
			
		||||
    trashed?: `${SelectTrashMode}`;
 | 
			
		||||
} & Record<string, any>;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 不带软删除的服务类数据列表查询类型
 | 
			
		||||
 */
 | 
			
		||||
type ServiceListQueryOptionNotWithTrashed<E extends ObjectLiteral> = Omit<
 | 
			
		||||
    ServiceListQueryOptionWithTrashed<E>,
 | 
			
		||||
    'trashed'
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Repository类型
 | 
			
		||||
 */
 | 
			
		||||
export type RepositoryType<E extends ObjectLiteral> =
 | 
			
		||||
    | Repository<E>
 | 
			
		||||
    | TreeRepository<E>
 | 
			
		||||
    | BaseRepository<E>
 | 
			
		||||
    | BaseTreeRepository<E>;
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user