feat: 数据验证和数据序列化处理
- 增加了DTO和验证 - 服务类和控制器加入了dto - 增加序列化拦截器,对数组和分页特殊处理 - 实体中分配了序列化装饰器 - 控制器引入拦截器
This commit is contained in:
parent
db1fcf05bc
commit
ef90d0550b
Binary file not shown.
@ -8,34 +8,50 @@ import {
|
|||||||
Patch,
|
Patch,
|
||||||
Post,
|
Post,
|
||||||
Query,
|
Query,
|
||||||
|
SerializeOptions,
|
||||||
|
UseInterceptors,
|
||||||
ValidationPipe,
|
ValidationPipe,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
|
||||||
|
import { CreatePostDto, UpdatePostDto } from '@/modules/content/dtos';
|
||||||
import { PostService } from '@/modules/content/services';
|
import { PostService } from '@/modules/content/services';
|
||||||
|
import { AppIntercepter } from '@/modules/core/providers';
|
||||||
import { PaginateOptions } from '@/modules/database/types';
|
import { PaginateOptions } from '@/modules/database/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文章控制器
|
* 文章控制器
|
||||||
* 负责处理与文章相关的请求,如获取文章列表、创建新文章等。
|
* 负责处理与文章相关的请求,如获取文章列表、创建新文章等。
|
||||||
*/
|
*/
|
||||||
|
@UseInterceptors(AppIntercepter)
|
||||||
@Controller('posts')
|
@Controller('posts')
|
||||||
export class PostController {
|
export class PostController {
|
||||||
constructor(private postService: PostService) {}
|
constructor(private postService: PostService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@SerializeOptions({ groups: ['post-list'] })
|
||||||
async list(
|
async list(
|
||||||
@Query()
|
@Query(
|
||||||
|
new ValidationPipe({
|
||||||
|
transform: true,
|
||||||
|
whitelist: true,
|
||||||
|
forbidNonWhitelisted: true,
|
||||||
|
forbidUnknownValues: true,
|
||||||
|
validationError: { target: false },
|
||||||
|
}),
|
||||||
|
)
|
||||||
options: PaginateOptions,
|
options: PaginateOptions,
|
||||||
) {
|
) {
|
||||||
return this.postService.paginate(options);
|
return this.postService.paginate(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
@SerializeOptions({ groups: ['post-detail'] })
|
||||||
async detail(@Param('id', new ParseUUIDPipe()) id: string) {
|
async detail(@Param('id', new ParseUUIDPipe()) id: string) {
|
||||||
return this.postService.detail(id);
|
return this.postService.detail(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@SerializeOptions({ groups: ['post-detail'] })
|
||||||
async store(
|
async store(
|
||||||
@Body(
|
@Body(
|
||||||
new ValidationPipe({
|
new ValidationPipe({
|
||||||
@ -46,12 +62,13 @@ export class PostController {
|
|||||||
groups: ['create'],
|
groups: ['create'],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
data: RecordAny,
|
data: CreatePostDto,
|
||||||
) {
|
) {
|
||||||
return this.postService.create(data);
|
return this.postService.create(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch()
|
@Patch()
|
||||||
|
@SerializeOptions({ groups: ['post-detail'] })
|
||||||
async update(
|
async update(
|
||||||
@Body(
|
@Body(
|
||||||
new ValidationPipe({
|
new ValidationPipe({
|
||||||
@ -62,12 +79,13 @@ export class PostController {
|
|||||||
groups: ['update'],
|
groups: ['update'],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
data: RecordAny,
|
data: UpdatePostDto,
|
||||||
) {
|
) {
|
||||||
return this.postService.update(data);
|
return this.postService.update(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
|
@SerializeOptions({ groups: ['post-detail'] })
|
||||||
async delete(@Param('id', new ParseUUIDPipe()) id: string) {
|
async delete(@Param('id', new ParseUUIDPipe()) id: string) {
|
||||||
return this.postService.delete(id);
|
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 './post.dto';
|
||||||
export * from './update-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 {
|
import {
|
||||||
BaseEntity,
|
BaseEntity,
|
||||||
Column,
|
Column,
|
||||||
@ -9,35 +10,46 @@ import {
|
|||||||
|
|
||||||
import { PostBodyType } from '@/modules/content/constants';
|
import { PostBodyType } from '@/modules/content/constants';
|
||||||
|
|
||||||
|
@Exclude()
|
||||||
@Entity('content_posts')
|
@Entity('content_posts')
|
||||||
export class PostEntity extends BaseEntity {
|
export class PostEntity extends BaseEntity {
|
||||||
|
@Expose()
|
||||||
@PrimaryColumn({ type: 'varchar', generated: 'uuid', length: '36' })
|
@PrimaryColumn({ type: 'varchar', generated: 'uuid', length: '36' })
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
@Column({ comment: '文章标题' })
|
@Column({ comment: '文章标题' })
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
|
@Expose({ groups: ['post-detail'] })
|
||||||
@Column({ comment: '文章内容', type: 'text' })
|
@Column({ comment: '文章内容', type: 'text' })
|
||||||
body: string;
|
body: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
@Column({ comment: '文章摘要', nullable: true })
|
@Column({ comment: '文章摘要', nullable: true })
|
||||||
summary: string;
|
summary: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
@Column({ comment: '关键字', type: 'simple-array', nullable: true })
|
@Column({ comment: '关键字', type: 'simple-array', nullable: true })
|
||||||
keywords?: string[];
|
keywords?: string[];
|
||||||
|
|
||||||
|
@Expose()
|
||||||
@Column({ comment: '文章类型', type: 'varchar', default: PostBodyType.MD })
|
@Column({ comment: '文章类型', type: 'varchar', default: PostBodyType.MD })
|
||||||
type: PostBodyType;
|
type: PostBodyType;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
@Column({ comment: '发布时间', type: 'varchar', nullable: true })
|
@Column({ comment: '发布时间', type: 'varchar', nullable: true })
|
||||||
publishedAt?: Date | null;
|
publishedAt?: Date | null;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
@CreateDateColumn({ comment: '创建时间' })
|
@CreateDateColumn({ comment: '创建时间' })
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
@UpdateDateColumn({ comment: '更新时间' })
|
@UpdateDateColumn({ comment: '更新时间' })
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
@Column({ comment: '文章自定义排序', default: 0 })
|
@Column({ comment: '文章自定义排序', default: 0 })
|
||||||
customOrder: number;
|
customOrder: number;
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import { isFunction, isNil, omit } from 'lodash';
|
|||||||
import { EntityNotFoundError, IsNull, Not, SelectQueryBuilder } from 'typeorm';
|
import { EntityNotFoundError, IsNull, Not, SelectQueryBuilder } from 'typeorm';
|
||||||
|
|
||||||
import { PostOrderType } from '@/modules/content/constants';
|
import { PostOrderType } from '@/modules/content/constants';
|
||||||
|
import { CreatePostDto, UpdatePostDto } from '@/modules/content/dtos';
|
||||||
import { PostEntity } from '@/modules/content/entities';
|
import { PostEntity } from '@/modules/content/entities';
|
||||||
import { PostRepository } from '@/modules/content/repositories';
|
import { PostRepository } from '@/modules/content/repositories';
|
||||||
|
|
||||||
@ -43,7 +44,7 @@ export class PostService {
|
|||||||
* 创建文章
|
* 创建文章
|
||||||
* @param data
|
* @param data
|
||||||
*/
|
*/
|
||||||
async create(data: Record<string, any>) {
|
async create(data: CreatePostDto) {
|
||||||
const item = await this.repository.save(data);
|
const item = await this.repository.save(data);
|
||||||
|
|
||||||
return this.detail(item.id);
|
return this.detail(item.id);
|
||||||
@ -53,7 +54,7 @@ export class PostService {
|
|||||||
* 更新文章
|
* 更新文章
|
||||||
* @param data
|
* @param data
|
||||||
*/
|
*/
|
||||||
async update(data: Record<string, any>) {
|
async update(data: UpdatePostDto) {
|
||||||
await this.repository.update(data.id, omit(data, ['id']));
|
await this.repository.update(data.id, omit(data, ['id']));
|
||||||
return this.detail(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