Nest.js 脚手架改造(三)参数校验和数据库连接

填坑 内容如题

参数校验

DTO

DTO可以用interface来写,也可以用class来写,不过TS最终会被编译成JS来执行,最终写的interface会在编译时消除。

按官方文档的意思,pipe等特性中可能需要获取DTO的元数据,所以用class,何况class还可以用装饰器…

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
// create-user.dto.ts
export class CreateUserDto {
readonly name: string
readonly age: number
readonly mobile: string
}

// user.controller.ts
@Post('/add')
async addUser(@Body() createUserDto: CreateUserDto): Promise<void> {
await this.userService.createUser(createUserDto)
}

DTO是有了,但是nest从客户端拿到的是啥就还是啥

1
{ name: 'abc', age: '1', mobile: 123 }

像这种数据类型错误的请求,照样会到达controller这儿,假如在controller中进行校验,又很丑,还会造成冗余。

pipe

pipe是进入控制器前的最后一道处理,可以用来做数据转换、数据验证等数据方面的处理。

自定义的管道需要实现PipeTransform接口

1
2
3
export interface PipeTransform<T = any, R = any> {
transform(value: T, metadata: ArgumentMetadata): R;
}

value是当前在处理的数据,metadata则是其元数据

1
2
3
4
5
export interface ArgumentMetadata {
readonly type: Paramtype;
readonly metatype?: Type<any> | undefined;
readonly data?: string | undefined;
}

type表示数据是来自body,query,param还是自定义参数

metatype表示数据的元类型,比如CreateUserDto,如果没有声明类型则是undefined

data是传递给装饰器的字符串,比如@Body('abc')data的值就是abc,没有传值则是undefined

class-validator

class-validator是官方推荐的校验器,它是基于装饰器的,所以和Nest、TypeScript相性较合

1
npm i --save class-validator class-transformer

class-transformer是同个作者做的配套用的库,能把数据转换成DTO的实例,比如上述参数会被转换成这样

1
CreateUserDto { name: 'abc', age: '1', mobile: 123 }

然后改造DTO

1
2
3
4
5
6
7
8
9
10
11
12
13
// create-user.dto.ts
import { Length, Min, Max } from 'class-validator'

export class CreateUserDto {
readonly name: string

@Min(6, { message: '年龄不能小于6' })
@Max(70, { message: '年龄不能大于70' })
readonly age: number

@Length(11, 11, { message: '手机号长度必须为11位' })
readonly mobile: string
}

改造完成后,就可以编写自定义的校验管道了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { Injectable, PipeTransform, ArgumentMetadata } from '@nestjs/common'
import { validate } from 'class-validator'
import { plainToClass } from 'class-transformer'
import { ApiException } from '../../exceptions/api.exception'
import { ApiCode } from '../../enums/api-code.enum'

@Injectable()
export class ValidationPipe implements PipeTransform {
async transform<T>(value: T, metadata: ArgumentMetadata): Promise<T> {
const { metatype } = metadata
if (!metatype || !this.toValidate(metatype)) {
return value
}

const object = plainToClass(metatype, value)
const errors = await validate(object)
if (errors.length > 0) {
const firstError = errors[0]
const { constraints } = firstError
const keys = Object.keys(constraints)

throw new ApiException(constraints[keys[0]], ApiCode.PARAMS_ERROR, 200)
}

return value
}

private toValidate(metatype): boolean {
const types = [String, Boolean, Number, Array, Object]

return !types.find(type => metatype === type)
}
}

这里我只抛出了校验失败的第一条信息,可以根据需要修改

最后将管道注册为全局管道

1
2
3
4
5
6
7
8
9
10
import { APP_PIPE } from '@nestjs/core'
import { ValidationPipe } from './providers/pipe/validation.pipe'

// app.module.ts (省略了其他模块)
@Module({
providers: [
{ provide: APP_PIPE, useClass: ValidationPipe }
]
})
export class AppModule {}

现在所有请求都会走一遍该管道进行参数校验了

1
2
3
4
5
6
7
8
9
10
11
12
13
// request
{
"name": "abc",
"age": 18,
"mobile": 123
}

// response
{
"msg": "手机号长度必须为11位",
"code": 4002,
"path": "/users/add"
}

数据库

这里数据库用的是mongoDB

数据库连接

还挺方便的,官方提供了@nestjs/mongoose模块帮助数据库连接,不过假如用的是TypeORM,官方文档也有对应的连接方法。

1
npm install --save @nestjs/mongoose mongoose
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// app.module.ts (省略了其他模块)
import { MongooseModule } from '@nestjs/mongoose'

@Module({
imports: [
MongooseModule.forRootAsync({
useFactory: async (configService: ConfigService) => ({
uri: configService.get('DATABASE'),
useNewUrlParser: true,
useUnifiedTopology: true
}),
inject: [ConfigService]
})
]
})
export class AppModule {}

如果不用配置文件就更方便了..

1
2
3
4
5
6
7
// app.module.ts (省略了其他模块)
import { MongooseModule } from '@nestjs/mongoose'

@Module({
imports: [MongooseModule.forRoot('mongodb://localhost/nest')]
})
export class AppModule {}

使用

首先是定义Schema

1
2
3
4
5
6
7
8
9
10
11
// user.schema.ts
import * as mongoose from 'mongoose'

export const UserSchema = new mongoose.Schema(
{
name: String,
age: Number,
mobile: String
},
{ collection: 'user' }
)

然后在用到的module处进行注册

1
2
3
4
5
6
7
8
9
10
11
12
13
// users.module.ts
import { Module } from '@nestjs/common'
import { MongooseModule } from '@nestjs/mongoose'
import { UsersController } from './users.controller'
import { UsersService } from './users.service'
import { UserSchema } from './schemas/user.schema'

@Module({
imports: [MongooseModule.forFeature([{ name: 'User', schema: UserSchema }])],
controllers: [UsersController],
providers: [UsersService]
})
export class UsersModule {}

注册完之后,就能在service层注入对应的model了,在这之前先定义UserModel

1
2
3
4
5
6
7
8
9
import { Document } from 'mongoose'

export interface User {
name: string
age: number
mobile: string
}

export interface UserModel extends User, Document {}

最后就能在service中使用了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Injectable } from '@nestjs/common'
import { UserModel } from './users.interface'
import { InjectModel } from '@nestjs/mongoose'
import { Model } from 'mongoose'
import { CreateUserDto } from './dto/create-user.dto'
import { ApiException } from '../../exceptions/api.exception'
import { ApiCode } from '../../enums/api-code.enum'

@Injectable()
export class UsersService {
constructor(
@InjectModel('User') private readonly userModel: Model<UserModel>
) {}

async createUser(createUserDto: CreateUserDto): Promise<boolean> {
await this.userModel.create(createUserDto).catch(() => {
throw new ApiException('创建失败', ApiCode.DATABASE_ERROR, 200)
})

return true
}
}