feat:数据操作代码抽象化

- 代码更简洁了
- 借鉴了classroom/nestjs#5 更新了Meilisearch软删除问题
This commit is contained in:
3R-喜东东 2023-12-18 17:09:05 +08:00
parent b37dfa8103
commit 4ec73cc0e7
21 changed files with 939 additions and 564 deletions

View File

@ -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,
);

View File

@ -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;
}

View File

@ -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>[];

View File

@ -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;
}

View File

@ -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[];
});
}
}

View File

@ -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')

View File

@ -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')

View File

@ -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();
}
}

View File

@ -2,22 +2,24 @@ import { ForbiddenException, Injectable } from '@nestjs/common';
import { isNil } from 'lodash';
import { EntityNotFoundError, In, SelectQueryBuilder } from 'typeorm';
import { EntityNotFoundError, SelectQueryBuilder } from 'typeorm';
import { CreateCommentDto, QueryCommentDto, QueryCommentTreeDto } from '@/modules/content/dtos';
import { CommentEntity } from '@/modules/content/entities';
import { CommentRepository, PostRepository } from '@/modules/content/repositories';
import { treePaginate } from '@/modules/database/helpers';
import { BaseService } from '@/modules/database/base';
/**
*
*/
@Injectable()
export class CommentService {
export class CommentService extends BaseService<CommentEntity, CommentRepository> {
constructor(
protected repository: CommentRepository,
protected postRepository: PostRepository,
) {}
) {
super(repository);
}
/**
*
@ -25,7 +27,7 @@ export class CommentService {
*/
async findTrees(options: QueryCommentTreeDto = {}) {
return this.repository.findTrees({
addQuery: (qb) => {
addQuery: async (qb) => {
return isNil(options.post) ? qb : qb.where('post.id = :id', { id: options.post });
},
});
@ -35,28 +37,17 @@ export class CommentService {
*
* @param dto
*/
async paginate(dto: QueryCommentDto) {
const { post, ...query } = dto;
async paginate(options: QueryCommentDto) {
const { post } = options;
const addQuery = (qb: SelectQueryBuilder<CommentEntity>) => {
const condition: Record<string, string> = {};
if (!isNil(post)) condition.post = post;
return Object.keys(condition).length > 0 ? qb.andWhere(condition) : qb;
};
const data = await this.repository.findRoots({ addQuery });
let comments: CommentEntity[] = [];
for (let i = 0; i < data.length; i++) {
const c = data[i];
comments.push(
await this.repository.findDescendantsTree(c, {
addQuery,
}),
);
}
comments = await this.repository.toFlatTrees(comments);
return treePaginate(query, comments);
return super.paginate({
...options,
addQuery,
});
}
/**
@ -66,32 +57,20 @@ export class CommentService {
*/
async create(data: CreateCommentDto) {
const parent = await this.getParent(undefined, data.parent);
if (!isNil(parent) && parent.post.id !== data.post) {
throw new ForbiddenException('Parent comment and child comment must belong same post!');
}
const item = await this.repository.save({
...data,
parent,
post: await this.getPost(data.post),
});
return this.repository.findOneOrFail({ where: { id: item.id } });
}
/**
*
* @param ids
*/
async delete(ids: string[]) {
const comments = await this.repository.find({ where: { id: In(ids) } });
return this.repository.remove(comments);
}
/**
*
* @parem id
* @param id
*/
protected async getPost(id: string) {
return !isNil(id) ? this.postRepository.findOneOrFail({ where: { id } }) : id;
@ -100,24 +79,21 @@ export class CommentService {
/**
*
* @param current ID
* @param parentId
* @param id
*/
protected async getParent(current?: string, parentId?: string) {
if (current === parentId) return undefined;
protected async getParent(current?: string, id?: string) {
if (current === id) return undefined;
let parent: CommentEntity | undefined;
if (parentId !== undefined) {
if (parentId === null) return null;
if (id !== undefined) {
if (id === null) return null;
parent = await this.repository.findOne({
relations: ['parent', 'post'],
where: { id: parentId },
where: { id },
});
if (!parent)
throw new EntityNotFoundError(
CommentEntity,
`Parent comment ${parentId} not exists !`,
);
if (!parent) {
throw new EntityNotFoundError(CommentEntity, `Parent comment ${id} not exists!`);
}
}
return parent;
}
}

View File

@ -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
*/

View File

@ -23,41 +23,48 @@ async function getPostData(
cmtRepo: CommentRepository,
post: PostEntity,
) {
const categories = [
...(await catRepo.findAncestors(post.category)).map((item) => {
return {
let categories: { id: string; name: string }[] = [];
if (post.category) {
categories = [
...(await catRepo.findAncestors(post.category)).map((item) => ({
id: item.id,
name: item.name,
};
}),
{ id: post.category.id, name: post.category.name },
];
})),
{ id: post.category.id, name: post.category.name },
];
}
const comments = (
let comments: { id: string; body: string }[] = [];
comments = (
await cmtRepo.find({
relations: ['post'],
where: { post: { id: post.id } },
})
).map((item) => ({ id: item.id, body: item.body }));
return [
{
...pick(instanceToPlain(post), [
'id',
'title',
'body',
'summary',
'commentCount',
'deletedAt',
'publishedAt',
'createdAt',
'updatedAt',
]),
categories,
tags: post.tags.map((item) => ({ id: item.id, name: item.name })),
comments,
},
];
let tags: { id: string; name: string }[] = [];
if (post.tags) {
tags = post.tags.map((item) => ({ id: item.id, name: item.name }));
}
return {
...pick(instanceToPlain(post), [
'id',
'title',
'body',
'summary',
'commentCount',
'deletedAt',
'publishedAt',
'createdAt',
'updatedAt',
]),
body: post.body,
categories,
tags,
comments,
};
}
@Injectable()
@ -101,7 +108,6 @@ export class SearchService {
sort: ['updatedAt:desc', 'commentCount:desc'],
filter,
});
return {
items: result.hits,
currentPage: result.page,
@ -115,19 +121,18 @@ export class SearchService {
async create(post: PostEntity) {
return this.client
.index(this.index)
.addDocuments(await getPostData(this.categoryRepository, this.commentRepository, post));
.addDocuments([
await getPostData(this.categoryRepository, this.commentRepository, post),
]);
}
async update(posts: PostEntity[]) {
return this.client
.index(this.index)
.updateDocuments(
await Promise.all(
posts.map((post) =>
getPostData(this.categoryRepository, this.commentRepository, post),
),
),
);
const payload = await Promise.all(
posts?.map((post) =>
getPostData(this.categoryRepository, this.commentRepository, post),
),
);
return this.client.index(this.index).updateDocuments(payload);
}
async delete(ids: string[]) {

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -0,0 +1,4 @@
export * from './repository';
export * from './service';
export * from './subcriber';
export * from './tree.repository';

View 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;
}
}

View 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;
}
}

View 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);
}
}

View 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[];
}
}

View File

@ -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',
}

View File

@ -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;
};

View File

@ -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>;