parent
b37dfa8103
commit
4ec73cc0e7
@ -32,6 +32,7 @@ export class ContentModule {
|
|||||||
repositories.CategoryRepository,
|
repositories.CategoryRepository,
|
||||||
services.CategoryService,
|
services.CategoryService,
|
||||||
repositories.TagRepository,
|
repositories.TagRepository,
|
||||||
|
repositories.CommentRepository,
|
||||||
{ token: services.SearchService, optional: true },
|
{ token: services.SearchService, optional: true },
|
||||||
],
|
],
|
||||||
useFactory(
|
useFactory(
|
||||||
@ -39,6 +40,7 @@ export class ContentModule {
|
|||||||
categoryRepository: repositories.CategoryRepository,
|
categoryRepository: repositories.CategoryRepository,
|
||||||
categoryService: services.CategoryService,
|
categoryService: services.CategoryService,
|
||||||
tagRepository: repositories.TagRepository,
|
tagRepository: repositories.TagRepository,
|
||||||
|
commentRepository: repositories.CommentRepository,
|
||||||
searchService: services.SearchService,
|
searchService: services.SearchService,
|
||||||
) {
|
) {
|
||||||
return new PostService(
|
return new PostService(
|
||||||
@ -46,6 +48,7 @@ export class ContentModule {
|
|||||||
categoryRepository,
|
categoryRepository,
|
||||||
categoryService,
|
categoryService,
|
||||||
tagRepository,
|
tagRepository,
|
||||||
|
commentRepository,
|
||||||
searchService,
|
searchService,
|
||||||
config.searchType,
|
config.searchType,
|
||||||
);
|
);
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { Exclude, Expose } from 'class-transformer';
|
import { Exclude, Expose, Type } from 'class-transformer';
|
||||||
import {
|
import {
|
||||||
BaseEntity,
|
BaseEntity,
|
||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
|
DeleteDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
Index,
|
Index,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
@ -46,10 +47,17 @@ export class CommentEntity extends BaseEntity {
|
|||||||
depth = 0;
|
depth = 0;
|
||||||
|
|
||||||
@Expose({ groups: ['comment-detail', 'comment-list'] })
|
@Expose({ groups: ['comment-detail', 'comment-list'] })
|
||||||
@TreeParent({ onDelete: 'NO ACTION' })
|
@TreeParent({ onDelete: 'CASCADE' })
|
||||||
parent: Relation<CommentEntity> | null;
|
parent: Relation<CommentEntity> | null;
|
||||||
|
|
||||||
@Expose({ groups: ['comment-tree'] })
|
@Expose({ groups: ['comment-tree'] })
|
||||||
@TreeChildren({ cascade: true })
|
@TreeChildren({ cascade: true })
|
||||||
children: Relation<CommentEntity[]>;
|
children: Relation<CommentEntity[]>;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@Type(() => Date)
|
||||||
|
@DeleteDateColumn({
|
||||||
|
comment: '删除时间',
|
||||||
|
})
|
||||||
|
deletedAt: Date;
|
||||||
}
|
}
|
||||||
|
@ -102,9 +102,7 @@ export class PostEntity extends BaseEntity {
|
|||||||
category: Relation<CategoryEntity>;
|
category: Relation<CategoryEntity>;
|
||||||
|
|
||||||
@Expose()
|
@Expose()
|
||||||
@ManyToMany(() => TagEntity, (tag) => tag.posts, {
|
@ManyToMany(() => TagEntity, (tag) => tag.posts)
|
||||||
cascade: true,
|
|
||||||
})
|
|
||||||
@JoinTable()
|
@JoinTable()
|
||||||
tags: Relation<TagEntity>[];
|
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 { CategoryEntity } from '@/modules/content/entities';
|
||||||
|
import { BaseTreeRepository } from '@/modules/database/base';
|
||||||
|
import { OrderType, TreeChildrenResolve } from '@/modules/database/constants';
|
||||||
import { CustomRepository } from '@/modules/database/decorators';
|
import { CustomRepository } from '@/modules/database/decorators';
|
||||||
|
|
||||||
@CustomRepository(CategoryEntity)
|
@CustomRepository(CategoryEntity)
|
||||||
export class CategoryRepository extends TreeRepository<CategoryEntity> {
|
export class CategoryRepository extends BaseTreeRepository<CategoryEntity> {
|
||||||
/**
|
protected _qbName = 'category';
|
||||||
* 构建基础查询器
|
|
||||||
*/
|
|
||||||
buildBaseQB() {
|
|
||||||
return this.createQueryBuilder('category').leftJoinAndSelect('category.parent', 'parent');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
protected orderBy = { name: 'customOrder', order: OrderType.ASC };
|
||||||
* 树形结构查询
|
|
||||||
* @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 _childrenResolve = TreeChildrenResolve.UP;
|
||||||
* 查询顶级分类
|
|
||||||
* @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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,137 +1,29 @@
|
|||||||
import { pick, unset } from 'lodash';
|
import { isNil } from 'lodash';
|
||||||
import {
|
import { FindTreeOptions, SelectQueryBuilder } from 'typeorm';
|
||||||
FindOptionsUtils,
|
|
||||||
FindTreeOptions,
|
|
||||||
SelectQueryBuilder,
|
|
||||||
TreeRepository,
|
|
||||||
TreeRepositoryUtils,
|
|
||||||
} from 'typeorm';
|
|
||||||
|
|
||||||
import { CommentEntity } from '@/modules/content/entities';
|
import { CommentEntity } from '@/modules/content/entities';
|
||||||
|
import { BaseTreeRepository } from '@/modules/database/base';
|
||||||
import { CustomRepository } from '@/modules/database/decorators';
|
import { CustomRepository } from '@/modules/database/decorators';
|
||||||
|
import { QueryParams } from '@/modules/database/types';
|
||||||
type FindCommentTreeOptions = FindTreeOptions & {
|
|
||||||
addQuery?: (query: SelectQueryBuilder<CommentEntity>) => SelectQueryBuilder<CommentEntity>;
|
|
||||||
};
|
|
||||||
|
|
||||||
@CustomRepository(CommentEntity)
|
@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> {
|
buildBaseQB(qb: SelectQueryBuilder<CommentEntity>): SelectQueryBuilder<CommentEntity> {
|
||||||
return qb
|
return super.buildBaseQB(qb).leftJoinAndSelect(`${this.qbName}.post`, 'post');
|
||||||
.leftJoinAndSelect(`comment.parent`, 'parent')
|
|
||||||
.leftJoinAndSelect(`comment.post`, 'post')
|
|
||||||
.orderBy('comment.createdAt', 'DESC');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
async findTrees(
|
||||||
* 查询树
|
options: FindTreeOptions & QueryParams<CommentEntity> & { post?: string } = {},
|
||||||
* @param options
|
): Promise<CommentEntity[]> {
|
||||||
*/
|
return super.findTrees({
|
||||||
async findTrees(options: FindCommentTreeOptions = {}) {
|
...options,
|
||||||
options.relations = ['parent', 'children'];
|
addQuery: async (qb) => {
|
||||||
|
return isNil(options.post) ? qb : qb.where('post.id = :id', { id: options.post });
|
||||||
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']),
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
|
|
||||||
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 { CommentEntity, PostEntity } from '@/modules/content/entities';
|
||||||
|
import { BaseRepository } from '@/modules/database/base';
|
||||||
import { CustomRepository } from '@/modules/database/decorators';
|
import { CustomRepository } from '@/modules/database/decorators';
|
||||||
|
|
||||||
@CustomRepository(PostEntity)
|
@CustomRepository(PostEntity)
|
||||||
export class PostRepository extends Repository<PostEntity> {
|
export class PostRepository extends BaseRepository<PostEntity> {
|
||||||
|
protected _qbName = 'post';
|
||||||
|
|
||||||
buildBaseQB() {
|
buildBaseQB() {
|
||||||
// 在查询之前先查询出评论数量在添加到commentCount字段上
|
// 在查询之前先查询出评论数量在添加到commentCount字段上
|
||||||
return this.createQueryBuilder('post')
|
return this.createQueryBuilder('post')
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { Repository } from 'typeorm';
|
|
||||||
|
|
||||||
import { PostEntity, TagEntity } from '@/modules/content/entities';
|
import { PostEntity, TagEntity } from '@/modules/content/entities';
|
||||||
|
import { BaseRepository } from '@/modules/database/base';
|
||||||
import { CustomRepository } from '@/modules/database/decorators';
|
import { CustomRepository } from '@/modules/database/decorators';
|
||||||
|
|
||||||
@CustomRepository(TagEntity)
|
@CustomRepository(TagEntity)
|
||||||
export class TagRepository extends Repository<TagEntity> {
|
export class TagRepository extends BaseRepository<TagEntity> {
|
||||||
|
protected _qbName = 'tag';
|
||||||
|
|
||||||
buildBaseQB() {
|
buildBaseQB() {
|
||||||
return this.createQueryBuilder('tag')
|
return this.createQueryBuilder('tag')
|
||||||
.leftJoinAndSelect('tag.posts', 'posts')
|
.leftJoinAndSelect('tag.posts', 'posts')
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
import { isNil, omit } from 'lodash';
|
import { isNil, omit } from 'lodash';
|
||||||
import { EntityNotFoundError, In } from 'typeorm';
|
import { EntityNotFoundError } from 'typeorm';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CreateCategoryDto,
|
CreateCategoryDto,
|
||||||
@ -11,6 +11,7 @@ import {
|
|||||||
} from '@/modules/content/dtos';
|
} from '@/modules/content/dtos';
|
||||||
import { CategoryEntity } from '@/modules/content/entities';
|
import { CategoryEntity } from '@/modules/content/entities';
|
||||||
import { CategoryRepository } from '@/modules/content/repositories';
|
import { CategoryRepository } from '@/modules/content/repositories';
|
||||||
|
import { BaseService } from '@/modules/database/base';
|
||||||
import { SelectTrashMode } from '@/modules/database/constants';
|
import { SelectTrashMode } from '@/modules/database/constants';
|
||||||
import { treePaginate } from '@/modules/database/helpers';
|
import { treePaginate } from '@/modules/database/helpers';
|
||||||
|
|
||||||
@ -18,15 +19,18 @@ import { treePaginate } from '@/modules/database/helpers';
|
|||||||
* 分类数据操作
|
* 分类数据操作
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CategoryService {
|
export class CategoryService extends BaseService<CategoryEntity, CategoryRepository> {
|
||||||
constructor(protected repository: CategoryRepository) {}
|
protected enableTrash = true;
|
||||||
|
|
||||||
|
constructor(protected repository: CategoryRepository) {
|
||||||
|
super(repository);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询分类树
|
* 查询分类树
|
||||||
*/
|
*/
|
||||||
async findTrees(options: QueryCategoryTreeDto) {
|
async findTrees(options: QueryCategoryTreeDto) {
|
||||||
const { trashed = SelectTrashMode.NONE } = options;
|
const { trashed = SelectTrashMode.NONE } = options;
|
||||||
|
|
||||||
return this.repository.findTrees({
|
return this.repository.findTrees({
|
||||||
withTrashed: trashed === SelectTrashMode.ALL || trashed === SelectTrashMode.ONLY,
|
withTrashed: trashed === SelectTrashMode.ALL || trashed === SelectTrashMode.ONLY,
|
||||||
onlyTrashed: trashed === SelectTrashMode.ONLY,
|
onlyTrashed: trashed === SelectTrashMode.ONLY,
|
||||||
@ -39,13 +43,11 @@ export class CategoryService {
|
|||||||
*/
|
*/
|
||||||
async paginate(options: QueryCategoryDto) {
|
async paginate(options: QueryCategoryDto) {
|
||||||
const { trashed = SelectTrashMode.NONE } = options;
|
const { trashed = SelectTrashMode.NONE } = options;
|
||||||
|
|
||||||
const tree = await this.repository.findTrees({
|
const tree = await this.repository.findTrees({
|
||||||
withTrashed: trashed === SelectTrashMode.ALL || trashed === SelectTrashMode.ONLY,
|
withTrashed: trashed === SelectTrashMode.ALL || trashed === SelectTrashMode.ONLY,
|
||||||
onlyTrashed: trashed === SelectTrashMode.ONLY,
|
onlyTrashed: trashed === SelectTrashMode.ONLY,
|
||||||
});
|
});
|
||||||
const data = await this.repository.toFlatTrees(tree);
|
const data = await this.repository.toFlatTrees(tree);
|
||||||
|
|
||||||
return treePaginate(options, data);
|
return treePaginate(options, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,7 +71,6 @@ export class CategoryService {
|
|||||||
...data,
|
...data,
|
||||||
parent: await this.getParent(undefined, data.parent),
|
parent: await this.getParent(undefined, data.parent),
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.detail(item.id);
|
return this.detail(item.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,58 +80,24 @@ export class CategoryService {
|
|||||||
*/
|
*/
|
||||||
async update(data: UpdateCategoryDto) {
|
async update(data: UpdateCategoryDto) {
|
||||||
await this.repository.update(data.id, omit(data, ['id', 'parent']));
|
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 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) && item.parent.id !== parent.id) ||
|
||||||
(isNil(item.parent) && !isNil(parent)) ||
|
(isNil(item.parent) && !isNil(parent)) ||
|
||||||
(!isNil(item.parent) && isNil(parent));
|
(!isNil(item.parent) && isNil(parent));
|
||||||
|
|
||||||
// 父分类单独更新
|
// 父分类单独更新
|
||||||
if (parent !== undefined && sholdUpdateParent) {
|
if (parent !== undefined && shouldUpdateParent) {
|
||||||
item.parent = parent;
|
item.parent = parent;
|
||||||
await this.repository.save(item, { reload: true });
|
await this.repository.save(item, { reload: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
return item;
|
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
|
* @param current 当前分类的ID
|
||||||
@ -150,22 +117,4 @@ export class CategoryService {
|
|||||||
}
|
}
|
||||||
return parent;
|
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 { isNil } from 'lodash';
|
||||||
|
|
||||||
import { EntityNotFoundError, In, SelectQueryBuilder } from 'typeorm';
|
import { EntityNotFoundError, SelectQueryBuilder } from 'typeorm';
|
||||||
|
|
||||||
import { CreateCommentDto, QueryCommentDto, QueryCommentTreeDto } from '@/modules/content/dtos';
|
import { CreateCommentDto, QueryCommentDto, QueryCommentTreeDto } from '@/modules/content/dtos';
|
||||||
import { CommentEntity } from '@/modules/content/entities';
|
import { CommentEntity } from '@/modules/content/entities';
|
||||||
import { CommentRepository, PostRepository } from '@/modules/content/repositories';
|
import { CommentRepository, PostRepository } from '@/modules/content/repositories';
|
||||||
import { treePaginate } from '@/modules/database/helpers';
|
import { BaseService } from '@/modules/database/base';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 评论数据操作
|
* 评论数据操作
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CommentService {
|
export class CommentService extends BaseService<CommentEntity, CommentRepository> {
|
||||||
constructor(
|
constructor(
|
||||||
protected repository: CommentRepository,
|
protected repository: CommentRepository,
|
||||||
protected postRepository: PostRepository,
|
protected postRepository: PostRepository,
|
||||||
) {}
|
) {
|
||||||
|
super(repository);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 直接查询评论树
|
* 直接查询评论树
|
||||||
@ -25,7 +27,7 @@ export class CommentService {
|
|||||||
*/
|
*/
|
||||||
async findTrees(options: QueryCommentTreeDto = {}) {
|
async findTrees(options: QueryCommentTreeDto = {}) {
|
||||||
return this.repository.findTrees({
|
return this.repository.findTrees({
|
||||||
addQuery: (qb) => {
|
addQuery: async (qb) => {
|
||||||
return isNil(options.post) ? qb : qb.where('post.id = :id', { id: options.post });
|
return isNil(options.post) ? qb : qb.where('post.id = :id', { id: options.post });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -35,28 +37,17 @@ export class CommentService {
|
|||||||
* 查找一篇文章的评论并分页
|
* 查找一篇文章的评论并分页
|
||||||
* @param dto
|
* @param dto
|
||||||
*/
|
*/
|
||||||
async paginate(dto: QueryCommentDto) {
|
async paginate(options: QueryCommentDto) {
|
||||||
const { post, ...query } = dto;
|
const { post } = options;
|
||||||
const addQuery = (qb: SelectQueryBuilder<CommentEntity>) => {
|
const addQuery = (qb: SelectQueryBuilder<CommentEntity>) => {
|
||||||
const condition: Record<string, string> = {};
|
const condition: Record<string, string> = {};
|
||||||
if (!isNil(post)) condition.post = post;
|
if (!isNil(post)) condition.post = post;
|
||||||
return Object.keys(condition).length > 0 ? qb.andWhere(condition) : qb;
|
return Object.keys(condition).length > 0 ? qb.andWhere(condition) : qb;
|
||||||
};
|
};
|
||||||
|
return super.paginate({
|
||||||
const data = await this.repository.findRoots({ addQuery });
|
...options,
|
||||||
|
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -66,32 +57,20 @@ export class CommentService {
|
|||||||
*/
|
*/
|
||||||
async create(data: CreateCommentDto) {
|
async create(data: CreateCommentDto) {
|
||||||
const parent = await this.getParent(undefined, data.parent);
|
const parent = await this.getParent(undefined, data.parent);
|
||||||
|
|
||||||
if (!isNil(parent) && parent.post.id !== data.post) {
|
if (!isNil(parent) && parent.post.id !== data.post) {
|
||||||
throw new ForbiddenException('Parent comment and child comment must belong same post!');
|
throw new ForbiddenException('Parent comment and child comment must belong same post!');
|
||||||
}
|
}
|
||||||
|
|
||||||
const item = await this.repository.save({
|
const item = await this.repository.save({
|
||||||
...data,
|
...data,
|
||||||
parent,
|
parent,
|
||||||
post: await this.getPost(data.post),
|
post: await this.getPost(data.post),
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.repository.findOneOrFail({ where: { id: item.id } });
|
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) {
|
protected async getPost(id: string) {
|
||||||
return !isNil(id) ? this.postRepository.findOneOrFail({ where: { id } }) : id;
|
return !isNil(id) ? this.postRepository.findOneOrFail({ where: { id } }) : id;
|
||||||
@ -100,24 +79,21 @@ export class CommentService {
|
|||||||
/**
|
/**
|
||||||
* 获取请求传入的父分类
|
* 获取请求传入的父分类
|
||||||
* @param current 当前分类的ID
|
* @param current 当前分类的ID
|
||||||
* @param parentId
|
* @param id
|
||||||
*/
|
*/
|
||||||
protected async getParent(current?: string, parentId?: string) {
|
protected async getParent(current?: string, id?: string) {
|
||||||
if (current === parentId) return undefined;
|
if (current === id) return undefined;
|
||||||
let parent: CommentEntity | undefined;
|
let parent: CommentEntity | undefined;
|
||||||
if (parentId !== undefined) {
|
if (id !== undefined) {
|
||||||
if (parentId === null) return null;
|
if (id === null) return null;
|
||||||
parent = await this.repository.findOne({
|
parent = await this.repository.findOne({
|
||||||
relations: ['parent', 'post'],
|
relations: ['parent', 'post'],
|
||||||
where: { id: parentId },
|
where: { id },
|
||||||
});
|
});
|
||||||
if (!parent)
|
if (!parent) {
|
||||||
throw new EntityNotFoundError(
|
throw new EntityNotFoundError(CommentEntity, `Parent comment ${id} not exists!`);
|
||||||
CommentEntity,
|
}
|
||||||
`Parent comment ${parentId} not exists !`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return parent;
|
return parent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,11 +7,17 @@ import { EntityNotFoundError, In, IsNull, Not, SelectQueryBuilder } from 'typeor
|
|||||||
import { PostOrderType } from '@/modules/content/constants';
|
import { PostOrderType } from '@/modules/content/constants';
|
||||||
import { CreatePostDto, QueryPostDto, UpdatePostDto } from '@/modules/content/dtos';
|
import { CreatePostDto, QueryPostDto, UpdatePostDto } from '@/modules/content/dtos';
|
||||||
import { PostEntity } from '@/modules/content/entities';
|
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 { CategoryService } from '@/modules/content/services/category.service';
|
||||||
import { SearchService } from '@/modules/content/services/search.service';
|
import { SearchService } from '@/modules/content/services/search.service';
|
||||||
import { SearchType } from '@/modules/content/types';
|
import { SearchType } from '@/modules/content/types';
|
||||||
|
import { BaseService } from '@/modules/database/base';
|
||||||
import { SelectTrashMode } from '@/modules/database/constants';
|
import { SelectTrashMode } from '@/modules/database/constants';
|
||||||
import { paginate } from '@/modules/database/helpers';
|
import { paginate } from '@/modules/database/helpers';
|
||||||
import { QueryHook } from '@/modules/database/types';
|
import { QueryHook } from '@/modules/database/types';
|
||||||
@ -20,16 +26,24 @@ type FindParams = {
|
|||||||
[key in keyof Omit<QueryPostDto, 'limit' | 'page'>]: QueryPostDto[key];
|
[key in keyof Omit<QueryPostDto, 'limit' | 'page'>]: QueryPostDto[key];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章数据操作
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PostService {
|
export class PostService extends BaseService<PostEntity, PostRepository, FindParams> {
|
||||||
|
protected enableTrash = true;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected repository: PostRepository,
|
protected repository: PostRepository,
|
||||||
protected categoryRepository: CategoryRepository,
|
protected categoryRepository: CategoryRepository,
|
||||||
protected categoryService: CategoryService,
|
protected categoryService: CategoryService,
|
||||||
protected tagRepository: TagRepository,
|
protected tagRepository: TagRepository,
|
||||||
|
protected commentRepository: CommentRepository,
|
||||||
protected searchService?: SearchService,
|
protected searchService?: SearchService,
|
||||||
protected search_type: SearchType = 'against',
|
protected search_type: SearchType = 'against',
|
||||||
) {}
|
) {
|
||||||
|
super(repository);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取分页数据
|
* 获取分页数据
|
||||||
@ -41,7 +55,7 @@ export class PostService {
|
|||||||
return this.searchService.search(
|
return this.searchService.search(
|
||||||
options.search,
|
options.search,
|
||||||
pick(options, ['trashed', 'page', 'limit']),
|
pick(options, ['trashed', 'page', 'limit']),
|
||||||
);
|
) as any;
|
||||||
}
|
}
|
||||||
const qb = await this.buildListQuery(this.repository.buildBaseQB(), options, callback);
|
const qb = await this.buildListQuery(this.repository.buildBaseQB(), options, callback);
|
||||||
return paginate(qb, options);
|
return paginate(qb, options);
|
||||||
@ -80,9 +94,7 @@ export class PostService {
|
|||||||
: [],
|
: [],
|
||||||
};
|
};
|
||||||
const item = await this.repository.save(createPostDto);
|
const item = await this.repository.save(createPostDto);
|
||||||
|
|
||||||
if (!isNil(this.searchService)) await this.searchService.create(item);
|
if (!isNil(this.searchService)) await this.searchService.create(item);
|
||||||
|
|
||||||
return this.detail(item.id);
|
return this.detail(item.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,16 +104,14 @@ export class PostService {
|
|||||||
*/
|
*/
|
||||||
async update(data: UpdatePostDto) {
|
async update(data: UpdatePostDto) {
|
||||||
const post = await this.detail(data.id);
|
const post = await this.detail(data.id);
|
||||||
|
|
||||||
if (data.category !== undefined) {
|
if (data.category !== undefined) {
|
||||||
// 更新分类
|
// 更新分类
|
||||||
const category = isNil(data.category)
|
const category = isNil(data.category)
|
||||||
? null
|
? null
|
||||||
: await this.categoryRepository.findOneByOrFail({ id: data.category });
|
: await this.categoryRepository.findOneByOrFail({ id: data.category });
|
||||||
post.category = category;
|
post.category = category;
|
||||||
this.repository.save(post, { reload: true });
|
await this.repository.save(post);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isArray(data.tags)) {
|
if (isArray(data.tags)) {
|
||||||
// 更新文章关联标签
|
// 更新文章关联标签
|
||||||
await this.repository
|
await this.repository
|
||||||
@ -110,11 +120,9 @@ export class PostService {
|
|||||||
.of(post)
|
.of(post)
|
||||||
.addAndRemove(data.tags, post.tags ?? []);
|
.addAndRemove(data.tags, post.tags ?? []);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.repository.update(data.id, omit(data, ['id', 'tags', 'category']));
|
await this.repository.update(data.id, omit(data, ['id', 'tags', 'category']));
|
||||||
const result = await this.detail(data.id);
|
const result = await this.detail(data.id);
|
||||||
if (!isNil(this.searchService)) await this.searchService.update([post]);
|
if (!isNil(this.searchService)) await this.searchService.update([post]);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,25 +134,27 @@ export class PostService {
|
|||||||
const items = await this.repository.find({
|
const items = await this.repository.find({
|
||||||
where: { id: In(ids) },
|
where: { id: In(ids) },
|
||||||
withDeleted: true,
|
withDeleted: true,
|
||||||
|
relations: ['category', 'comments', 'tags'],
|
||||||
});
|
});
|
||||||
|
|
||||||
let result: PostEntity[] = [];
|
let result: PostEntity[] = [];
|
||||||
if (trash) {
|
if (trash) {
|
||||||
// 对已软删除的数据再次删除时直接通过remove方法从数据库中清除
|
// 对已软删除的数据再次删除时直接通过remove方法从数据库中清除
|
||||||
const directs = items.filter((item) => !isNil(item.deletedAt));
|
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));
|
const softs = items.filter((item) => isNil(item.deletedAt));
|
||||||
result = [
|
result = [
|
||||||
...(await this.repository.remove(directs)),
|
...(await this.repository.remove(directs)),
|
||||||
...(await this.repository.softRemove(softs)),
|
...(await this.repository.softRemove(softs)),
|
||||||
];
|
];
|
||||||
if (!isNil(this.searchService)) {
|
if (!isNil(this.searchService)) {
|
||||||
await this.searchService.delete(directs.map(({ id }) => id));
|
await this.searchService.delete(directIds);
|
||||||
await this.searchService.update(softs);
|
await this.searchService.update(softs);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
result = await this.repository.remove(items);
|
result = await this.repository.remove(items);
|
||||||
if (!isNil(this.searchService)) {
|
if (!isNil(this.searchService)) {
|
||||||
await this.searchService.delete(result.map(({ id }) => id));
|
await this.searchService.delete(ids);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@ -163,6 +173,14 @@ export class PostService {
|
|||||||
const trasheds = items.filter((item) => !isNil(item)).map((item) => item.id);
|
const trasheds = items.filter((item) => !isNil(item)).map((item) => item.id);
|
||||||
if (trasheds.length < 1) return [];
|
if (trasheds.length < 1) return [];
|
||||||
await this.repository.restore(trasheds);
|
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) =>
|
const qb = await this.buildListQuery(this.repository.buildBaseQB(), {}, async (qbuilder) =>
|
||||||
qbuilder.andWhereInIds(trasheds),
|
qbuilder.andWhereInIds(trasheds),
|
||||||
);
|
);
|
||||||
@ -181,7 +199,7 @@ export class PostService {
|
|||||||
callback?: QueryHook<PostEntity>,
|
callback?: QueryHook<PostEntity>,
|
||||||
) {
|
) {
|
||||||
const { category, tag, orderBy, isPublished, trashed = SelectTrashMode.NONE } = options;
|
const { category, tag, orderBy, isPublished, trashed = SelectTrashMode.NONE } = options;
|
||||||
|
// 是否查询回收站
|
||||||
if (trashed === SelectTrashMode.ALL || trashed === SelectTrashMode.ONLY) {
|
if (trashed === SelectTrashMode.ALL || trashed === SelectTrashMode.ONLY) {
|
||||||
qb.withDeleted();
|
qb.withDeleted();
|
||||||
if (trashed === SelectTrashMode.ONLY) qb.where(`post.deletedAt is not null`);
|
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}%` })
|
qb.andWhere('title LIKE :search', { search: `%${search}%` })
|
||||||
.orWhere('body LIKE :search', { search: `%${search}%` })
|
.orWhere('body LIKE :search', { search: `%${search}%` })
|
||||||
.orWhere('summary LIKE :search', { search: `%${search}%` })
|
.orWhere('summary LIKE :search', { search: `%${search}%` })
|
||||||
.orWhere('category.name LIKE :search', { search: `%${search}%` })
|
.orWhere('category.name LIKE :search', {
|
||||||
.orWhere('tags.name LIKE :search', { search: `%${search}%` });
|
search: `%${search}%`,
|
||||||
|
})
|
||||||
|
.orWhere('tags.name LIKE :search', {
|
||||||
|
search: `%${search}%`,
|
||||||
|
});
|
||||||
} else if (this.search_type === 'against') {
|
} else if (this.search_type === 'against') {
|
||||||
qb.andWhere('MATCH(title) AGAINST (:search IN BOOLEAN MODE)', {
|
qb.andWhere('MATCH(title) AGAINST (:search IN BOOLEAN MODE)', {
|
||||||
search: `${search}*`,
|
search: `${search}*`,
|
||||||
@ -259,7 +281,7 @@ export class PostService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询出分类及后代分类下的所有文章的Query构建
|
* 查询出分类及其后代分类下的所有文章的Query构建
|
||||||
* @param id
|
* @param id
|
||||||
* @param qb
|
* @param qb
|
||||||
*/
|
*/
|
||||||
|
@ -23,41 +23,48 @@ async function getPostData(
|
|||||||
cmtRepo: CommentRepository,
|
cmtRepo: CommentRepository,
|
||||||
post: PostEntity,
|
post: PostEntity,
|
||||||
) {
|
) {
|
||||||
const categories = [
|
let categories: { id: string; name: string }[] = [];
|
||||||
...(await catRepo.findAncestors(post.category)).map((item) => {
|
if (post.category) {
|
||||||
return {
|
categories = [
|
||||||
|
...(await catRepo.findAncestors(post.category)).map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
name: item.name,
|
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({
|
await cmtRepo.find({
|
||||||
relations: ['post'],
|
relations: ['post'],
|
||||||
where: { post: { id: post.id } },
|
where: { post: { id: post.id } },
|
||||||
})
|
})
|
||||||
).map((item) => ({ id: item.id, body: item.body }));
|
).map((item) => ({ id: item.id, body: item.body }));
|
||||||
|
|
||||||
return [
|
let tags: { id: string; name: string }[] = [];
|
||||||
{
|
if (post.tags) {
|
||||||
...pick(instanceToPlain(post), [
|
tags = post.tags.map((item) => ({ id: item.id, name: item.name }));
|
||||||
'id',
|
}
|
||||||
'title',
|
|
||||||
'body',
|
return {
|
||||||
'summary',
|
...pick(instanceToPlain(post), [
|
||||||
'commentCount',
|
'id',
|
||||||
'deletedAt',
|
'title',
|
||||||
'publishedAt',
|
'body',
|
||||||
'createdAt',
|
'summary',
|
||||||
'updatedAt',
|
'commentCount',
|
||||||
]),
|
'deletedAt',
|
||||||
categories,
|
'publishedAt',
|
||||||
tags: post.tags.map((item) => ({ id: item.id, name: item.name })),
|
'createdAt',
|
||||||
comments,
|
'updatedAt',
|
||||||
},
|
]),
|
||||||
];
|
body: post.body,
|
||||||
|
categories,
|
||||||
|
tags,
|
||||||
|
comments,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -101,7 +108,6 @@ export class SearchService {
|
|||||||
sort: ['updatedAt:desc', 'commentCount:desc'],
|
sort: ['updatedAt:desc', 'commentCount:desc'],
|
||||||
filter,
|
filter,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: result.hits,
|
items: result.hits,
|
||||||
currentPage: result.page,
|
currentPage: result.page,
|
||||||
@ -115,19 +121,18 @@ export class SearchService {
|
|||||||
async create(post: PostEntity) {
|
async create(post: PostEntity) {
|
||||||
return this.client
|
return this.client
|
||||||
.index(this.index)
|
.index(this.index)
|
||||||
.addDocuments(await getPostData(this.categoryRepository, this.commentRepository, post));
|
.addDocuments([
|
||||||
|
await getPostData(this.categoryRepository, this.commentRepository, post),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(posts: PostEntity[]) {
|
async update(posts: PostEntity[]) {
|
||||||
return this.client
|
const payload = await Promise.all(
|
||||||
.index(this.index)
|
posts?.map((post) =>
|
||||||
.updateDocuments(
|
getPostData(this.categoryRepository, this.commentRepository, post),
|
||||||
await Promise.all(
|
),
|
||||||
posts.map((post) =>
|
);
|
||||||
getPostData(this.categoryRepository, this.commentRepository, post),
|
return this.client.index(this.index).updateDocuments(payload);
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(ids: string[]) {
|
async delete(ids: string[]) {
|
||||||
|
@ -1,46 +1,21 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
import { isNil, omit } from 'lodash';
|
import { omit } from 'lodash';
|
||||||
|
|
||||||
import { In, SelectQueryBuilder } from 'typeorm';
|
import { CreateTagDto, UpdateTagDto } from '@/modules/content/dtos';
|
||||||
|
|
||||||
import { CreateTagDto, QueryTagsDto, UpdateTagDto } from '@/modules/content/dtos';
|
|
||||||
import { TagEntity } from '@/modules/content/entities';
|
import { TagEntity } from '@/modules/content/entities';
|
||||||
import { TagRepository } from '@/modules/content/repositories';
|
import { TagRepository } from '@/modules/content/repositories';
|
||||||
import { SelectTrashMode } from '@/modules/database/constants';
|
import { BaseService } from '@/modules/database/base';
|
||||||
import { paginate } from '@/modules/database/helpers';
|
|
||||||
import { QueryHook } from '@/modules/database/types';
|
|
||||||
|
|
||||||
type FindParams = {
|
|
||||||
[key in keyof Omit<QueryTagsDto, 'limit' | 'page'>]: QueryTagsDto[key];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 标签数据操作
|
* 标签数据操作
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TagService {
|
export class TagService extends BaseService<TagEntity, TagRepository> {
|
||||||
constructor(protected repository: TagRepository) {}
|
protected enableTrash = true;
|
||||||
|
|
||||||
/**
|
constructor(protected repository: TagRepository) {
|
||||||
* 获取标签数据
|
super(repository);
|
||||||
* @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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -60,67 +35,4 @@ export class TagService {
|
|||||||
await this.repository.update(data.id, omit(data, ['id']));
|
await this.repository.update(data.id, omit(data, ['id']));
|
||||||
return this.detail(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 { DataSource, EventSubscriber } from 'typeorm';
|
||||||
|
|
||||||
import { PostBodyType } from '@/modules/content/constants';
|
import { PostBodyType } from '@/modules/content/constants';
|
||||||
import { PostEntity } from '@/modules/content/entities';
|
import { PostEntity } from '@/modules/content/entities';
|
||||||
import { PostRepository } from '@/modules/content/repositories';
|
import { PostRepository } from '@/modules/content/repositories';
|
||||||
import { SanitizeService } from '@/modules/content/services/sanitize.service';
|
import { SanitizeService } from '@/modules/content/services/sanitize.service';
|
||||||
|
import { BaseSubscriber } from '@/modules/database/base';
|
||||||
|
|
||||||
@EventSubscriber()
|
@EventSubscriber()
|
||||||
export class PostSubscriber {
|
export class PostSubscriber extends BaseSubscriber<PostEntity> {
|
||||||
|
protected entity = PostEntity;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected dataSource: DataSource,
|
protected dataSource: DataSource,
|
||||||
protected sanitizeService: SanitizeService,
|
|
||||||
protected postRepository: PostRepository,
|
protected postRepository: PostRepository,
|
||||||
) {}
|
@Optional() protected sanitizeService?: SanitizeService,
|
||||||
|
) {
|
||||||
|
super(dataSource);
|
||||||
|
}
|
||||||
|
|
||||||
listenTo() {
|
listenTo() {
|
||||||
return PostEntity;
|
return PostEntity;
|
||||||
@ -19,6 +25,7 @@ export class PostSubscriber {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载文章数据的处理
|
* 加载文章数据的处理
|
||||||
|
* @param entity
|
||||||
*/
|
*/
|
||||||
async afterLoad(entity: PostEntity) {
|
async afterLoad(entity: PostEntity) {
|
||||||
if (entity.type === PostBodyType.HTML) {
|
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',
|
ONLY = 'only',
|
||||||
NONE = 'none',
|
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 { 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,
|
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;
|
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