diff --git a/src/modules/database/base/service.ts b/src/modules/database/base/service.ts new file mode 100644 index 0000000..09328a5 --- /dev/null +++ b/src/modules/database/base/service.ts @@ -0,0 +1,130 @@ +import { NotFoundException } from '@nestjs/common'; +import { isNil } from 'lodash'; +import { In, ObjectLiteral, SelectQueryBuilder } from 'typeorm'; + +import { BaseRepository } from '@/modules/database/base/repository'; +import { BaseTreeRepository } from '@/modules/database/base/tree.repository'; +import { SelectTrashMode, TreeChildrenResolve } from '@/modules/database/constants'; +import { + PaginateOptions, + PaginateReturn, + QueryHook, + ServiceListQueryOption, +} from '@/modules/database/types'; +import { paginate, treePaginate } from '@/modules/database/utils'; + +export abstract class BaseService< + T extends ObjectLiteral, + R extends BaseRepository | BaseTreeRepository, + P extends ServiceListQueryOption = ServiceListQueryOption, +> { + protected repository: R; + + protected enableTrash = false; + + protected constructor(repository: R) { + this.repository = repository; + if ( + !( + this.repository instanceof BaseRepository || + this.repository instanceof BaseTreeRepository + ) + ) { + throw new Error('Repository init error.'); + } + } + + protected async buildItemQB(id: string, qb: SelectQueryBuilder, callback?: QueryHook) { + qb.where(`${this.repository.qbName}.id = :id`, { id }); + if (callback) { + return callback(qb); + } + return qb; + } + + protected async buildListQB(qb: SelectQueryBuilder, options?: P, callback?: QueryHook) { + const { trashed } = options ?? {}; + const queryName = this.repository.qbName; + if (this.enableTrash && trashed in [SelectTrashMode.ALL, SelectTrashMode.ONLY]) { + qb.withDeleted(); + if (trashed === SelectTrashMode.ONLY) { + qb.where(`${queryName}.deletedAt IS NOT NULL`); + } + } + if (callback) { + return callback(qb); + } + return qb; + } + + async list(options?: P, callback?: QueryHook) { + const { trashed: isTrashed = false } = options ?? {}; + const trashed = isTrashed || SelectTrashMode.NONE; + if (this.repository instanceof BaseTreeRepository) { + const withTrashed = + this.enableTrash && trashed in [SelectTrashMode.ALL, 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(); + } + + async paginate(options?: PaginateOptions & P, callback?: QueryHook) { + const queryOptions = (options ?? {}) as P; + if (this.repository instanceof BaseTreeRepository) { + const data = await this.list(queryOptions, callback); + return treePaginate(options, data) as PaginateReturn; + } + const qb = await this.buildListQB(this.repository.buildBaseQB(), queryOptions, callback); + return paginate(qb, options); + } + + async detail(id: string, callback?: QueryHook) { + const qb = await this.buildItemQB(id, this.repository.buildBaseQB(), callback); + const item = qb.getOne(); + if (!item) { + throw new NotFoundException(`${this.repository.qbName} ${id} NOT FOUND`); + } + return item; + } + + async delete(ids: string[], trash?: boolean) { + let items: T[]; + 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 children = [...item.children].map((o) => { + o.parent = item.parent; + return item; + }); + await this.repository.save(children); + } + } + } 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); + } +} diff --git a/src/modules/database/types.ts b/src/modules/database/types.ts index 7500941..36f6aef 100644 --- a/src/modules/database/types.ts +++ b/src/modules/database/types.ts @@ -1,6 +1,6 @@ -import { ObjectLiteral, SelectQueryBuilder } from 'typeorm'; +import { FindTreeOptions, ObjectLiteral, SelectQueryBuilder } from 'typeorm'; -import { OrderType } from '@/modules/database/constants'; +import { OrderType, SelectTrashMode } from '@/modules/database/constants'; export type QueryHook = ( qb: SelectQueryBuilder, @@ -38,3 +38,17 @@ export interface QueryParams { onlyTrashed?: boolean; } + +export type ServiceListQueryOptionWithTrashed = Omit< + FindTreeOptions & QueryParams, + 'withTrashed' +> & { trashed?: `${SelectTrashMode}` } & RecordAny; + +export type ServiceListQueryOptionNotWithTrashed = Omit< + ServiceListQueryOptionWithTrashed, + 'trashed' +>; + +export type ServiceListQueryOption = + | ServiceListQueryOptionNotWithTrashed + | ServiceListQueryOptionWithTrashed;