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',
|
// database: 'ink_apps',
|
||||||
// 以下为sqlite配置
|
// 以下为sqlite配置
|
||||||
type: 'better-sqlite3',
|
type: 'better-sqlite3',
|
||||||
database: resolve(__dirname, '../../back/database6.db'),
|
database: resolve(__dirname, '../../back/database9.db'),
|
||||||
synchronize: true,
|
synchronize: true,
|
||||||
autoLoadEntities: true,
|
autoLoadEntities: true,
|
||||||
});
|
});
|
||||||
|
@ -11,8 +11,14 @@ import {
|
|||||||
SerializeOptions,
|
SerializeOptions,
|
||||||
} from '@nestjs/common';
|
} 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 { CategoryService } from '@/modules/content/services';
|
||||||
|
import { DeleteWithTrashDto, RestoreDto } from '@/modules/restful/dtos';
|
||||||
|
|
||||||
@Controller('categories')
|
@Controller('categories')
|
||||||
export class CategoryController {
|
export class CategoryController {
|
||||||
@ -20,8 +26,8 @@ export class CategoryController {
|
|||||||
|
|
||||||
@Get('tree')
|
@Get('tree')
|
||||||
@SerializeOptions({ groups: ['category-tree'] })
|
@SerializeOptions({ groups: ['category-tree'] })
|
||||||
async tree() {
|
async tree(@Query() options: QueryCategoryTreeDto) {
|
||||||
return this.service.findTress();
|
return this.service.findTrees(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ -57,9 +63,19 @@ export class CategoryController {
|
|||||||
return this.service.update(data);
|
return this.service.update(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete()
|
||||||
@SerializeOptions({ groups: ['category-detail'] })
|
@SerializeOptions({ groups: ['category-detail'] })
|
||||||
async delete(@Param('id', new ParseUUIDPipe()) id: string) {
|
async delete(@Body() data: DeleteWithTrashDto) {
|
||||||
return this.service.delete(id);
|
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 {
|
import { Body, Controller, Delete, Get, Post, Query, SerializeOptions } from '@nestjs/common';
|
||||||
Body,
|
|
||||||
Controller,
|
|
||||||
Delete,
|
|
||||||
Get,
|
|
||||||
Param,
|
|
||||||
ParseUUIDPipe,
|
|
||||||
Post,
|
|
||||||
Query,
|
|
||||||
SerializeOptions,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
|
|
||||||
import { CreateCommentDto, QueryCommentDto, QueryCommentTreeDto } from '@/modules/content/dtos';
|
import { CreateCommentDto, QueryCommentDto, QueryCommentTreeDto } from '@/modules/content/dtos';
|
||||||
import { CommentService } from '@/modules/content/services';
|
import { CommentService } from '@/modules/content/services';
|
||||||
|
import { DeleteDto } from '@/modules/restful/dtos';
|
||||||
|
|
||||||
@Controller('comments')
|
@Controller('comments')
|
||||||
export class CommentController {
|
export class CommentController {
|
||||||
@ -44,9 +35,10 @@ export class CommentController {
|
|||||||
return this.service.create(data);
|
return this.service.create(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete()
|
||||||
@SerializeOptions({ groups: ['comment-detail'] })
|
@SerializeOptions({ groups: ['comment-detail'] })
|
||||||
async delete(@Param('id', new ParseUUIDPipe()) id: string) {
|
async delete(@Body() data: DeleteDto) {
|
||||||
return this.service.delete(id);
|
const { ids } = data;
|
||||||
|
return this.service.delete(ids);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ import {
|
|||||||
|
|
||||||
import { CreatePostDto, QueryPostDto, UpdatePostDto } from '@/modules/content/dtos';
|
import { CreatePostDto, QueryPostDto, UpdatePostDto } from '@/modules/content/dtos';
|
||||||
import { PostService } from '@/modules/content/services';
|
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);
|
return this.postService.update(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete()
|
||||||
@SerializeOptions({ groups: ['post-detail'] })
|
@SerializeOptions({ groups: ['post-detail'] })
|
||||||
async delete(@Param('id', new ParseUUIDPipe()) id: string) {
|
async delete(@Body() data: DeleteWithTrashDto) {
|
||||||
return this.postService.delete(id);
|
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,
|
SerializeOptions,
|
||||||
} from '@nestjs/common';
|
} 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 { TagService } from '@/modules/content/services';
|
||||||
|
import { DeleteDto, DeleteWithTrashDto } from '@/modules/restful/dtos';
|
||||||
|
|
||||||
@Controller('tags')
|
@Controller('tags')
|
||||||
export class TagController {
|
export class TagController {
|
||||||
@ -22,7 +23,7 @@ export class TagController {
|
|||||||
@SerializeOptions({})
|
@SerializeOptions({})
|
||||||
async list(
|
async list(
|
||||||
@Query()
|
@Query()
|
||||||
options: QueryCategoryDto,
|
options: QueryTagsDto,
|
||||||
) {
|
) {
|
||||||
return this.service.paginate(options);
|
return this.service.paginate(options);
|
||||||
}
|
}
|
||||||
@ -54,9 +55,19 @@ export class TagController {
|
|||||||
return this.service.update(data);
|
return this.service.update(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete()
|
||||||
@SerializeOptions({})
|
@SerializeOptions({ groups: ['post-list'] })
|
||||||
async delete(@Param('id', new ParseUUIDPipe()) id: string) {
|
async delete(@Body() data: DeleteWithTrashDto) {
|
||||||
return this.service.delete(id);
|
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 { Transform } from 'class-transformer';
|
||||||
import {
|
import {
|
||||||
IsDefined,
|
IsDefined,
|
||||||
|
IsEnum,
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
IsNumber,
|
IsNumber,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
@ -14,12 +15,23 @@ import { toNumber } from 'lodash';
|
|||||||
|
|
||||||
import { CategoryEntity } from '@/modules/content/entities';
|
import { CategoryEntity } from '@/modules/content/entities';
|
||||||
import { DtoValidation } from '@/modules/core/decorators';
|
import { DtoValidation } from '@/modules/core/decorators';
|
||||||
|
import { SelectTrashMode } from '@/modules/database/constants';
|
||||||
import { IsDataExist, IsTreeUnique, IsTreeUniqueExist } from '@/modules/database/constraints';
|
import { IsDataExist, IsTreeUnique, IsTreeUniqueExist } from '@/modules/database/constraints';
|
||||||
|
|
||||||
import { PaginateOptions } from '@/modules/database/types';
|
import { PaginateOptions } from '@/modules/database/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 树形分类查询验证
|
||||||
|
*/
|
||||||
@DtoValidation({ type: 'query' })
|
@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))
|
@Transform(({ value }) => toNumber(value))
|
||||||
@Min(1, { message: '当前页数必须大于1' })
|
@Min(1, { message: '当前页数必须大于1' })
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
|
@ -21,6 +21,7 @@ import { PostOrderType } from '@/modules/content/constants';
|
|||||||
import { CategoryEntity, TagEntity } from '@/modules/content/entities';
|
import { CategoryEntity, TagEntity } from '@/modules/content/entities';
|
||||||
import { DtoValidation } from '@/modules/core/decorators';
|
import { DtoValidation } from '@/modules/core/decorators';
|
||||||
import { toBoolean } from '@/modules/core/helpers';
|
import { toBoolean } from '@/modules/core/helpers';
|
||||||
|
import { SelectTrashMode } from '@/modules/database/constants';
|
||||||
import { IsDataExist } from '@/modules/database/constraints';
|
import { IsDataExist } from '@/modules/database/constraints';
|
||||||
import { PaginateOptions } from '@/modules/database/types';
|
import { PaginateOptions } from '@/modules/database/types';
|
||||||
|
|
||||||
@ -66,6 +67,10 @@ export class QueryPostDto implements PaginateOptions {
|
|||||||
@IsUUID(undefined, { message: '标签ID必须是UUID' })
|
@IsUUID(undefined, { message: '标签ID必须是UUID' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
tag?: string;
|
tag?: string;
|
||||||
|
|
||||||
|
@IsEnum(SelectTrashMode)
|
||||||
|
@IsOptional()
|
||||||
|
trashed?: SelectTrashMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2,6 +2,7 @@ import { PartialType } from '@nestjs/swagger';
|
|||||||
import { Transform } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
import {
|
import {
|
||||||
IsDefined,
|
IsDefined,
|
||||||
|
IsEnum,
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
IsNumber,
|
IsNumber,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
@ -12,6 +13,7 @@ import {
|
|||||||
import { toNumber } from 'lodash';
|
import { toNumber } from 'lodash';
|
||||||
|
|
||||||
import { DtoValidation } from '@/modules/core/decorators';
|
import { DtoValidation } from '@/modules/core/decorators';
|
||||||
|
import { SelectTrashMode } from '@/modules/database/constants';
|
||||||
import { PaginateOptions } from '@/modules/database/types';
|
import { PaginateOptions } from '@/modules/database/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -19,6 +21,10 @@ import { PaginateOptions } from '@/modules/database/types';
|
|||||||
*/
|
*/
|
||||||
@DtoValidation({ type: 'query' })
|
@DtoValidation({ type: 'query' })
|
||||||
export class QueryTagsDto implements PaginateOptions {
|
export class QueryTagsDto implements PaginateOptions {
|
||||||
|
@IsEnum(SelectTrashMode)
|
||||||
|
@IsOptional()
|
||||||
|
trashed?: SelectTrashMode;
|
||||||
|
|
||||||
@Transform(({ value }) => toNumber(value))
|
@Transform(({ value }) => toNumber(value))
|
||||||
@Min(1, { message: '当前页数必须大于1' })
|
@Min(1, { message: '当前页数必须大于1' })
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Exclude, Expose } from 'class-transformer';
|
import { Exclude, Expose, Type } from 'class-transformer';
|
||||||
import { Column, Entity, ManyToMany, PrimaryColumn, Relation } from 'typeorm';
|
import { Column, DeleteDateColumn, Entity, ManyToMany, PrimaryColumn, Relation } from 'typeorm';
|
||||||
|
|
||||||
import { PostEntity } from '@/modules/content/entities/post.entity';
|
import { PostEntity } from '@/modules/content/entities/post.entity';
|
||||||
|
|
||||||
@ -26,4 +26,11 @@ export class TagEntity {
|
|||||||
*/
|
*/
|
||||||
@Expose()
|
@Expose()
|
||||||
postCount: number;
|
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 { FindOptionsUtils, FindTreeOptions, TreeRepository } from 'typeorm';
|
||||||
|
|
||||||
import { CategoryEntity } from '@/modules/content/entities';
|
import { CategoryEntity } from '@/modules/content/entities';
|
||||||
@ -17,7 +17,12 @@ export class CategoryRepository extends TreeRepository<CategoryEntity> {
|
|||||||
* 树形结构查询
|
* 树形结构查询
|
||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
async findTrees(options?: FindTreeOptions) {
|
async findTrees(
|
||||||
|
options?: FindTreeOptions & {
|
||||||
|
onlyTrashed?: boolean;
|
||||||
|
withTrashed?: boolean;
|
||||||
|
},
|
||||||
|
) {
|
||||||
const roots = await this.findRoots(options);
|
const roots = await this.findRoots(options);
|
||||||
await Promise.all(roots.map((root) => this.findDescendantsTree(root, options)));
|
await Promise.all(roots.map((root) => this.findDescendantsTree(root, options)));
|
||||||
return roots;
|
return roots;
|
||||||
@ -27,19 +32,28 @@ export class CategoryRepository extends TreeRepository<CategoryEntity> {
|
|||||||
* 查询顶级分类
|
* 查询顶级分类
|
||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
findRoots(options?: FindTreeOptions) {
|
findRoots(
|
||||||
|
options?: FindTreeOptions & {
|
||||||
|
onlyTrashed?: boolean;
|
||||||
|
withTrashed?: boolean;
|
||||||
|
},
|
||||||
|
) {
|
||||||
const escapeAlias = (alias: string) => this.manager.connection.driver.escape(alias);
|
const escapeAlias = (alias: string) => this.manager.connection.driver.escape(alias);
|
||||||
const escapeColumn = (column: string) => this.manager.connection.driver.escape(column);
|
const escapeColumn = (column: string) => this.manager.connection.driver.escape(column);
|
||||||
|
|
||||||
const joinColumn = this.metadata.treeParentRelation!.joinColumns[0];
|
const joinColumn = this.metadata.treeParentRelation!.joinColumns[0];
|
||||||
const parentPropertyName = joinColumn.givenDatabaseName || joinColumn.databaseName;
|
const parentPropertyName = joinColumn.givenDatabaseName || joinColumn.databaseName;
|
||||||
|
|
||||||
const qb = this.buildBaseQB().orderBy('category.customOrder', 'ASC');
|
const qb = this.buildBaseQB().orderBy('category.customOrder', 'ASC');
|
||||||
FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options);
|
qb.where(`${escapeAlias('category')}.${escapeColumn(parentPropertyName)} IS NULL`);
|
||||||
|
|
||||||
return qb
|
FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, pick(options, ['relations', 'depth']));
|
||||||
.where(`${escapeAlias('category')}.${escapeColumn(parentPropertyName)} IS NULL`)
|
|
||||||
.getMany();
|
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 entity
|
||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
findDescendants(entity: CategoryEntity, options?: FindTreeOptions) {
|
findDescendants(
|
||||||
|
entity: CategoryEntity,
|
||||||
|
options?: FindTreeOptions & {
|
||||||
|
onlyTrashed?: boolean;
|
||||||
|
withTrashed?: boolean;
|
||||||
|
},
|
||||||
|
) {
|
||||||
const qb = this.createDescendantsQueryBuilder('category', 'treeClosure', entity);
|
const qb = this.createDescendantsQueryBuilder('category', 'treeClosure', entity);
|
||||||
FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options);
|
FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options);
|
||||||
qb.orderBy('category.customOrder', 'ASC');
|
qb.orderBy('category.customOrder', 'ASC');
|
||||||
|
|
||||||
|
if (options?.withTrashed) {
|
||||||
|
qb.withDeleted();
|
||||||
|
if (options?.onlyTrashed) qb.where(`category.deletedAt IS NOT NULL`);
|
||||||
|
}
|
||||||
|
|
||||||
return qb.getMany();
|
return qb.getMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,11 +85,22 @@ export class CategoryRepository extends TreeRepository<CategoryEntity> {
|
|||||||
* @param entity
|
* @param entity
|
||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
findAncestors(entity: CategoryEntity, options?: FindTreeOptions) {
|
findAncestors(
|
||||||
|
entity: CategoryEntity,
|
||||||
|
options?: FindTreeOptions & {
|
||||||
|
onlyTrashed?: boolean;
|
||||||
|
withTrashed?: boolean;
|
||||||
|
},
|
||||||
|
) {
|
||||||
const qb = this.createAncestorsQueryBuilder('category', 'treeClosure', entity);
|
const qb = this.createAncestorsQueryBuilder('category', 'treeClosure', entity);
|
||||||
FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options);
|
FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options);
|
||||||
qb.orderBy('category.customOrder', 'ASC');
|
qb.orderBy('category.customOrder', 'ASC');
|
||||||
|
|
||||||
|
if (options?.withTrashed) {
|
||||||
|
qb.withDeleted();
|
||||||
|
if (options?.onlyTrashed) qb.where(`category.deletedAt IS NOT NULL`);
|
||||||
|
}
|
||||||
|
|
||||||
return qb.getMany();
|
return qb.getMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,4 +124,42 @@ export class CategoryRepository extends TreeRepository<CategoryEntity> {
|
|||||||
|
|
||||||
return data as 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 { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
import { isNil, omit } from 'lodash';
|
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 { CategoryEntity } from '@/modules/content/entities';
|
||||||
import { CategoryRepository } from '@/modules/content/repositories';
|
import { CategoryRepository } from '@/modules/content/repositories';
|
||||||
|
import { SelectTrashMode } from '@/modules/database/constants';
|
||||||
import { treePaginate } from '@/modules/database/helpers';
|
import { treePaginate } from '@/modules/database/helpers';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -17,9 +23,15 @@ export class CategoryService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询分类树
|
* 查询分类树
|
||||||
|
* @param options
|
||||||
*/
|
*/
|
||||||
async findTress() {
|
async findTrees(options: QueryCategoryTreeDto) {
|
||||||
return this.repository.findTrees();
|
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 分页选项
|
* @param options 分页选项
|
||||||
*/
|
*/
|
||||||
async paginate(options: QueryCategoryDto) {
|
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);
|
const data = await this.repository.toFlatTrees(tree);
|
||||||
|
|
||||||
return treePaginate(options, data);
|
return treePaginate(options, data);
|
||||||
@ -83,23 +100,36 @@ export class CategoryService {
|
|||||||
* 删除分类
|
* 删除分类
|
||||||
* @param id
|
* @param id
|
||||||
*/
|
*/
|
||||||
async delete(id: string) {
|
async delete(ids: string[], trash?: boolean) {
|
||||||
const item = await this.repository.findOneOrFail({
|
const items = await this.repository.find({
|
||||||
where: { id },
|
where: { id: In(ids) },
|
||||||
|
withDeleted: true,
|
||||||
relations: ['parent', 'children'],
|
relations: ['parent', 'children'],
|
||||||
});
|
});
|
||||||
|
|
||||||
// 把子分类提升一级
|
// 把子分类提升一级
|
||||||
if (!isNil(item.children) && item.children.length > 0) {
|
for (const item of items) {
|
||||||
const nchildren = [...item.children].map((c) => {
|
if (!isNil(item.children) && item.children.length > 0) {
|
||||||
c.parent = item.parent;
|
const nchildren = [...item.children].map((c) => {
|
||||||
return item;
|
c.parent = item.parent;
|
||||||
});
|
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;
|
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 { isNil } from 'lodash';
|
||||||
|
|
||||||
import { EntityNotFoundError, SelectQueryBuilder } from 'typeorm';
|
import { EntityNotFoundError, In, SelectQueryBuilder } from 'typeorm';
|
||||||
|
|
||||||
import { CreateCommentDto, QueryCommentDto, QueryCommentTreeDto } from '@/modules/content/dtos';
|
import { CreateCommentDto, QueryCommentDto, QueryCommentTreeDto } from '@/modules/content/dtos';
|
||||||
import { CommentEntity } from '@/modules/content/entities';
|
import { CommentEntity } from '@/modules/content/entities';
|
||||||
@ -82,11 +82,11 @@ export class CommentService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除评论
|
* 删除评论
|
||||||
* @param id
|
* @param ids
|
||||||
*/
|
*/
|
||||||
async delete(id: string) {
|
async delete(ids: string[]) {
|
||||||
const comment = await this.repository.findOneOrFail({ where: { id: id ?? null } });
|
const comments = await this.repository.find({ where: { id: In(ids) } });
|
||||||
return this.repository.remove(comment);
|
return this.repository.remove(comments);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -10,6 +10,7 @@ import { PostEntity } from '@/modules/content/entities';
|
|||||||
import { CategoryRepository, PostRepository, TagRepository } from '@/modules/content/repositories';
|
import { CategoryRepository, PostRepository, TagRepository } from '@/modules/content/repositories';
|
||||||
|
|
||||||
import { CategoryService } from '@/modules/content/services/category.service';
|
import { CategoryService } from '@/modules/content/services/category.service';
|
||||||
|
import { SelectTrashMode } from '@/modules/database/constants';
|
||||||
import { paginate } from '@/modules/database/helpers';
|
import { paginate } from '@/modules/database/helpers';
|
||||||
import { QueryHook } from '@/modules/database/types';
|
import { QueryHook } from '@/modules/database/types';
|
||||||
|
|
||||||
@ -108,9 +109,45 @@ export class PostService {
|
|||||||
* 删除文章
|
* 删除文章
|
||||||
* @param id
|
* @param id
|
||||||
*/
|
*/
|
||||||
async delete(id: string) {
|
async delete(ids: string[], trash?: boolean) {
|
||||||
const item = await this.repository.findOneByOrFail({ id });
|
const items = await this.repository.find({
|
||||||
return this.repository.remove(item);
|
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,
|
options: FindParams,
|
||||||
callback?: QueryHook<PostEntity>,
|
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') {
|
if (typeof isPublished === 'boolean') {
|
||||||
isPublished
|
isPublished
|
||||||
? qb.where({
|
? qb.where({
|
||||||
|
@ -1,10 +1,19 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
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 { CreateTagDto, QueryTagsDto, UpdateTagDto } from '@/modules/content/dtos';
|
||||||
|
import { TagEntity } from '@/modules/content/entities';
|
||||||
import { TagRepository } from '@/modules/content/repositories';
|
import { TagRepository } from '@/modules/content/repositories';
|
||||||
|
import { SelectTrashMode } from '@/modules/database/constants';
|
||||||
import { paginate } from '@/modules/database/helpers';
|
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 添加额外的查询
|
* @param callback 添加额外的查询
|
||||||
*/
|
*/
|
||||||
async paginate(options: QueryTagsDto) {
|
async paginate(options: QueryTagsDto) {
|
||||||
const qb = this.repository.buildBaseQB();
|
const qb = await this.buildListQuery(this.repository.buildBaseQB(), options);
|
||||||
return paginate(qb, options);
|
return paginate(qb, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,10 +63,64 @@ export class TagService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除标签
|
* 删除标签
|
||||||
* @param id
|
* @param ids
|
||||||
*/
|
*/
|
||||||
async delete(id: string) {
|
async delete(ids: string[], trash: boolean) {
|
||||||
const item = await this.repository.findOneByOrFail({ id });
|
const items = await this.repository.find({
|
||||||
return this.repository.remove(item);
|
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 元数据
|
* 自定义 Repository 元数据
|
||||||
*/
|
*/
|
||||||
export const CUSTOM_REPOSITORY_METADATA = 'CUSTOM_REPOSITORY_METADATA';
|
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