post and soft delete

This commit is contained in:
liuyi 2025-05-31 18:46:12 +08:00
parent 427997f1cb
commit b7bb509b70
7 changed files with 109 additions and 9 deletions

View File

@ -14,6 +14,8 @@ import {
import { CreatePostDto, QueryPostDto, UpdatePostDto } from '@/modules/content/dtos/post.dto';
import { PostService } from '@/modules/content/services/post.service';
import { DeleteWithTrashDto, RestoreDto } from '../dtos/delete.with.trash.dto';
@Controller('posts')
export class PostController {
constructor(private postService: PostService) {}
@ -51,9 +53,18 @@ 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) {
return this.postService.delete(data.ids, data.trash);
}
@Patch('restore')
@SerializeOptions({ groups: ['post-detail'] })
async restore(
@Body()
data: RestoreDto,
) {
return this.postService.restore(data.ids);
}
}

View File

@ -0,0 +1,10 @@
import { IsUUID, IsDefined } from 'class-validator';
import { DtoValidation } from '@/modules/core/decorator/dto.validation.decorator';
@DtoValidation()
export class DeleteDto {
@IsUUID(undefined, { each: true, always: true, message: 'The ID format is incorrect' })
@IsDefined({ each: true, message: 'The ID must be specified' })
ids: string[];
}

View File

@ -0,0 +1,19 @@
import { Transform } from 'class-transformer';
import { IsBoolean, IsDefined, IsOptional, IsUUID } from 'class-validator';
import { toBoolean } from 'validator';
import { DeleteDto } from './delete.dto';
export class DeleteWithTrashDto extends DeleteDto {
@Transform(({ value }) => toBoolean(value))
@IsBoolean()
@IsOptional()
trash?: boolean;
}
export class RestoreDto {
@IsUUID(undefined, { each: true, always: true, message: 'The ID format is incorrect' })
@IsDefined({ each: true, message: 'The ID must be specified' })
ids: string[];
}

View File

@ -19,6 +19,7 @@ import { isNil, toNumber } from 'lodash';
import { PostOrder } from '@/modules/content/constants';
import { DtoValidation } from '@/modules/core/decorator/dto.validation.decorator';
import { toBoolean } from '@/modules/core/helpers';
import { SelectTrashMode } from '@/modules/database/constants';
import { IsDataExist } from '@/modules/database/constraints/data.exist.constraint';
import { PaginateOptions } from '@/modules/database/types';
@ -35,7 +36,7 @@ export class QueryPostDto implements PaginateOptions {
message: `The sorting rule must be one of ${Object.values(PostOrder).join(',')}`,
})
@IsOptional()
orderBy: PostOrder;
orderBy?: PostOrder;
@Transform(({ value }) => toNumber(value))
@Min(1, { always: true, message: 'The current page must be greater than 1.' })
@ -52,6 +53,10 @@ export class QueryPostDto implements PaginateOptions {
@IsOptional()
limit = 10;
@IsEnum(SelectTrashMode)
@IsOptional()
trashed?: SelectTrashMode;
@IsDataExist(CategoryEntity, { always: true, message: 'The category does not exist' })
@IsUUID(undefined, { message: 'The ID format is incorrect' })
@IsOptional()

View File

@ -3,6 +3,7 @@ import {
BaseEntity,
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
JoinTable,
ManyToMany,
@ -64,6 +65,11 @@ export class PostEntity extends BaseEntity {
@UpdateDateColumn({ comment: '更新时间', nullable: true })
updatedAt?: Date;
@Expose()
@Type(() => Date)
@DeleteDateColumn({ comment: '删除时间' })
deleteAt: Date;
@Expose()
commentCount: number;
@ -76,7 +82,7 @@ export class PostEntity extends BaseEntity {
@Expose()
@Type(() => TagEntity)
@ManyToMany(() => TagEntity, (tag) => tag.posts, { cascade: true })
@ManyToMany(() => TagEntity, (tag) => tag.posts, { cascade: ['insert', 'update', 'remove'] })
@JoinTable()
tags: Relation<TagEntity>[];

View File

@ -9,6 +9,7 @@ import { CreatePostDto, QueryPostDto, UpdatePostDto } from '@/modules/content/dt
import { PostEntity } from '@/modules/content/entities/post.entity';
import { CategoryRepository } from '@/modules/content/repositories';
import { PostRepository } from '@/modules/content/repositories/post.repository';
import { SelectTrashMode } from '@/modules/database/constants';
import { QueryHook } from '@/modules/database/types';
import { paginate } from '@/modules/database/utils';
@ -88,9 +89,42 @@ export class PostService {
return this.detail(data.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
.buildBaseQB()
.where('post.id IN (:...ids)', { ids })
.withDeleted()
.getMany();
let result: PostEntity[] = [];
if (trash) {
const directs = items.filter((item) => !isNil(item.deleteAt));
const softs = items.filter((item) => isNil(item.deleteAt));
result = [
...(await this.repository.remove(directs)),
...(await this.repository.softRemove(softs)),
];
} else {
result = await this.repository.remove(items);
}
return result;
}
async restore(ids: string[]) {
const items = await this.repository
.buildBaseQB()
.where('post.id IN (:...ids)', { ids })
.withDeleted()
.getMany();
const trasheds = items.filter((item) => !isNil(item.deleteAt));
const trashedIds = trasheds.map((item) => item.id);
if (trashedIds.length < 1) {
return [];
}
await this.repository.restore(trashedIds);
const qb = await this.buildListQuery(this.repository.buildBaseQB(), {}, async (qbuilder) =>
qbuilder.andWhereInIds(trashedIds),
);
return qb.getMany();
}
protected async buildListQuery(
@ -98,12 +132,18 @@ export class PostService {
options: FindParams,
callback?: QueryHook<PostEntity>,
) {
const { orderBy, isPublished, category, tag } = options;
const { orderBy, isPublished, category, tag, trashed } = options;
if (typeof isPublished === 'boolean') {
isPublished
? qb.where({ publishedAt: Not(IsNull()) })
: qb.where({ publishedAt: IsNull() });
}
if (trashed === SelectTrashMode.ALL || trashed === SelectTrashMode.ONLY) {
qb.withDeleted();
if (trashed === SelectTrashMode.ONLY) {
qb.where('post.deletedAt is not null');
}
}
this.queryOrderBy(qb, orderBy);
if (category) {
await this.queryByCategory(category, qb);

View File

@ -1 +1,10 @@
export const CUSTOM_REPOSITORY_METADATA = 'CUSTOM_REPOSITORY_METADATA';
export enum SelectTrashMode {
// ALL: 包含已软删除和未软删除的数据(同时查询正常数据和回收站中的数据)
ALL = 'all',
// ONLY: 只包含软删除的数据 (只查询回收站中的数据)
ONLY = 'only',
// NONE: 只包含未软删除的数据 (只查询正常数据)
NONE = 'none',
}