Nestjs最佳實踐教程(七): 批次操作與軟刪除

pincman1988發表於2022-09-01

注意: 此處文件只起配合作用,為了您的身心愉悅,請看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;
    }
}

訂閱者

修改BaseSubscriberafterLoad方法,為每個支援軟刪除的模型的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列表
}

內容模組

模型

為需要支援軟刪除的模型(CategoryEntityPostEntity)新增上deletedAttrashed欄位以支援軟刪除

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

觀察者

CategorySubscriberPostSubscribersetting屬性新增上trash: true

DTO

修改QueryCategoryDTOQueryPostDto以支援軟刪除,新增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[] = [];
}

服務類

CategoryServicePostService新增上以下函式功能

  • 支援軟刪除
  • 支援批次刪除
  • 支援軟刪除後恢復
  • 支援軟刪除後批次恢復
  • 支援查詢樹或列表資料時可查詢回收站(軟刪除後)的資料,正常資料,全部資料(包含軟刪除)
  • 支援查詢單條資料時可以包含軟刪除處於軟刪除狀態中的資料
  • 分類軟刪除時與硬刪除一樣直接把其子分類的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 協議》,轉載必須註明作者和本文連結

相關文章