- 教程地址: pincman.com/docs/courses/nestjs-pr...
- 影片地址: www.bilibili.com/video/BV1pG4y167c...
- qq: yjosscom(加微信後拉群交流)
注意: 此處文件只起配合作用,為了您的身心愉悅,請看B站影片教程,不要直接略過影片直接看這個文件,這樣你將什麼都看不到?
學習目標
- 實現資料的批次操作
- 實現軟刪除和資料恢復
- 使用軟刪除實現回收站功能
核心模組
常量
新增一個列舉常量用於定義軟刪除的查詢型別,以便實現回收站功能
// src/modules/core/constants.ts
export enum QueryTrashMode {
ALL = 'all', // 包含已軟刪除和未軟刪除的資料
ONLY = 'only', // 只包含軟刪除的資料
NONE = 'none', // 只包含未軟刪除的資料
}
型別
更改原來的查詢和觀察者設定型別以支援軟刪除
// src/modules/core/types.ts
/**
* 軟刪除DTO介面
*/
export interface TrashedDto {
trashed?: QueryTrashMode;
}
export interface QueryParams<E extends ObjectLiteral> {
addQuery?: (query: SelectQueryBuilder<E>) => SelectQueryBuilder<E>;
orderBy?: OrderQueryType;
withTrashed?: boolean;
}
export type TreeQueryParams<E extends ObjectLiteral> = FindTreeOptions & QueryParams<E>;
export type QueryListParams<E extends ObjectLiteral> = Omit<TreeQueryParams<E>, 'withTrashed'> & {
trashed?: `${QueryTrashMode}`;
};
export type SubcriberSetting = {
// 監聽的模型是否為樹模型
tree?: boolean;
// 是否支援軟刪除
trash?: boolean;
};
儲存類
更改樹形儲存基類BaseTreeRepository
中的一些查詢方法以支援軟刪除
// src/modules/core/crud/tree.repository.ts
export class BaseTreeRepository<E extends ObjectLiteral> extends TreeRepository<E> {
async findTrees(params: TreeQueryParams<E> = {}): Promise<E[]> {
params.withTrashed = params.withTrashed ?? false;
// ...
}
findRoots(params: TreeQueryParams<E> = {}): Promise<E[]> {
// ...
if (withTrashed) qb.withDeleted();
return qb.getMany();
}
createDtsQueryBuilder(
closureTableAlias: string,
entity: E,
params: TreeQueryParams<E> = {},
): SelectQueryBuilder<E> {
// ...
return withTrashed ? qb.withDeleted() : qb;
}
createAtsQueryBuilder(
closureTableAlias: string,
entity: E,
params: TreeQueryParams<E> = {},
): SelectQueryBuilder<E> {
// ...
return withTrashed ? qb.withDeleted() : qb;
}
}
訂閱者
修改BaseSubscriber
的afterLoad
方法,為每個支援軟刪除的模型的trashed
只是值為前端作為判斷是否處於回收站狀態的依據
// src/modules/core/crud/subscriber.ts
@EventSubscriber()
export abstract class BaseSubscriber<E extends ObjectLiteral>
implements EntitySubscriberInterface<E>
{
// ...
async afterLoad(entity: any) {
// 是否啟用樹形
if (this.setting.tree && isNil(entity.level)) entity.level = 0;
// 是否啟用軟刪除
if (this.setting.trash) entity.trashed = !!entity.deletedAt;
}
}
DTO
新增幾個公共的DTO用於支援控制器的批次刪除,批次恢復,單個刪除和單個查詢實現軟刪除功能
// // src/modules/core/crud/dtos
@Injectable()
export class QueryDetailDto {
@Transform(({ value }) => tBoolean(value))
@IsBoolean()
@IsOptional()
trashed?: boolean; // 在查詢單個資料時,是否包含軟刪除後的資料
}
@DtoValidation()
export class DeleteDto {
@Transform(({ value }) => tBoolean(value))
@IsBoolean()
@IsOptional()
trash?: boolean; // 在刪除資料時是否軟刪除
}
export class DeleteMultiDto extends DeleteDto {
@IsUUID(undefined, {
each: true,
message: 'ID格式錯誤',
})
@IsDefined({
each: true,
message: 'ID必須指定',
})
items: string[] = []; // 批次刪除資料的ID列表
}
export class DeleteRestoreDto {
@IsUUID(undefined, {
each: true,
message: 'ID格式錯誤',
})
@IsDefined({
each: true,
message: 'ID必須指定',
})
items: string[] = []; // 批次恢復資料的ID列表
}
內容模組
模型
為需要支援軟刪除的模型(CategoryEntity
和PostEntity
)新增上deletedAt
和trashed
欄位以支援軟刪除
以CategoryEntity
為例
// src/modules/content/entities/category.entity.ts
@Exclude()
@Tree('materialized-path')
@Entity('content_categories')
export class CategoryEntity extends BaseEntity {
// ...
@Expose()
@Type(() => Date)
@DeleteDateColumn({
comment: '建立時間',
})
deletedAt!: Date;
@Expose()
trashed!: boolean;
}
觀察者
為CategorySubscriber
和PostSubscriber
的setting
屬性新增上trash: true
DTO
修改QueryCategoryDTO
和QueryPostDto
以支援軟刪除,新增DeleteCommentMultiDto
以支援評論的批次刪除
因為評論不需要軟刪除,所以沒有使用前面在核心模組中新增的
DeleteMultiDto
// src/modules/content/dtos
@DtoValidation({ type: 'query' })
export class QueryCategoryDto implements PaginateDto, TrashedDto {
// ...
@IsEnum(QueryTrashMode)
@IsOptional()
trashed?: QueryTrashMode;
}
@DtoValidation({ type: 'query' })
export class QueryPostDto implements PaginateDto, TrashedDto {
//...
@IsEnum(QueryTrashMode)
@IsOptional()
trashed?: QueryTrashMode;
}
@DtoValidation()
export class DeleteCommentMultiDto {
@IsUUID(undefined, {
each: true,
message: '評論ID格式錯誤',
groups: ['delete-multi'],
})
@IsDefined({
each: true,
groups: ['delete-multi'],
message: '評論ID必須指定',
})
items: string[] = [];
}
服務類
為CategoryService
和PostService
新增上以下函式功能
- 支援軟刪除
- 支援批次刪除
- 支援軟刪除後恢復
- 支援軟刪除後批次恢復
- 支援查詢樹或列表資料時可查詢回收站(軟刪除後)的資料,正常資料,全部資料(包含軟刪除)
- 支援查詢單條資料時可以包含軟刪除處於軟刪除狀態中的資料
- 分類軟刪除時與硬刪除一樣直接把其子分類的
parent
設定成null
,同時其自身的parent
也設定成null
為CommentService
新增批次刪除方法
以PostService
為例
// src/modules/content/services/post.service.ts
protected async buildListQuery(
queryBuilder: SelectQueryBuilder<PostEntity>,
options: FindParams,
callback?: QueryHook<PostEntity>,
) {
// ...
const { trashed } = options;
// 是否查詢回收站
if (trashed === QueryTrashMode.ALL || trashed === QueryTrashMode.ONLY) {
qb.withDeleted();
if (trashed === QueryTrashMode.ONLY) {
qb.where(`${queryName}.deletedAt = :deleted`, { deleted: Not(IsNull()) });
}
}
if (callback) return callback(qb);
return qb;
}
async detail(id: string, trashed?: boolean, callback?: QueryHook<PostEntity>) {
// ...
if (trashed) qb.withDeleted();
const item = await qb.getOne();
if (!item)
throw new NotFoundException(`${this.postRepository.getQBName()} ${id} not exists!`);
return item;
}
async delete(id: string, trash = true) {
const item = await this.postRepository.findOneOrFail({
where: { id } as any,
withDeleted: true,
});
if (trash && isNil(item.deletedAt)) {
// await this.repository.softRemove(item);
(item as any).deletedAt = new Date();
await (this.postRepository as any).save(item);
return this.detail(id, true);
}
return this.postRepository.remove(item);
}
/**
* 批次刪除文章
*/
async deleteList(
data: string[],
params?: FindParams,
trash?: boolean,
callback?: QueryHook<PostEntity>,
) {
const isTrash = trash === undefined ? true : trash;
for (const id of data) {
await this.delete(id, isTrash);
}
return this.list(params, callback);
}
/**
* 批次刪除文章(分頁)
*/
async deletePaginate(
data: string[],
options: PaginateDto & FindParams,
trash?: boolean,
callback?: QueryHook<PostEntity>,
) {
const isTrash = trash === undefined ? true : trash;
for (const id of data) {
await this.delete(id, isTrash);
}
return this.paginate(options, callback);
}
/**
* 恢復回收站中的文章
*/
async restore(id: string, callback?: QueryHook<PostEntity>) {
const item = await this.postRepository.findOneOrFail({
where: { id },
withDeleted: true,
});
if (item.deletedAt) {
await this.postRepository.restore(item.id);
}
return this.detail(item.id, false, callback);
}
/**
* 批次恢復回收站中的文章
*/
async restoreList(data: string[], params?: FindParams, callback?: QueryHook<PostEntity>) {
for (const id of data) {
await this.restore(id);
}
return this.list(params, callback);
}
/**
* 批次恢復回收站中的資料(分頁)
*/
async restorePaginate(
data: string[],
options: PaginateDto & FindParams,
callback?: QueryHook<PostEntity>,
) {
for (const id of data) {
await this.restore(id);
}
return this.paginate(options, callback);
}
控制器
最後修改控制器中的詳情查詢,刪除的DTO,並新增批次刪除,恢復和批次恢復方法
以PostController
為例
// src/modules/content/controllers/post.controller.ts
@Controller('posts')
export class PostController {
// ...
@Get(':item')
async detail(
@Query() { trashed }: QueryDetailDto,
@Param('item', new ParseUUIDPipe())
item: string,
) {
return this.postService.detail(item, trashed);
}
@Delete(':item')
async delete(
@Param('item', new ParseUUIDPipe())
item: string,
@Body()
{ trash }: DeleteDto,
) {
return this.postService.delete(item, trash);
}
@Delete()
async deleteMulti(
@Query()
options: QueryPostDto,
@Body()
{ trash, items }: DeleteMultiDto,
) {
return this.postService.deletePaginate(items, options, trash);
}
@Patch('restore/:item')
async restore(
@Param('item', new ParseUUIDPipe())
item: string,
) {
return this.postService.restore(item);
}
@Patch('restore')
async restoreMulti(
@Query()
options: QueryPostDto,
@Body()
{ items }: DeleteRestoreDto,
) {
return this.postService.restorePaginate(items, options);
}
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結