Nest 从零开始/Nest 操作记录/Nest 操作示例/Nest 简单教程

1 篇文章 0 订阅
1 篇文章 0 订阅

新建项目

官方英文文档: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新建第一张表

  1. 安装必要包
$ npm i pnpm -g
$ npm i class-validator typeorm @nestjs/typeorm mysql
  1. 新建模块
$ nest g res users --no-spec
  1. 配置数据库 [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],
})
  1. 添加第一个实体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;
}
  1. 引入实体 [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],
})
  1. 运行后生成第一张数据表
$ pnpm run start:dev

实现第一个接口

  1. 添加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) {}

  1. 修改接口文件 [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方法:在数据库中保存给定的实体。如果数据库中不存在实体,则插入,否则更新。

开启请求参数校验和格式转换

  1. 安装必要包
$ pnpm i class-transformer
  1. 添加全局配置 [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()

分页查询

  1. 新建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;
}
  1. 修改控制器 [edit file : src/users/users.controller.ts]
......
@Get()
findAll(@Query() paginationQuery: PaginationQueryDto) {
  return this.usersService.findAll(paginationQuery);
}
......
  1. 修改服务 [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)
  }
}

使用响应拦截器和异常拦截器统一返回数据格式

  1. 新建响应拦截器 [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,
      }
    }));
  }
}
  1. 新建异常拦截器 [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]
    })
  }
}
  1. 使用响应拦截器和请求拦截器 [edit file : src/main.ts]
......
const app = await NestFactory.create(AppModule);
// 注入响应拦截器
app.useGlobalInterceptors(new ResponseInterceptor())
// 注入异常拦截器
app.useGlobalFilters(new ExceptionInterceptor())
......

使用passport包中间件获取信息

按照固定的 strategy (策略)插件自动的读取藏在 header 或者是 body 里面的信息,再序列化回去放在指定的 context 或者其他什么地方中。就是中间件。

  1. 安装包
$ pnpm install --save @nestjs/passport passport passport-local
$ pnpm install --save-dev @types/passport-local
  1. 生成auth模块 用来把 strategy (策略,就是中间件方法)提供出去
$ nest g module auth
$ nest g service auth
  1. 新建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
  }
}
  1. 在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 {}
  1. 在路由上使用
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
  }
}
  1. 发送请求

使用POST请求,无论是body中携带有对象username、password 还是 Query参数带有username、password, 都能在控制台看到输出了123 321

使用passport实现进入接口需登录验证

实现效果:在路由上使用装饰器@UseGuards(AuthGuard('local'))后,该接口需要请求中携带username和password字段并且验证通过才能访问该路由。

  1. 引入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 {}
  1. 修改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没有关系,所以。

  1. 删除local.strategy.ts相关内容,后面要新建一个jwt-auth.guard.ts。
  2. 安装包
$ pnpm uninstall passport-local
$ pnpm install --save @nestjs/jwt passport-jwt
$ pnpm install --save-dev @types/passport-jwt
  1. 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})}
    }
  }

  1. 修改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}
}
  1. 新增配置文件 [create file : src/auth/constants.ts]
export const jwtConstants = {
  secret: 'this is a secret',
}
  1. 新增文件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
    }
  }

  1. 新建路由守卫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);
  }
}
  1. 在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
  1. 使用路由守卫

这样可以实现查询当前用户信息接口,从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的用户访问

  1. 往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})}
  }
}

  1. 定义用户角色枚举 [edit file : src/auth/constants.ts]
export const roleConstants = {
  admin: 'admin',
  user: 'user',
}
  1. 登录时往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}
}
  1. 新增角色路由守卫 [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(`权限不足`)
  }
}

  1. 使用
  @UseGuards(JwtAuthGuard, RoleGuard)
  @Get('/info')
  findOne(@Req() request: Request) {
    const user = request.user as User;
    return this.usersService.findOne(user.id);
  }

加密存储密码

  1. 安装包
$ npm i bcrypt
$ npm i -D @types/bcrypt
  1. 修改注册和登录方法 [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}
}
......

文章多对一分类

  1. 修改文章实体 [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
......
}

  1. 修改分类实体 [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[]
......
}

  1. 引入实体 [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 {}
  1. 引入实体 [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 {}

文章多对多标签

  1. 修改文章实体 [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[]
......
}

  1. 修改标签实体 [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[]
......
}

  1. 引入实体 [edit file : src/articles/articles.module.ts]

同上

  1. 引入实体 [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多对多查询关联数据

查询文章的时候,希望得到文章被打上的标签

  1. 使用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多对多关联时如何级联创建和更新关联数据

  1. 修改创建文章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[];
}

  1. 新建预处理方法 [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})
  }
}


  1. 新增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);
}
  1. 修改创建文章方法 [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实现图像验证码

  1. 安装包
pnpm i express-session svg-captcha @types/express-session
  1. 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();
  1. 获取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);
  }
}
  1. 使用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限制接口请求

  1. 安装包
pnpm install @nestjs/throttler
  1. 全局配置 修改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 {}
  1. 修改超时返回错误字符串 修改之前的错误拦截器 [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. 应用节流到特定路由

我没找到如何单独给一个路由设置节流,但是我们可以给全局设置一个宽松的节流,比如1秒100次,然后使用@Throttle()装饰器单独严格限制一个路由

// 在全局应用的基础上
@Throttle({ default: { limit: 1, ttl: 60000 } })
@Post('list')
findAll(@Body() body: FindAllArticleDto) {
  return this.articlesService.findAll(body);
}

简单使用redis

  1. 安装包
pnpm i @nestjs-modules/ioredis ioredis
  1. 修改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 {}

  1. 在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

  • 20
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学途路漫漫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值