自定义验证规则与验证转义
学习目标
- 编写自定义
class-validator
验证规则 - 编译自定义
ID
验证管道 - 验证后对数据进行自动转义为模型对象
应用编码
在实现本节内容之前,需要先把AppPipe
的forbidUnknownValues
改成false
,否则会报400
错误
编写验证规则
验证器既可以定义为一个类而直接使用
@Validate(CustomTextLength, [3, 20], {
message: 'Wrong post title',
})
title: string;
也可以定义成一个装饰器,定义装饰器验证器需要预先编写好验证逻辑的函数或类
export function IsLongerThan(property: string, validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
name: 'isLongerThan',
target: object.constructor,
propertyName: propertyName,
constraints: [property],
options: validationOptions,
validator: {
validate(value: any, args: ValidationArguments) {
const [relatedPropertyName] = args.constraints;
const relatedValue = (args.object as any)[relatedPropertyName];
return typeof value === 'string' && typeof relatedValue === 'string' && value.length > relatedValue.length; // you can return a Promise<boolean> here as well, if you want to make async validation
},
},
});
};
新增以下验证规则
IsMatch
:判断两个字段的值是否相等的验证规则IsMatchPhone
: 验证是否为手机号(必须是"区域号.手机号"的形式)IsPassword
: 密码复杂度验证IsModelExist
: 数据存在性验证IsUnique
: 针对创建数据时的唯一性验证IsUniqueExist
: 针对更新数据时的唯一性验证IsTreeUnique
: 针对树形模型创建数据时同级别的唯一性验证IsTreeUniqueExist
: 针对树形模型更新数据时同级别的唯一性验证
所有验证类需要添加@ValidatorConstraint
装饰器
如果是与数据库相关的异步验证需要把async
选项设置为true
@ValidatorConstraint({ name: 'entityItemUniqueExist', async: true })
每个规则的代码都有注释,详细请看源文件
使用验证规则
为需要验证数据存在性的DTO
属性添加@IsModelExist
,例如
多个验证请设置
each:true
export class CreateCategoryDto {
@IsModelExist(CategoryEntity, { always: true, message: '父分类不存在' })
parent?: string;
}
export class CreatePostDto {
@IsModelExist(CategoryEntity, {
each: true,
always: true,
message: '分类不存在',
})
categories?: string[];
}
对category
的name
属性进行同级别唯一性验证
export class CreateCategoryDto {
...
@IsTreeUnique(CategoryEntity, {
groups: ['create'],
message: '分类名称重复',
})
@IsTreeUniqueExist(CategoryEntity, {
groups: ['update'],
message: '分类名称重复',
})
@MaxLength(25, {
always: true,
message: '分类名称长度不能超过$constraint1',
})
@IsNotEmpty({ groups: ['create'], message: '分类名称不得为空' })
@IsOptional({ groups: ['update'] })
name!: string;
}
验证中转义
为了提高代码的可用性,可以直接在DTO
中对一些数据进行查询,并把对象作为参数代替id
传入服务中的方法
ID
验证管道
自定义一个针对数据存在性验证的UUID
管道
// src/core/providers/parse-uuid-entity.pipe.ts
export class ParseUUIDEntityPipe<ET>
implements PipeTransform<string, Promise<ET | string | undefined>>
{
protected config: Config;
constructor(
// 需要验证的模型
protected readonly entity: ObjectType<ET>,
config?: Partial<Config>,
) {
// 合并配置
this.config = merge(
{
// 数据库连接名称
manager: 'default',
// 是否转义
transform: false,
// 查询中是否包含软删除数据
withDeleted: false,
},
config ?? {},
);
}
async transform(value: string, _metadata: ArgumentMetadata) {
if (value === undefined) return undefined;
// UUID验证
if (!isUUID(value)) {
throw new BadRequestException('id param must be an UUID');
}
const em = getManager(this.config.manager);
const val = await em.findOne(this.entity, {
where: { id: value },
withDeleted: this.config.withDeleted,
});
if (!val) {
throw new EntityNotFoundError(this.entity, value);
}
// 是否返回查询出来的对象
return this.config.transform ? val : value;
}
}
修改BaseDataService
中的以下方法,把原来传入的string
类型的ID
改成传入模型对象
有了预转义,getParent
方法就没用了,直接去掉即可,后面写好DTO
后在子服务类中修改
async delete(item: E, trash = true)
async deleteList(
data: E[],
params?: P,
trash?: boolean,
callback?: QueryHook<E>,
)
async deletePaginate(
data: E[],
pageOptions: PaginateDto<M>,
params?: P,
trash?: boolean,
callback?: QueryHook<E>,
)
async restore(item: E, callback?: QueryHook<E>)
async restoreList(data: E[], params?: P, callback?: QueryHook<E>)
修改传入单个参数的方法(destory
和restore
),启用自定义管道验证与转义
show
与服务中的detail
不需要提前转义和验证
在三个控制器中做如下修改,以CategoryController
为例
@Patch('restore/:category')
@SerializeOptions({ groups: ['category-detail'] })
async resore(
@Param(
'category',
new ParseUUIDEntityPipe(CategoryEntity, {
withDeleted: true,
transform: true,
}),
)
category: CategoryEntity,
) {
return this.categoryService.restore(category);
}
...
验证后转义
前面在自定义的全局验证管道中我们添加了一个transform
静态方法调用,用于验证后的转义
// src/core/providers/app.pipe.ts
if (typeof result.transform === 'function') {
result = await result.transform(result);
const { transform, ...data } = result;
result = data;
}
现在就可以在DTO
中定义这个静态方法来实现验证后转义了,以category.dto.ts
为例
定义两个方法,分别转义创建与更新时的parent
以及批量操作时的categories
属性
注意,类无法继承静态方法,所以子类中需要重新定义一下
// src/modules/content/dtos/category.dto.ts
const transformParent = async (obj: CreateCategoryDto | UpdateCategoryDto) => {
const em = getManager();
if (obj.parent) {
obj.parent = await em
.getCustomRepository(CategoryRepository)
.findOneOrFail(obj.parent);
}
return obj;
};
const transformCategories = async (
obj: DeleteCategoryMultiDto | RestoreCategoryMultiDto,
) => {
const em = getManager();
obj.categories = await em
.getCustomRepository(CategoryRepository)
.findByIds(obj.categories, { withDeleted: true });
return obj;
};
export class CreateCategoryDto {
...
static async transform(obj: CreateCategoryDto) {
return transformParent(obj);
}
}
export class UpdateCategoryDto extends PartialType(CreateCategoryDto) {
...
static async transform(obj: UpdateCategoryDto) {
return transformParent(obj);
}
}
最后别忘了在各个服务类中去掉一些重新性查找和修改一下参数类型(以vscode不报错为准即可)