feat:增加批量删除、软删除和软删除恢复
- 完成基础的软删除 - 完成树形数据的软删除(children未处理)
This commit is contained in:
parent
c151657116
commit
5bcb4853e5
Binary file not shown.
Binary file not shown.
@ -17,7 +17,7 @@ export const database = (): TypeOrmModuleOptions => ({
|
||||
// database: 'ink_apps',
|
||||
// 以下为sqlite配置
|
||||
type: 'better-sqlite3',
|
||||
database: resolve(__dirname, '../../back/database6.db'),
|
||||
database: resolve(__dirname, '../../back/database9.db'),
|
||||
synchronize: true,
|
||||
autoLoadEntities: true,
|
||||
});
|
||||
|
@ -11,8 +11,14 @@ import {
|
||||
SerializeOptions,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { CreateCategoryDto, QueryCategoryDto, UpdateCategoryDto } from '@/modules/content/dtos';
|
||||
import {
|
||||
CreateCategoryDto,
|
||||
QueryCategoryDto,
|
||||
QueryCategoryTreeDto,
|
||||
UpdateCategoryDto,
|
||||
} from '@/modules/content/dtos';
|
||||
import { CategoryService } from '@/modules/content/services';
|
||||
import { DeleteWithTrashDto, RestoreDto } from '@/modules/restful/dtos';
|
||||
|
||||
@Controller('categories')
|
||||
export class CategoryController {
|
||||
@ -20,8 +26,8 @@ export class CategoryController {
|
||||
|
||||
@Get('tree')
|
||||
@SerializeOptions({ groups: ['category-tree'] })
|
||||
async tree() {
|
||||
return this.service.findTress();
|
||||
async tree(@Query() options: QueryCategoryTreeDto) {
|
||||
return this.service.findTrees(options);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ -57,9 +63,19 @@ export class CategoryController {
|
||||
return this.service.update(data);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Delete()
|
||||
@SerializeOptions({ groups: ['category-detail'] })
|
||||
async delete(@Param('id', new ParseUUIDPipe()) id: string) {
|
||||
return this.service.delete(id);
|
||||
async delete(@Body() data: DeleteWithTrashDto) {
|
||||
const { ids, trash } = data;
|
||||
|
||||
return this.service.delete(ids, trash);
|
||||
}
|
||||
|
||||
@Patch('restore')
|
||||
@SerializeOptions({ groups: ['category-detail'] })
|
||||
async restore(@Body() data: RestoreDto) {
|
||||
const { ids } = data;
|
||||
|
||||
return this.service.restore(ids);
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,8 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
ParseUUIDPipe,
|
||||
Post,
|
||||
Query,
|
||||
SerializeOptions,
|
||||
} from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, Post, Query, SerializeOptions } from '@nestjs/common';
|
||||
|
||||
import { CreateCommentDto, QueryCommentDto, QueryCommentTreeDto } from '@/modules/content/dtos';
|
||||
import { CommentService } from '@/modules/content/services';
|
||||
import { DeleteDto } from '@/modules/restful/dtos';
|
||||
|
||||
@Controller('comments')
|
||||
export class CommentController {
|
||||
@ -44,9 +35,10 @@ export class CommentController {
|
||||
return this.service.create(data);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Delete()
|
||||
@SerializeOptions({ groups: ['comment-detail'] })
|
||||
async delete(@Param('id', new ParseUUIDPipe()) id: string) {
|
||||
return this.service.delete(id);
|
||||
async delete(@Body() data: DeleteDto) {
|
||||
const { ids } = data;
|
||||
return this.service.delete(ids);
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
|
||||
import { CreatePostDto, QueryPostDto, UpdatePostDto } from '@/modules/content/dtos';
|
||||
import { PostService } from '@/modules/content/services';
|
||||
import { DeleteDto, DeleteWithTrashDto } from '@/modules/restful/dtos';
|
||||
|
||||
/**
|
||||
* 文章控制器
|
||||
@ -56,9 +57,19 @@ export class PostController {
|
||||
return this.postService.update(data);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Delete()
|
||||
@SerializeOptions({ groups: ['post-detail'] })
|
||||
async delete(@Param('id', new ParseUUIDPipe()) id: string) {
|
||||
return this.postService.delete(id);
|
||||
async delete(@Body() data: DeleteWithTrashDto) {
|
||||
const { ids, trash } = data;
|
||||
|
||||
return this.postService.delete(ids, trash);
|
||||
}
|
||||
|
||||
@Patch('restore')
|
||||
@SerializeOptions({ groups: ['post-detail'] })
|
||||
async restore(@Body() data: DeleteDto) {
|
||||
const { ids } = data;
|
||||
|
||||
return this.postService.restore(ids);
|
||||
}
|
||||
}
|
||||
|
@ -11,8 +11,9 @@ import {
|
||||
SerializeOptions,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { CreateTagDto, QueryCategoryDto, UpdateTagDto } from '@/modules/content/dtos';
|
||||
import { CreateTagDto, QueryTagsDto, UpdateTagDto } from '@/modules/content/dtos';
|
||||
import { TagService } from '@/modules/content/services';
|
||||
import { DeleteDto, DeleteWithTrashDto } from '@/modules/restful/dtos';
|
||||
|
||||
@Controller('tags')
|
||||
export class TagController {
|
||||
@ -22,7 +23,7 @@ export class TagController {
|
||||
@SerializeOptions({})
|
||||
async list(
|
||||
@Query()
|
||||
options: QueryCategoryDto,
|
||||
options: QueryTagsDto,
|
||||
) {
|
||||
return this.service.paginate(options);
|
||||
}
|
||||
@ -54,9 +55,19 @@ export class TagController {
|
||||
return this.service.update(data);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@SerializeOptions({})
|
||||
async delete(@Param('id', new ParseUUIDPipe()) id: string) {
|
||||
return this.service.delete(id);
|
||||
@Delete()
|
||||
@SerializeOptions({ groups: ['post-list'] })
|
||||
async delete(@Body() data: DeleteWithTrashDto) {
|
||||
const { ids, trash } = data;
|
||||
|
||||
return this.service.delete(ids, trash);
|
||||
}
|
||||
|
||||
@Patch('restore')
|
||||
@SerializeOptions({ groups: ['post-list'] })
|
||||
async restore(@Body() data: DeleteDto) {
|
||||
const { ids } = data;
|
||||
|
||||
return this.service.restore(ids);
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { PartialType } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import {
|
||||
IsDefined,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
@ -14,12 +15,23 @@ import { toNumber } from 'lodash';
|
||||
|
||||
import { CategoryEntity } from '@/modules/content/entities';
|
||||
import { DtoValidation } from '@/modules/core/decorators';
|
||||
import { SelectTrashMode } from '@/modules/database/constants';
|
||||
import { IsDataExist, IsTreeUnique, IsTreeUniqueExist } from '@/modules/database/constraints';
|
||||
|
||||
import { PaginateOptions } from '@/modules/database/types';
|
||||
|
||||
/**
|
||||
* 树形分类查询验证
|
||||
*/
|
||||
@DtoValidation({ type: 'query' })
|
||||
export class QueryCategoryDto implements PaginateOptions {
|
||||
export class QueryCategoryTreeDto {
|
||||
@IsEnum(SelectTrashMode)
|
||||
@IsOptional()
|
||||
trashed?: SelectTrashMode;
|
||||
}
|
||||
|
||||
@DtoValidation({ type: 'query' })
|
||||
export class QueryCategoryDto extends QueryCategoryTreeDto implements PaginateOptions {
|
||||
@Transform(({ value }) => toNumber(value))
|
||||
@Min(1, { message: '当前页数必须大于1' })
|
||||
@IsNumber()
|
||||
|
@ -21,6 +21,7 @@ import { PostOrderType } from '@/modules/content/constants';
|
||||
import { CategoryEntity, TagEntity } from '@/modules/content/entities';
|
||||
import { DtoValidation } from '@/modules/core/decorators';
|
||||
import { toBoolean } from '@/modules/core/helpers';
|
||||
import { SelectTrashMode } from '@/modules/database/constants';
|
||||
import { IsDataExist } from '@/modules/database/constraints';
|
||||
import { PaginateOptions } from '@/modules/database/types';
|
||||
|
||||
@ -66,6 +67,10 @@ export class QueryPostDto implements PaginateOptions {
|
||||
@IsUUID(undefined, { message: '标签ID必须是UUID' })
|
||||
@IsOptional()
|
||||
tag?: string;
|
||||
|
||||
@IsEnum(SelectTrashMode)
|
||||
@IsOptional()
|
||||
trashed?: SelectTrashMode;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2,6 +2,7 @@ import { PartialType } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import {
|
||||
IsDefined,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
@ -12,6 +13,7 @@ import {
|
||||
import { toNumber } from 'lodash';
|
||||
|
||||
import { DtoValidation } from '@/modules/core/decorators';
|
||||
import { SelectTrashMode } from '@/modules/database/constants';
|
||||
import { PaginateOptions } from '@/modules/database/types';
|
||||
|
||||
/**
|
||||
@ -19,6 +21,10 @@ import { PaginateOptions } from '@/modules/database/types';
|
||||
*/
|
||||
@DtoValidation({ type: 'query' })
|
||||
export class QueryTagsDto implements PaginateOptions {
|
||||
@IsEnum(SelectTrashMode)
|
||||
@IsOptional()
|
||||
trashed?: SelectTrashMode;
|
||||
|
||||
@Transform(({ value }) => toNumber(value))
|
||||
@Min(1, { message: '当前页数必须大于1' })
|
||||
@IsNumber()
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Exclude, Expose } from 'class-transformer';
|
||||
import { Column, Entity, ManyToMany, PrimaryColumn, Relation } from 'typeorm';
|
||||
import { Exclude, Expose, Type } from 'class-transformer';
|
||||
import { Column, DeleteDateColumn, Entity, ManyToMany, PrimaryColumn, Relation } from 'typeorm';
|
||||
|
||||
import { PostEntity } from '@/modules/content/entities/post.entity';
|
||||
|
||||
@ -26,4 +26,11 @@ export class TagEntity {
|
||||
*/
|
||||
@Expose()
|
||||
postCount: number;
|
||||
|
||||
@Expose()
|
||||
@Type(() => Date)
|
||||
@DeleteDateColumn({
|
||||
comment: '删除时间',
|
||||
})
|
||||
deletedAt: Date;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { unset } from 'lodash';
|
||||
import { pick, unset } from 'lodash';
|
||||
import { FindOptionsUtils, FindTreeOptions, TreeRepository } from 'typeorm';
|
||||
|
||||
import { CategoryEntity } from '@/modules/content/entities';
|
||||
@ -17,7 +17,12 @@ export class CategoryRepository extends TreeRepository<CategoryEntity> {
|
||||
* 树形结构查询
|
||||
* @param options
|
||||
*/
|
||||
async findTrees(options?: FindTreeOptions) {
|
||||
async findTrees(
|
||||
options?: FindTreeOptions & {
|
||||
onlyTrashed?: boolean;
|
||||
withTrashed?: boolean;
|
||||
},
|
||||
) {
|
||||
const roots = await this.findRoots(options);
|
||||
await Promise.all(roots.map((root) => this.findDescendantsTree(root, options)));
|
||||
return roots;
|
||||
@ -27,19 +32,28 @@ export class CategoryRepository extends TreeRepository<CategoryEntity> {
|
||||
* 查询顶级分类
|
||||
* @param options
|
||||
*/
|
||||
findRoots(options?: FindTreeOptions) {
|
||||
findRoots(
|
||||
options?: FindTreeOptions & {
|
||||
onlyTrashed?: boolean;
|
||||
withTrashed?: boolean;
|
||||
},
|
||||
) {
|
||||
const escapeAlias = (alias: string) => this.manager.connection.driver.escape(alias);
|
||||
const escapeColumn = (column: string) => this.manager.connection.driver.escape(column);
|
||||
|
||||
const joinColumn = this.metadata.treeParentRelation!.joinColumns[0];
|
||||
const parentPropertyName = joinColumn.givenDatabaseName || joinColumn.databaseName;
|
||||
|
||||
const qb = this.buildBaseQB().orderBy('category.customOrder', 'ASC');
|
||||
FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options);
|
||||
qb.where(`${escapeAlias('category')}.${escapeColumn(parentPropertyName)} IS NULL`);
|
||||
|
||||
return qb
|
||||
.where(`${escapeAlias('category')}.${escapeColumn(parentPropertyName)} IS NULL`)
|
||||
.getMany();
|
||||
FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, pick(options, ['relations', 'depth']));
|
||||
|
||||
if (options?.withTrashed) {
|
||||
qb.withDeleted();
|
||||
if (options?.onlyTrashed) qb.where(`category.deletedAt IS NOT NULL`);
|
||||
}
|
||||
|
||||
return qb.getMany();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -47,11 +61,22 @@ export class CategoryRepository extends TreeRepository<CategoryEntity> {
|
||||
* @param entity
|
||||
* @param options
|
||||
*/
|
||||
findDescendants(entity: CategoryEntity, options?: FindTreeOptions) {
|
||||
findDescendants(
|
||||
entity: CategoryEntity,
|
||||
options?: FindTreeOptions & {
|
||||
onlyTrashed?: boolean;
|
||||
withTrashed?: boolean;
|
||||
},
|
||||
) {
|
||||
const qb = this.createDescendantsQueryBuilder('category', 'treeClosure', entity);
|
||||
FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options);
|
||||
qb.orderBy('category.customOrder', 'ASC');
|
||||
|
||||
if (options?.withTrashed) {
|
||||
qb.withDeleted();
|
||||
if (options?.onlyTrashed) qb.where(`category.deletedAt IS NOT NULL`);
|
||||
}
|
||||
|
||||
return qb.getMany();
|
||||
}
|
||||
|
||||
@ -60,11 +85,22 @@ export class CategoryRepository extends TreeRepository<CategoryEntity> {
|
||||
* @param entity
|
||||
* @param options
|
||||
*/
|
||||
findAncestors(entity: CategoryEntity, options?: FindTreeOptions) {
|
||||
findAncestors(
|
||||
entity: CategoryEntity,
|
||||
options?: FindTreeOptions & {
|
||||
onlyTrashed?: boolean;
|
||||
withTrashed?: boolean;
|
||||
},
|
||||
) {
|
||||
const qb = this.createAncestorsQueryBuilder('category', 'treeClosure', entity);
|
||||
FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options);
|
||||
qb.orderBy('category.customOrder', 'ASC');
|
||||
|
||||
if (options?.withTrashed) {
|
||||
qb.withDeleted();
|
||||
if (options?.onlyTrashed) qb.where(`category.deletedAt IS NOT NULL`);
|
||||
}
|
||||
|
||||
return qb.getMany();
|
||||
}
|
||||
|
||||
@ -88,4 +124,42 @@ export class CategoryRepository extends TreeRepository<CategoryEntity> {
|
||||
|
||||
return data as CategoryEntity[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计后代元素数量
|
||||
* @param entity
|
||||
* @param options
|
||||
*/
|
||||
async countDescendants(
|
||||
entity: CategoryEntity,
|
||||
options?: { withTrashed?: boolean; onlyTrashed?: boolean },
|
||||
) {
|
||||
const qb = this.createDescendantsQueryBuilder('category', 'treeClosure', entity);
|
||||
|
||||
if (options?.withTrashed) {
|
||||
qb.withDeleted();
|
||||
if (options?.onlyTrashed) qb.where(`category.deleteAt IS NOT NULL`);
|
||||
}
|
||||
|
||||
return qb.getCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计后代元素数量
|
||||
* @param entity
|
||||
* @param options
|
||||
*/
|
||||
async countAncestors(
|
||||
entity: CategoryEntity,
|
||||
options?: { withTrashed?: boolean; onlyTrashed?: boolean },
|
||||
) {
|
||||
const qb = this.createAncestorsQueryBuilder('category', 'treeClosure', entity);
|
||||
|
||||
if (options?.withTrashed) {
|
||||
qb.withDeleted();
|
||||
if (options?.onlyTrashed) qb.where(`category.deleteAt IS NOT NULL`);
|
||||
}
|
||||
|
||||
return qb.getCount();
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,17 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isNil, omit } from 'lodash';
|
||||
import { EntityNotFoundError } from 'typeorm';
|
||||
import { EntityNotFoundError, In } from 'typeorm';
|
||||
|
||||
import { CreateCategoryDto, QueryCategoryDto, UpdateCategoryDto } from '@/modules/content/dtos';
|
||||
import {
|
||||
CreateCategoryDto,
|
||||
QueryCategoryDto,
|
||||
QueryCategoryTreeDto,
|
||||
UpdateCategoryDto,
|
||||
} from '@/modules/content/dtos';
|
||||
import { CategoryEntity } from '@/modules/content/entities';
|
||||
import { CategoryRepository } from '@/modules/content/repositories';
|
||||
import { SelectTrashMode } from '@/modules/database/constants';
|
||||
import { treePaginate } from '@/modules/database/helpers';
|
||||
|
||||
/**
|
||||
@ -17,9 +23,15 @@ export class CategoryService {
|
||||
|
||||
/**
|
||||
* 查询分类树
|
||||
* @param options
|
||||
*/
|
||||
async findTress() {
|
||||
return this.repository.findTrees();
|
||||
async findTrees(options: QueryCategoryTreeDto) {
|
||||
const { trashed = SelectTrashMode.NONE } = options;
|
||||
|
||||
return this.repository.findTrees({
|
||||
withTrashed: trashed === SelectTrashMode.ALL || trashed === SelectTrashMode.ONLY,
|
||||
onlyTrashed: trashed === SelectTrashMode.ONLY,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -27,7 +39,12 @@ export class CategoryService {
|
||||
* @param options 分页选项
|
||||
*/
|
||||
async paginate(options: QueryCategoryDto) {
|
||||
const tree = await this.repository.findTrees();
|
||||
const { trashed = SelectTrashMode.NONE } = options;
|
||||
|
||||
const tree = await this.repository.findTrees({
|
||||
withTrashed: trashed === SelectTrashMode.ALL || trashed === SelectTrashMode.ONLY,
|
||||
onlyTrashed: trashed === SelectTrashMode.ONLY,
|
||||
});
|
||||
const data = await this.repository.toFlatTrees(tree);
|
||||
|
||||
return treePaginate(options, data);
|
||||
@ -83,23 +100,36 @@ export class CategoryService {
|
||||
* 删除分类
|
||||
* @param id
|
||||
*/
|
||||
async delete(id: string) {
|
||||
const item = await this.repository.findOneOrFail({
|
||||
where: { id },
|
||||
async delete(ids: string[], trash?: boolean) {
|
||||
const items = await this.repository.find({
|
||||
where: { id: In(ids) },
|
||||
withDeleted: true,
|
||||
relations: ['parent', 'children'],
|
||||
});
|
||||
|
||||
// 把子分类提升一级
|
||||
for (const item of items) {
|
||||
if (!isNil(item.children) && item.children.length > 0) {
|
||||
const nchildren = [...item.children].map((c) => {
|
||||
c.parent = item.parent;
|
||||
return item;
|
||||
return c;
|
||||
});
|
||||
|
||||
await this.repository.save(nchildren, { reload: true });
|
||||
await this.repository.save(nchildren);
|
||||
}
|
||||
}
|
||||
|
||||
return this.repository.remove(item);
|
||||
if (trash) {
|
||||
const directs = items.filter((item) => !isNil(item.deletedAt));
|
||||
const sorts = items.filter((item) => isNil(item.deletedAt));
|
||||
|
||||
return [
|
||||
...(await this.repository.remove(directs)),
|
||||
...(await this.repository.softRemove(sorts)),
|
||||
];
|
||||
}
|
||||
|
||||
return this.repository.remove(items);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -121,4 +151,22 @@ export class CategoryService {
|
||||
}
|
||||
return parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复分类
|
||||
* @param ids
|
||||
*/
|
||||
async restore(ids: string[]) {
|
||||
const items = await this.repository.find({
|
||||
where: { id: In(ids) } as any,
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
const trasheds = items.filter((item) => !isNil(item)).map((item) => item.id);
|
||||
if (trasheds.length < 1) return [];
|
||||
await this.repository.restore(trasheds);
|
||||
const qb = this.repository.buildBaseQB();
|
||||
qb.andWhereInIds(trasheds);
|
||||
return qb.getMany();
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { ForbiddenException, Injectable } from '@nestjs/common';
|
||||
|
||||
import { isNil } from 'lodash';
|
||||
|
||||
import { EntityNotFoundError, SelectQueryBuilder } from 'typeorm';
|
||||
import { EntityNotFoundError, In, SelectQueryBuilder } from 'typeorm';
|
||||
|
||||
import { CreateCommentDto, QueryCommentDto, QueryCommentTreeDto } from '@/modules/content/dtos';
|
||||
import { CommentEntity } from '@/modules/content/entities';
|
||||
@ -82,11 +82,11 @@ export class CommentService {
|
||||
|
||||
/**
|
||||
* 删除评论
|
||||
* @param id
|
||||
* @param ids
|
||||
*/
|
||||
async delete(id: string) {
|
||||
const comment = await this.repository.findOneOrFail({ where: { id: id ?? null } });
|
||||
return this.repository.remove(comment);
|
||||
async delete(ids: string[]) {
|
||||
const comments = await this.repository.find({ where: { id: In(ids) } });
|
||||
return this.repository.remove(comments);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -10,6 +10,7 @@ import { PostEntity } from '@/modules/content/entities';
|
||||
import { CategoryRepository, PostRepository, TagRepository } from '@/modules/content/repositories';
|
||||
|
||||
import { CategoryService } from '@/modules/content/services/category.service';
|
||||
import { SelectTrashMode } from '@/modules/database/constants';
|
||||
import { paginate } from '@/modules/database/helpers';
|
||||
import { QueryHook } from '@/modules/database/types';
|
||||
|
||||
@ -108,9 +109,45 @@ export class PostService {
|
||||
* 删除文章
|
||||
* @param id
|
||||
*/
|
||||
async delete(id: string) {
|
||||
const item = await this.repository.findOneByOrFail({ id });
|
||||
return this.repository.remove(item);
|
||||
async delete(ids: string[], trash?: boolean) {
|
||||
const items = await this.repository.find({
|
||||
where: { id: In(ids) } as any,
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
if (trash) {
|
||||
// 对已软删除的数据再次删除时直接通过remove方法从数据库中清除
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复文章
|
||||
* @param ids
|
||||
*/
|
||||
async restore(ids: string[]) {
|
||||
const items = await this.repository.find({
|
||||
where: { id: In(ids) } as any,
|
||||
withDeleted: true,
|
||||
});
|
||||
// 过滤掉不在回收站中的数据
|
||||
const trasheds = items.filter((item) => !isNil(item)).map((item) => item.id);
|
||||
|
||||
if (trasheds.length < 1) return [];
|
||||
|
||||
await this.repository.restore(trasheds);
|
||||
const qb = await this.buildListQuery(this.repository.buildBaseQB(), {}, async (qbuilder) =>
|
||||
qbuilder.andWhereInIds(trasheds),
|
||||
);
|
||||
return qb.getMany();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -124,7 +161,12 @@ export class PostService {
|
||||
options: FindParams,
|
||||
callback?: QueryHook<PostEntity>,
|
||||
) {
|
||||
const { category, tag, orderBy, isPublished } = options;
|
||||
const { category, tag, orderBy, isPublished, trashed = SelectTrashMode.NONE } = options;
|
||||
|
||||
if (trashed === SelectTrashMode.ALL || trashed === SelectTrashMode.ONLY) {
|
||||
qb.withDeleted();
|
||||
if (trashed === SelectTrashMode.ONLY) qb.where(`post.deletedAt is not null`);
|
||||
}
|
||||
if (typeof isPublished === 'boolean') {
|
||||
isPublished
|
||||
? qb.where({
|
||||
|
@ -1,10 +1,19 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { omit } from 'lodash';
|
||||
import { isNil, omit } from 'lodash';
|
||||
|
||||
import { In, SelectQueryBuilder } from 'typeorm';
|
||||
|
||||
import { CreateTagDto, QueryTagsDto, UpdateTagDto } from '@/modules/content/dtos';
|
||||
import { TagEntity } from '@/modules/content/entities';
|
||||
import { TagRepository } from '@/modules/content/repositories';
|
||||
import { SelectTrashMode } from '@/modules/database/constants';
|
||||
import { paginate } from '@/modules/database/helpers';
|
||||
import { QueryHook } from '@/modules/database/types';
|
||||
|
||||
type FindParams = {
|
||||
[key in keyof Omit<QueryTagsDto, 'limit' | 'page'>]: QueryTagsDto[key];
|
||||
};
|
||||
|
||||
/**
|
||||
* 标签数据操作
|
||||
@ -19,7 +28,7 @@ export class TagService {
|
||||
* @param callback 添加额外的查询
|
||||
*/
|
||||
async paginate(options: QueryTagsDto) {
|
||||
const qb = this.repository.buildBaseQB();
|
||||
const qb = await this.buildListQuery(this.repository.buildBaseQB(), options);
|
||||
return paginate(qb, options);
|
||||
}
|
||||
|
||||
@ -54,10 +63,64 @@ export class TagService {
|
||||
|
||||
/**
|
||||
* 删除标签
|
||||
* @param id
|
||||
* @param ids
|
||||
*/
|
||||
async delete(id: string) {
|
||||
const item = await this.repository.findOneByOrFail({ id });
|
||||
return this.repository.remove(item);
|
||||
async delete(ids: string[], trash: boolean) {
|
||||
const items = await this.repository.find({
|
||||
where: { id: In(ids) } as any,
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
if (trash) {
|
||||
const directs = items.filter((item) => !isNil(item.deletedAt));
|
||||
const sorts = items.filter((item) => isNil(item.deletedAt));
|
||||
|
||||
return [
|
||||
...(await this.repository.remove(directs)),
|
||||
...(await this.repository.softRemove(sorts)),
|
||||
];
|
||||
}
|
||||
|
||||
return this.repository.remove(items);
|
||||
}
|
||||
|
||||
/**
|
||||
* 软删除标签
|
||||
* @param ids
|
||||
*/
|
||||
async restore(ids: string[]) {
|
||||
const items = await this.repository.find({
|
||||
where: { id: In(ids) } as any,
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
// 过滤掉不在回收站的标签
|
||||
const trashed = items.filter((item) => !isNil(item.deletedAt)).map((item) => item.id);
|
||||
if (trashed.length < 1) return trashed;
|
||||
await this.repository.restore(trashed);
|
||||
const qb = this.repository.buildBaseQB().where({ id: In(trashed) });
|
||||
return qb.getMany();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建标签列表查询器(需要查到软删除)
|
||||
* @param qb
|
||||
* @param options
|
||||
* @param callback
|
||||
*/
|
||||
protected async buildListQuery(
|
||||
qb: SelectQueryBuilder<TagEntity>,
|
||||
options: FindParams,
|
||||
callback?: QueryHook<TagEntity>,
|
||||
) {
|
||||
const { trashed } = options;
|
||||
|
||||
if (trashed === SelectTrashMode.ALL || trashed === SelectTrashMode.ONLY) {
|
||||
qb.withDeleted();
|
||||
if (trashed === SelectTrashMode.ONLY) qb.where(`tag.deletedAt IS NOT NULL`);
|
||||
}
|
||||
|
||||
if (callback) return callback(qb);
|
||||
return qb;
|
||||
}
|
||||
}
|
||||
|
@ -2,3 +2,12 @@
|
||||
* 自定义 Repository 元数据
|
||||
*/
|
||||
export const CUSTOM_REPOSITORY_METADATA = 'CUSTOM_REPOSITORY_METADATA';
|
||||
|
||||
/**
|
||||
* 软删除数据查询类型
|
||||
*/
|
||||
export enum SelectTrashMode {
|
||||
ALL = 'all',
|
||||
ONLY = 'only',
|
||||
NONE = 'none',
|
||||
}
|
||||
|
18
src/modules/restful/dtos/delete-with-trash.dto.ts
Normal file
18
src/modules/restful/dtos/delete-with-trash.dto.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
import { IsBoolean, IsOptional } from 'class-validator';
|
||||
|
||||
import { DtoValidation } from '@/modules/core/decorators';
|
||||
import { toBoolean } from '@/modules/core/helpers';
|
||||
import { DeleteDto } from '@/modules/restful/dtos/delete.dto';
|
||||
|
||||
/**
|
||||
* 带软删除的批量删除验证
|
||||
*/
|
||||
@DtoValidation()
|
||||
export class DeleteWithTrashDto extends DeleteDto {
|
||||
@Transform(({ value }) => toBoolean(value))
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
trash?: boolean;
|
||||
}
|
19
src/modules/restful/dtos/delete.dto.ts
Normal file
19
src/modules/restful/dtos/delete.dto.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { IsDefined, IsUUID } from 'class-validator';
|
||||
|
||||
import { DtoValidation } from '@/modules/core/decorators';
|
||||
|
||||
/**
|
||||
* 批量删除验证
|
||||
*/
|
||||
@DtoValidation()
|
||||
export class DeleteDto {
|
||||
@IsUUID(undefined, {
|
||||
each: true,
|
||||
message: 'ID格式错误',
|
||||
})
|
||||
@IsDefined({
|
||||
each: true,
|
||||
message: 'ID必须指定',
|
||||
})
|
||||
ids: string[] = [];
|
||||
}
|
3
src/modules/restful/dtos/index.ts
Normal file
3
src/modules/restful/dtos/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './delete-with-trash.dto';
|
||||
export * from './delete.dto';
|
||||
export * from './restore.dto';
|
19
src/modules/restful/dtos/restore.dto.ts
Normal file
19
src/modules/restful/dtos/restore.dto.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { IsDefined, IsUUID } from 'class-validator';
|
||||
|
||||
import { DtoValidation } from '@/modules/core/decorators';
|
||||
|
||||
/**
|
||||
* 批量恢复验证
|
||||
*/
|
||||
@DtoValidation()
|
||||
export class RestoreDto {
|
||||
@IsUUID(undefined, {
|
||||
each: true,
|
||||
message: 'ID格式错误',
|
||||
})
|
||||
@IsDefined({
|
||||
each: true,
|
||||
message: 'ID必须指定',
|
||||
})
|
||||
ids: string[] = [];
|
||||
}
|
Loading…
Reference in New Issue
Block a user