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, {
|
||||
return super.paginate({
|
||||
...options,
|
||||
addQuery,
|
||||
}),
|
||||
);
|
||||
}
|
||||
comments = await this.repository.toFlatTrees(comments);
|
||||
return treePaginate(query, comments);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -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,25 +23,32 @@ 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 },
|
||||
];
|
||||
}
|
||||
|
||||
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 [
|
||||
{
|
||||
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',
|
||||
@ -53,11 +60,11 @@ async function getPostData(
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
]),
|
||||
body: post.body,
|
||||
categories,
|
||||
tags: post.tags.map((item) => ({ id: item.id, name: item.name })),
|
||||
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) =>
|
||||
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