baserepositry

master
wei 2024-07-26 07:08:47 +00:00
parent 7d6b4aaf79
commit c9ba07db76
11 changed files with 408 additions and 18 deletions

View File

@ -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<PostEntity> {
buildBaseQB() {
return this.createQueryBuilder('post')
.leftJoinAndSelect('post.category', 'category')
.leftJoinAndSelect('post.tags', 'tags')
export class PostRepository extends BaseRepository<PostEntity> {
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`);
}
}

View File

@ -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<TagEntity> {
buildBaseQB() {
return this.createQueryBuilder('tag')
.leftJoinAndSelect('tag.posts', 'posts')
export class TagRepository extends BaseRepository<TagEntity> {
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`);
}
}

View File

@ -0,0 +1,23 @@
import { ObjectLiteral, Repository, SelectQueryBuilder } from 'typeorm';
import { getOrderBy } from '../helpers';
import { OrderQueryType } from '../types';
export abstract class BaseRepository<E extends ObjectLiteral> extends Repository<E> {
protected abstract _qbName: string;
protected abstract orderBy?: OrderQueryType;
get qbName(): string {
return this._qbName;
}
buildBaseQB() {
return this.createQueryBuilder(this.qbName);
}
addOrderBy(qb: SelectQueryBuilder<E>, orderby?: OrderQueryType) {
const orderBy = orderby ?? this.orderBy;
return getOrderBy(qb, this.qbName, orderBy);
}
}

View File

@ -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<E> | BaseTreeRepository<E>,
P extends ServiceListQueryOption<E> = ServiceListQueryOption<E>,
> {
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<E>, callback?: QueryHook<E>) {
qb.where(`${this.repository.qbName}.id = :id`, { id });
if (callback) return callback(qb);
// 短路运算符,不会阻塞后续代码执行 如果callback执行
// callback && callback(qb);
return qb;
}
protected async buildListQB(qb: SelectQueryBuilder<E>, options: P, callback?: QueryHook<E>) {
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<E>): Promise<E[]> {
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<E>,
): Promise<PaginateReturn<E>> {
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<E>): Promise<E> {
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<E> {
throw new ForbiddenException(`Can not to create ${this.repository.qbName}!`);
}
update(data: any, ...others: any[]): Promise<E> {
throw new ForbiddenException(`Can not to update ${this.repository.qbName}!`);
}
}

View File

@ -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<E extends ObjectLiteral> extends TreeRepository<E> {
protected _qbName = 'treeEntity';
protected _childrenResolve?: TreeChildrenResolve;
protected orderBy?: OrderQueryType;
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
constructor(target: EntityTarget<E>, manager: EntityManager, queryRunner?: QueryRunner) {
super(target, manager, queryRunner);
}
get qbName(): string {
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');
}
addOrderByQuery(qb: SelectQueryBuilder<E>, orderBy: OrderQueryType): SelectQueryBuilder<E> {
const ob = orderBy ?? this.orderBy;
return getOrderBy(qb, this.qbName, ob);
}
async findDescendantsTree(entity: E, options?: FindTreeOptions & QueryParams<E>) {
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<E>) {
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<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

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

View File

@ -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<E extends ObjectLiteral>(
},
};
}
export function getOrderBy<E extends ObjectLiteral>(
qb: SelectQueryBuilder<E>,
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;
}

View File

@ -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<E> = (qb: SelectQueryBuilder<E>) => Promise<SelectQueryBuilder<E>>;
/**
@ -48,3 +52,46 @@ export interface PaginateReturn<E extends ObjectLiteral> {
meta: PaginateMeta;
items: E[];
}
/**
* ,{: }
*
* ,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'
>;

View File

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