新建项目
官方英文文档:https://nestjs.com/
中文网:https://nestjs.bootcss.com/
中文网2:https://docs.nestjs.cn/
$ npm i -g @nestjs/cli
$ nest new project-name
$ cd project-name
$ pnpm run start:dev
通过orm新建第一张表
- 安装必要包
$ npm i pnpm -g
$ npm i class-validator typeorm @nestjs/typeorm mysql
- 新建模块
$ nest g res users --no-spec
- 配置数据库 [edit file : app.module.ts]
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
UsersModule,
TypeOrmModule.forRoot({
type: 'mysql', //数据库类型
host: "localhost", //主机
port: 3306, //端口
username: "admin", //用户名
password: "123456", //密码
database: "nest_blog", //数据库名
autoLoadEntities: true, //自动加载模块
synchronize: true, // 同步:确保TypeORM实体每次运行应用程序时都会与数据库同步 !生产环境中时必须禁用! 会根据Entity生成mysql表格
logging: true // 打印日志 启动printSql()
}),
],
controllers: [AppController],
providers: [AppService],
})
- 添加第一个实体User [edit file : src/users/entities/user.entity.ts]
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, Unique, UpdateDateColumn } from "typeorm";
import { Exclude } from "class-transformer";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ type: "varchar", length: 128, comment: "用户名" })
username: string;
@Exclude()
@Column({ type: "varchar", length: 128, comment: "密码" })
password: string;
@Column({ type: "varchar", length: 255, comment: "用户头像", nullable: true })
avatarUrl: string;
@Column({ type: "varchar", length: 50, comment: "邮箱", nullable: true, unique: true })
email: string;
@Column({ type: "varchar", length: 20, comment: "手机号", nullable: true, unique: true })
phone: string;
@Column({ type: "varchar", length: 10, comment: "权限: admin user", default: "user" })
role: string
@CreateDateColumn()
createTime: Date;
@UpdateDateColumn()
updateTime: Date;
@Column({ nullable: true })
deleteTime: Date;
@Column({ comment: "是否已删除", default: false})
isDeleted: boolean = false;
}
- 引入实体 [edit file : src/users/users.module.ts]
import { TypeOrmModule } from "@nestjs/typeorm";
import { User } from "./entities/user.entity";
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
})
- 运行后生成第一张数据表
$ pnpm run start:dev
实现第一个接口
- 添加user的创建和更新DTO(Data Transfer Object) [edit file : src/users/dto/create-user.dto.ts , /users/dto/update-user.dto.ts]
import { IsBoolean, IsOptional, IsString } from "class-validator";
export class CreateUserDto {
@IsString()
readonly username: string;
@IsString()
readonly password: string;
@IsString()
@IsOptional()
readonly avatarUrl?: string;
@IsString()
@IsOptional()
readonly phone?: string;
@IsString()
@IsOptional()
readonly email?: string;
@IsString()
@IsOptional()
readonly role?: string;
@IsBoolean()
@IsOptional()
readonly isDeleted?: boolean;
}
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
// PartialType 用来 继承 CreateUserDto 的所有属性和验证规则并设置所有属性为可选
export class UpdateUserDto extends PartialType(CreateUserDto) {}
- 修改接口文件 [edit file : src/users/]
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { Repository } from "typeorm";
import { User } from "./entities/user.entity";
import { InjectRepository } from "@nestjs/typeorm";
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly usersRepository: Repository<User>,
) {
}
create(createUserDto: CreateUserDto) {
const user = this.usersRepository.create(createUserDto);
return this.usersRepository.save(user);
}
}
User
是实体类,通常对应数据库中的一张表(在这里为user表),Repository<User>
则是TypeORM提供的一个类,它包含了诸如查找、保存、删除等操作数据库的方法,在这里专门用于操作User
实体的记录。
关于
usersRepository.save
方法:在数据库中保存给定的实体。如果数据库中不存在实体,则插入,否则更新。
开启请求参数校验和格式转换
- 安装必要包
$ pnpm i class-transformer
- 添加全局配置 [edit file : main.ts]
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from "@nestjs/common";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
// 开启字段白名单验证 可以剔除掉上传对象中的不在DTO中的字段
whitelist: true,
// 当不在请求对象中包含不在白名单的字段时拒绝这个请求并提示 与 whitelist 组合使用
forbidNonWhitelisted: true,
// 开启DTO转换 使的 请求对象 与 DTO 匹配时的instanceof 为 true
// 如果@Param('id') id:number,transform配置将试图把id转化成number,而不是经由网络后的字符串id
transform: true,
transformOptions: {
// 开启全局隐式转换 可以省略@Type修饰符 隐式指定所有DTO都使用Type修饰符
enableImplicitConversion: true,
}
}));
await app.listen(3000);
}
bootstrap();
配置跨域
app.enableCors()
分页查询
- 新建DTO [create file : src/common/dto/pagination-query.dto.ts]
import { IsOptional, IsPositive } from 'class-validator';
export class PaginationQueryDto {
// 检查值是否为大于零的正数。
@IsPositive()
@IsOptional()
page: number;
@IsPositive()
@IsOptional()
pageSize: number;
}
- 修改控制器 [edit file : src/users/users.controller.ts]
......
@Get()
findAll(@Query() paginationQuery: PaginationQueryDto) {
return this.usersService.findAll(paginationQuery);
}
......
- 修改服务 [edit file : src/users/users.service.ts]
......
findAll(paginationQuery: PaginationQueryDto) {
const {page=1, pageSize=10} = paginationQuery
return this.usersRepository.find({
skip: (page - 1) * pageSize,
take: pageSize
});
}
......
实现简单的增删改查
import { HttpException, HttpStatus, Injectable } from "@nestjs/common"
import { CreateUserDto } from './dto/create-user.dto'
import { UpdateUserDto } from './dto/update-user.dto'
import { Repository } from "typeorm"
import { User } from "./entities/user.entity"
import { InjectRepository } from "@nestjs/typeorm"
import { PaginationQueryDto } from "../common/dto/pagination-query.dto"
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly usersRepository: Repository<User>
) {
}
create(createUserDto: CreateUserDto) {
const user = this.usersRepository.create(createUserDto)
return this.usersRepository.save(user)
}
findAll(paginationQuery: PaginationQueryDto) {
const {page=1, pageSize=10} = paginationQuery
return this.usersRepository.find({
where: {isDeleted: false},
skip: (page - 1) * pageSize,
take: pageSize
})
}
async findOne(id: number) {
const user = await this.usersRepository.findOne({where: {id}})
if (!user) throw new HttpException(`User #${id} not found`, HttpStatus.NOT_FOUND)
return user
}
async update(id: number, updateUserDto: UpdateUserDto) {
const user = await this.usersRepository.preload({id, ...updateUserDto})
if (!user) throw new HttpException(`User #${id} not found`, HttpStatus.NOT_FOUND)
return this.usersRepository.save(user)
}
async remove(id: number) {
let user = await this.usersRepository.findOneBy({id})
if (!user) throw new HttpException(`User #${id} not found`, HttpStatus.NOT_FOUND)
user.deleteTime = new Date()
user.isDeleted = true
return this.usersRepository.save(user)
}
}
使用响应拦截器和异常拦截器统一返回数据格式
- 新建响应拦截器 [create file : src/common/interceptor/responseInterceptor.ts]
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { map, Observable } from 'rxjs';
interface Data<T>{
data:T,
}
@Injectable()
export class ResponseInterceptor<T> implements NestInterceptor{
intercept(context: ExecutionContext, next: CallHandler): Observable<Data<T>> | Promise<Observable<any>> {
return next.handle().pipe(map(data => {
return {
success: true,
status: 0,
data,
}
}));
}
}
- 新建异常拦截器 [create file : src/common/interceptor/exceptionInterceptor.ts]
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common';
import {Request, Response} from 'express';
// 拓展HttpException类型 兼容参数校验包抛出的错误格式
class ValidationException extends HttpException {
public validationResponse: {message: string[]}
constructor(message: string[], status?: number) {
super(message, status || HttpStatus.BAD_REQUEST);
this.validationResponse = { message };
}
getResponse(): {message: string[]} {
return this.validationResponse;
}
}
type ExceptionResponseType = {message: string[]} | {message?: string};
@Catch(HttpException, ValidationException)
export class ExceptionInterceptor implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost): any {
const context = host.switchToHttp();
const request = context.getRequest<Request>()
const response = context.getResponse<Response>()
const status = exception.getStatus()
// 类型判断
let exceptionResponse: ExceptionResponseType
if (exception instanceof ValidationException) {
exceptionResponse = exception.getResponse()
} else {
exceptionResponse = exception.getResponse() as ExceptionResponseType
}
response.status(status).json({
success: false,
time: new Date(),
status,
path: request.url,
data: Array.isArray(exceptionResponse?.message) ? exceptionResponse.message : [exception.message]
})
}
}
- 使用响应拦截器和请求拦截器 [edit file : src/main.ts]
......
const app = await NestFactory.create(AppModule);
// 注入响应拦截器
app.useGlobalInterceptors(new ResponseInterceptor())
// 注入异常拦截器
app.useGlobalFilters(new ExceptionInterceptor())
......
使用passport包中间件获取信息
按照固定的 strategy (策略)插件自动的读取藏在 header 或者是 body 里面的信息,再序列化回去放在指定的 context 或者其他什么地方中。就是中间件。
- 安装包
$ pnpm install --save @nestjs/passport passport passport-local
$ pnpm install --save-dev @types/passport-local
- 生成auth模块 用来把 strategy (策略,就是中间件方法)提供出去
$ nest g module auth
$ nest g service auth
- 新建strategy策略文件 [create file : src/auth/local.strategy.ts]
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { Strategy } from "passport-local";
@Injectable()
// 没有配置Strategy 默认的AuthGuard传值为‘local’
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor() {
super();
}
// 默认拿出请求体的 username 和 password
validate(username: string, password: string) {
console.log(username,password);
return true
}
}
- 在auth.module.ts提供local.strategy
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { PassportModule } from "@nestjs/passport";
import { LocalStrategy } from "./local.strategy";
@Module({
// 引入passport的模块
imports: [PassportModule],
providers: [AuthService, LocalStrategy],
})
export class AuthModule {}
- 在路由上使用
import { Body, Controller, Get, Post, UseGuards } from "@nestjs/common";
import { AppService } from './app.service';
import { AuthGuard } from "@nestjs/passport";
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
@UseGuards(AuthGuard('local'))
@Post('test')
test(@Body() body, @Query() query){
return body
}
}
- 发送请求
使用POST请求,无论是body中携带有对象username、password 还是 Query参数带有username、password, 都能在控制台看到输出了123 321
使用passport实现进入接口需登录验证
实现效果:在路由上使用装饰器@UseGuards(AuthGuard('local'))
后,该接口需要请求中携带username和password字段并且验证通过才能访问该路由。
- 引入UsersModule,需要调用用户登录方法 [edit file : src/auth/auth.module.ts]
import { forwardRef, Module } from "@nestjs/common";
import { AuthService } from './auth.service';
import { PassportModule } from "@nestjs/passport";
import { LocalStrategy } from "./local.strategy";
import { UsersModule } from "../users/users.module";
@Module({
// 避免循环依赖 使用forwardRef导入
imports: [PassportModule,forwardRef(()=>UsersModule)],
providers: [AuthService, LocalStrategy],
})
export class AuthModule {}
- 修改local.strategy调用login进行验证。[edit file : src/auth/local.strategy.ts]
import { forwardRef, Inject, Injectable, UnauthorizedException } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { Strategy } from "passport-local";
import { UsersService } from "../users/users.service";
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(
// 避免循环依赖 使用forwardRef注入
@Inject(forwardRef(()=>UsersService))
private readonly userService: UsersService
) {
super();
}
//ts-ignore
async validate(username: string, password: string) {
const user = await this.userService.login({username,password})
if (user!=="登录成功") {
return true
}
return new UnauthorizedException('用户名或密码错误')
}
}
使用passport-jwt实现JWT
我发现官网的local.strategy.ts跟jwt没有关系,所以。
- 删除local.strategy.ts相关内容,后面要新建一个jwt-auth.guard.ts。
- 安装包
$ pnpm uninstall passport-local
$ pnpm install --save @nestjs/jwt passport-jwt
$ pnpm install --save-dev @types/passport-jwt
- auth模块中添加login方法返回token
import { Injectable } from '@nestjs/common';
import { JwtService } from "@nestjs/jwt";
@Injectable()
export class AuthService {
constructor(
private readonly jwtService: JwtService,
) {}
login({username, userId}: {username: string, userId: string}) {
return {token: this.jwtService.sign({username, userId})}
}
}
- 修改users模块下的login方法,返回token给用户。[edit file : src/users/users.service.ts]
async login(loginUserDto: LoginUserDto) {
const {username, password} = loginUserDto
const user = await this.usersRepository.createQueryBuilder('user')
.andWhere(`user.password = "${password}"`)
.andWhere(`user.username = "${username}"`)
.orWhere(`user.email = "${username}"`)
.orWhere(`user.phone = "${username}"`)
.getOne()
if (!user) throw new HttpException(`用户名或密码不正确`,HttpStatus.BAD_REQUEST )
const {token} = this.authService.login({username,userId:String(user.id)})
return {token}
}
- 新增配置文件 [create file : src/auth/constants.ts]
export const jwtConstants = {
secret: 'this is a secret',
}
- 新增文件
jwt.strategy
[create file : src/auth/jwt.strategy.ts]
import { Injectable } from '@nestjs/common'
import { PassportStrategy } from '@nestjs/passport'
import { ExtractJwt, Strategy } from 'passport-jwt'
import { jwtConstants } from "./constants";
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy,'jwt') {
constructor(
) {
super({
jwtFromRequest: ExtractJwt.fromHeader('token'),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret,
})
}
async validate(payload:any) {
return payload
}
}
- 新建路由守卫jwt-auth.guard。[create file : src/auth/jwt-auth.guard.ts]
import { ExecutionContext } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
export class JwtAuthGuard extends AuthGuard('jwt'){
constructor() {
super();
}
async canActivate(context: ExecutionContext): Promise<any> {
return super.canActivate(context);
}
}
- 在auth模块中导出 [edit file : src/auth/auth.module.ts]
import { Module } from "@nestjs/common";
import { AuthService } from './auth.service';
import { PassportModule } from "@nestjs/passport";
import { JwtModule } from "@nestjs/jwt";
import { jwtConstants } from "./constants";
import { JwtStrategy } from "./jwt.strategy";
@Module({
imports: [
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: "1 days" }
})],
providers: [AuthService, JwtStrategy],
exports: [AuthService]
})
export class AuthModule {}
i
- 使用路由守卫
这样可以实现查询当前用户信息接口,从token中解析出id然后查询当前用户信息
@UseGuards(JwtAuthGuard)
@Patch(':id')
update(@Req() request: Request, @Body() updateUserDto: UpdateUserDto) {
const user = request.user as User;
return this.usersService.update(user.id, updateUserDto);
}
自定义守卫实现路由权限控制
实现在路由上加一个装饰器只让身份为admin的用户访问
- 往token中添加role信息 [edit file : src/auth/auth.service.ts]
import { Injectable } from '@nestjs/common';
import { JwtService } from "@nestjs/jwt";
@Injectable()
export class AuthService {
constructor(
private readonly jwtService: JwtService,
) {}
login({username, id, role}: {username: string, id: string, role: string}) {
return {token: this.jwtService.sign({username, id, role})}
}
}
- 定义用户角色枚举 [edit file : src/auth/constants.ts]
export const roleConstants = {
admin: 'admin',
user: 'user',
}
- 登录时往token中存放role信息 [edit file : src/users/users.service.ts]
async login(loginUserDto: LoginUserDto) {
const {username, password} = loginUserDto
const user = await this.usersRepository.createQueryBuilder('user')
.andWhere(`user.password = "${password}"`)
.andWhere(`user.username = "${username}"`)
.orWhere(`user.email = "${username}"`)
.orWhere(`user.phone = "${username}"`)
.getOne()
if (!user) throw new HttpException(`用户名或密码不正确`,HttpStatus.BAD_REQUEST )
const {token} = this.authService.login({username,id:String(user.id),role:user.role})
return {token}
}
- 新增角色路由守卫 [create file : src/auth/role-auth.guard.ts]
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { Observable } from 'rxjs';
import type {Request} from 'express';
import { User } from "../users/entities/user.entity";
import { roleConstants } from "./constants";
@Injectable()
export class RoleGuard implements CanActivate {
constructor(){}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
// 获取request
const req: Request = context.switchToHttp().getRequest<Request>()
if ((req.user as User)?.role === roleConstants.admin) return true
throw new UnauthorizedException(`权限不足`)
}
}
- 使用
@UseGuards(JwtAuthGuard, RoleGuard)
@Get('/info')
findOne(@Req() request: Request) {
const user = request.user as User;
return this.usersService.findOne(user.id);
}
加密存储密码
- 安装包
$ npm i bcrypt
$ npm i -D @types/bcrypt
- 修改注册和登录方法 [edit file : src/users/users.service.ts]
......
import * as bcrypt from "bcrypt";
......
async register(createUserDto: CreateUserDto) {
if (createUserDto.email && !createUserDto.email.match(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/)) {
throw new ValidationException(["邮箱格式不正确"], HttpStatus.BAD_REQUEST);
}
if (createUserDto.phone && !createUserDto.phone.match(/^\d{10,}$/)) {
throw new ValidationException(["手机号格式不正确"], HttpStatus.BAD_REQUEST);
}
const user = this.usersRepository.create(createUserDto)
const errorList: string[] = []
const hasUser = await this.usersRepository.createQueryBuilder('user')
.orWhere(`user.username = :username`, { username: user.username })
.orWhere(`user.email = :email`, { email: user.email })
.orWhere(`user.phone = :phone`, { phone: user.phone })
.getOne()
if (hasUser?.username === user.username && hasUser?.username) {
errorList.push("用户名已存在")
}
if (hasUser?.email === user.email && hasUser?.email) {
console.log(hasUser?.email);
errorList.push("邮箱已存在")
}
if (hasUser?.phone === user.phone && hasUser?.phone) {
errorList.push("手机号已存在")
}
// 盐和密码加密生成哈希密码
user.password = await bcrypt.hash(user.password, await bcrypt.genSalt())
if (errorList.length > 0) throw new ValidationException(errorList,HttpStatus.BAD_REQUEST)
const {password, ...result} = await this.usersRepository.save(user)
return result
}
......
async login(loginUserDto: LoginUserDto) {
const {username, password} = loginUserDto
const user = await this.usersRepository.createQueryBuilder('user')
.andWhere(`user.username = "${username}"`)
.orWhere(`user.email = "${username}"`)
.orWhere(`user.phone = "${username}"`)
.getOne()
if (!user) throw new HttpException(`用户名或密码不正确`,HttpStatus.BAD_REQUEST )
const isMatch =await (bcrypt.compare(password, user.password))
if (!isMatch) throw new HttpException(`用户名或密码不正确`,HttpStatus.BAD_REQUEST )
const {token} = this.authService.login({username,id:String(user.id),role:user.role})
return {token}
}
......
文章多对一分类
- 修改文章实体 [edit file : src/articles/entities/article.entity.ts]
import { Column, CreateDateColumn, Entity, JoinTable, ManyToMany, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
import { Category } from "../../categories/entities/category.entity";
import { Tag } from "../../tags/entities/tag.entity";
@Entity()
export class Article {
......
// 一个category 对应 一条 category记录
@ManyToOne(() => Category, (category) => category.id)
category: Category
......
}
- 修改分类实体 [edit file : src/categories/entities/category.entity.ts]
import { Column, CreateDateColumn, Entity, JoinTable, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
import { Article } from "../../articles/entities/article.entity";
@Entity()
export class Category {
......
// 一个分类 对应多个文章里的category字段
@OneToMany(()=> Article, article=>article.category)
articles: Article[]
......
}
- 引入实体 [edit file : src/articles/articles.module.ts]
import { Module } from '@nestjs/common';
import { ArticlesService } from './articles.service';
import { ArticlesController } from './articles.controller';
import { Article } from "./entities/article.entity";
import { TypeOrmModule } from '@nestjs/typeorm';
import { Tag } from "../tags/entities/tag.entity";
import { Category } from "../categories/entities/category.entity";
@Module({
imports: [TypeOrmModule.forFeature([Article, Tag, Category])],
controllers: [ArticlesController],
providers: [ArticlesService],
})
export class ArticlesModule {}
- 引入实体 [edit file : src/categories/categories.module.ts]
import { Module } from '@nestjs/common';
import { CategoriesService } from './categories.service';
import { CategoriesController } from './categories.controller';
import { Article } from "../articles/entities/article.entity";
import { Category } from "./entities/category.entity";
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [TypeOrmModule.forFeature([Article, Category])],
controllers: [CategoriesController],
providers: [CategoriesService],
})
export class CategoriesModule {}
文章多对多标签
- 修改文章实体 [edit file : src/articles/entities/article.entity.ts]
import { Column, CreateDateColumn, Entity, JoinTable, ManyToMany, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
import { Category } from "../../categories/entities/category.entity";
import { Tag } from "../../tags/entities/tag.entity";
@Entity()
export class Article {
......
// 多对多 对应标签表里的articles字段 name自定义关联表名
@ManyToMany(type => Tag, (tag) => tag.articles )
@JoinTable({
name: 'articles_tags'
})
tags: Tag[]
......
}
- 修改标签实体 [edit file : src/tags/entities/tag.entity.ts]
import { Column, CreateDateColumn, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
import { Article } from "../../articles/entities/article.entity";
@Entity()
export class Tag {
......
// 多对多 对应文章表里的tags字段
@ManyToMany(type => Article, article => article.tags)
articles: Article[]
......
}
- 引入实体 [edit file : src/articles/articles.module.ts]
同上
- 引入实体 [edit file : src/tags/tags.module.ts]
import { Module } from '@nestjs/common';
import { TagsService } from './tags.service';
import { TagsController } from './tags.controller';
import { Article } from "../articles/entities/article.entity";
import { Tag } from "./entities/tag.entity";
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [TypeOrmModule.forFeature([Article, Tag])],
controllers: [TagsController],
providers: [TagsService],
})
export class TagsModule {}
orm多对多查询关联数据
查询文章的时候,希望得到文章被打上的标签
- 使用orm提供的查询方法时添加relations参数 [edit file : src/articles/articles.service.ts]
async findOne(id: number) {
const article = await this.articlesRepository.findOne({
// 关联Article实体中的多对多tags属性
relations: ['tags'],
where:{id}
})
if (!article) throw new HttpException(`未找到文章 #${id}`, HttpStatus.NOT_FOUND);
return mapArticleTagsToStrings(article);
}
async findAll() {
const articles = await this.articlesRepository.find({
relations: ['tags'],
where: { status: 1 } }
)
return mapArticlesTagsToStrings(articles)
}
orm多对多关联时如何级联创建和更新关联数据
- 修改创建文章DTO [edit file : src/articles/dto/create-article.dto.ts]
import { IsBoolean, IsNumber, IsOptional, IsString } from "class-validator";
export class CreateArticleDto {
......
@IsString({each: true})
readonly tags: string[];
}
- 新建预处理方法 [edit file : src/articles/articles.service.ts]
......
@Injectable()
export class ArticlesService {
......
// 输入label 返回orm可以被orm的save方法调用的对象
private async preloadTagByLabel(label: string): Promise<Tag> {
const existingTag = await this.tagRepository.findOne({ where: { label } });
if (existingTag) return existingTag
return this.tagRepository.create({label})
}
}
- 新增tags转换方法 [create file : src/utils/mapArticlesTagsToStrings.ts]
对orm返回的数据进行处理,orm放回的tag数组是对象,但我只需要其中的label字段。新增一个提取对象label的方法。
import { Article } from "../articles/entities/article.entity";
export type ArticleWithTagLabels = Omit<Article, 'tags'> & {
tags: string[]
}
/**
* 将单个Article对象的tags转换为标签字符串数组。
*/
export function mapArticleTagsToStrings(article: Article): ArticleWithTagLabels {
return {
...article,
tags: article.tags.map((tag) => tag.label),
};
}
/**
* 批量将Article数组中的每个对象的tags转换为标签字符串数组。
*/
export function mapArticlesTagsToStrings(articles: Article[]): ArticleWithTagLabels[] {
return articles.map(mapArticleTagsToStrings);
}
- 修改创建文章方法 [edit file : src/articles/articles.service.ts]
import { HttpException, HttpStatus, Injectable } from "@nestjs/common";
import { CreateArticleDto } from "./dto/create-article.dto";
import { UpdateArticleDto } from "./dto/update-article.dto";
import { Article } from "./entities/article.entity";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { Tag } from "../tags/entities/tag.entity";
import { mapArticlesTagsToStrings, ArticleWithTagLabels, mapArticleTagsToStrings } from "src/utils/mapArticlesTagsToStrings";
@Injectable()
export class ArticlesService {
constructor(
@InjectRepository(Article)
private readonly articlesRepository: Repository<Article>,
@InjectRepository(Tag)
private readonly tagRepository: Repository<Tag>,
) {
}
async create(createArticleDto: CreateArticleDto) {
const tags = await Promise.all(
createArticleDto.tags.map(label => this.preloadTagByLabel(label))
)
const article = this.articlesRepository.create({
...createArticleDto,
tags
})
return this.articlesRepository.save(article)
}
async update(id: number, updateArticleDto: UpdateArticleDto) {
const tags = updateArticleDto.tags && await Promise.all(
updateArticleDto.tags.map(label => this.preloadTagByLabel(label))
)
const article = await this.articlesRepository.preload({
id,
...updateArticleDto,
tags
})
if (!article) throw new HttpException(`未找到文章 #${id}`, HttpStatus.NOT_FOUND);
return this.articlesRepository.save(article)
}
}
PS:想要删除文章关联标签,例如要删除"标签1",更新文章时将tags参数数组中的"标签1"去掉即可删除关联表中的记录。
使用orm事务
async remove(id: number) {
const queryRunner = this.connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// 删除id为传入id的评论 和 删除 root为传入id的评论
await queryRunner.manager.delete(Comment, { id });
await queryRunner.manager.delete(Comment, { root: id });
await queryRunner.commitTransaction();
} catch (error) {
await queryRunner.rollbackTransaction();
} finally {
await queryRunner.release();
}
}
使用class-validator自定义DTO字段验证装饰器
import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator';
export function IsStringOrNumber(validationOptions?: ValidationOptions) {
return function (target: object, propertyName: string) {
registerDecorator({
name: 'isStringOrNumber',
target: target.constructor,
propertyName: propertyName,
constraints: [],
options: validationOptions,
validator: {
validate(value: any) {
return typeof value === 'string' || typeof value === 'number';
},
defaultMessage(args: ValidationArguments) {
return `${args.property} must be a string or a number`;
},
},
});
};
}
使用Session实现图像验证码
- 安装包
pnpm i express-session svg-captcha @types/express-session
- main.ts 配置和注入
......
import * as session from 'express-session';
async function bootstrap() {
......
// 注入session插件 使用场景:验证码
app.use(session({
secret: 'salt', // 加盐
rolling: true, // 每次请求强行设置cookie时 将重置cookie过期时间
name: "connect.sid", // 生成客户端cookie的名字 默认为 connect.sid
// 设置返回到前端key的属性,默认值为{path:'/',httpOnly:true,secure:false,maxAge:null}
cookie: {
path:'/',
httpOnly:true,
secure:false,
maxAge:null
}
}))
await app.listen(3000);
}
bootstrap();
- 获取session对象
import { Controller, Get, Res, Session } from '@nestjs/common';
import { Response } from 'express';
@Controller('users')
export class UsersController {
......
@Get('/captcha')
createCAPTCHA(@Res() res: Response, @Session() session: Record<string, any>) {
return this.usersService.createCAPTCHA(res, session);
}
}
- 使用session、生成captcha和验证
.....
import * as svgCaptcha from 'svg-captcha'
import { Response } from 'express';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly usersRepository: Repository<User>,
@Inject(forwardRef(() => AuthService))
private readonly authService: AuthService,
) {}
async register(createUserDto: CreateUserDto, session: Record<string, any>) {
if (!(createUserDto.code.toLocaleLowerCase() === session.code.toLocaleLowerCase())) {
throw new ValidationException(['图型验证码错误'], HttpStatus.BAD_REQUEST);
}
......
}
async createCAPTCHA(res: Response, session: Record<string, any>){
const captcha = svgCaptcha.create({
size: 4, //验证码几个字符
fontSize: 50, //字体大小
width: 100, //宽度
height: 34, //高度
background: '#cc9966' //背景颜色
})
session["code"] = captcha.text
res.set('Content-Type', 'image/svg+xml');
res.send(captcha.data);
}
}
使用节流Throttler限制接口请求
- 安装包
pnpm install @nestjs/throttler
- 全局配置 修改AppModule
import { Module } from '@nestjs/common';
import { ThrottlerModule } from '@nestjs/throttler';
import { APP_GUARD } from '@nestjs/core';
import { ThrottlerGuard } from '@nestjs/throttler';
@Module({
imports: [
// 配置节流模块
ThrottlerModule.forRoot([{
ttl: 10000, // 单位时间内 毫秒
limit: 1, // 最大允许请求次数
}]),
],
providers: [
// 提供全局策略
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
})
export class AppModule {}
- 修改超时返回错误字符串 修改之前的错误拦截器 [edit file : src/common/interceptor/exceptionInterceptor.ts]
......
export class ExceptionInterceptor implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost): any {
const context = host.switchToHttp();
const request = context.getRequest<Request>();
const response = context.getResponse<Response>();
const status = exception.getStatus();
// 类型判断
let exceptionResponse: ExceptionResponseType;
if (exception instanceof ValidationException) {
exceptionResponse = exception.getResponse();
} else {
exceptionResponse = exception.getResponse() as ExceptionResponseType;
}
// 如果错误类型是ThrottlerException 修改他的返回提示
if (exception instanceof ThrottlerException) {
exception.message = '请求过于频繁,请稍后再试。'
}
response.status(status).json({
success: false,
time: new Date(),
status,
path: request.url,
data: Array.isArray(exceptionResponse?.message) ? exceptionResponse.message : [exception.message],
});
}
}
- 应用节流到特定路由
我没找到如何单独给一个路由设置节流,但是我们可以给全局设置一个宽松的节流,比如1秒100次,然后使用@Throttle()装饰器单独严格限制一个路由
// 在全局应用的基础上
@Throttle({ default: { limit: 1, ttl: 60000 } })
@Post('list')
findAll(@Body() body: FindAllArticleDto) {
return this.articlesService.findAll(body);
}
简单使用redis
- 安装包
pnpm i @nestjs-modules/ioredis ioredis
- 修改
app.module.ts
配置redis
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { RedisModule } from '@nestjs-modules/ioredis';
......
@Module({
imports: [
......
// 配置redis
RedisModule.forRootAsync({
useFactory: () => ({
type: 'single',
url: 'redis://localhost:6379',
options: {
password: '123456',
stringNumbers: true // 启用后redis返回的数字会被转换为字符串
}
}),
})
],
......
})
export class AppModule {}
- 在service中使用
import { Injectable } from '@nestjs/common';
import { InjectRedis } from '@nestjs-modules/ioredis';
import Redis from 'ioredis';
@Injectable()
export class AppService {
constructor(
@InjectRedis() private readonly redis: Redis
) {}
async getHello() {
await this.redis.set('first_step', 'hello world!')
const redisData = await this.redis.get('first_step');
return {msg:redisData}
}
}
未完待续…
学习资源:
https://www.npmjs.com/package/@nestjs/throttler#usage
http://t.csdnimg.cn/Vwrll
https://github.com/Microsoft/TypeScript/issues/26239
http://t.csdnimg.cn/GfOVE
http://t.csdnimg.cn/8DZaI
https://juejin.cn/post/6844903935002542088
https://www.typeorm.org/
https://blog.csdn.net/m0_37890289/article/details/135395951
https://www.npmjs.com/package/@nestjs-modules/ioredis