NestJS

0x01 概述

(1)简介

  • 官网:https://nestjs.com/
  • NestJS 是一个用于构建高效、可扩展的 NodeJS 服务器端应用的框架,使用渐进式 JavaScript 构建并完全支持 TypeScript,且结合了 OOP、FP、FRP 的元素
    • 渐进式 JavaScript 构建:通过逐步增强用户体验和功能,构建高性能、可维护且适应不同设备和网络环境的 Web 应用程序
    • OOP:面向对象编程
    • FP:函数式编程
    • FRP:函数式反应式编程
  • NestJS 使用强大的 HTTP 服务器框架,如 Express 或 Fastify

(2)安装

NestJS 支持通过脚手架工具克隆启动项目安装核心和支持包来初始化新项目

版本信息:

  • NodeJS v23.2.0
  • npm v10.9.0

a. 脚手架工具

@nestjs/cli v11.0.5

  1. 使用命令 npm install -g @nestjs/cli 全局安装脚手架
  2. 使用命令 nest new nest-app 创建一个名为 nest-app 的应用
    • 选择包管理工具,如 npm
  3. 使用命令 cd nest-app 进入项目目录
  4. 使用命令 npm run start:dev 启动项目并访问 http://localhost:3000/

b. 克隆启动项目

  1. 使用命令 git clone https://github.com/nestjs/typescript-starter.git nest-app 克隆
  2. 使用命令 cd nest-app 进入项目目录
  3. 使用命令 npm install 安装依赖
  4. 使用命令 npm run start:dev 启动项目并访问 http://localhost:3000/

c. 安装核心和支持包

  1. 使用命令 mkdir nest-app 创建项目目录
  2. 使用命令 cd nest-app 进入项目目录
  3. 使用命令 npm install @nestjs/core @nestjs/common rxjs reflect-metadata @nestjs/platform-express 安装核心和支持包
    1. @nestjs/core:核心模块,用于构建、启动、管理 NestJS 应用
    2. @nestjs/common:包含构建 NestJS 应用的基础设施和常用装饰器
    3. rxjs:用于构建异步和事件驱动程序
    4. reflect-metadata:实现元编程,提供元数据反射 API,并且可以在运行时检查和操作对象的元数据
    5. @nestjs/platform-express:NestJS 的 Express 平台适配器
  4. 创建并编辑 main.ts
  5. 使用命令 npm run start:dev 启动项目并访问 http://localhost:3000/

(3)项目结构

以脚手架工具创建的项目命目录为准

  • dist:打包目录

  • node_modules:NodeJS 依赖目录

  • src:代码资源目录

    • app.controller.spec.ts:控制器层测试文件

    • app.controller.ts:控制器层文件

    • app.module.ts:模块层文件

    • app.service.ts:服务层文件

    • main.ts:入口文件

      import { NestFactory } from '@nestjs/core';
      import { AppModule } from './app.module';
      
      /**
       * 该引导函数负责创建新的嵌套应用程序对象
       * 启动服务器并监听端口
       */
      async function bootstrap() {
        const app = await NestFactory.create(AppModule);
        await app.listen(process.env.PORT ?? 3000);
      }
      bootstrap();
      
      
  • test:E2E 测试目录

  • .eslintrc.js:Eslint 配置文件

  • .gitignore:Git 忽略配置文件

  • .prettierrc:Prettier 配置文件

  • nest-cli.json:NestJS 脚手架配置文件

  • package.json:NodeJS 包配置文件

  • tsconfig.json:TypeScript 配置文件

0x02 基础使用

(1)控制器

  • 控制器是用 @Controller 装饰器注解的类

  • 控制器中有与特定路由相关联的动作,即函数方法

  • 控制器的功能是在应用中,使用特定的路由与 HTTP 请求方式来创建端点

    之后接收请求,并通过相关代码来处理该请求

    最后返回响应到客户端

  • @Controller 的参数有三种类型:

    类型 说明
    顶级路由
    字符串 一般路由
    对象 路由配置对象

    举例:app.controller.ts

    import { Controller, Get } from '@nestjs/common';
    
    @Controller()
    export class AppController {
      constructor() {}
    
      @Get()
      getHello(): string {
        return 'Hello';
      }
      @Get('/bye')
      getBye(): string {
        return 'Bye';
      }
    }
    
    

    启动项目后,使用以下方法进行测试

  • 控制器中还支持使用其他注解实现更多 HTTP 请求方法

    举例:

    1. events.controller.ts

      import { Controller, Delete, Get, Patch, Post } from '@nestjs/common';
      
      @Controller('events')
      export class EventsController {
        @Post()
        create() {
          return 'create event';
        }
      
        @Get()
        readOne() {
          return 'read-one event';
        }
        @Get()
        readAll() {
          return 'read-all event';
        }
      
        @Patch()
        update() {
          return 'update event';
        }
      
        @Delete()
        delete() {
          return 'delete event';
        }
      }
      
      
    2. events.module.ts

      import { Module } from '@nestjs/common';
      import { EventsController } from './events.controller';
      
      @Module({
        imports: [],
        controllers: [EventsController],
        providers: [],
      })
      export class EventsModule {}
      
      
    3. main.ts

      import { NestFactory } from '@nestjs/core';
      import { EventsModule } from './events.module';
      
      async function bootstrap() {
        const app = await NestFactory.create(EventsModule);
        await app.listen(process.env.PORT ?? 3000);
      }
      bootstrap();
      
      
    4. 启动项目并按控制器中的 HTTP 请求方法尝试请求 http://localhost:3000/events

(2)路由参数

  • 路由参数有三种形式,分别对应不同的装饰器
  • 各自的装饰器中传空值时,表示获取该参数类型中的所有参数

a. param 参数

举例:events.controller.ts

import { Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common';

@Controller('events')
export class EventsController {
  // ...
  @Get(':id')
  readOne(@Param('id') id: string) {
    return id;
  }
  // ...
}

启动项目并尝试访问 http://localhost:3000/events/123,http://localhost:3000/events/hello

b. query 参数

举例:events.controller.ts

import { Controller, Delete, Get, Patch, Post, Query } from '@nestjs/common';

@Controller('events')
export class EventsController {
  // ...
  @Get()
  readAll(@Query() query: object) {
    return query;
  }
  // ...
}

启动项目并尝试访问 http://localhost:3000/events?hello=world

c. body 参数

举例:events.controller.ts

import { Body, Controller, Delete, Get, Patch, Post } from '@nestjs/common';

@Controller('events')
export class EventsController {
  @Post()
  create(@Body() body: object) {
    return body;
  }
  // ...
}

启动项目并尝试向 http://localhost:3000/events 发生 POST 请求以及以下 JSON 数据

{
  "hello": "world"
}

(3)响应与状态码

HTTP 状态码参考《HTTP 常见状态码说明 | 博客园-SRIGT

  • 响应数据默认使用 JSON 格式

  • HTTP 状态码使用 @HttpCode 装饰器注解相关动作

  • 举例:events.controller.ts

    import { Controller, Delete, Get, HttpCode, Patch, Post } from '@nestjs/common';
    
    @Controller('events')
    export class EventsController {
      // ...
      @Get()
      @HttpCode(200)
      readOne() {
        return {
          id: 1,
          title: 'event title',
          description: 'event description',
          date: 'event date',
        };
      }
      // ...
    }
    
    

    启动项目并尝试访问 http://localhost:3000/events

(4)请求有效负载

  • 请求有效负载指通过定义数据传输对象类(Data Transfer Object class,DTO class)来控制接收到的数据类型

  • 对于不同的请求方法(POST / PATCH / PUT),可以通过 @nestjs/mapped-types 包减少重复代码,实现对 DTO 类的类型继承,并且变为可选参数

    • 使用命令 npm install --save @nestjs/mapped-types 安装
  • 举例:

    1. create-event.dto.ts

      export class CreateEventDto {
        id: number;
        title: string;
        description: string;
        date: Date | string;
      }
      
      
    2. update-event.dto.ts

      import { PartialType } from '@nestjs/mapped-types';
      import { CreateEventDto } from './create-event.dto';
      
      export class UpdateEventDto extends PartialType(CreateEventDto) {}
      
      
    3. events.controller.ts

      import { Body, Controller, Delete, Get, Patch, Post } from '@nestjs/common';
      import { CreateEventDto } from './create-event.dto';
      import { UpdateEventDto } from './update-event.dto';
      
      @Controller('events')
      export class EventsController {
        @Post()
        create(@Body() body: CreateEventDto) {
          return body;
        }
        // ...
        @Patch()
        update(@Body() body: UpdateEventDto) {
          return body;
        }
        // ...
      }
      
      

0x03 数据库

(1)对象关系映射 ORM

  • 对象关系映射(Object Relational Mapping,ORM)用于解决面向对象编程语言与关系型数据库之间不匹配的问题
    • TypeORM 是适用于 NodeJS 的 ORM 库,NestJS 模块提供了 TypeORM 的集成
  • 重要概念:
    • 实体(Entity)是一个映射到数据库表的类,实体与表一一对应
    • 仓库(Repository)提供程序对特定实体的数据访问
    • 查询构建器(Query Builder)用于以面向对象的方法构建 SQL 查询

(2)连接数据库

MySQL 数据库基础参考《MySQL | 博客园-SRIGT

  1. 使用命令 npm install --save @nestjs/typeorm typeorm mysql 安装相关依赖

  2. events.module.ts

    import { Module } from '@nestjs/common';
    import { EventsController } from './events.controller';
    import { TypeOrmModule } from '@nestjs/typeorm';
    
    @Module({
      imports: [
        TypeOrmModule.forRoot({
          type: 'mysql', // 数据库类型
          host: 'localhost', // 数据库地址
          port: 3306, // 数据库端口
          username: 'root', // 数据库用户名
          password: 'root', // 数据库密码
          database: 'nest-db', // 数据库名
        }),
      ],
      controllers: [EventsController],
      providers: [],
    })
    export class EventsModule {}
    
    
  3. 使用命令 npm run start:dev 启动项目

    • 异常:TypeORM 报错 Client does not support authentication protocol requested by server; consider upgrading MySQL client
    • 原因:可能是因为最新的 mysql 模块并未完全支持 MySQL 8 的 caching_sha2_password 加密方式
    • 解决方法:使用以下 SQL 语句:ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password_string';(其中 password_string 可以替换为自定义的密码字符串)

(3)实体(表)

  • 实体类的文件名通常在后缀名前添加 .entity

  • 实体类需要使用 @Entity 装饰器做注解,其中的参数类型包括:

    类型 说明
    字符串 表名
    对象 实体特定选项集合
  • 在实体类中,所有属性均对应一个数据列,每个列都需要 @Column 做注解

    • @Column 的参数为类的数据类型与选项对象,如 @Column("tinyint", { default: 0 })

    可以通过以下方法指定主键列:

    • @PrimaryGeneratedColumn 用于指定自增主键列,其中参数可以为 "uuid""rowid""increment""identity"
    • @PrimaryColumn 用于指定列为主键列
  • 举例:

    1. event.entity.ts

      import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
      
      @Entity('event')
      export class Event {
        @PrimaryGeneratedColumn("increment")
        id: number;
        
        @Column()
        title: string;
      
        @Column()
        description: string;
      
        @Column()
        date: Date;
      }
      
      
    2. events.module.ts

      import { Module } from '@nestjs/common';
      import { EventsController } from './events.controller';
      import { TypeOrmModule } from '@nestjs/typeorm';
      import { Event } from './event.entity';
      
      @Module({
        imports: [
          TypeOrmModule.forRoot({
            type: 'mysql',
            host: 'localhost',
            port: 3306,
            username: 'root',
            password: 'root',
            database: 'nest-db',
            entities: [Event], // 数据表实体
            synchronize: true, // 是否自动同步数据库
          }),
        ],
        controllers: [EventsController],
        providers: [],
      })
      export class EventsModule {}
      

(4)仓库(查询语句)

  • 仓库类分为两种:

    • 一般仓库类包含 save()find()findOne()remove() 等基本方法,大多数方法可以通过一般仓库类实现
      • save():创建或更新一个存在的实体
      • find():查找多个实体,可以选择使用条件
      • findOne():通过 ID 或指定条件来查找一个实体
      • remove():移除实体
    • 特定仓库类是为某个实体创建的独立类,主要用于某个查询使用了多次的情况
  • 举例:

    1. events.controller.ts

      import {
        Body,
        Controller,
        Delete,
        Get,
        Param,
        Patch,
        Post,
      } from '@nestjs/common';
      import { InjectRepository } from '@nestjs/typeorm';
      import { Repository } from 'typeorm';
      import { Event } from './event.entity';
      import { CreateEventDto } from './create-event.dto';
      import { UpdateEventDto } from './update-event.dto';
      
      @Controller('events')
      export class EventsController {
        constructor(
          @InjectRepository(Event)
          private readonly repository: Repository<Event>,
        ) {}
      
        @Post()
        async create(@Body() body: CreateEventDto) {
          return await this.repository.save({
            ...body,
            date: new Date(body.date),
          });
        }
      
        @Get(':id')
        async readOne(@Param('id') id) {
          return await this.repository.findOne(id);
        }
        @Get()
        async readAll() {
          return await this.repository.find();
        }
      
        @Patch(':id')
        async update(@Param('id') id, @Body() body: UpdateEventDto) {
          const event = await this.repository.findOne(id);
      
          return await this.repository.save({
            ...event,
            ...body,
            date: body.date ? new Date(body.date) : event!.date,
          });
        }
      
        @Delete(':id')
        async delete(@Param('id') id) {
          const event = await this.repository.findOne(id);
      
          return await this.repository.remove(event!);
        }
      }
      
      
    2. events.module.ts

      import { Module } from '@nestjs/common';
      import { EventsController } from './events.controller';
      import { TypeOrmModule } from '@nestjs/typeorm';
      import { Event } from './event.entity';
      
      @Module({
        imports: [
          TypeOrmModule.forRoot({
            // ...
          }),
          TypeOrmModule.forFeature([Event]),
        ],
        controllers: [EventsController],
        providers: [],
      })
      export class EventsModule {}
      
      
  • 仓库类支持多种查询条件与选项:

    举例:where 条件查询:

    1. SELECT * FROM event WHERE event.id = 1;

      return await this.repository.find({
        where: { id: 1 }
      });
      
    2. SELECT * FROM event WHERE event.id > 1;

      return await this.repository.find({
        where: { id: MoreThan(1) } // import { MoreThan } from 'typeorm';
      });
      

    更多方法参考《Find Options | TypeORM

0x04 验证器

(1)管道

管道(Pipe)介于输入与输出之间,用于自动对数据进行处理

举例:events.controller.ts

import {
  // ...
  ParseIntPipe,
  // ...
} from '@nestjs/common';
// ...

@Controller('events')
export class EventsController {
  // ...
  @Get(':id')
  async readOne(@Param('id', ParseIntPipe) id) {
    // ...
  }
  // ...
}

其他内置管道参考《Built-in pipes | NestJS Documentation

(2)输入验证

  1. 使用命令 npm install --save class-validator class-transformer 安装类验证器与类转换器库

  2. create-event.dto.ts

    import { IsDateString, IsString, Length } from 'class-validator';
    
    export class CreateEventDto {
      @IsString({ message: '标题应为字符串' })
      @Length(1, 255, { message: '标题应为1~255个字符' })
      title: string;
    
      @IsString({ message: '描述应为字符串' })
      @Length(1, 255, { message: '描述应为1~255个字符' })
      description: string;
    
      @IsDateString({}, { message: '日期格式不正确' })
      date: Date | string;
    }
    
    
  3. main.ts

    import { NestFactory } from '@nestjs/core';
    import { EventsModule } from './events.module';
    import { ValidationPipe } from '@nestjs/common';
    
    async function bootstrap() {
      const app = await NestFactory.create(EventsModule);
      app.useGlobalPipes(new ValidationPipe()); // 全局校验管道
      await app.listen(process.env.PORT ?? 3000);
    }
    bootstrap();
    
    

(3)组验证

通过分组的方式,实现区分应用在某个数据的同名类验证器注解

举例:

  1. create-event.dto.ts

    import { IsDateString, IsString, Length } from 'class-validator';
    
    export class CreateEventDto {
      // ...
      @IsString({ message: '描述应为字符串' })
      @Length(1, 255, { message: '描述应为1~255个字符', groups: ['create'] })
      @Length(5, 255, { message: '描述应为5~255个字符', groups: ['update'] })
      description: string;
      // ...
    }
    
    
  2. events.controller.ts

    import {
      Body,
      Controller,
      Delete,
      Get,
      Param,
      Patch,
      Post,
      ValidationPipe,
    } from '@nestjs/common';
    import { InjectRepository } from '@nestjs/typeorm';
    import { Repository } from 'typeorm';
    import { Event } from './event.entity';
    import { CreateEventDto } from './create-event.dto';
    import { UpdateEventDto } from './update-event.dto';
    
    @Controller('events')
    export class EventsController {
      // ...
      @Post()
      async create(
        @Body(new ValidationPipe({ groups: ['create'] })) body: CreateEventDto,
      ) {
        return await this.repository.save({
          ...body,
          date: new Date(body.date),
        });
      }
    	// ...
      @Patch(':id')
      async update(
        @Param('id') id,
        @Body(new ValidationPipe({ groups: ['update'] })) body: UpdateEventDto,
      ) {
        const event = await this.repository.findOne(id);
    
        return await this.repository.save({
          ...event,
          ...body,
          date: body.date ? new Date(body.date) : event!.date,
        });
      }
      // ...
    }
    
    
  3. main.ts 中禁用全局校验管道 app.useGlobalPipes(new ValidationPipe());

0x05 模块

(1)概念

  • 模块(Module)是一个拥有特定工具的类
  • 控制器(Controller)由模块注册
  • 提供者(Providers)是使用依赖注入的类的统称,使用 @Injectable 装饰器做注解,由 NestJS 创建和管理
  • 依赖注入(Dependency Injection,DI)用于将类所依赖的其他类或对象通过构造函数或属性自动注入
    • 优点:代码解耦、容易测试

(2)自定义模块

  1. 使用命令 nest generate module events 通过 NestJS 脚手架生成名为 events 的模块

    • 会自动创建 events 目录与 events.module.ts,并在 app.module.ts 中的 @Module 注解中新增 imports: [EventsModule]
  2. 将原来 events 模块相关文件复制到 events 目录中

    • create-event.dto.ts、update-event.dto.ts、event.entity.ts、events.controller.ts

    • events.module.ts

      import { Module } from '@nestjs/common';
      import { EventsController } from './events.controller';
      import { TypeOrmModule } from '@nestjs/typeorm';
      import { Event } from './event.entity';
      
      @Module({
        imports: [
          TypeOrmModule.forFeature([Event]),
        ],
        controllers: [EventsController],
        providers: [],
      })
      export class EventsModule {}
      
      
  3. app.module.ts

    import { Module } from '@nestjs/common';
    import { AppController } from './app.controller';
    import { AppService } from './app.service';
    import { EventsModule } from './events/events.module';
    import { TypeOrmModule } from '@nestjs/typeorm';
    
    @Module({
      imports: [
        TypeOrmModule.forRoot({
          type: 'mysql',
          host: 'localhost',
          port: 3306,
          username: 'root',
          password: 'root',
          database: 'nest-db',
          entities: [Event],
          synchronize: true,
        }),
        EventsModule,
      ],
      controllers: [AppController],
      providers: [AppService],
    })
    export class AppModule {}
    
    
  4. main.ts

    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    
    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
      await app.listen(process.env.PORT ?? 3000);
    }
    bootstrap();
    
    

(3)静态模块与动态模块

  • 静态模块通过 @Module 装饰器定义的,具有固定提供者和导入的模块
  • 动态模块通过模块类中的静态方法(如 registerforRoot)返回的,允许在运行时根据条件动态配置提供者或导入的模块
  • 举例:静态模块——Events,动态模块——TypeOrmModule

(4)提供者

提供者分为三种:类提供者(Class Provider)、值提供者(Value Provider)、工厂提供者(Factory Provider)

a. 类提供者

  1. app.chinese.service.ts

    export class AppChineseService {
      getHello(): string {
        return '你好,世界!';
      }
    }
    
    
  2. app.module.ts

    // ...
    import { AppService } from './app.service';
    import { AppChineseService } from './app.chinese.service';
    
    @Module({
      // ...
      providers: [
        {
          provide: AppService,
          useClass: AppChineseService,
        },
      ],
    })
    export class AppModule {}
    
    
  3. 启动项目并访问 http://localhost:3000/

b. 值提供者

  1. app.module.ts

    // ...
    
    @Module({
      // ...
      providers: [
        AppService,
        {
          provide: 'APP_NAME',
          useValue: 'Nest App',
        },
      ],
    })
    export class AppModule {}
    
    
  2. app.service.ts

    import { Inject, Injectable } from '@nestjs/common';
    
    @Injectable()
    export class AppService {
      constructor(
        @Inject('APP_NAME')
        private readonly appName: string,
      ) {}
    
      getHello(): string {
        return `Hello World! from ${this.appName}`;
      }
    }
    
    

c. 工厂提供者

  1. app.dummy.ts

    export class AppDummy {
      public dummy(): string {
        return 'dummy';
      }
    }
    
    
  2. app.module.ts

    // ...
    import { AppDummy } from './app.dummy';
    
    @Module({
      // ...
      providers: [
        AppService,
        {
          provide: 'APP_MESSAGE',
          inject: [AppDummy],
          useFactory: (appDummy) => `${appDummy.dummy()} Factory`,
        },
        AppDummy,
      ],
    })
    export class AppModule {}
    
    
  3. app.service.ts

    import { Inject, Injectable } from '@nestjs/common';
    
    @Injectable()
    export class AppService {
      constructor(
        @Inject('APP_MESSAGE')
        private readonly message: string,
      ) {}
    
      getHello(): string {
        return `Hello World! from ${this.message}`;
      }
    }
    
    

0x06 配置

(1)应用配置与环境

NestJS 官方提供了 config 模块用于配置管理

使用命令 npm install --save @nestjs/config 安装 config 模块

a. .env

  1. 在根目录新建 .env 文件

    DB_TYPE=mysql
    DB_HOST=localhost
    DB_PORT=3306
    DB_USERNAME=root
    DB_PASSWORD=root
    DB_DATABASE=nest-db
    
  2. app.module.ts

    // ...
    import { ConfigModule } from '@nestjs/config';
    import { TypeOrmModule } from '@nestjs/typeorm';
    
    @Module({
      imports: [
        ConfigModule.forRoot(),
        TypeOrmModule.forRoot({
          type: process.env.DB_TYPE as any,
          host: process.env.DB_HOST,
          port: Number(process.env.DB_PORT),
          username: process.env.DB_USERNAME,
          password: process.env.DB_PASSWORD,
          database: process.env.DB_DATABASE,
          entities: [Event],
          synchronize: true,
        }),
        EventsModule,
      ],
      // ...
    })
    export class AppModule {}
    
    

b. 自定义配置文件

  1. src/config/orm.config.ts

    import { registerAs } from '@nestjs/config';
    import { TypeOrmModuleOptions } from '@nestjs/typeorm';
    import { Event } from 'src/events/event.entity';
    
    export default registerAs(
      'orm.config',
      (): TypeOrmModuleOptions => ({
        type: process.env.DB_TYPE as any,
        host: process.env.DB_HOST,
        port: Number(process.env.DB_PORT),
        username: process.env.DB_USERNAME,
        password: process.env.DB_PASSWORD,
        database: process.env.DB_DATABASE,
        entities: [Event],
        synchronize: true,
      }),
    );
    
    

    src/config/orm.config.prod.ts

    import { registerAs } from '@nestjs/config';
    import { TypeOrmModuleOptions } from '@nestjs/typeorm';
    import { Event } from 'src/events/event.entity';
    
    export default registerAs(
      'orm.config',
      (): TypeOrmModuleOptions => ({
        type: process.env.DB_TYPE as any,
        host: process.env.DB_HOST,
        port: Number(process.env.DB_PORT),
        username: process.env.DB_USERNAME,
        password: process.env.DB_PASSWORD,
        database: process.env.DB_DATABASE,
        entities: [Event],
        synchronize: false, // 生产环境禁用数据库同步
      }),
    );
    
    
  2. app.module.ts

    // ...
    import { TypeOrmModule } from '@nestjs/typeorm';
    import { ConfigModule } from '@nestjs/config';
    import ormConfig from './config/orm.config';
    import ormConfigProd from './config/orm.config.prod';
    
    @Module({
      imports: [
        ConfigModule.forRoot({
          isGlobal: true, // 是否作为全局配置
          load: [ormConfig, ormConfigProd], // 加载配置文件
        }),
        TypeOrmModule.forRootAsync({ // 异步
          useFactory:
            process.env.NODE_ENV !== 'production' ? ormConfig : ormConfigProd,
        }),
        // ...
      ],
      // ...
    })
    export class AppModule {}
    
    

c. 变量展开

  1. .env

    APP_URL=example.com
    SUPPORT_EMAIL=support@${APP_URL}
    
  2. app.module.ts

    // ...
    
    @Module({
      imports: [
        ConfigModule.forRoot({
          // ...
          expandVariables: true, // 是否展开变量
          // process.env.SUPPORT_EMAIL === "support@example.com"
        }),
        // ...
      ],
      // ...
    })
    export class AppModule {}
    
    

(2)日志

  1. events.controller.ts

    import {
      Logger,
      // ...
    } from '@nestjs/common';
    // ...
    
    @Controller('events')
    export class EventsController {
      private readonly logger = new Logger(EventsController.name);
      // ...
      @Get(':id')
      async readOne(@Param('id') id) {
        this.logger.log(`Reading event with id ${id}`);
        const events = await this.repository.findOne(id);
        this.logger.debug(`Found event: ${events}`);
        return events;
      }
      // ...
    }
    
    
  2. main.ts

    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    
    async function bootstrap() {
      const app = await NestFactory.create(AppModule, {
        logger: ['error', 'warn', 'debug', 'log', 'verbose'],
      });
      await app.listen(process.env.PORT ?? 3000);
    }
    bootstrap();
    
    

0x07 关系

关系可以分为三类:

  • 一对一关系:如个人与身份证号码
  • 一对多关系:如文章与评论
  • 多对多关系:如老师与学生

(1)一对多关系

单个事件(Event)中存在多个参与者(Attendee)

a. 创建关系

  1. attendee.entity.ts

    import {
      Column,
      Entity,
      JoinColumn,
      ManyToOne,
      PrimaryGeneratedColumn,
    } from 'typeorm';
    import { Event } from './event.entity';
    
    @Entity('attendee')
    export class Attendee {
      @PrimaryGeneratedColumn('uuid')
      id: number;
    
      @Column()
      name: string;
    
      // 多对一注解
      @ManyToOne(
        () => Event, // 关联的实体类型
        (event) => event.attendees, // 关联的字段
        {
          nullable: false, // 不能为空
        },
      )
      // 连接列注解
      @JoinColumn()
      event: Event;
    }
    
    
  2. events.entity.ts

    import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
    import { Attendee } from './attendee.entity';
    
    @Entity('event')
    export class Event {
      @PrimaryGeneratedColumn('increment')
      id: number;
    
      @Column()
      title: string;
    
      @Column()
      description: string;
    
      @Column()
      date: Date;
    
      // 一对多注解
      @OneToMany(
        () => Attendee, // 关联的实体类型
        (attendee) => attendee.event, // 关联的字段
        {
          nullable: false, // 是否允许为空
        },
      )
      attendees: Attendee[];
    }
    
    
  3. orm.config.ts

    import { registerAs } from '@nestjs/config';
    import { TypeOrmModuleOptions } from '@nestjs/typeorm';
    import { Attendee } from 'src/events/attendee.entity';
    import { Event } from 'src/events/event.entity';
    
    export default registerAs(
      'orm.config',
      (): TypeOrmModuleOptions => ({
        // ...
        entities: [Attendee, Event],
        synchronize: true,
      }),
    );
    
    

b. 载入关系实体

载入方法包括:立即加载(eager)、延迟加载(懒加载,lazy)

  1. event.entity.ts

    import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
    import { Attendee } from './attendee.entity';
    
    @Entity('event')
    export class Event {
      // ...
      @OneToMany(
        () => Attendee,
        (attendee) => attendee.event,
        {
          eager: true,
        },
      )
      attendees: Attendee[];
    }
    
    
  2. events.controller.ts

    // ...
    
    @Controller('events')
    export class EventsController {
      // ...
      @Get(':id/attendees')
      async getAttendees(@Param('id') id) {
        const event = await this.repository.findOne({
          where: { id },
          relations: ['attendees'],
        });
        if (!event) return null;
    
        return event.attendees;
      }
    }
    
    

c. 联合关系实体

  1. events.module.ts

    import { Module } from '@nestjs/common';
    import { EventsController } from './events.controller';
    import { TypeOrmModule } from '@nestjs/typeorm';
    import { Attendee } from './attendee.entity';
    import { Event } from './event.entity';
    
    @Module({
      imports: [TypeOrmModule.forFeature([Attendee, Event])],
      controllers: [EventsController],
      providers: [],
    })
    export class EventsModule {}
    
    
  2. events.controller.ts

    // ...
    
    @Controller('events')
    export class EventsController {
      // ...
      constructor(
        @InjectRepository(Attendee)
        private readonly attendeeRepository: Repository<Attendee>,
        @InjectRepository(Event)
        private readonly eventRepository: Repository<Event>,
      ) {}
    	// ...
    }
    
    
  3. 方法一:

    @Get(':id/attendees')
    async saveAttendees(@Param('id') id) {
      const event = await this.eventRepository.findOne(id);
      if (!event) return null;
    
      const attendee = new Attendee();
      attendee.name = 'Alice';
      attendee.event = event;
    
      return await this.attendeeRepository.save(attendee);
    }
    

    方法二:

    @Get(':id/attendees')
    async saveAttendees(@Param('id') id) {
      const event = new Event();
      event.id = id;
    
      const attendee = new Attendee();
      attendee.name = 'Alice';
      attendee.event = event;
    
      return await this.attendeeRepository.save(attendee);
    }
    

    方法三:

    1. event.entity.ts

      import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
      import { Attendee } from './attendee.entity';
      
      @Entity('event')
      export class Event {
        // ...
        @OneToMany(
          () => Attendee,
          (attendee) => attendee.event,
          {
            cascade: true, // 是否级联保存、更新、删除
          },
        )
        attendees: Attendee[];
      }
      
      
    2. events.controller.ts

      @Get(':id/attendees')
      async saveAttendees(@Param('id') id) {
        const event = await this.repository.findOne({
          where: { id },
          relations: ['attendees'],
        });
      
        const attendee = new Attendee();
        attendee.name = 'Alice';
        
        event.relations.push(attendee);
      
        return await this.eventRepository.save(event);
      }
      

(2)一对一关系

单个事件(Event)中存在单个委员会(Committee)

  1. committee.entity.ts

    import { Column, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
    import { Event } from './event.entity';
    
    @Entity('committee')
    export class Committee {
      @PrimaryGeneratedColumn('uuid')
      id: number;
    
      @Column()
      name: string;
    
      @OneToOne(() => Event)
      @JoinColumn()
      event: Event;
    }
    
    
  2. event.entity.ts

    import {
      // ...
      OneToOne,
    } from 'typeorm';
    // ...
    import { Committee } from './committee.entity';
    
    @Entity('event')
    export class Event {
      // ...
    
      // 一对一注解
      @OneToOne(() => Committee)
      committee: Committee;
    }
    
    
  3. orm.config.ts

    // ...
    import { Committee } from 'src/events/committee.entity';
    
    export default registerAs(
      'orm.config',
      (): TypeOrmModuleOptions => ({
        // ...
        entities: [Committee, Event],
        synchronize: true,
      }),
    );
    
    

(3)多对多关系

多个事件(Event)中存在多个主持人(Presenter)

  1. presenter.entity.ts

    import {
      Column,
      Entity,
      JoinTable,
      ManyToMany,
      PrimaryGeneratedColumn,
    } from 'typeorm';
    import { Event } from './event.entity';
    
    @Entity('presenter')
    export class Presenter {
      @PrimaryGeneratedColumn('uuid')
      id: number;
    
      @Column()
      name: string;
    
      // 多对多注解
      @ManyToMany(() => Presenter, (presenter) => presenter.id, {
        cascade: true,
      })
      // 连接表注解
      @JoinTable()
      events: Event[];
    }
    
    
  2. event.entity.ts

    import {
      Column,
      Entity,
      ManyToMany,
      OneToMany,
      PrimaryGeneratedColumn,
    } from 'typeorm';
    import { Presenter } from './presenter.entity';
    // ...
    
    @Entity('event')
    export class Event {
      // ...
      // 多对多注解
      @ManyToMany(() => Presenter, (presenter) => presenter.events)
      presenters: Presenter[];
    }
    
    
  3. orm.config.ts

    // ...
    import { Presenter } from 'src/events/presenter.entity';
    
    export default registerAs(
      'orm.config',
      (): TypeOrmModuleOptions => ({
        // ...
        entities: [Attendee, Event, Presenter],
        synchronize: true,
      }),
    );
    
    
  4. events.module.ts

    // ...
    import { Presenter } from './presenter.entity';
    
    @Module({
      imports: [TypeOrmModule.forFeature([Attendee, Event, Presenter])],
      controllers: [EventsController],
      providers: [],
    })
    export class EventsModule {}
    
    
  5. events.controller.ts

    // ...
    import { Presenter } from './presenter.entity';
    
    @Controller('events')
    export class EventsController {
      constructor(
        // ...
        @InjectRepository(Presenter)
        private readonly presenterRepository: Repository<Presenter>,
      ) {}
      // ...
      @Get(':id/presenters')
      async savePresenters(@Param('id') id) {
        const event = await this.eventRepository.findOne(id);
        if (!event) return null;
    
        const presenter1 = new Presenter();
        presenter1.name = 'Bob';
    
        const presenter2 = new Presenter();
        presenter2.name = 'Carol';
    
        event.presenters = [presenter1, presenter2];
    
        return await this.eventRepository.save(event);
      }
    
      @Delete(':id/presenters/:name')
      async deletePresenters(@Param('id') id, @Param('name') name) {
        const event = await this.eventRepository.findOne(id);
        if (!event) return null;
    
        event.presenters = event.presenters.filter(
          (presenter) => presenter.name !== name,
        );
    
        return await this.eventRepository.save(event);
      }
    }
    
    

(4)查询构建器

查询构建器用于使用面向对象的语法构建查询

a. 创建查询构建器

  1. events.service.ts

    import { Repository } from 'typeorm';
    import { Event } from './event.entity';
    import { InjectRepository } from '@nestjs/typeorm';
    import { Injectable, Logger } from '@nestjs/common';
    
    @Injectable()
    export class EventsService {
      private readonly logger = new Logger(EventsService.name);
    
      constructor(
        @InjectRepository(Event)
        private readonly repository: Repository<Event>,
      ) {}
    
      private getEventsBaseQuery() {
        return this.repository
          .createQueryBuilder('e') // 创建一个查询构建器,e 是表的别名
          .orderBy('e.id', 'DESC'); // 按照 id 降序排列
      }
    
      public async getEvent(id: number): Promise<Event | null> {
        const query = this.getEventsBaseQuery() // 获取基础查询
          .andWhere('e.id = :id', { id }); // 添加一个条件,id 等于传入的 id
    
        this.logger.debug(query.getSql()); // 打印 SQL 语句
    
        return await query.getOne();
      }
    }
    
    
  2. events.controller.ts

    import {
      NotFoundException,
      // ...
    } from '@nestjs/common';
    // ...
    import { EventsService } from './events.service';
    
    @Controller('events')
    export class EventsController {
      constructor(
        // ...
        private readonly eventsService: EventsService,
      ) {}
      // ...
      @Get(':id')
      async readOne(@Param('id') id) {
        const event = await this.eventsService.getEvent(id);
        if (!event) throw new NotFoundException();
        return event;
      }
      // ...
    }
    
    
  3. events.module.ts

    // ...
    import { EventsService } from './events.service';
    
    @Module({
      // ...
      providers: [EventsService],
    })
    export class EventsModule {}
    
    

b. 聚合

  1. event.entity.ts

    // ...
    @Entity('event')
    export class Event {
      // ...
      attendeeCount?: number;
    }
    
    
  2. events.service.ts

    // ...
    
    @Injectable()
    export class EventsService {
      // ...
      private getEventsWithAttendeeCountQuery() {
        return this.getEventsBaseQuery().loadRelationCountAndMap(
          'e.attendeeCount',
          'e.attendees',
        );
      }
    
      public async getEvent(id: number): Promise<Event | null> {
        const query = this.getEventsWithAttendeeCountQuery().andWhere(
          'e.id = :id',
          { id },
        );
    
        this.logger.debug(query.getSql());
    
        return await query.getOne();
      }
    }
    
    

c. 连接

  1. attendee.entity.ts

    // ...
    
    export enum AttendeeAnswerEnum {
      Accepted = 1,
      Maybe = 2,
      Rejected = 3,
    }
    
    @Entity('attendee')
    export class Attendee {
      // ...
      @Column({
        type: 'enum',
        enum: AttendeeAnswerEnum,
        default: AttendeeAnswerEnum.Maybe,
      })
      answer: AttendeeAnswerEnum;
    }
    
    
  2. events.entity.ts

    // ...
    
    @Entity('event')
    export class Event {
      // ...
      attendeeRejected?: number;
      attendeeMaybe?: number;
      attendeeAccepted?: number;
    }
    
    
  3. events.service.ts

    // ...
    import { AttendeeAnswerEnum } from './attendee.entity';
    
    @Injectable()
    export class EventsService {
      // ...
      private getEventsWithAttendeeCountQuery() {
        return this.getEventsBaseQuery()
          .loadRelationCountAndMap(
            'e.attendeeAccepted',
            'e.attendees',
            'attendee',
            (queryBuilder) =>
              queryBuilder.where('attendee.answer = :answer', {
                answer: AttendeeAnswerEnum.Accepted,
              }),
          )
          .loadRelationCountAndMap(
            'e.attendeeAccepted',
            'e.attendees',
            'attendee',
            (queryBuilder) =>
              queryBuilder.where('attendee.answer = :answer', {
                answer: AttendeeAnswerEnum.Maybe,
              }),
          )
          .loadRelationCountAndMap(
            'e.attendeeAccepted',
            'e.attendees',
            'attendee',
            (queryBuilder) =>
              queryBuilder.where('attendee.answer = :answer', {
                answer: AttendeeAnswerEnum.Rejected,
              }),
          );
      }
      // ...
    }
    
    

d. 过滤

  1. list.events.ts

    export enum DateEventFilter {
      All = 1,
      Today,
      Tomorrow,
      ThisWeek,
      NextWeek,
    }
    
    export class ListEvents {
      date?: DateEventFilter = DateEventFilter.All;
    }
    
    
  2. events.service.ts

    // ...
    import { ListEvents, DateEventFilter } from './list.events';
    
    @Injectable()
    export class EventsService {
      // ...
      public async getEventsWithAttendeeCountFiltered(filter?: ListEvents) {
        let query = this.getEventsWithAttendeeCountQuery();
        if (!filter) return await query.getMany();
    
        if (filter.date) {
          switch (filter.date) {
            case DateEventFilter.Today:
              query = query.andWhere(
                'e.date >= CURDATE() and e.date <= CURDATE() + INTERVAL 1 DAY',
              );
              break;
            case DateEventFilter.Tomorrow:
              query = query.andWhere(
                'e.date >= CURDATE() + INTERVAL 1 DAY and e.date <= CURDATE() + INTERVAL 2 DAY',
              );
              break;
            case DateEventFilter.ThisWeek:
              query = query.andWhere(
                'YEARWEEK(e.date, 1) = YEARWEEK(CURDATE(), 1)',
              );
              break;
            case DateEventFilter.NextWeek:
              query = query.andWhere(
                'YEARWEEK(e.date, 1) = YEARWEEK(CURDATE(), 1) + 1',
              );
              break;
          }
        }
    
        return await query.getMany();
      }
    }
    
    
  3. events.controller.ts

    import {
      ParseIntPipe,
      // ...
    } from '@nestjs/common';
    // ...
    import { EventsService } from './events.service';
    import { ListEvents } from './list.events';
    
    @Controller('events')
    export class EventsController {
      constructor(
      	// ...
        private readonly eventsService: EventsService,
      ) {}
      // ...
      @Get()
    	async readAll(@Query(ParseIntPipe) filter: ListEvents) {
        return await this.eventsService.getEventsWithAttendeeCountFiltered(filter);
      }
      // ...
    }
    
    

e. 分页

  1. src\pagination\paginator.ts

    import { ObjectLiteral, SelectQueryBuilder } from 'typeorm';
    
    /**
     * 分页选项
     */
    export interface PaginateOptions {
      limit: number;
      currentPage: number;
      total?: boolean;
    }
    
    /**
     * 分页结果
     */
    export interface PaginationResult<T> {
      firstPage: number;
      lastPage: number;
      limit: number;
      total?: number;
      data: T[];
    }
    
    /**
     * 分页查询
     * @param queryBuilder 查询构建器
     * @param options 分页选项
     * @returns 分页结果
     */
    export async function paginate<T extends ObjectLiteral>(
      queryBuilder: SelectQueryBuilder<T>,
      options: PaginateOptions = {
        limit: 10,
        currentPage: 1,
        total: false,
      },
    ): Promise<PaginationResult<T>> {
      const offset = (options.currentPage - 1) * options.limit;
      const data = await queryBuilder.limit(options.limit).offset(offset).getMany();
      return {
        firstPage: offset + 1,
        lastPage: offset + data.length,
        limit: options.limit,
        total: options.total ? await queryBuilder.getCount() : undefined,
        data,
      };
    }
    
    
  2. list.events.ts

    // ...
    export class ListEvents {
      // ...
      page: number = 1;
    }
    
    
  3. events.service.ts

    // ...
    import { paginate, PaginateOptions } from 'src/pagination/paginator';
    
    @Injectable()
    export class EventsService {
      // ...
    
      // 改为私有方法,并移除 .getMany()
      private async getEventsWithAttendeeCountFiltered(filter?: ListEvents) {
        let query = this.getEventsWithAttendeeCountQuery();
        if (!filter) return query;
    
        // ...
    
        return query;
      }
    
      public async getEventsWithAttendeeCountFilteredPaginated(
        filter: ListEvents,
        paginateOptions: PaginateOptions,
      ) {
        return await paginate(
          await this.getEventsWithAttendeeCountFiltered(filter),
          paginateOptions,
        );
      }
    }
    
    
  4. events.controller.ts

    import {
      // ...
      UsePipes,
      ValidationPipe,
    } from '@nestjs/common';
    // ...
    import { EventsService } from './events.service';
    import { ListEvents } from './list.events';
    
    @Controller('events')
    export class EventsController {
      // ...
      @Get()
      @UsePipes(
        new ValidationPipe({
          transform: true, // 将查询参数转换为数字
        }),
      )
      async readAll(@Query(ParseIntPipe) filter: ListEvents) {
        return await this.eventsService.getEventsWithAttendeeCountFilteredPaginated(
          filter,
          {
            total: true,
            currentPage: filter.page,
            limit: 10,
          },
        );
      }
    	// ...
    }
    
    

f. 其他操作

举例:删除实体

  1. events.service.ts

     import { DeleteResult, Repository } from 'typeorm';
    // ...
    
    @Injectable()
    export class EventsService {
      constructor(
        @InjectRepository(Event)
        private readonly repository: Repository<Event>,
      ) {}
    	// ...
      public async deleteEvent(id: number): Promise<DeleteResult> {
        return this.repository
          .createQueryBuilder('e')
          .delete()
          .where('id = :id', {
            id,
          })
          .execute();
      }
    }
    
    
  2. events.controller.ts

    // ...
    
    @Controller('events')
    export class EventsController {
      // ...
      @Delete(':id')
      async delete(@Param('id') id) {
        const result = await this.eventsService.deleteEvent(id);
        if (result?.affected !== 1) throw new NotFoundException(); // affected 为 1 表示删除操作成功
        return;
      }
    	// ...
    }
    
    

修改实体:.update().set(新数据对象).where().execute();

修改关系:.relation(目标实体, 列).of(当前实体).add(新数据列表);

0x08 认证

(1)概述

  • 认证指当用户进入系统时,需要验证用户是否拥有且有效的身份凭证
  • Passport 是常用的认证库
    • 策略(Strategy)是一个用于校验用户凭证合法性的类,通过本地数据库校验(本地策略)或 JWT 策略来实现
      • 使用命令 npm install --save @nestjs/passport passport 安装适用于 NestJS 的 Passport 及其他相关依赖
  • Bcrypt 是常用的密码哈希算法
    • 对于同一字符串的加密结果相同,适用于密码加密与验证
      • 使用命令 npm install bcryptnpm install --save-dev @types/bcrypt 安装 Bcrypt 及类型声明包

使用命令 nest generate module auth 生成 auth 模块

(2)Passport

a. 本地策略

  1. 使用命令 npm install --save passport-local 安装 Passport 本地策略依赖

  2. 使用命令 npm install --save-dev @types/passport-local 安装类型声明包

  3. src\auth\user.entity.ts

    import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
    
    @Entity('user')
    export class User {
      @PrimaryGeneratedColumn('uuid')
      id: number;
    
      @Column()
      username: string;
    
      @Column()
      password: string;
    }
    
    
  4. src\config\orm.config.ts

    // ...
    import { User } from 'src/auth/user.entity';
    
    export default registerAs(
      'orm.config',
      (): TypeOrmModuleOptions => ({
        // ...
        entities: [/* ... */ User],
        synchronize: true,
      }),
    );
    
    
  5. src\auth\local.strategy.ts

    import { Injectable, UnauthorizedException } from '@nestjs/common';
    import { PassportStrategy } from '@nestjs/passport';
    import { InjectRepository } from '@nestjs/typeorm';
    import { Strategy } from 'passport-local';
    import { User } from './user.entity';
    import { Repository } from 'typeorm';
    
    @Injectable()
    export class LocalStrategy extends PassportStrategy(Strategy) {
      constructor(
        @InjectRepository(User)
        private readonly userRepository: Repository<User>,
      ) {
        super();
      }
    
      public async validate(username: string, password: string): Promise<any> {
        const user = await this.userRepository.findOne({ where: { username } });
    
        if (!user || user.password !== password) throw new UnauthorizedException();
    
        return user;
      }
    }
    
    
  6. src\auth\auth.controller.ts

    import { Controller, Post, Request, UseGuards } from '@nestjs/common';
    import { AuthGuard } from '@nestjs/passport';
    
    @Controller('auth')
    export class AuthController {
      @Post('login')
      @UseGuards(AuthGuard('local')) // 使用守卫,调用策略来验证凭证('local' 字符串在 LocalStrategy 类继承时确定)
      async login(@Request() request) {
        return {
          userId: request.user.id,
        };
      }
    }
    
    
  7. src\auth\auth.module.ts

    import { Module } from '@nestjs/common';
    import { TypeOrmModule } from '@nestjs/typeorm';
    import { User } from './user.entity';
    import { AuthController } from './auth.controller';
    import { LocalStrategy } from './local.strategy';
    
    @Module({
      imports: [TypeOrmModule.forFeature([User])],
      controllers: [AuthController],
      providers: [LocalStrategy],
    })
    export class AuthModule {}
    
    

b. JWT

Json Web Token(JWT)是一种令牌,安全且轻量,优缺点参考《NodeJS 与 Express-登录鉴权-JWT | 博客园-SRIGT

  1. 使用命令 npm install --save @nestjs/jwt passport-jwt 安装 Passport.js JWT 策略与其他依赖

  2. 使用命令 npm install --save-dev @types/passport-jwt 安装类型声明包

  3. .env

    JWT_SECRET=JSONWEBTOKENSECRET
    
  4. src\auth\auth.service.ts

    import { Injectable } from '@nestjs/common';
    import { JwtService } from '@nestjs/jwt';
    import { User } from './user.entity';
    
    @Injectable()
    export class AuthService {
      constructor(private readonly jwtService: JwtService) {}
    
      public getTokenForUser(user: User): string {
        return this.jwtService.sign({
          username: user.username,
          sub: user.id,
        });
      }
    }
    
    
  5. src\auth\jwt.strategy.ts

    import { Injectable } from '@nestjs/common';
    import { PassportStrategy } from '@nestjs/passport';
    import { InjectRepository } from '@nestjs/typeorm';
    import { ExtractJwt, Strategy, StrategyOptionsWithRequest } from 'passport-jwt';
    import { User } from './user.entity';
    import { Repository } from 'typeorm';
    
    @Injectable()
    export class JwtStrategy extends PassportStrategy(Strategy) {
      constructor(
        @InjectRepository(User)
        private readonly userRepository: Repository<User>,
      ) {
        super({
          jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), // 从请求头中提取 JWT
          ignoreExpiration: false, // 不在此处检查 JWT 是否过期
          secretOrKey: process.env.JWT_SECRET, // 用于验证 JWT 的密钥
        } as StrategyOptionsWithRequest);
      }
    
      async validate(payload: any) {
        return await this.userRepository.findOne(payload.sub);
      }
    }
    
    
  6. src\auth\auth.module.ts

    // ...
    import { JwtModule } from '@nestjs/jwt';
    import { JwtStrategy } from './jwt.strategy';
    import { AuthService } from './auth.service';
    
    @Module({
      imports: [
        // ...
        JwtModule.registerAsync({
          useFactory: () => ({
            secret: process.env.JWT_SECRET, // JWT 加密密钥
            signOptions: { expiresIn: '24h' }, // JWT 有效期,如 24 小时
          }),
        }),
      ],
      // ...
      providers: [/* ... */, JwtStrategy AuthService],
    })
    export class AuthModule {}
    
    
  7. src\auth\profile.entity.ts

    import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
    
    @Entity('profile')
    export class Profile {
      @PrimaryGeneratedColumn()
      id: number;
    
      @Column()
      age: number;
    }
    
    
  8. src\config\orm.config.ts

    // ...
    import { Profile } from 'src/auth/profile.entity';
    import { User } from 'src/auth/user.entity';
    
    export default registerAs(
      'orm.config',
      (): TypeOrmModuleOptions => ({
        // ...
        entities: [/* ... */ Profile, User],
        synchronize: true,
      }),
    );
    
    
  9. src\auth\user.entity.ts

    import {
      Column,
      Entity,
      JoinColumn,
      OneToOne,
      PrimaryGeneratedColumn,
    } from 'typeorm';
    import { Profile } from './profile.entity';
    
    @Entity('user')
    export class User {
      // ...
      @OneToOne(() => Profile)
      @JoinColumn()
      profile: Profile;
    }
    
    
  10. src\auth\auth.controller.ts

    import { Controller, Get, Post, Request, UseGuards } from '@nestjs/common';
    import { AuthGuard } from '@nestjs/passport';
    import { AuthService } from './auth.service';
    
    @Controller('auth')
    export class AuthController {
      // ...
      @Get('profile')
      @UseGuards(AuthGuard('jwt'))
      async getProfile(@Request() request) {
        return request.user;
      }
    }
    
    

(3)Bcrypt

a. 密码哈希加密与比较

  1. auth.service.ts

    // ...
    import * as bcrypt from 'bcrypt';
    
    @Injectable()
    export class AuthService {
      // ...
      public async hashPassword(password: string): Promise<string> {
        return await bcrypt.hash(password, 10);
      }
    }
    
    
  2. local.strategy.ts

    // ...
    import * as bcrypt from 'bcrypt';
    
    @Injectable()
    export class LocalStrategy extends PassportStrategy(Strategy) {
      // ...
      public async validate(username: string, password: string): Promise<any> {
        const user = await this.userRepository.findOne({ where: { username } });
    
        if (!(user && (await bcrypt.compare(password, user.password))))
          throw new UnauthorizedException();
    
        return user;
      }
    }
    
    

b. 自定义修饰器用于验证用户

  1. src\auth\current-user.decorator.ts

    import { createParamDecorator, ExecutionContext } from '@nestjs/common';
    
    export const CurrentUser = createParamDecorator(
      (data: unknown, context: ExecutionContext) => {
        const request = context.switchToHttp().getRequest();
        return request.user ?? null;
      },
    );
    
    
  2. auth.controller.ts

    // ...
    import { CurrentUser } from './current-user.decorator';
    
    @Controller('auth')
    export class AuthController {
      constructor(private readonly authService: AuthService) {}
    
      @Post('login')
      @UseGuards(AuthGuard('local'))
      async login(@CurrentUser() user) {
        return {
          userId: user.id,
          token: this.authService.getTokenForUser(user),
        };
      }
    
      @Get('profile')
      @UseGuards(AuthGuard('jwt'))
      async getProfile(@CurrentUser() user) {
        return user;
      }
    }
    
    

c. 用户注册

  1. user.entity.ts

    // ...
    
    @Entity('user')
    export class User {
      // ...
      @Column({ unique: true })
      username: string;
    	// ...
    }
    
    
  2. src\auth\create-user.dto.ts

    import { IsString, Length } from 'class-validator';
    
    export class CreateUserDto {
      @IsString({ message: '用户名应为字符串' })
      @Length(1, 255, { message: '用户名应为1~255个字符' })
      username: string;
    
      @IsString({ message: '密码应为字符串' })
      @Length(1, 255, { message: '密码应为1~255个字符' })
      password: string;
    
      @IsString({ message: '确认密码应为字符串' })
      @Length(1, 255, { message: '确认密码应为1~255个字符' })
      retypedPassword: string;
    }
    
    
  3. src\auth\users.controller.ts

    import { BadRequestException, Body, Controller, Post } from '@nestjs/common';
    import { AuthService } from './auth.service';
    import { CreateUserDto } from './create-user.dto';
    import { User } from './user.entity';
    import { InjectRepository } from '@nestjs/typeorm';
    import { Repository } from 'typeorm';
    
    @Controller('users')
    export class UsersController {
      constructor(
        private readonly authService: AuthService,
        @InjectRepository(User)
        private readonly userRepository: Repository<User>,
      ) {}
    
      @Post()
      async create(@Body() body: CreateUserDto) {
        const user = new User();
    
        if (body.password !== body.retypedPassword)
          throw new BadRequestException(['密码不一致']);
    
        const existingUser = await this.userRepository.findOne({
          where: { username: body.username },
        });
        if (existingUser) throw new BadRequestException(['用户名已存在']);
    
        user.username = body.username;
        user.password = await this.authService.hashPassword(body.password);
    
        return {
          ...(await this.userRepository.save(user)),
          token: this.authService.getTokenForUser(user),
        };
      }
    }
    
    
  4. auth.module.ts

    // ...
    import { UsersController } from './users.controller';
    
    @Module({
      // ...
      controllers: [/* ... */ UsersController],
      // ...
    })
    export class AuthModule {}
    
    

0x09 序列化

(1)拦截器与序列化

  • 拦截器(Interceptor)可以拦截并修改控制器返回的响应,由 NestInterceptor 实现,具有以下功能:
    • 在方法前后绑定额外的逻辑
    • 通过函数转换返回的结果与抛出的异常
    • 扩展基础函数的功能
    • 重写指定函数
  • 序列化(Serialization)用于将一个对象从一种状态转换为另一种状态
    • 如:将一个实体转换为 JSON,其中仅包含白名单中的属性

(2)序列化数据

  1. 使用命令 npm installl --save class-transformer 安装类转换器库(在 0x04 章(2)节安装过)

  2. events.controller.ts

    import {
      // ...
      ClassSerializerInterceptor,
      SerializeOptions,
      UseInterceptors,
    } from '@nestjs/common';
    // ...
    
    @Controller('events')
    @SerializeOptions({
      strategy: 'excludeAll', // 不包含所有未标记的属性
    })
    export class EventsController {
      // ...
      @Get(':id')
      @UseInterceptors(ClassSerializerInterceptor) // 序列化响应
      async readOne(@Param('id') id) {
        // ...
      }
      // ...
    }
    
    
  3. event.entity.ts

    // ...
    import { Expose } from 'class-transformer';
    
    @Entity('event')
    export class Event {
      @PrimaryGeneratedColumn('increment')
      id: number;
    
      @Column()
      @Expose()
      title: string;
    
      @Column()
      @Expose()
      description: string;
    
      @Column()
      @Expose()
      date: Date;
    
      // ...
    
      @Expose()
      attendeeCount?: number;
    
      @Expose()
      attendeeRejected?: number;
      @Expose()
      attendeeMaybe?: number;
      @Expose()
      attendeeAccepted?: number;
    }
    
    

(3)序列化嵌套对象

序列化分页器处返回的对象

paginator.ts

import { Expose } from 'class-transformer';
import { ObjectLiteral, SelectQueryBuilder } from 'typeorm';

export interface PaginateOptions {
  limit: number;
  currentPage: number;
  total?: boolean;
}

// 将接口转换为类
export class PaginationResult<T> {
  constructor(partial: Partial<PaginationResult<T>>) {
    Object.assign(this, partial);
  }

  @Expose()
  firstPage: number;
  @Expose()
  lastPage: number;
  @Expose()
  limit: number;
  @Expose()
  total?: number;
  @Expose()
  data: T[];
}

export async function paginate<T extends ObjectLiteral>(
  queryBuilder: SelectQueryBuilder<T>,
  options: PaginateOptions = {
    limit: 10,
    currentPage: 1,
    total: false,
  },
): Promise<PaginationResult<T>> {
  const offset = (options.currentPage - 1) * options.limit;
  const data = await queryBuilder.limit(options.limit).offset(offset).getMany();

  // 返回实例化对象
  return new PaginationResult({
    firstPage: offset + 1,
    lastPage: offset + data.length,
    limit: options.limit,
    total: options.total ? await queryBuilder.getCount() : undefined,
    data,
  });
}

-End-

posted @ 2025-03-08 19:03  SRIGT  阅读(105)  评论(0)    收藏  举报