Parameter verification
We often need to perform type checking and parameter conversion when calling a method. Midway provides a simple ability to quickly check the type of a parameter. This ability comes from joi.
Related information:
Description | |
---|---|
Can be used for standard projects | ✅ |
Can be used for Serverless | ✅ |
Can be used for integration | ✅ |
Contains independent main framework | ❌ |
Contains independent logs | ❌ |
Background
The most commonly used parameter check is the controller (Controller), and you can also use this capability in any Class.
Let's take the user used in the controller (Controller) as an example.
➜ my_midway_app tree
.
├── src
│ ├── controller
│ │ └── user.ts
│ ├── interface.ts
│ └── service
│ └── user.ts
├── test
├── package.json
└── tsconfig.json
Under normal circumstances, we obtain all post results from the body
and perform some verifications.
// src/interface.ts
export interface User {
id: number;
firstName: string;
lastName: string;
age: number;
}
// src/controller/home.ts
import { Controller, Get, Provide } from '@midwayjs/core';
@Controller('/api/user')
export class HomeController {
@Post('/')
async updateUser(@Body() user: User ) {
if ( !user.id || typeof user.id !== 'number') {
throw new Error('id error');
}
if (user.age <= 30) {
throw new Error('age not match');
}
// xxx
}
}
If each method needs to be verified in this way, it will be very complicated.
In response to this situation, Midway provides Validate components. @Validate
and @Rule
decorators are used to quickly define verification rules to help users reduce these duplicate codes.
Note that starting with v3, @Rule
and @Validate
decorators are exported from @midwayjs/validate
.
Installation dependency
$ npm i @midwayjs/validate@3 --save
Or reinstall the following dependencies in package.json
.
{
"dependencies": {
"@midwayjs/validate": "^3.0.0",
// ...
},
"devDependencies": {
// ...
}
}
Open the component
Add components to configuration.ts
.
import { Configuration, App } from '@midwayjs/core';
import * as koa from '@midwayjs/koa';
import * as validate from '@midwayjs/validate';
import { join } from 'path';
@Configuration({
imports: [koa, validate],
importConfigs: [join(__dirname, './config')]
})
export class MainConfiguration {
@App()
app: koa.Application;
async onReady() {
// ...
}
}
Define inspection rules
According to the above logic, we need to redefine a new Class because the decorator can only be decorated on the actual Class, not the interface.
To facilitate subsequent processing, we put the user in a src/dto
directory.
Data Transfer Object DTO is a simple container for a set of aggregated data that needs to be transmitted across process or network boundaries. It should not contain business logic and limit its behavior to activities such as internal consistency checking and basic verification.
// src/dto/user.ts
import { Rule, RuleType } from '@midwayjs/validate';
export class UserDTO {
@Rule(RuleType.number().required())
id: number;
@Rule(RuleType.string().required())
firstName: string;
@Rule(RuleType.string().max(10))
lastName: string;
@Rule(RuleType.number().max(60))
age: number;
}
Since this class belongs to a PlainObject
and does not need to be managed by dependency injection, we do not need to provide a @Provide
decorator.
This User Class provides three attributes and their corresponding verification rules.
id
is a required number type.firstName
a required string typelastName
an optional string type with a maximum length of 10- A maximum number of
age
is not more than 60.
The @Rule
decorator is used to modify the attributes that need to be verified. Its parameters are a chain method of verification rules provided by the RuleType
object.
The RuleType
here is the joi object itself.
joi provides a lot of verification types. You can also verify fields in objects and arrays, such as RuleType.string().email()
commonly used for strings, and regular check for RuleType.string().pattern(/xxxx/)
. You can query API documents of joi.
Check parameters
After defining the type, it can be directly used in the business code.
// src/controller/home.ts
import { Controller, Get, Provide, Body } from '@midwayjs/core';
import { UserDTO } from './dto/user';
@Controller('/api/user')
export class HomeController {
@Post('/')
async updateUser(@Body() user: UserDTO ) {
// user.id
}
}
All the verification codes have disappeared, and the business has become purer. Of course, remember to replace the original user interface with Class.
Once the verification fails, the browser or console will report a similar error.
ValidationError: "id" is required
In addition, because the type of id
is defined, the id is automatically changed to a number when a string is obtained.
async updateUser(@Body() user: UserDTO ) {
// typeof user.id === 'number'
}
If you need to configure information separately at the method level, you can use the @Validate
decorator, such as configuring the error status separately.
// src/controller/home.ts
import { Controller, Get, Provide } from '@midwayjs/core';
import { Validate } from '@midwayjs/validate';
import { UserDTO } from './dto/user';
@Controller('/api/user')
export class HomeController {
@Post('/')
@Validate({
errorStatus: 422,
})
async updateUser(@Body() user: UserDTO ) {
// user.id
}
}
In general, use the global default configuration.
General scenario verification
If the parameter is not a DTO, you can use the @Valid
decorator for verification. The @Valid
decorator can directly pass a Joi rule.
// src/controller/home.ts
import { Controller, Get, Query } from '@midwayjs/core';
import { Valid, RuleType } from '@midwayjs/validate';
import { UserDTO } from './dto/user';
@Controller('/api/user')
export class HomeController {
@get('/')
async getUser(@Valid(RuleType.number().required()) @Query('id') id: number) {
// ...
}
}
In non-Web scenarios, if there are no Web class decorators such as @Body
, you can also use the @Valid
decorator for verification. If no parameters are passed, the DTO rules will be reused.
For example in a service:
import { Valid } from '@midwayjs/validate';
import { Provide } from '@midwayjs/core';
import { UserDTO } from './dto/user';
@Provide()
export class UserService {
async updateUser(@Valid() user: UserDTO ) {
//...
}
}
If the parameter is not DTO, there is no rule, and a validation rule in Joi format can also be passed through the parameter.
import { Valid, RuleType } from '@midwayjs/validate';
import { Provide } from '@midwayjs/core';
@Provide()
export class UserService {
async updateUser(@Valid(RuleType. number(). required()) userAge: number ) {
//...
}
}
Validate pipeline
If your parameters are basic types, such as number
, string
, boolean
, you can use the pipeline provided by the component for validation.
The default web parameter decorators can all be piped as the second argument.
for example:
import { ParseIntPipe } from '@midwayjs/validate';
@Controller('/api/user')
export class HomeController {
@Post('/update_age')
async updateAge(@Body('age', [ParseIntPipe]) age: number ) {
//...
}
}
The ParseIntPipe
pipeline can convert strings and numeric data into numbers, so that the age
field obtained from the request parameters will pass the validation of the pipeline and be converted into a numeric format.
The built-in pipelines that can be used are:
ParseIntPipe
ParseFloatPipe
ParseBoolPipe
DefaultValuePipe
ParseIntPipe
is used to convert an argument to an integer.
import { ParseIntPipe } from '@midwayjs/validate';
//...
async update(@Body('age', [ParseIntPipe]) age: number) {
return age;
}
update({ age: '12'} ); => 12
update({ age: '12.2'} ); => Error
update({ age: 'abc'} ); => Error
ParseFloatPipe
is used to convert the parameter to a floating point number.
import { ParseFloatPipe } from '@midwayjs/validate';
//...
async update(@Body('size', [ParseFloatPipe]) size: number) {
return size;
}
update({ size: '12.2'} ); => 12.2
update({ size: '12'} ); => 12
ParseBoolPipe
is used to convert parameters to boolean values.
import { ParseBoolPipe } from '@midwayjs/validate';
//...
async update(@Body('isMale', [ParseBoolPipe]) isMale: boolean) {
return isMale;
}
update({ isMale: 'true'} ); => true
update({ isMale: '0'} ); => Error
DefaultValuePipe
is used to set the default value.
import { DefaultValuePipe } from '@midwayjs/validate';
//...
async update(@Body('nickName', [new DefaultValuePipe('anonymous')]) nickName: string) {
return nickName;
}
update({ nickName: undefined} ); => 'anonymous'
Custom validate pipeline
If the default pipeline does not meet the requirements, you can quickly implement a custom validation pipeline through inheritance. The component has provided a ParsePipe
class for quick writing.
import { Pipe } from '@midwayjs/core';
import { ParsePipe, RuleType } from '@midwayjs/validate';
@Pipe()
export class ParseCustomDataPipe extends ParsePipe {
getSchema(): RuleType. AnySchema<any> {
//...
}
}
getSchema
method is used to return a validation rule conforming to Joi
format.
For example, the code of ParseIntPipe
is as follows. When the pipeline is executed, the schema will be automatically obtained for verification, and the value will be returned after the verification is successful.
import { Pipe } from '@midwayjs/core';
import { ParsePipe, RuleType } from '@midwayjs/validate';
@Pipe()
export class ParseIntPipe extends ParsePipe {
getSchema() {
return RuleType.number().integer().required();
}
}
Check rule
Common verification writing
RuleType.number().required(); // Number, required
RuleType.string().empty('') // string is not required
RuleType.number().max(10).min(1); //Number, Maximum and Minimum
RuleType.number().greater(10).less(50); // Number, greater than 10, less than 50
RuleType.string().max(10).min(5); //String, maximum length 10, minimum 5
RuleType.string().length(20); //String, length 20
RuleType.string().pattern(/^[abc]+$/); // String, matching regular format
RuleType.object().length(5); // Object, key number equals 5
RuleType.array().items(RuleType.string()); //Array, each element is a string
RuleType.array().max(10); // Array, maximum length is 10
RuleType.array().min(10); //Array, minimum length is 10
RuleType.array().length(10); // Array, length 10
RuleType.string().allow('') // non-required fields pass in an empty string
export enum DeviceType {
iOS = 'ios',
Android = 'android',
}
RuleType.string().valid(...Object.values(DeviceType)) // validate by enum
Cascade Check
Midway supports that the attribute in the Class for each check is still an object.
We add an attribute school
to UserDTO
and give a SchoolDTO
type.
import { Rule, RuleType } from '@midwayjs/validate';
export class SchoolDTO {
@Rule(RuleType.string().required())
name: string;
@Rule(RuleType.string())
address: string;
}
export class UserDTO {
@Rule(RuleType.number().required())
id: number;
@Rule(RuleType.string().required())
firstName: string;
@Rule(RuleType.string().max(10))
lastName: string;
// Complex object
@Rule(getSchema(SchoolDTO).required())
school: SchoolDTO;
// Object array.
@Rule(RuleType.array().items(getSchema(SchoolDTO)).required())
schoolList: SchoolDTO[];
}
In this case, the parameter of the @Rule
decorator can be the type that needs to be verified.
Inheritance check
Midway supports the verification inheritance method, which allows developers to verify parameters when they extract common object attributes.
For example, we CommonUserDTO
the following to extract some common attributes of the interface, and then UserDTO
specific parameters required as special interfaces.
import { Rule, RuleType } from '@midwayjs/validate';
export class CommonUserDTO {
@Rule(RuleType.string().required())
token: string;
@Rule(RuleType.string())
workId: string;
}
export class UserDTO extends CommonUserDTO {
@Rule(RuleType.string().required())
name: string;
}
The old version needs to be added to the subclass, the new version does not need ~
If the attribute name is the same, the rule of the current attribute is taken for verification and will not be merged with the parent class.
Multi-type verification
Starting from v3.4.5, Midway supports different types of verification for a certain attribute.
For example, a type can be either a normal type or a complex type.
import { Rule, RuleType, getSchema } from '@midwayjs/validate';
export class SchoolDTO {
@Rule(RuleType.string().required())
name: string;
@Rule(RuleType.string())
address: string;
}
export class UserDTO {
@Rule(RuleType.string().required())
name: string;
@Rule(RuleType.alternatives([RuleType.string(), getSchema(SchoolDTO)]).required())
school: string | SchoolDTO;
}
We can use getSchema
methods to get the current joi schema from a DTO to perform complex logical processing.
Create a new DTO from the original DTO
Sometimes, we want to get some attributes from a DTO and become a new DTO class.
Midway provides PickDto
and OmitDto
methods to create a new DTO based on the existing DTO type.
The PickDto
is used to get some attributes from the existing DTO and become the new DTO, while the OmitDto
is used to remove some of them, such:
// src/dto/user.ts
import { Rule, RuleType, PickDto } from '@midwayjs/validate';
export class UserDTO {
@Rule(RuleType.number().required())
id: number;
@Rule(RuleType.string().required())
firstName: string;
@Rule(RuleType.string().max(10))
lastName: string;
@Rule(RuleType.number().max(60))
age: number;
}
// Inherit a new DTO
export class SimpleUserDTO extends PickDto(UserDTO, ['firstName', 'lastName']) {}
// const simpleUser = new SimpleUserDTO();
// Contains only firstName and lastName attributes
// simpleUser.firstName = xxx
export class NewUserDTO extends OmitDto(UserDTO, ['age']) {}
// const newUser = new NewUserDTO();
// newUser.age definition and attribute do not exist
// Use
async login(@Body() user: NewUserDTO) {
// ...
}
Reuse verification rules
If many fields are required for strings or similar requirements, writing the RuleType.string().required()
is a bit long, and the duplicate part can be assigned to a new rule object for reuse.
// Define your department's specifications or commonly used ones in a document yourself.
const requiredString = RuleType.string().required();
export class UserDTO {
@Rule(requiredString) // So you don't have to write it so long
name: string;
@Rule(requiredString) // Same as above
nickName: string;
@Rule(requiredString) // Same as above
description: string;
}
// Define your department's specifications or commonly used ones in a document yourself.
const maxString = (length)=> RuleType.string().max(length);
export class UserDTO {
@Rule(requiredString) // Same as above
name: string;
@Rule(requiredString) // Same as above
nickName: string;
@Rule(requiredString) // Same as above
description: string;
@Rule(maxString(50)) // This way, you can change the parameter
info: string;
@Rule(maxString(50).required()) // This will do
info2: string;
}
Multilingual
In Validate, the i18n component is also relied on to internationalize check messages.
By default, both en_US
and zh_CN
are available. When a request fails, the specified language is returned.
Specify the language through the decorator
By default, messages will be returned following the defaultLocale
of i18n components and the browser's access language. However, we can specify the currently translated language in the decorator, such:
@Controller('/user')
export class UserController {
@Post('/')
@Validate({
locale: 'en_US',
})
async getUser(@Body() bodyData: UserDTO) {
// ...
}
}
Specify language through parameters
In addition to decorator designation, we can also use the standard i18n to specify the language through parameters.
For example, Query parameters.
Get /user/get_user?locale=zh_CN
For more information, see i18n.
Translation in other languages
By default, Midway provides both en_US
and zh_CN
translations. If additional translations are required, you can configure them in i18n.
For example:
// src/config/config.default.ts
export default {
// ...
i18n: {
// Add translation
zh_TW: {
validate: require('../../locales/zh_TW.json')
},
}
}
If possible, we hope you will submit the translation to Midway for everyone to use.
Custom error text
Specifies the text of a single rule
If you only want to define an error message for a rule in a DTO, you can simply specify.
export class UserDTO {
@Rule(RuleType.number().required().error(new Error('my custom message')))
id: number;
}
All rules on this id
attribute will return a custom message if the verification fails.
Global Specify Partial Text
By configuring the validate
multilingual text table of the i18n component, you can selectively replace most of the check text, and all rules will apply the text.
// src/config/config.default.ts
export default {
// ...
i18n: {
// Put your translated text here
localeTable: {
zh_CN: {
validate: {
'string. Max': 'Hello World',
},
},
},
}
}
The validate
here is the language table keyword configured by the @midwayjs/validate
component in the i18n component.
Because the default language table is also in the form of an object, we can easily find the fields and replace them.
Since these texts distinguish languages, they need to be handled carefully, for example, replacing different languages.
// src/config/config.default.ts
export default {
// ...
i18n: {
// Put your translated text here
localeTable: {
zh_CN: {
validate: {
'string.max': '字符超长',
},
},
en_US: {
validate: {
'string.max': 'string is too long',
},
},
},
}
}
Fully customize error text
If you want to completely customize the wrong text, you can solve it by replacing the built-in language translation text.
For example:
// src/config/config.default.ts
export default {
// ...
i18n: {
localeTable: {
// Replace Chinese translation
zh_CN: {
validate: require('../../locales/custom.json'),
},
},
}
}
Default configuration
We can do some configuration for validate components.
Configuration Item | Type | Description |
---|---|---|
errorStatus | number | When the verification error occurs, the returned Http status code takes effect in the http scenario. The default 422 |
locale | string | The default language for verifying the error text. Currently, there are two languages: en_US and zh_CN . The default language is en_US . |
validationOptions | Joi's ValidationOptions options | Commonly used options are allowUnknown, stripUnknown and other options. If configured, the global validation allows undefined fields to appear. For more information, please see joi's [ValidationOptions option](https://joi.dev/api/?v= 17.6.0#anyvalidatevalue-options). |
Independent verification service
The bottom layer of the component provides a single instance of ValidateService
verification service class, which can be used in middleware or independent services if necessary. In fact, all the verification decorators will eventually go to this method.
ValidateService
provides a validate
method for verifying DTO.
Let's take the UserDTO
defined above as an example.
import { ValidateService } from '@midwayjs/validate';
export class UserService {
@Inject()
validateService: ValidateService;
async inovke() {
// ...
const result = this.validateService.validate(UserDTO, {
name: 'harry',
nickName: 'harry'
});
// Failed to return to re. Error
// Successfully returned result.value
}
}
The result returned by the validate
method contains two attributes: error
and value
. Failure will return a MidwayValidationError
error, and success will return a formatted DTO object.
Frequently Asked Questions
1. Allow undefined fields
Since some users want to allow undefined fields during parameter verification, they can be set separately on the global configuration and decorator. The former takes effect on the global and the latter takes effect on a single verification.
// src/config/config.default.ts
export default {
// ...
validate: {
validationOptions: {
allowUnknown: true, // global takes effect
}
}
}
Or on the decorator.
@Controller('/api/user')
export class HomeController {
@Post('/')
@Validate({
validationOptions: {
allowUnknown: true
}
})
async updateUser(@Body() user: UserDTO ) {
// user.id
}
}
2. Remove undefined attributes from parameters
It is also a validationOptions attribute, which can directly eliminate some attributes in the passed-in parameters.
// src/config/config.default.ts
export default {
// ...
validate: {
validationOptions: {
stripUnknown: true, // global takes effect
}
}
}
Or on the decorator.
@Controller('/api/user')
export class HomeController {
@Post('/')
@Validate({
validationOptions: {
stripUnknown: true
}
})
async updateUser(@Body() user: UserDTO ) {
}
}
3. Handling verification errors
As mentioned above, Midway will throw MidwayValidationError
error when the check fails, which we can handle in the exception handler.
For example:
// src/filter/validate.filter
import { Catch } from '@midwayjs/core';
import { MidwayValidationError } from '@midwayjs/validate';
import { Context } from '@midwayjs/koa';
@Catch(MidwayValidationError)
export class ValidateErrorFilter {
async catch(err: MidwayValidationError, ctx: Context) {
// ...
return {
status: 422
message: 'Check parameter error,' + err.message
}
}
}
4. Temporarily disable global verification
After the component is enabled, as long as the parameter uses DTO, it will be automatically verified. If a parameter does not need to be verified temporarily, you can use the following writing method.
@Controller('/api/user')
export class HomeController {
@Post('/')
async updateUser(@Body() user: Partial<UserDTO> ) {
}
}