feat: 数据验证和数据序列化处理
- 增加了DTO和验证 - 服务类和控制器加入了dto - 增加序列化拦截器,对数组和分页特殊处理 - 实体中分配了序列化装饰器 - 控制器引入拦截器
This commit is contained in:
parent
db1fcf05bc
commit
ef90d0550b
Binary file not shown.
@ -8,34 +8,50 @@ import {
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
SerializeOptions,
|
||||
UseInterceptors,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { CreatePostDto, UpdatePostDto } from '@/modules/content/dtos';
|
||||
import { PostService } from '@/modules/content/services';
|
||||
import { AppIntercepter } from '@/modules/core/providers';
|
||||
import { PaginateOptions } from '@/modules/database/types';
|
||||
|
||||
/**
|
||||
* 文章控制器
|
||||
* 负责处理与文章相关的请求,如获取文章列表、创建新文章等。
|
||||
*/
|
||||
@UseInterceptors(AppIntercepter)
|
||||
@Controller('posts')
|
||||
export class PostController {
|
||||
constructor(private postService: PostService) {}
|
||||
|
||||
@Get()
|
||||
@SerializeOptions({ groups: ['post-list'] })
|
||||
async list(
|
||||
@Query()
|
||||
@Query(
|
||||
new ValidationPipe({
|
||||
transform: true,
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
forbidUnknownValues: true,
|
||||
validationError: { target: false },
|
||||
}),
|
||||
)
|
||||
options: PaginateOptions,
|
||||
) {
|
||||
return this.postService.paginate(options);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@SerializeOptions({ groups: ['post-detail'] })
|
||||
async detail(@Param('id', new ParseUUIDPipe()) id: string) {
|
||||
return this.postService.detail(id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@SerializeOptions({ groups: ['post-detail'] })
|
||||
async store(
|
||||
@Body(
|
||||
new ValidationPipe({
|
||||
@ -46,12 +62,13 @@ export class PostController {
|
||||
groups: ['create'],
|
||||
}),
|
||||
)
|
||||
data: RecordAny,
|
||||
data: CreatePostDto,
|
||||
) {
|
||||
return this.postService.create(data);
|
||||
}
|
||||
|
||||
@Patch()
|
||||
@SerializeOptions({ groups: ['post-detail'] })
|
||||
async update(
|
||||
@Body(
|
||||
new ValidationPipe({
|
||||
@ -62,12 +79,13 @@ export class PostController {
|
||||
groups: ['update'],
|
||||
}),
|
||||
)
|
||||
data: RecordAny,
|
||||
data: UpdatePostDto,
|
||||
) {
|
||||
return this.postService.update(data);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@SerializeOptions({ groups: ['post-detail'] })
|
||||
async delete(@Param('id', new ParseUUIDPipe()) id: string) {
|
||||
return this.postService.delete(id);
|
||||
}
|
||||
|
@ -1,24 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { IsNotEmpty, IsOptional, MaxLength } from 'class-validator';
|
||||
|
||||
@Injectable()
|
||||
export class CreatePostDto {
|
||||
@MaxLength(255, {
|
||||
always: true,
|
||||
message: '帖子标题长度最大为$constraint1',
|
||||
})
|
||||
@IsNotEmpty({ groups: ['create'], message: '帖子标题必需填写' })
|
||||
@IsOptional({ groups: ['update'] })
|
||||
title: string;
|
||||
|
||||
@IsNotEmpty({ groups: ['create'], message: '帖子内容必需填写' })
|
||||
@IsOptional({ groups: ['updatae'] })
|
||||
body: string;
|
||||
|
||||
@MaxLength(500, {
|
||||
always: true,
|
||||
message: '帖子描述长度最大为$constraint1',
|
||||
})
|
||||
@IsOptional({ always: true })
|
||||
summary: string;
|
||||
}
|
@ -1,2 +1 @@
|
||||
export * from './create-post.dto';
|
||||
export * from './update-post.dto';
|
||||
export * from './post.dto';
|
||||
|
105
src/modules/content/dtos/post.dto.ts
Normal file
105
src/modules/content/dtos/post.dto.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
import {
|
||||
IsBoolean,
|
||||
IsDateString,
|
||||
IsDefined,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsUUID,
|
||||
MaxLength,
|
||||
Min,
|
||||
ValidateIf,
|
||||
} from 'class-validator';
|
||||
|
||||
import { isNil, toNumber } from 'lodash';
|
||||
|
||||
import { PostOrderType } from '@/modules/content/constants';
|
||||
import { toBoolean } from '@/modules/core/helpers';
|
||||
import { PaginateOptions } from '@/modules/database/types';
|
||||
|
||||
/**
|
||||
* 文章分页查询验证
|
||||
*/
|
||||
export class QueryPostDto implements PaginateOptions {
|
||||
@Transform(({ value }) => toBoolean(value))
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
isPublished?: boolean;
|
||||
|
||||
@IsEnum(PostOrderType, {
|
||||
message: `排序规则必需是${Object.values(PostOrderType).join(',')}中的一项`,
|
||||
})
|
||||
orderBy?: PostOrderType;
|
||||
|
||||
@Transform(({ value }) => toNumber(value))
|
||||
@Min(1, { message: '当前页必需大于1' })
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
page = 1;
|
||||
|
||||
@Transform(({ value }) => toNumber(value))
|
||||
@Min(1, { message: '每页显示数据必须大于1' })
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
limit: 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章创建验证
|
||||
*/
|
||||
export class CreatePostDto {
|
||||
@MaxLength(255, {
|
||||
always: true,
|
||||
message: '文章标题长度最大为$constraint1',
|
||||
})
|
||||
@IsNotEmpty({
|
||||
groups: ['create'],
|
||||
message: '文章标题为必填',
|
||||
})
|
||||
@IsOptional({ groups: ['update'] })
|
||||
title: string;
|
||||
|
||||
@IsNotEmpty({ groups: ['create'], message: '文章内容为必填' })
|
||||
@IsOptional({ groups: ['update'] })
|
||||
body: string;
|
||||
|
||||
@MaxLength(500, {
|
||||
always: true,
|
||||
message: '文章摘要长度最大为$constraint1',
|
||||
})
|
||||
@IsOptional({ always: true })
|
||||
summary?: string;
|
||||
|
||||
@IsDateString({ strict: true }, { always: true })
|
||||
@IsOptional({ always: true })
|
||||
@ValidateIf((value) => !isNil(value.publishedAt))
|
||||
@Transform(({ value }) => (value === 'null' ? null : value))
|
||||
publishedAt?: Date;
|
||||
|
||||
@MaxLength(20, {
|
||||
each: true,
|
||||
always: true,
|
||||
message: '每个关键字长度最大为$constraint1',
|
||||
})
|
||||
@IsOptional({ always: true })
|
||||
keywords?: string[];
|
||||
|
||||
@Transform(({ value }) => toNumber(value))
|
||||
@Min(0, { always: true, message: '排序值必须大于0' })
|
||||
@IsNumber(undefined, { always: true })
|
||||
@IsOptional({ always: true })
|
||||
customOrder = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章更新验证
|
||||
*/
|
||||
export class UpdatePostDto extends PartialType(CreatePostDto) {
|
||||
@IsUUID(undefined, { groups: ['update'], message: '文章ID格式错误' })
|
||||
@IsDefined({ groups: ['update'], message: '文章ID必须指定' })
|
||||
id: string;
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
|
||||
import { IsDefined, IsNumber } from 'class-validator';
|
||||
|
||||
import { CreatePostDto } from './create-post.dto';
|
||||
|
||||
@Injectable()
|
||||
export class UpdatePostDto extends PartialType(CreatePostDto) {
|
||||
@IsNumber(undefined, { groups: ['update'], message: '帖子ID格式错误' })
|
||||
@IsDefined({ groups: ['update'], message: '帖子ID必需指定' })
|
||||
id: number;
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import { Exclude, Expose } from 'class-transformer';
|
||||
import {
|
||||
BaseEntity,
|
||||
Column,
|
||||
@ -9,35 +10,46 @@ import {
|
||||
|
||||
import { PostBodyType } from '@/modules/content/constants';
|
||||
|
||||
@Exclude()
|
||||
@Entity('content_posts')
|
||||
export class PostEntity extends BaseEntity {
|
||||
@Expose()
|
||||
@PrimaryColumn({ type: 'varchar', generated: 'uuid', length: '36' })
|
||||
id: string;
|
||||
|
||||
@Expose()
|
||||
@Column({ comment: '文章标题' })
|
||||
title: string;
|
||||
|
||||
@Expose({ groups: ['post-detail'] })
|
||||
@Column({ comment: '文章内容', type: 'text' })
|
||||
body: string;
|
||||
|
||||
@Expose()
|
||||
@Column({ comment: '文章摘要', nullable: true })
|
||||
summary: string;
|
||||
|
||||
@Expose()
|
||||
@Column({ comment: '关键字', type: 'simple-array', nullable: true })
|
||||
keywords?: string[];
|
||||
|
||||
@Expose()
|
||||
@Column({ comment: '文章类型', type: 'varchar', default: PostBodyType.MD })
|
||||
type: PostBodyType;
|
||||
|
||||
@Expose()
|
||||
@Column({ comment: '发布时间', type: 'varchar', nullable: true })
|
||||
publishedAt?: Date | null;
|
||||
|
||||
@Expose()
|
||||
@CreateDateColumn({ comment: '创建时间' })
|
||||
createdAt: Date;
|
||||
|
||||
@Expose()
|
||||
@UpdateDateColumn({ comment: '更新时间' })
|
||||
updatedAt: Date;
|
||||
|
||||
@Expose()
|
||||
@Column({ comment: '文章自定义排序', default: 0 })
|
||||
customOrder: number;
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import { isFunction, isNil, omit } from 'lodash';
|
||||
import { EntityNotFoundError, IsNull, Not, SelectQueryBuilder } from 'typeorm';
|
||||
|
||||
import { PostOrderType } from '@/modules/content/constants';
|
||||
import { CreatePostDto, UpdatePostDto } from '@/modules/content/dtos';
|
||||
import { PostEntity } from '@/modules/content/entities';
|
||||
import { PostRepository } from '@/modules/content/repositories';
|
||||
|
||||
@ -43,7 +44,7 @@ export class PostService {
|
||||
* 创建文章
|
||||
* @param data
|
||||
*/
|
||||
async create(data: Record<string, any>) {
|
||||
async create(data: CreatePostDto) {
|
||||
const item = await this.repository.save(data);
|
||||
|
||||
return this.detail(item.id);
|
||||
@ -53,7 +54,7 @@ export class PostService {
|
||||
* 更新文章
|
||||
* @param data
|
||||
*/
|
||||
async update(data: Record<string, any>) {
|
||||
async update(data: UpdatePostDto) {
|
||||
await this.repository.update(data.id, omit(data, ['id']));
|
||||
return this.detail(data.id);
|
||||
}
|
||||
|
0
src/modules/core/providers/app.interceptor.ts
Normal file
0
src/modules/core/providers/app.interceptor.ts
Normal file
40
src/modules/core/providers/index.ts
Normal file
40
src/modules/core/providers/index.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import {
|
||||
ClassSerializerContextOptions,
|
||||
ClassSerializerInterceptor,
|
||||
PlainLiteralObject,
|
||||
StreamableFile,
|
||||
} from '@nestjs/common';
|
||||
import { PinoLoggerOptions } from 'fastify/types/logger';
|
||||
import { isArray, isNil, isObject } from 'lodash';
|
||||
|
||||
export class AppIntercepter extends ClassSerializerInterceptor {
|
||||
serialize(
|
||||
response: PlainLiteralObject | PlainLiteralObject[],
|
||||
options: ClassSerializerContextOptions,
|
||||
): PlainLiteralObject | PlainLiteralObject[] {
|
||||
if ((!isObject(response) && !isArray(response)) || response instanceof StreamableFile) {
|
||||
return response;
|
||||
}
|
||||
|
||||
// 数组处理 - 如果是数组则对数组每一项元素序列化
|
||||
if (isArray(response)) {
|
||||
return (response as PlainLiteralObject[]).map((item) =>
|
||||
!isObject(item) ? item : this.transformToPlain(item, options),
|
||||
);
|
||||
}
|
||||
|
||||
// 分页处理 - 对items中的每一项进行序列化
|
||||
if ('meta' in response && 'items' in response) {
|
||||
const items = !isNil(response.items) && isArray(response.items) ? response.items : [];
|
||||
|
||||
return {
|
||||
...response,
|
||||
items: (items as PinoLoggerOptions[]).map((item) =>
|
||||
!isObject(item) ? item : this.transformToPlain(item, options),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return this.transformToPlain(response, options);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user