自定义验证规则与验证转义

学习目标

  • 编写自定义class-validator验证规则
  • 编译自定义ID验证管道
  • 验证后对数据进行自动转义为模型对象

应用编码

在实现本节内容之前,需要先把AppPipeforbidUnknownValues改成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[];
}

categoryname属性进行同级别唯一性验证

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>) 

修改传入单个参数的方法(destoryrestore),启用自定义管道验证与转义

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不报错为准即可)

发表评论

您的电子邮箱地址不会被公开。