From c9ba07db768da2c294acce697c43b5fe985f7af3 Mon Sep 17 00:00:00 2001 From: wei Date: Fri, 26 Jul 2024 07:08:47 +0000 Subject: [PATCH] baserepositry --- .../content/repositories/post.repository.ts | 21 ++- .../content/repositories/tag.repository.ts | 17 +- apps/api/src/modules/database/base/index.ts | 0 .../src/modules/database/base/repository.ts | 23 +++ apps/api/src/modules/database/base/service.ts | 155 ++++++++++++++++++ .../src/modules/database/base/subcriber.ts | 0 .../modules/database/base/tree.repository.ts | 101 ++++++++++++ apps/api/src/modules/database/constants.ts | 15 ++ apps/api/src/modules/database/helpers.ts | 28 +++- apps/api/src/modules/database/types.ts | 49 +++++- apps/api/src/test.ts | 17 ++ 11 files changed, 408 insertions(+), 18 deletions(-) create mode 100644 apps/api/src/modules/database/base/index.ts create mode 100644 apps/api/src/modules/database/base/repository.ts create mode 100644 apps/api/src/modules/database/base/service.ts create mode 100644 apps/api/src/modules/database/base/subcriber.ts create mode 100644 apps/api/src/modules/database/base/tree.repository.ts create mode 100644 apps/api/src/test.ts diff --git a/apps/api/src/modules/content/repositories/post.repository.ts b/apps/api/src/modules/content/repositories/post.repository.ts index 8dc4c98..3809152 100644 --- a/apps/api/src/modules/content/repositories/post.repository.ts +++ b/apps/api/src/modules/content/repositories/post.repository.ts @@ -1,22 +1,25 @@ -import { Repository } from 'typeorm'; - +import { BaseRepository } from '@/modules/database/base/repository'; import { CustomRepository } from '@/modules/database/decorators/repository.decorator'; import { CommentEntity } from '../entities'; import { PostEntity } from '../entities/post.entity'; @CustomRepository(PostEntity) -export class PostRepository extends Repository { - buildBaseQB() { - return this.createQueryBuilder('post') - .leftJoinAndSelect('post.category', 'category') - .leftJoinAndSelect('post.tags', 'tags') +export class PostRepository extends BaseRepository { + protected _qbName = 'post'; + + protected orderBy = 'createdAt'; + + postBuildBaseQB() { + return this.buildBaseQB() + .leftJoinAndSelect(`${this.qbName}.category`, 'category') + .leftJoinAndSelect(`${this.qbName}.tags`, 'tags') .addSelect((subQuery) => { return subQuery .select('COUNT(c.id)', 'count') .from(CommentEntity, 'c') - .where('c.post.id = post.id'); + .where(`c.post.id = ${this.qbName}.tags`); }, 'commentCount') - .loadRelationCountAndMap('post.commentCount', 'post.comments'); + .loadRelationCountAndMap(`${this.qbName}.commentCount`, `${this.qbName}.commentCount`); } } diff --git a/apps/api/src/modules/content/repositories/tag.repository.ts b/apps/api/src/modules/content/repositories/tag.repository.ts index ce7fca4..4d6e5a5 100644 --- a/apps/api/src/modules/content/repositories/tag.repository.ts +++ b/apps/api/src/modules/content/repositories/tag.repository.ts @@ -1,19 +1,22 @@ -import { Repository } from 'typeorm'; - +import { BaseRepository } from '@/modules/database/base/repository'; import { CustomRepository } from '@/modules/database/decorators/repository.decorator'; import { TagEntity, PostEntity } from '../entities'; @CustomRepository(TagEntity) -export class TagRepository extends Repository { - buildBaseQB() { - return this.createQueryBuilder('tag') - .leftJoinAndSelect('tag.posts', 'posts') +export class TagRepository extends BaseRepository { + protected _qbName = 'tag'; + + protected orderBy = 'name'; + + tagbuildBaseQB() { + return this.buildBaseQB() + .leftJoinAndSelect(`${this.qbName}.posts`, 'posts') .addSelect( (subQuery) => subQuery.select('COUNT(p.id)', 'count').from(PostEntity, 'p'), 'postCount', ) .orderBy('postCount', 'DESC') - .loadRelationCountAndMap('tag.postCount', 'tag.posts'); + .loadRelationCountAndMap(`${this.qbName}.postCount`, `${this.qbName}.posts`); } } diff --git a/apps/api/src/modules/database/base/index.ts b/apps/api/src/modules/database/base/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/apps/api/src/modules/database/base/repository.ts b/apps/api/src/modules/database/base/repository.ts new file mode 100644 index 0000000..01b26fc --- /dev/null +++ b/apps/api/src/modules/database/base/repository.ts @@ -0,0 +1,23 @@ +import { ObjectLiteral, Repository, SelectQueryBuilder } from 'typeorm'; + +import { getOrderBy } from '../helpers'; +import { OrderQueryType } from '../types'; + +export abstract class BaseRepository extends Repository { + protected abstract _qbName: string; + + protected abstract orderBy?: OrderQueryType; + + get qbName(): string { + return this._qbName; + } + + buildBaseQB() { + return this.createQueryBuilder(this.qbName); + } + + addOrderBy(qb: SelectQueryBuilder, orderby?: OrderQueryType) { + const orderBy = orderby ?? this.orderBy; + return getOrderBy(qb, this.qbName, orderBy); + } +} diff --git a/apps/api/src/modules/database/base/service.ts b/apps/api/src/modules/database/base/service.ts new file mode 100644 index 0000000..9f5b9fd --- /dev/null +++ b/apps/api/src/modules/database/base/service.ts @@ -0,0 +1,155 @@ +import { ForbiddenException, NotFoundException } from '@nestjs/common'; +import { isNil } from 'lodash'; +import { In, ObjectLiteral, SelectQueryBuilder } from 'typeorm'; + +import { SelectTrashMode } from '@/modules/content/constants'; + +import { TreeChildrenResolve } from '../constants'; +import { paginate, treePaginate } from '../helpers'; +import { PaginateOptions, PaginateReturn, QueryHook, ServiceListQueryOption } from '../types'; + +import { BaseRepository } from './repository'; +import { BaseTreeRepository } from './tree.repository'; + +export abstract class BaseService< + E extends ObjectLiteral, + R extends BaseRepository | BaseTreeRepository, + P extends ServiceListQueryOption = ServiceListQueryOption, +> { + protected repository: R; + + protected enableTrash = false; + + constructor(repository: R) { + this.repository = repository; + if ( + !(this.repository instanceof BaseTreeRepository) && + !(this.repository instanceof BaseRepository) + ) { + throw new Error( + 'Repository must instance of BaseRepository or BaseTreeRepository in DataService!', + ); + } + } + + protected async buildItemQB(id: string, qb: SelectQueryBuilder, callback?: QueryHook) { + qb.where(`${this.repository.qbName}.id = :id`, { id }); + if (callback) return callback(qb); + // 短路运算符,不会阻塞后续代码执行 如果callback执行 + // callback && callback(qb); + return qb; + } + + protected async buildListQB(qb: SelectQueryBuilder, options: P, callback?: QueryHook) { + const { trashed } = options ?? {}; + if ( + this.enableTrash && + (trashed === SelectTrashMode.ALL || trashed === SelectTrashMode.ONLY) + ) { + qb.withDeleted(); + if (trashed === SelectTrashMode.ONLY) { + qb.where(`${this.repository.qbName}.deletedAt IS NOT NULL`); + } + } + return callback ? callback(qb) : qb; + } + + async list(options?: P, callback?: QueryHook): Promise { + if (this.repository instanceof BaseRepository) { + const qb = await this.buildListQB(this.repository.buildBaseQB(), options, callback); + return qb.getMany(); + } + const { trashed = SelectTrashMode.NONE } = options ?? {}; + const tree = await this.repository.findTrees({ + ...options, + withTrashed: + this.enableTrash && + (trashed === SelectTrashMode.ALL || trashed === SelectTrashMode.ONLY), + onlyTrashed: this.enableTrash && trashed === SelectTrashMode.ONLY, + }); + return this.repository.toFlatTrees(tree); + } + + async basePaginate( + options?: PaginateOptions & P, + callback?: QueryHook, + ): Promise> { + const queryOptions = options ?? {}; + if (this.repository instanceof BaseRepository) { + const qb = await this.buildListQB(this.repository.buildBaseQB(), options, callback); + return paginate(qb, queryOptions); + } + const data = await this.list(options, callback); + return treePaginate(queryOptions, data); + } + + async detail(id: string, callback?: QueryHook): Promise { + const qb = await this.buildItemQB(id, this.repository.buildBaseQB(), callback); + + const item = qb.getOne(); + if (!item) { + throw new NotFoundException(`${this.repository.qbName} ${id} not exists!`); + } + return item; + } + + // 首先判断是否是树形结构,如果是树形结构,那么就会调用findDescendantsTree方法,否则直接调用find方法 + 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); + } + + async restore(ids: string[]) { + const qb = this.repository.buildBaseQB(); + if (!this.enableTrash) { + throw new ForbiddenException( + `Can't to restore ${this.repository.qbName} because Trash is not enabled!`, + ); + } + const items = await qb.withDeleted().whereInIds(ids).getMany(); + const trashedIds = items.map((item) => item.id); + if (trashedIds.length > 0) { + this.repository.restore(trashedIds); + return qb.whereInIds(trashedIds).getMany(); + } + return []; + } + + create(data: any, ...others: any[]): Promise { + throw new ForbiddenException(`Can not to create ${this.repository.qbName}!`); + } + + update(data: any, ...others: any[]): Promise { + throw new ForbiddenException(`Can not to update ${this.repository.qbName}!`); + } +} diff --git a/apps/api/src/modules/database/base/subcriber.ts b/apps/api/src/modules/database/base/subcriber.ts new file mode 100644 index 0000000..e69de29 diff --git a/apps/api/src/modules/database/base/tree.repository.ts b/apps/api/src/modules/database/base/tree.repository.ts new file mode 100644 index 0000000..1f94151 --- /dev/null +++ b/apps/api/src/modules/database/base/tree.repository.ts @@ -0,0 +1,101 @@ +import { pick, unset } from 'lodash'; +import { + EntityManager, + EntityTarget, + FindOptionsUtils, + FindTreeOptions, + ObjectLiteral, + QueryRunner, + SelectQueryBuilder, + TreeRepository, + TreeRepositoryUtils, +} from 'typeorm'; + +import { TreeChildrenResolve } from '../constants'; +import { getOrderBy } from '../helpers'; +import { OrderQueryType, QueryParams } from '../types'; + +export abstract class BaseTreeRepository extends TreeRepository { + protected _qbName = 'treeEntity'; + + protected _childrenResolve?: TreeChildrenResolve; + + protected orderBy?: OrderQueryType; + + // eslint-disable-next-line @typescript-eslint/no-useless-constructor + constructor(target: EntityTarget, manager: EntityManager, queryRunner?: QueryRunner) { + super(target, manager, queryRunner); + } + + get qbName(): string { + return this._qbName; + } + + get childrenResolve() { + return this._childrenResolve; + } + + buildBaseQB(qb?: SelectQueryBuilder): SelectQueryBuilder { + const queryBuilder = qb || this.createQueryBuilder(this.qbName); + return queryBuilder.leftJoinAndSelect(`${this.qbName}.parent`, 'parent'); + } + + addOrderByQuery(qb: SelectQueryBuilder, orderBy: OrderQueryType): SelectQueryBuilder { + const ob = orderBy ?? this.orderBy; + return getOrderBy(qb, this.qbName, ob); + } + + async findDescendantsTree(entity: E, options?: FindTreeOptions & QueryParams) { + const { addQuery, onlyTrashed, orderBy, withTrashed } = options ?? {}; + const qb = this.buildBaseQB( + this.createDescendantsQueryBuilder(this.qbName, 'treeClosure', entity), + ); + addQuery + ? await addQuery(this.addOrderByQuery(qb, orderBy)) + : this.addOrderByQuery(qb, orderBy); + withTrashed + ? qb.withDeleted() + : onlyTrashed && qb.where(`${this.qbName}.deletedAt IS NOT NULL`); + FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, pick(options, ['relations', 'depth'])); + const entites = await qb.getRawAndEntities(); + const relationMaps = TreeRepositoryUtils.createRelationMaps( + this.manager, + this.metadata, + this.qbName, + entites.raw, + ); + TreeRepositoryUtils.buildChildrenEntityTree( + this.metadata, + entity, + entites.entities, + relationMaps, + { + depth: -1, + ...pick(options, ['relations']), + }, + ); + + return entity; + } + + async findTrees(options?: FindTreeOptions & QueryParams) { + const roots = await this.findRoots(options); + // foreach 不会等待异步操作,所以这里使用 Promise.all 来等待所有异步操作完成 + // roots.forEach(async (root) => this.findDescendantsTree(root, options)); + Promise.all(roots.map((root) => this.findDescendantsTree(root, options))); + return roots; + } + + async toFlatTrees(trees: E[], depth = 0, parent: E | null = null): Promise { + const data: Omit[] = []; + 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[]; + } +} diff --git a/apps/api/src/modules/database/constants.ts b/apps/api/src/modules/database/constants.ts index b3495e8..5d5c3ce 100644 --- a/apps/api/src/modules/database/constants.ts +++ b/apps/api/src/modules/database/constants.ts @@ -1 +1,16 @@ export const CUSTOM_REPOSITORY_METADATA = 'CUSTOM_REPOSITORY_METADATA'; +/** + * 排序方式 + */ +export enum OrderType { + ASC = 'ASC', + DESC = 'DESC', +} +/** + * 树形模型在删除父级后子级的处理方式 + */ +export enum TreeChildrenResolve { + DELETE = 'delete', + UP = 'up', + ROOT = 'root', +} diff --git a/apps/api/src/modules/database/helpers.ts b/apps/api/src/modules/database/helpers.ts index dd311f8..3650ddf 100644 --- a/apps/api/src/modules/database/helpers.ts +++ b/apps/api/src/modules/database/helpers.ts @@ -1,6 +1,6 @@ import { ObjectLiteral, SelectQueryBuilder } from 'typeorm'; -import { PaginateOptions, PaginateReturn } from './types'; +import { OrderQueryType, PaginateOptions, PaginateReturn } from './types'; /** * 分页函数 @@ -62,3 +62,29 @@ export function treePaginate( }, }; } + +export function getOrderBy( + qb: SelectQueryBuilder, + alians: string, + order?: OrderQueryType, +) { + if (!order) { + return qb; + } + if (typeof order === 'string') { + return qb.orderBy(`${alians}.${order}`, 'DESC'); + } + if (Array.isArray(order)) { + for (const item of order) { + if (typeof item === 'string') { + return qb.orderBy(`${alians}.${item}`, 'DESC'); + } + if (item.name && item.order) { + return qb.orderBy(`${alians}.${item.name}`, item.order); + } + } + } else { + return qb.orderBy(`${alians}.${order.name}`, order.order); + } + return qb; +} diff --git a/apps/api/src/modules/database/types.ts b/apps/api/src/modules/database/types.ts index 30b68fe..b248de0 100644 --- a/apps/api/src/modules/database/types.ts +++ b/apps/api/src/modules/database/types.ts @@ -1,4 +1,8 @@ -import { ObjectLiteral, SelectQueryBuilder } from 'typeorm'; +import { FindTreeOptions, ObjectLiteral, SelectQueryBuilder } from 'typeorm'; + +import { SelectTrashMode } from '../content/constants'; + +import { OrderType } from './constants'; export type QueryHook = (qb: SelectQueryBuilder) => Promise>; /** @@ -48,3 +52,46 @@ export interface PaginateReturn { meta: PaginateMeta; items: E[]; } +/** + * 排序类型,{字段名称: 排序方法} + * 如果多个值则传入数组即可 + * 排序方法不设置,默认DESC + */ +export type OrderQueryType = + | string + | { name: string; order: `${OrderType}` } + | Array<{ name: string; order: `${OrderType}` } | string>; + +/** + * 数据列表查询类型 + */ +export interface QueryParams { + addQuery?: QueryHook; + orderBy?: OrderQueryType; + withTrashed?: boolean; + onlyTrashed?: boolean; +} +/** + * 服务类数据列表查询类型 + */ +export type ServiceListQueryOption = + | ServiceListQueryOptionWithTrashed + | ServiceListQueryOptionNotWithTrashed; + +/** + * 带有软删除的服务类数据列表查询类型 + */ +type ServiceListQueryOptionWithTrashed = Omit< + FindTreeOptions & QueryParams, + 'withTrashed' +> & { + trashed?: `${SelectTrashMode}`; +} & Record; + +/** + * 不带软删除的服务类数据列表查询类型 + */ +type ServiceListQueryOptionNotWithTrashed = Omit< + ServiceListQueryOptionWithTrashed, + 'trashed' +>; diff --git a/apps/api/src/test.ts b/apps/api/src/test.ts new file mode 100644 index 0000000..36cc235 --- /dev/null +++ b/apps/api/src/test.ts @@ -0,0 +1,17 @@ +async function test1(obj: any) { + return new Promise((resolve) => { + setTimeout( + (o) => { + o.name = '1111'; + return resolve(o); + }, + 1000, + obj, + ); + }); +} +const aa = { name: '2222' }; + +test1(aa).then((res) => { + console.log(res, '2'); +});