说明

学习NestJS 官方基础课程【中英字幕 NestJS Fundamentals Course】​个人笔记
因为用不到测试,所以暂时学到了P65
后续项目可能用不到MogoDB,后面的也就没看

基础结构

  • 生成controller
    ​​ ​nest g controller coffee​
  • 生成service
    ​​ ​nest g service coffee​
  • 生成module
    ​​ ​nest g module coffee​
  • 生成entities
    ​​ ​nest g class coffee/entities/coffee.entity --no-spec​
  • 生成DTO
    ​nest g class coffee/dto/create-coffee.dto --no-spec​ ​DTO和Entity的区别,
  • Entity可能带有ID,是查询数据时定义的接口,
  • DTO是生成数据或更新时候用的

验证数据正确性

NextJS提供了​ ​ValidationPipe​ ​进行数据验证

​ValidationPipe​ ​提供了对所有传入客户端有效负载强制执行验证规则的便捷方式

  1. 将整个应用程序设置为使用ValidationPipe
  • 在main.ts中加入​ ​app.useGlobalPipes(new ValidationPipe());​
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());
await app.listen(3000);
}

bootstrap();a
  • 安装两个包​ ​yarn add class-validator class-transformer​
  1. 在DTO中进行验证
import { IsString } from "class-validator";

export class CreateCoffeeDto {
@IsString()
readonly name: string;

@IsString()
readonly brand: string;

@IsString({ each: true })
readonly flavors: string[];
}

DTO代码抽离

  1. 安装包​ ​yarn add @nestjs/mapped-types​
  2. 在​ ​update-coffee.dto.ts​ ​中,使用​ ​PartialType​ ​进行验证
    PartialType:表示继承所有属性,但是所有属性都是可选的,相当于只验证正确性,不验证存在性
import { PartialType } from "@nestjs/mapped-types";
import { CreateCoffeeDto } from "./create-coffee.dto";

export class UpdateCoffeeDto extends PartialType(CreateCoffeeDto) {
}

配置参数白名单,进行参数过滤

在ValidationPipe中传入一个对象,其中包含键/值白名单:true

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({
whitelist: true
}));
await app.listen(3000);
}

bootstrap();

开启后,通过post上传参数,将自动过滤掉不需要的参数

如果开启​ ​forbidNonWhitelisted:true​ ​,即

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({
whitelist: true, // 参数白名单
forbidNonWhitelisted: true // 上传白名单之外的参数,报错
}));
await app.listen(3000);
}

bootstrap();

如果上传不需要参数,会报错

instanceof

默认接受的参数instanceof dto是false

@Post()
create(@Body() createCoffeeDto: CreateCoffeeDto) {
console.log(createCoffeeDto instanceof CreateCoffeeDto);
return this.coffeesService.create(createCoffeeDto);
}

通过在​ ​ValidationPipe​ ​​配置​ ​transform:true​ ​,可以返回true

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({
whitelist: true, // 参数白名单
forbidNonWhitelisted: true, // 上传白名单之外的参数,报错
transform: true
}));
await app.listen(3000);
}

bootstrap();

Docker配置

参考

DockerId:kaisarh

Email:hkzxh1104

password:hkzxh1104

使用Docker

  1. 在根目录下新建​ ​docker-compose.yml​
version: "3"

services:
db:
image: postgres
restart: always
ports:
- "5432:5432"
environment:
POSTGRES_PASSWORD:
  1. 命令行启动​ ​docker-compose up -d ​ ​-d 标志意味着我们以“分离”模式运行我们的容器,意味着它们将在 background 中运行

目前Docker Compose YAML文件中只列出了一项服务,但供将来参考

  1. 执行完后就在Docker中创建了数据库,并可以在本机使用

使用typeorm关联数据库

  1. 安装
    ​​ ​yarn add @nestjs/typeorm typeorm@2 pg​
  2. 在app.module.ts中进行imports设置
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { CoffeeModule } from "./coffee/coffee.module";
import { TypeOrmModule } from "@nestjs/typeorm";

@Module({
imports: [CoffeeModule, TypeOrmModule.forRoot({
type: "postgres",
host: "localhost",
port: 5432,
username: "postgres",
password: "pass123",
database: "postgres",
autoLoadEntities: true, // 有助于自动加载模块,而不是指定实体数组
synchronize: true // 同步,确保我们的TypeORM实体在每次运行应用程序时都会与数据库同步 生产环境设置为true,开发环境设置为false
})],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {
}
  1. 重新启动项目后即可与数据库进行连接
[Nest] 8316  - 2022/05/24下午2:15:07     LOG [InstanceLoader]
  1. 在​ ​coffee.entities.ts​ ​中
  • 通过​ ​@Entity​ ​注解标注实体表
  • 通过​ ​PrimaryGeneratedColumn​ ​标注自增主键
  • 通过​ ​@Column​ ​​标注行,可以通过设置options配置参数。例如​ ​nullable​ ​设置非空
  1. 在应用程序中注册实体
    在​ ​coffee.module.ts​ ​中进行导入​ ​imports: [TypeOrmModule.forFeature([CoffeeEntity])],​
import { Module } from "@nestjs/common";
import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { CoffeeEntity } from "./entities/coffee.entity";

// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([CoffeeEntity])],
controllers: [CoffeeController],
providers: [CoffeeService]
})
export class CoffeeModule {
}

通过使用​ ​forFeature()​ ​将TypeORM注册到此模块中

我们在主​ ​AppModule​ ​​中使用了​ ​forRoot()​ ​​,但我们只这样做了一次,注册实体时,所有其他模块都将使用​ ​forFeature()​

在这里的​ ​forFeature()​ ​内部,传入一个实体数组,在咖啡例子中,只有一个咖啡实体

  1. 配置完成后,在数据库中会自动生成coffee数据表

typeorm操作数据库

  1. typeorm为每一个实体都生成一张对应的数据表
  2. ​TypeORM​ ​​提供的​ ​Repository​ ​类作为对数据源的抽象,并通过方法与数据进行交互
  3. 因为已经在​ ​CoffeesModule​ ​的范围内注册了​ ​Coffee Entity​ ​,可以使用从​ ​@nestjs/typeorm​ ​包导出的​ ​@InjectRepository​ ​装饰器将自动生成的​ ​存储库​ ​注册到​ ​CoffeeService​ ​中
  4. 之前通过数组模拟数据
private coffees: CoffeeEntity[] = [
{
id: 1,
name: "Shipwreck Roast",
brand: "Buddy Brew",
flavors: ["chocolate", "vanilla"]
},
{
id: 2,
name: "Raw coconut Latte",
brand: "Lucky Coffee",
flavors: ["coconut", "vanilla"]
}
];

将数据表注入后,可以删除这部分数据,并通过与数据库交互直接操作数据库

import { HttpException, HttpStatus, Injectable, NotFoundException } from "@nestjs/common";
import { CoffeeEntity } from "./entities/coffee.entity";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { CreateCoffeeDto } from "./dto/create-coffee.dto";
import { UpdateCoffeeDto } from "./dto/update-coffee.dto";

// 创建命令 nest g module coffee
@Injectable()
export class CoffeeService {
constructor(
@InjectRepository(CoffeeEntity)
private readonly coffeeEntityRepository: Repository<CoffeeEntity>
) {
}

findAll() {
return this.coffeeEntityRepository.find();
}

async findOne(id: string) {
// 抛出JS错误会返回服务器500
// throw "A random error";
const coffee = await this.coffeeEntityRepository.findOne(id);
// 错误处理,抛出异常
if (!coffee) {
//throw new HttpException(`Coffee #${id} not found`, HttpStatus.NOT_FOUND);
throw new NotFoundException(`Coffee #${id} not found!`);
} else {
return coffee;
}
}

create(createCoffeeDto: CreateCoffeeDto) {
const coffee = this.coffeeEntityRepository.create(createCoffeeDto);
return this.coffeeEntityRepository.save(coffee);
}

async update(id: string, updateCoffeeDto: UpdateCoffeeDto) {
// preload首先查看数据库中是否存在实体,存在更新实体中的所有值,不存在返回undefined
// 注意:preload只会查找并更新实体,不会更新数据库
const coffee = await this.coffeeEntityRepository.preload({
id: +id,
...updateCoffeeDto
});
if (!coffee) {
throw new NotFoundException(`Coffee #${id} not found`);
}
return this.coffeeEntityRepository.save(coffee);
}

async remove(id: string) {
const coffee = await this.findOne(id);
return this.coffeeEntityRepository.remove(coffee);
}
}

表之间关系

  • 一对一:​ ​@OneToOne()​
  • 一对多:​ ​@OneToMany()​ ​​ 或者​ ​@ManyToOne()​
  • 多对多:​ ​@ManyToMany()​

不同表之间建立关联

  1. 新建​ ​风味实体​ ​​ ​nest g class coffee/entities/flavor.entity --no-spec​ 实体中,导出的类不要有 Entity 后缀,因为我们不希望数据库表名存在entity
    定义实体
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";

@Entity()
export class FlavorEntity {
@PrimaryGeneratedColumn()
id: number;

@Column()
name: string;
}
  1. 在​ ​Coffee Entity​ ​中更新​ ​flavor​ ​属性
  • 删除​ ​flavors​ ​的​ ​@Column()​ ​装饰器,并通过其他装饰器与​ ​FlavorEntity​ ​设置​ ​Relation​
  • 从​ ​typeorm​ ​中引入​ ​@JoinTable()​ ​装饰器,其可以指定关系的​ ​OWNER​ ​端,在这里是​ ​Coffee Entity​
  • 通过​ ​@ManyToMany​ ​在​ ​Coffee Entity​ ​中指定与​ ​Flavor Entity​ ​的关系
  • 第一个参数指定​ ​type​
  • 第二个参数绑定与​ ​type​ ​中的哪个参数绑定关联
import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from "typeorm";
import { JoinTable } from "typeorm/browser";
import { Flavor } from "./flavor.entity";

@Entity()
// 默认 sql table === 'coffee'
// 可以在Entity('TABLE_NAME')进行指定
export class Coffee {
@PrimaryGeneratedColumn()
id: number;

@Column()
name: string;

@Column()
brand: string;

@JoinTable()
@ManyToMany(
type => Flavor,
flavor => flavor.coffees)
flavors: string[];
}
  • 通过​ ​@ManyToMany​ ​在​ ​Flavor Entity​ ​中指定与​ ​Coffee Entity​ ​的关系
import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from "typeorm";
import { Coffee } from "./coffee.entity";

@Entity()
export class Flavor {
@PrimaryGeneratedColumn()
id: number;

@Column()
name: string;

@ManyToMany(
type => Coffee,
coffee => coffee.flavors
)
coffees: Coffee[];
}
  1. 在​ ​Coffee.module.ts​ ​中引入​ ​Flavor​
import { Module } from "@nestjs/common";
import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Coffee } from "./entities/coffee.entity";
import { Flavor } from "./entities/flavor.entity";

// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor])],
controllers: [CoffeeController],
providers: [CoffeeService]
})
export class CoffeeModule {
}
  1. 配置完成后关联建立成功,同时数据库中多出一张表​ ​coffee_flavors_flavor​
  2. 建立关系后,更新​ ​Get​ ​方案,更新​ ​findAll​ ​与​ ​findOne​ ​,通过配置​ ​relations​ ​关联参数
  • 关于​ ​relations​ ​的解释为:
  • Indicates what relations of entity should be loaded (simplified left join form).
  • 指示应加载的实体关系(简化的左联接形式)。
findAll() {
return this.coffeeRepository.find({
relations:['flavors']
});
}

async findOne(id: string) {
// 抛出JS错误会返回服务器500
// throw "A random error";
const coffee = await this.coffeeRepository.findOne(id,{
relations:['flavors']
});
// 错误处理,抛出异常
if (!coffee) {
//throw new HttpException(`Coffee #${id} not found`, HttpStatus.NOT_FOUND);
throw new NotFoundException(`Coffee #${id} not found!`);
} else {
return coffee;
}
}

级联插入

添加新的咖啡​ ​Coffee​ ​​的时候,如果口味​ ​Flavor​ ​不存在?

  1. 在使用​ ​@ManyToMany()​ ​进行关联的时候,配置第三个参数,设置​ ​cascade: true​ ​,或者设置为仅在插入时候生效​ ​cascade:['insert']​
import { Column, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn } from "typeorm";
import { Flavor } from "./flavor.entity";

@Entity()
// 默认 sql table === 'coffee'
// 可以在Entity('TABLE_NAME')进行指定
export class Coffee {
@PrimaryGeneratedColumn()
id: number;

@Column()
name: string;

@Column()
brand: string;


@JoinTable()
@ManyToMany(
type => Flavor,
(Flavor) => Flavor.coffees,
{
cascade:true // ['insert']
})
flavors: string[];
}
  1. 回顾​ ​create-coffee.dto​ ​,其包含一个风味属性,是一个字符串数组,为了确保将这些字符串(风味的名称)映射到真实的实体,即风味实体的实例,需要做以下事情
  • 将​ ​Flavor Repository​ ​注入到​ ​CoffeesService​ ​类中
constructor(
@InjectRepository(Coffee)
private readonly coffeeRepository: Repository<Coffee>,
@InjectRepository(Flavor) // 將Flavor注入到coffeeService中
private readonly flavorRepository:Repository<Flavor>
) {
}
  • 定义一个新的私有方法并将其命名为:​ ​preloadFlavorByName​
private async preloadFlavorByName(name:string):Promise<Flavor>{
const existingFlavor = await this.flavorRepository.findOne({name});
if(existingFlavor){
return existingFlavor;
}
this.flavorRepository.create({name});
}
  • 调整​ ​create()​ ​方法
async create(createCoffeeDto: CreateCoffeeDto) {
// 使用map遍历CreateCoffeeDto所中有风味,对不存在的数据进行创建
const flavors = await Promise.all(
createCoffeeDto.flavors.map(name => this.preloadFlavorByName(name))
);
const coffee = this.coffeeRepository.create({
...createCoffeeDto,
flavors
});
return this.coffeeRepository.save(coffee);
}

调整​ ​coffee.entity.ts​ ​​中​ ​flavors​ ​的类型

import { Column, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn } from "typeorm";
import { Flavor } from "./flavor.entity";

@Entity()
// 默认 sql table === 'coffee'
// 可以在Entity('TABLE_NAME')进行指定
export class Coffee {
@PrimaryGeneratedColumn()
id: number;

@Column()
name: string;

@Column()
brand: string;


@JoinTable()
@ManyToMany(
type => Flavor,
(Flavor) => Flavor.coffees,
{
cascade:true // ['insert']
})
flavors: Flavor[]; // 将flavors的类型设置为Flavor
}
  • 调整​ ​update()​ ​方法
async update(id: string, updateCoffeeDto: UpdateCoffeeDto) {
// preload首先查看数据库中是否存在实体,存在更新实体中的所有值,不存在返回undefined
// 注意:preload只会查找并更新实体,不会更新数据库
const flavors =
updateCoffeeDto.flavors &&
(await Promise.all(
updateCoffeeDto.flavors.map(name => this.preloadFlavorByName(name))
));
const coffee = await this.coffeeRepository.preload({
id: +id,
...updateCoffeeDto,
flavors
});
if (!coffee) {
throw new NotFoundException(`Coffee #${id} not found`);
}
return this.coffeeRepository.save(coffee);
}
  1. 重新新增或更新数据时,如果风味属性不存在,就会直接创建

分页查询

  1. 创建分页dto
    ​​ ​nest g class common/dto/pagination-query.dto --no-spec​
  2. 新增​ ​limit​ ​​和​ ​offset​ ​两个属性
export class PaginationQueryDto {
limit: number;
offset: number;
}
  1. 通过​ ​@Type()​ ​​装饰器保证值被返回为​ ​Number​
import { Type } from "class-transformer";

export class PaginationQueryDto {
@Type(() => Number)
limit: number;

@Type(() => Number)
offset: number;
}

这一步也可以通过在​ ​ValidationPipe​ ​​中添加​ ​transformOptions​ ​​对象,将​ ​enableImplicitConversion​ ​​设置为​ ​true​ ​,在全局层面上启用隐式类型转换

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({
whitelist: true, // 参数白名单
forbidNonWhitelisted: true, // 上传白名单之外的参数,报错
transform: true,
transformOptions: {
enableImplicitConversion: true
}
}));
await app.listen(3000);
}

bootstrap();
  1. 通过​ ​@IsOptional()​ ​​装饰器,将属性标记为​ ​可选​ ​,意味着没有如果它缺失或未定义,将抛出错误
import { Type } from "class-transformer";
import { IsOptional } from "class-validator";

export class PaginationQueryDto {
@IsOptional()
@Type(() => Number)
limit: number;

@Type(() => Number)
@IsOptional()
offset: number;
}
  1. 通过​ ​@IsPositive()​ ​装饰器,检查值是否为正数大于0
import { Type } from "class-transformer";
import { IsOptional, IsPositive } from "class-validator";

export class PaginationQueryDto {
@IsPositive()
@IsOptional()
@Type(() => Number)
limit: number;

@IsPositive()
@IsOptional()
@Type(() => Number)
offset: number;
}
  1. 最终定义为
import { IsOptional, IsPositive } from "class-validator";

export class PaginationQueryDto {
@IsPositive()
@IsOptional()
limit: number;

@IsPositive()
@IsOptional()
offset: number;
}
  1. 更新​ ​coffee.controller​ ​​中的​ ​findAll()​ ​​方法,将​ ​paginationQuery​ ​​类型设置为​ ​PaginationQueryDto​
@Get()
findAll(@Query() paginationQuery: PaginationQueryDto) {
// const { limit, offset } = paginationQuery;
return this.coffeesService.findAll(paginationQuery);
}
  1. 更新​ ​coffee.service​ ​​中的​ ​findAll()​ ​​方法,接收参数,并通过​ ​skip​ ​​和​ ​take​ ​​向​ ​findAll()​ ​​方法传递给​ ​find()​ ​方法
findAll(paginationQuery: PaginationQueryDto) {
const { limit, offset } = paginationQuery;
return this.coffeeRepository.find({
relations: ["flavors"],
skip: offset,
take: limit
});
}

事务

  1. 创建新​ ​entity​ ​​ ​nest g class events/entities/event.entity --no-spec​
  2. 初始化​ ​Event​
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";

@Entity()
export class Event {
@PrimaryGeneratedColumn()
id: number;

@Column()
type: string;

@Column()
name: string;

// payload 是存储事件有效负载通用列
@Column("json")
payload: Record<string, any>;
}
  1. 在​ ​coffee​ ​​模块中引入​ ​Event​
import { Module } from "@nestjs/common";
import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Coffee } from "./entities/coffee.entity";
import { Flavor } from "./entities/flavor.entity";
import { Event } from "../events/entities/event.entity";

// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
controllers: [CoffeeController],
providers: [CoffeeService]
})
export class CoffeeModule {
}
  1. 在​ ​咖啡实体​ ​中新增推荐属性
import { Column, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn } from "typeorm";
import { Flavor } from "./flavor.entity";

@Entity()
// 默认 sql table === 'coffee'
// 可以在Entity('TABLE_NAME')进行指定
export class Coffee {
@PrimaryGeneratedColumn()
id: number;

@Column()
name: string;

@Column()
brand: string;

// 新增推荐属性
@Column({ default: 0 })
recommendations: number;

@JoinTable()
@ManyToMany(
type => Flavor,
(Flavor) => Flavor.coffees,
{
cascade: true // ['insert']
})
flavors: Flavor[]; // 将flavors的类型设置为Flavor
}
  1. 在​ ​coffee.service.ts​ ​​中,引入​ ​Connection​ ​创建事务
constructor(
@InjectRepository(Coffee)
private readonly coffeeRepository: Repository<Coffee>,
@InjectRepository(Flavor) // 將Flavor注入到coffeeService中
private readonly flavorRepository: Repository<Flavor>,
// 引入Connection用来创建事务
private readonly connection: Connection
) {
}
  1. 在​ ​coffee.service.ts​ ​​中,新建异步方法,并命名为​ ​推荐Coffee​ ​​,接收一个参数​ ​咖啡​
async recommendCoffee(coffee: Coffee) {
const queryRunner = this.connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
}
  • 首先创建一个新的​ ​queryRunner​
  • 使用创建的​ ​queryRunner​ ​创建到数据库的新连接
  • 建立连接后,可以开始交易过程
  • 将整个事务包装在​ ​try / catch / finally​ ​​中,以确保如果出现任何问题,​ ​catch​ ​可以回滚整个事务
  • 事务是我们能够回滚和撤销发生的任何事情,以防出现问题
async recommendCoffee(coffee: Coffee) {
const queryRunner = this.connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();

try {
coffee.recommendations++;
const recommendEvent = new Event();
recommendEvent.name = "recommend_coffee";
recommendEvent.type = "coffee";
recommendEvent.payload = { coffeeId: coffee.id };

await queryRunner.manager.save(coffee);
await queryRunner.manager.save(recommendEvent);

await queryRunner.commitTransaction();
} catch (err) {
await queryRunner.rollbackTransaction();
} finally {
await queryRunner.release();
}
}
  • 在​ ​try​ ​​中,增加​ ​coffee​ ​​的推荐属性并创建一个新的​ ​推荐咖啡事件​ ​,使用查询运行器实体管理器来保存咖啡和事件实体
  • 在​ ​catch​ ​语句中看到,如果出现任何问题,保存任一实体失败,通过回滚整个事务来防止数据库中的不一致
  • 在​ ​finallye​ ​​中,保证一切结束后释放或关闭​ ​queryRunner​

缓存

  • 使用​ ​@Index()​ ​装饰器在列上定义一个索引
@Index()
@Column()
name: string;
  • 列的复合索引,可以通过将​ ​@Index()​ ​装饰器应用在类本身,并在装饰器内传递一个列名数组作为参数
import { Column, Entity, Index, PrimaryGeneratedColumn } from "typeorm";

@Index(["name", "type"])
@Entity()
export class Event {
@PrimaryGeneratedColumn()
id: number;

@Column()
type: string;

@Index()
@Column()
name: string;

// payload 是存储事件有效负载通用列
@Column("json")
payload: Record<string, any>;
}

数据库迁移

数据库迁移提供了一种增量更新我们的数据库模式并使其与应用程序数据模型保持同步的方法,同时保留我们数据库中的现有数据。

To generate, run and revert migrations

生成、运行和恢复迁移

在创建新的迁移之前,我们需要创建一个新的TypeORM配置文件并正确连接我们的数据库

  • 在项目的根目录中创建一个​ ​ormconfig.js​ ​文件
module.exports = {
type: "postgres",
host: "localhost",
port: 5432,
username: "postgres",
password: "pass123",
database: "postgres",
entities: ["dist/**/*.entity.js"],
migrations: ["dist/migrations/*.js"],
cli:{
migrationsDir:'src/migrations'
}
};

这里的配置设置是我们从Docker Compose文件中使用的所有端口、密码等,还有一些额外的关键值用于让TypeORM迁移,知道我们的实体和迁移文件将在哪里

  • 执行迁移命令,并将此迁移命名为:CoffeeRefactor
    ​​ ​npx typeorm migration:create -n CoffeeRefactor​
  • 该命令在​ ​/src/migrations​ ​目录中生成一个新的迁移文件
  • 假设需要更改​ ​coffee.entity​ ​​,将​ ​name​ ​​更改为​ ​title​
@Column()
title: string;
  • 对实体的更新会自动更新开发数据库,因为设置了​ ​synchronize: true​ ​,但是不会更新生产数据库,这是迁移非常方便的主要原因之一
  • 更新​ ​name​ ​​为​ ​title​ ​后,不仅会删除名称列,还会删除该列中的所有数据
  • 只有在删除该列后,才会创建没有任何旧数据的新标题列
  • 迁移帮助我们 重命名现有列并维护我们以前的所有数据
  • 打开迁移文件并增加迁移逻辑,让数据库知道需要进行更改
  • 基础迁移文件都有一个​ ​up()​ ​​和​ ​down()​ ​方法
import {MigrationInterface, QueryRunner} from "typeorm";

export class CoffeeRefactor1653399205455 implements MigrationInterface {

public async up(queryRunner: QueryRunner): Promise<void> {
}

public async down(queryRunner: QueryRunner): Promise<void> {
}

}
  • ​up()​ ​是只是需要更改的内容以及如何更改的内容
  • ​down()​ ​​是撤销或回滚任何这些更改的地方,万一出现问题,需要一个退出策略,帮助撤销一切,​ ​down()​ ​就可以保证我们的迁移回滚
  • 在​ ​up()​ ​中新增更改表字段名称的数据库语句
await queryRunner.query(
'ALTER TABLE "coffee" RENAME COLUMN "name" TO "title"',
)

在这里可以执行所需的任何类型的数据库迁移,同时,必须为 回滚迁移提供逻辑

  • 在​ ​down()​ ​中新增回滚逻辑
await queryRunner.query(
'ALTER TABLE "coffee" RENAME COLUMN "title" TO "name"',
)
  • 迁移文件完整代码
import {MigrationInterface, QueryRunner} from "typeorm";

export class CoffeeRefactor1653399205455 implements MigrationInterface {

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'ALTER TABLE "coffee" RENAME COLUMN "name" TO "title"',
)
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'ALTER TABLE "coffee" RENAME COLUMN "title" TO "name"',
)
}

}
  • 测试迁移
  • 确保构建源代码,以便​ ​TypeORM CLI​ ​​可以在​ ​/dist​ ​​目录下找到身份和迁移文件
    构建代码​​ ​yarn run build​
  • 构建完成后,生成​ ​dist​ ​目录
  • 通过以下方式运行"迁移"命令类型:
  • ​npx typeorm migration:run​
  • NestJS学习笔记_数据库

  • 再次执行
  • NestJS学习笔记_数据库_02

  • 恢复更改
  • ​npx typeorm migration:revert​
  • NestJS学习笔记_学习_03

  • ​TypeORMCLI​ ​​可以自动生成迁移,连接到数据库并将现有表与提供的实体定义进行比较,如果发现差异,​ ​TypeORM​ ​会生成一个新的迁移
  • ​coffee.entity​ ​​中新增​ ​description​
@Column({ nullable: true })
description: string;
  • 编译代码:​ ​yarn run build​
  • 输入命令,让​ ​TypeORM​ ​​生成迁移,并将其命名为​ ​SchemaSync​
  • ​npx typeorm migration:generate -n SchemaSync​
  • 打开​ ​/src/migrations/​ ​​中新生成的迁移文件,查看​ ​up()​ ​​和​ ​down()​
import {MigrationInterface, QueryRunner} from "typeorm";

export class SchemaSync1653400611645 implements MigrationInterface {
name = 'SchemaSync1653400611645'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "coffee" ADD "description" character varying`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "coffee" DROP COLUMN "description"`);
}

}
  • 执行迁移:​ ​npx typeorm migration:run​

依赖注入

当我使用​ ​CoffeeService​ ​并将其注入到构造函数中时

constructor(private readonly coffeesService: CoffeeService) {}

NextJS通过以下三件事实现:

  1. 在​ ​CoffeeService​ ​​中通过​ ​@Injectable()​ ​​装饰器声明了一个可以由Next"容器"管理的类。
    此装饰器将​​ ​CoffeeService​ ​​类标记为​ ​Provider​
  2. 在​ ​CoffeeController​ ​​中,在构造函数中请求​ ​CoffeeService​
coffeesService:

这个请求高速Nest将提供程序注入到我们的控制器类中

  1. Nest知道​ ​-this-​ ​​类也是一个​ ​Provider​ ​​,因为在​ ​CoffeeModule​ ​​中包含了​ ​-here-​ ​​,它向Nest反转控制(IoC
    )容器注册了这个容器

封装

  1. 新建​ ​coffee-rating module​ ​​ ​nest g mo coffee-rating​
  2. 新建​ ​coffee-rating service​ ​​并将其作为providers写入​ ​coffee-rating module​ ​​ ​nest g s coffee-rating​
  3. ​coffee-rating​
  • ​coffee-rating-module​
import { Module } from '@nestjs/common';
import { CoffeeRatingService } from './coffee-rating.service';

@Module({
providers: [CoffeeRatingService]
})
export class CoffeeRatingModule {}
  • ​coffee-rating-service​
import { Injectable } from '@nestjs/common';

@Injectable()
export class CoffeeRatingService {}
  1. 假设​ ​CoffeeRatingService​ ​​依赖​ ​CoffeeService​ ​从数据库中获取咖啡
  • 因为属于不同模块,
  • 所以在​ ​CoffeeRtaingModule​ ​​中导入​ ​CoffeeModule​
import { Module } from "@nestjs/common";
import { CoffeeRatingService } from "./coffee-rating.service";
import { CoffeeModule } from "../coffee/coffee.module";

@Module({
imports: [CoffeeModule],
providers: [CoffeeRatingService]
})
export class CoffeeRatingModule {
}
  • 切换到​ ​CoffeeRatingService​ ​​, 并使用基于构造函数的注入来添加​ ​CoffeeService​
import { Injectable } from "@nestjs/common";
import { CoffeeService } from "../coffee/coffee.service";

@Injectable()
export class CoffeeRatingService {
constructor(private readonly coffeeService: CoffeeService) {
}
}
  • 这样运行后会报错
  • NestJS学习笔记_javascript_04

  • 原因:默认情况下,所有模块都封装了他们的提供者(Provider)如果想在另外一个模块中使用它们,必须明确地将他们定义为导出(exported),使它们成为该模块的公共API的一部分
  • 解决:
  • 在​ ​coffee.module.ts​ ​​中将​ ​CoffeeService​ ​导出
import { Module } from "@nestjs/common";
import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Coffee } from "./entities/coffee.entity";
import { Flavor } from "./entities/flavor.entity";
import { Event } from "../events/entities/event.entity";

// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
controllers: [CoffeeController],
providers: [CoffeeService],
exports: [CoffeeService]
})
export class CoffeeModule {
}
  • 更改后可以正常运行

自定义提供程序

以下场景:

  • 创建我们的提供者自定义实例,而不是让Nest实例化该类
  • 在第二个依赖项中重用现有类
  • 用模拟版本覆盖一个类进行测试
  • 使用策略模式,提供一个抽象类并根据不同条件交换实际实现(或要使用的实际类)

通过Nest定义自定义提供程序来处理这些场景。​ ​providers​ ​​数组形式只是简写,实际上只是提供​ ​TOKEN​ ​​并在该​ ​TOKEN​ ​​的位置提供​ ​"要注入的内容"​ ​的简写版本。完整写法为:

providers:[
{
provider: CoffeesService,
useClass: CoffeesService
}
]

Nest提供了不同方法进行自定义提供者。【​ ​useValue​ ​​、​ ​useClass​ ​】

  1. 通过​ ​useValue​ ​​,​ ​useValue​ ​​语法对于注入常量​ ​constant​ ​​值很有用。
    假设在Nest容器中添加一个外部库,或者用​​ ​Mock\{}​ ​​对象替代服务的真实实现。
    例如:将​​ ​CoffeeService​ ​​替换为​ ​自定义Provider​ ​​并使用​ ​useValue​ ​​语法,当在程序中注入​ ​CoffeeService​ ​​时,每当​ ​CoffeeService TOKEN​ ​​被解析时,他将指向新的​ ​MockCoffeeService​ ​,
import { Module } from "@nestjs/common";
import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Coffee } from "./entities/coffee.entity";
import { Flavor } from "./entities/flavor.entity";
import { Event } from "../events/entities/event.entity";

class MockCoffeeService {
}

// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
controllers: [CoffeeController],
providers: [{ provide: CoffeeService, useValue: new MockCoffeeService() }],
exports: [CoffeeService]
})
export class CoffeeModule {
}

因此,可以通过使用​ ​useValue​

重命名提供者令牌

在之前,均是使用类名作为​ ​Provider tokens​ ​​,​ ​provider tokens​ ​​是我们传递给​ ​provider​ ​属性的任何内容,通过使用更灵活的字符串或符号作为依赖注入令牌

例如提供一个字符串值标记​ ​"COFFEE_BRANDS"​ ​​,通过​ ​useValue​ ​将值设置为字符串数组

import { Module } from "@nestjs/common";
import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Coffee } from "./entities/coffee.entity";
import { Flavor } from "./entities/flavor.entity";
import { Event } from "../events/entities/event.entity";

class MockCoffeeService {
}

// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
controllers: [CoffeeController],
providers: [
CoffeeService,
{ provide: "COFFEE_BRANDS", useValue: ["buddy brew", "nescafe"] }
],
exports: [CoffeeService]
})
export class CoffeeModule {
}

使用​ ​@Inject()​ ​装饰器,并将需要查找的令牌作为参数进行赋值

constructor(
@InjectRepository(Coffee)
private readonly coffeeRepository: Repository<Coffee>,
@InjectRepository(Flavor) // 將Flavor注入到coffeeService中
private readonly flavorRepository: Repository<Flavor>,
// 引入Connection用来创建事务
private readonly connection: Connection,
@Inject("COFFEE_BRANDS") coffeeBrands: string[]
) {
}

这样就可以使用​ ​COFFEE_BRANDS​ ​并访问我们传递给此提供程序的值数组

最好在一个单独的CONSTANT常量文件夹中定义TOKEN并导出、导入使用

  • 在​ ​coffee​ ​​目录下新建​ ​coffee.constants.ts​
export const COFFEE_BRANDS = "COFFEE_BRANDS";
  • 在Module和Service中,引入常量并使用
import { Module } from "@nestjs/common";
import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Coffee } from "./entities/coffee.entity";
import { Flavor } from "./entities/flavor.entity";
import { Event } from "../events/entities/event.entity";
import { COFFEE_BRANDS } from "./coffee.constants";

class MockCoffeeService {
}

// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
controllers: [CoffeeController],
providers: [
CoffeeService,
{ provide: COFFEE_BRANDS, useValue: ["buddy brew", "nescafe"] }
],
exports: [CoffeeService]
})
export class CoffeeModule {
}
  1. 通过​ ​useClass​ ​​,​ ​useClass​ ​​允许动态确定一个​ ​Token​ ​​应该解析到的​ ​Class​ ​例如,有一个抽象或默认的ConfigService类,根据当前环境,需要Nest为每个配置服务提供不同的实现
import { Module } from "@nestjs/common";
import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Coffee } from "./entities/coffee.entity";
import { Flavor } from "./entities/flavor.entity";
import { Event } from "../events/entities/event.entity";
import { COFFEE_BRANDS } from "./coffee.constants";

class ConfigService {
}

class DevelopmentConfigService {
}

class ProductionConfigService {
}

// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
controllers: [CoffeeController],
providers: [
CoffeeService,
{
provide: ConfigService,
useClass:
process.env.NODE_ENV === "development" ?
DevelopmentConfigService :
ProductionConfigService
},
{ provide: COFFEE_BRANDS, useValue: ["buddy brew", "nescafe"] }
],
exports: [CoffeeService]
})
export class CoffeeModule {
}
  1. 通过​ ​useFactory​ ​​,允许​ ​"动态"​ ​​创建提供者,如果需要将提供者的值基于各种其他依赖项、值,这将非常有用。
    ​​ ​useFactory​ ​的返回值将被提供者(provider)使用。
{ provide: COFFEE_BRANDS, useFactory: () => ["buddy brew", "nescafe"] }

新的更现实的例子,并在其中注入一些提供程序:

定义一个随机提供者,并确保将其注册为提供者(​ ​@Injectable()​ ​)

@Injectable()
export class CoffeeBrandsFactory {
create() {
// do something
return ["buddy brew", "nescafe"];
}
}

更新现有的​ ​COFFEE_BRANDS​ ​​提供程序以使用​ ​CoffeeBrandsFactory​ ​​,新增一个名为​ ​inject​ ​的属性

​inject​ ​​本身接收一个提供者(provider)数组,这些提供者被传递到​ ​useFactory​ ​函数中后可以随意使用,返回需要的值

import { Injectable, Module } from "@nestjs/common";
import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Coffee } from "./entities/coffee.entity";
import { Flavor } from "./entities/flavor.entity";
import { Event } from "../events/entities/event.entity";
import { COFFEE_BRANDS } from "./coffee.constants";

@Injectable()
export class CoffeeBrandsFactory {
create() {
// do something
return ["buddy brew", "nescafe"];
}
}

// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
controllers: [CoffeeController],
providers: [
CoffeeService,
CoffeeBrandsFactory,
{
provide: COFFEE_BRANDS,
useFactory: (brandsFactory: CoffeeBrandsFactory) => brandsFactory.create(),
inject: [CoffeeBrandsFactory]
}
],
exports: [CoffeeService]
})
export class CoffeeModule {
}

通过使用Promise,将async/await与useFactory语法结合使用,可以实现异步(比如数据库未连接不接受请求)

import { Injectable, Module } from "@nestjs/common";
import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Coffee } from "./entities/coffee.entity";
import { Flavor } from "./entities/flavor.entity";
import { Event } from "../events/entities/event.entity";
import { COFFEE_BRANDS } from "./coffee.constants";
import { Connection } from "typeorm";

// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
controllers: [CoffeeController],
providers: [
CoffeeService,
{
provide: COFFEE_BRANDS,
useFactory: async (connection: Connection): Promise<string[]> => {
// const coffeeBrands = await connection.query('SELECT *** ...');
const coffeeBrands = await Promise.resolve(["buddy brew", "nescafe"]);
console.log("[!] Async Factory");
return coffeeBrands;
},
inject: [Connection]
}
],
exports: [CoffeeService]
})
export class CoffeeModule {
}

动态模块

有时在使用模块时需要更多的灵活性,例如:静态模块不能由使用它们的模块配置其Provider

比如:有一个通用模块,该模块需要在不同情况下表现不同

动态模块需要一些配置才可以被消费者使用

测试

  1. 创建一个动态模块​ ​DatabaseModule​ ​​,可以在实例化之前传递配置
    ​​ ​nest g mo database​
  2. 在provider中定义​ ​"CONNECTION"​ ​​,并使用​ ​useValue​ ​​从​ ​typeorm​ ​​中调用​ ​createConnection()​ ​方法,传入一些任意值建立数据库连接
import { Module } from "@nestjs/common";
import { createConnection } from "typeorm";

@Module({
providers: [
{
provide: "CONNECTION",
useValue: createConnection({
type: "postgres",
host: "localhost",
port: 5432
})
}
]
})
export class DatabaseModule {
}
  1. 由于​ ​DatabaseModule​ ​​对这些连接选项进行了硬编码,所以不能轻易地在不同应用程序之间共享这个模块。这是因为在这个模块的配置是静态配置的,不能自定义。
    如果另一个应用程序想要使用这个模块但是需要使用不同的端口怎么办?
    通过使用​​ ​Nest​ ​​的动态模块功能,可以让消费模块使用API来控制导入时如自定义​ ​DatabaseModule​
  • 在​ ​DatabaseModule​ ​​上定义一个名为​ ​register()​ ​的静态方法
  • ​register()​ ​可以接收消费模块传递过来的参数
  • ​register()​ ​​返回​ ​DynamicModule​ ​​类型结果,它与典型的​ ​@Module()​ ​​具有基本相同的接口,但需要传递一个​ ​module​ ​属性,也就是当前模块本身
  • 通过​ ​register()​ ​,可以将接收的参数用于创建数据库连接中
import { DynamicModule, Module } from "@nestjs/common";
import { ConnectionOptions, createConnection } from "typeorm";

@Module({})
export class DatabaseModule {
static register(options: ConnectionOptions): DynamicModule {
return {
module: DatabaseModule,
providers: [
{
provide: "CONNECTION",
useValue: createConnection(options)
}
]
};
}
}

使用方法如下:

import { Module } from "@nestjs/common";
import { CoffeeRatingService } from "./coffee-rating.service";
import { CoffeeModule } from "../coffee/coffee.module";
import { DatabaseModule } from "../database/database.module";

@Module({
imports: [DatabaseModule.register({
type: "postgres",
host: "localhost",
password: "password",
port: 5432
}), CoffeeModule],
providers: [CoffeeRatingService]
})
export class CoffeeRatingModule {
}

服务提供者的Scope

SpringBoot中提供了Scope注解来指明Bean的作用域,NestJs也提供了类似的​ ​@Scope()​ ​装饰器:

scope名称

说明

SINGLETON

单例模式,整个应用内只存在一份实例

REQUEST

每个请求初始化一次

TRANSIENT

每次注入都会实例化

Config Module

  1. 安装​ ​nestjs/config​ ​​ ​yarn add @nestjs/config​
  2. 打开​ ​AppModule​ ​​,将来自​ ​@nestjs/config​ ​​的​ ​ConfigModule​ ​​添加到​ ​imports:[]​ ​​数组中,并调用其静态方法​ ​forRoot()​ ​​,它将从默认位置加载和解析​ ​.env​ ​文件,即项目的根目录
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { CoffeeModule } from "./coffee/coffee.module";
import { TypeOrmModule } from "@nestjs/typeorm";
import { CoffeeRatingModule } from "./coffee-rating/coffee-rating.module";
import { DatabaseModule } from "./database/database.module";
import { ConfigModule } from "@nestjs/config";

@Module({
imports: [ConfigModule.forRoot(), CoffeeModule, TypeOrmModule.forRoot({
type: "postgres",
host: "localhost",
port: 5432,
username: "postgres",
password: "pass123",
database: "postgres",
autoLoadEntities: true, // 有助于自动加载模块,而不是指定实体数组
synchronize: true // 同步,确保我们的TypeORM实体在每次运行应用程序时都会与数据库同步 生产环境设置为true,开发环境设置为false
}), CoffeeRatingModule, DatabaseModule],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {
}
  1. 在NodeJS中,通常使用​ ​.env​ ​​保存重要的应用程序配置数据的键值对,无论是密钥、数据库选项、密码等,通过​ ​.env​ ​​文件,在不同环境运行一个应用程序,只需要正确交换​ ​.env​ ​即可
  2. 在项目根目录创建​ ​.env​ ​文件
DATABASE_USER=postgres
DATABASE_PASSWORD=pass123
DATABASE_NAME=postgres
DATABASE_PORT=5432
DATABASE_HOST=localhost

配置均与数据库配置相关,来此docker-compose

  1. 确保​ ​.env​ ​​不会被git追踪
    打开​​ ​.gitignore​ ​,增加以下行
# ENV
  1. 打开​ ​AppModule​ ​​,更新传递给​ ​TypeOrmModule​ ​的选项对象
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { CoffeeModule } from "./coffee/coffee.module";
import { TypeOrmModule } from "@nestjs/typeorm";
import { CoffeeRatingModule } from "./coffee-rating/coffee-rating.module";
import { DatabaseModule } from "./database/database.module";
import { ConfigModule } from "@nestjs/config";

@Module({
imports: [ConfigModule.forRoot(), CoffeeModule, TypeOrmModule.forRoot({
type: "postgres",
host: process.env.DATABASE_HOST,
port: +process.env.DATABASE_PORT,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
autoLoadEntities: true, // 有助于自动加载模块,而不是指定实体数组
synchronize: true // 同步,确保我们的TypeORM实体在每次运行应用程序时都会与数据库同步 生产环境设置为true,开发环境设置为false
}), CoffeeRatingModule, DatabaseModule],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {
}
  1. ​yarn start:dev​ ​重启后,数据库配置正常,正在使用从当前环境中加载的配置
  2. 默认情况下,设置的​ ​ConfigModule​ ​​会在应用程序的根目录中查找​ ​.env​ ​​文件,通过​ ​envFilePath​ ​为这个文件指定另一个路径。
ConfigModule.forRoot({
envFilePath: ".environment"
}),

除了传递字符串值,也可以传递字符串数组来为​ ​.env​ ​文件指定多个路径

如果在多个文件中找到相同变量,优先使用第一个匹配文件中的变量

  1. 部署到生产环境,可能不需要​ ​.env​ ​​文件,可以设置​ ​ignoreEnvFile​ ​​完全禁止加载​ ​.env​ ​文件
ConfigModule.forRoot({
ignoreEnvFile: true
}),
  1. 利用​ ​@nestjs/config​ ​​包中的​ ​joi​ ​​包确保任何重要的环境变量都得到验证,使用​ ​Joi​ ​,可以定义对象模式并针对它验证JavaScript对象
  • 安装:​ ​yarn add @hapi/joi​
  • 安装types:​ ​yarn add -D @types/hapi__joi​
  • 定义验证模式。在​ ​ConfigModule.forRoot()​ ​​方法内部,通过​ ​validationSchema​ ​确保以正确的格式传入某些环境变量
ConfigModule.forRoot({
validationSchema: Joi.object({
DATABASE_HOST: Joi.required(),
DATABASE_PORT: Joi.number().default(5432)
})
}),

完整配置:

import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { CoffeeModule } from "./coffee/coffee.module";
import { TypeOrmModule } from "@nestjs/typeorm";
import { CoffeeRatingModule } from "./coffee-rating/coffee-rating.module";
import { DatabaseModule } from "./database/database.module";
import { ConfigModule } from "@nestjs/config";
import * as Joi from "@hapi/joi";

@Module({
imports: [
ConfigModule.forRoot({
validationSchema: Joi.object({
DATABASE_HOST: Joi.required(),
DATABASE_PORT: Joi.number().default(5432)
})
}),
CoffeeModule, TypeOrmModule.forRoot({
type: "postgres",
host: process.env.DATABASE_HOST,
port: +process.env.DATABASE_PORT,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
autoLoadEntities: true, // 有助于自动加载模块,而不是指定实体数组
synchronize: true // 同步,确保我们的TypeORM实体在每次运行应用程序时都会与数据库同步 生产环境设置为true,开发环境设置为false
}), CoffeeRatingModule, DatabaseModule],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {
}
  1. 在应用程序中设置的​ ​ConfigModule​ ​​带有一个名为​ ​ConfigService​ ​​的有用服务,该服务提供了一个​ ​get()​ ​方法来读取解析的配置变量
  • 在​ ​coffee.module​ ​​中导入​ ​ConfigModule​ ​​。
    我们在主​​ ​AppModule​ ​​中使用了​ ​forRoot()​ ​方法,其他地方不需要做任何事
import { Injectable, Module } from "@nestjs/common";
import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Coffee } from "./entities/coffee.entity";
import { Flavor } from "./entities/flavor.entity";
import { Event } from "../events/entities/event.entity";
import { COFFEE_BRANDS } from "./coffee.constants";
import { Connection } from "typeorm";
import { ConfigModule } from "@nestjs/config";

// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event]), ConfigModule],
controllers: [CoffeeController],
providers: [
CoffeeService,
{
provide: COFFEE_BRANDS,
useFactory: async (connection: Connection): Promise<string[]> => {
// const coffeeBrands = await connection.query('SELECT *** ...');
const coffeeBrands = await Promise.resolve(["buddy brew", "nescafe"]);
console.log("[!] Async Factory");
return coffeeBrands;
},
inject: [Connection]
}
],
exports: [CoffeeService]
})
export class CoffeeModule {
}
  • 在​ ​CoffeeService​ ​​中注入并通过​ ​get()​ ​方法获取参数
export class CoffeeService {
constructor(
@InjectRepository(Coffee)
private readonly coffeeRepository: Repository<Coffee>,
@InjectRepository(Flavor) // 將Flavor注入到coffeeService中
private readonly flavorRepository: Repository<Flavor>,
// 引入Connection用来创建事务
private readonly connection: Connection,
@Inject(COFFEE_BRANDS) coffeeBrands: string[],
private readonly configService: ConfigService
) {
const databaseHost = this.configService.get<string>("DATABASE_HOST");
console.log(databaseHost);
}

​get()​ ​可以接收第二个参数,设置默认值,如果获取不到值则取默认值

const databaseHost = this.configService.get<string>("DATABASE_HOSTa", "demo");

配置文件

通过配置文件对不同配置进行管理与使用

  1. 定义:新建​ ​src/config/app.config.ts​
export default () => ({
environment: process.env.NODE_ENV || "development",
database: {
host: process.env.DATABASE_HOST,
port: parseInt(process.env.DATABASE_PORT, 10) | 5432
}
})

通过工厂函数导出配置,包括环境和数据库,主机和端口通过​ ​env​ ​类指定

  1. 使用:
  • 配置:在​ ​app.module.ts​ ​​中​ ​ConfigModule.forRoot​ ​​传入一个一个​ ​load​ ​新属性,它接收一个配置工厂数组
ConfigModule.forRoot({
load: [appConfig]
}),
  • 获取:在​ ​coffee.service.ts​ ​​中,通过​ ​get()​ ​​方法获取配置的时候,不需要使用​ ​DATABASE_HOST​ ​​获取,直接使用​ ​database.host​ ​即可
const databaseHost = this.configService.get("database.host", "demo");
  1. ​ConfigModule​ ​​允许使用嵌套对象定义和加载多个自定义配置文件,并通过提供的​ ​ConfigService​ ​​(来自​ ​@nestjs/config​ ​​)访问这些变量。
    随着项目增长,配置文件增多,可能需要位于多个不同目录的"特定于功能"的配置文件
    随着拥有越来越多的配置键,使用非类型化的​​ ​configService.get()​ ​​方法获取所有配置值很容易出错,由于必须使用"点表示法"(即a.b)来检索嵌套项
    为了防止这种情况,结合两项技术:​​ ​配置命名空间​ ​​和​ ​部分注册​ ​以进行验证配置。
  • 新建​ ​src/config/coffee.config.ts​ ​​,通过​ ​registerAs()​ ​​函数可以在命名空间内定义一个​ ​token​ ​,也就是第一个参数
import { registerAs } from "@nestjs/config";

export default registerAs("coffee", () => ({
foo: "bar"
}));
  • 在​ ​coffee.module.ts​ ​​中使用​ ​ConfigModule.forFeature()​ ​​注册这个​ ​coffeeConfig​ ​​,也就是​ ​部分配准​
imports: [
TypeOrmModule.forFeature([Coffee, Flavor, Event]),
ConfigModule.forFeature(coffeeConfig)
],
  • 在​ ​CoffeeService​ ​​中通过​ ​get()​ ​获取配置
const coffeeConfig = this.configService.get("coffee");
console.log(coffeeConfig);

也可以通过点语法获取对应值

const coffeeConfig = this.configService.get("coffee");
const coffeeConfigFoo = this.configService.get("coffee.foo");
console.log(coffeeConfig);
console.log(coffeeConfigFoo);
  1. 在使用点语法获取配置时,仍容易出错。因此,直接注入整个命名空间配置对象是一种更优的替代方案/最佳做法
@Inject(coffeeConfig.KEY)
private readonly coffeeConfiguration: ConfigType<typeof coffeeConfig>

每个命名空间配置都暴露了一个​ ​Token​ ​​也就是​ ​key​ ​属性,可以使用该属性将整个对象注入到Nest容器中注册的任何类

​ConfigType​ ​是一个开箱即用的辅助类型,推断函数的返回类型

console.log(coffeeConfiguration.foo);

可以直接通过该对象获取配置,甚至有强类型的好处

异步import

目前​ ​app.module.ts​ ​中配置如下

import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { CoffeeModule } from "./coffee/coffee.module";
import { TypeOrmModule } from "@nestjs/typeorm";
import { CoffeeRatingModule } from "./coffee-rating/coffee-rating.module";
import { DatabaseModule } from "./database/database.module";
import { ConfigModule } from "@nestjs/config";
import * as Joi from "@hapi/joi";
import appConfig from "./config/app.config";

@Module({
imports: [
// ConfigModule.forRoot({
// validationSchema: Joi.object({
// DATABASE_HOST: Joi.required(),
// DATABASE_PORT: Joi.number().default(5432)
// })
// }),
ConfigModule.forRoot({ load: [appConfig] }),
CoffeeModule,
TypeOrmModule.forRoot({
type: "postgres",
host: process.env.DATABASE_HOST,
port: +process.env.DATABASE_PORT,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
autoLoadEntities: true, // 有助于自动加载模块,而不是指定实体数组
synchronize: true // 同步,确保我们的TypeORM实体在每次运行应用程序时都会与数据库同步 生产环境设置为true,开发环境设置为false
}),
CoffeeRatingModule,
DatabaseModule
],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {
}

使用​ ​process.env​ ​​的配置是在加载环境配置​ ​ConfigModule.forRoot({load: [appConfig]})​ ​之后的,如果在之前使用,会报错

通过异步加载可以解决,使用​ ​forRootAsync​ ​结合工厂函数进行配置

import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { CoffeeModule } from "./coffee/coffee.module";
import { TypeOrmModule } from "@nestjs/typeorm";
import { CoffeeRatingModule } from "./coffee-rating/coffee-rating.module";
import { DatabaseModule } from "./database/database.module";
import { ConfigModule } from "@nestjs/config";
import * as Joi from "@hapi/joi";
import appConfig from "./config/app.config";

@Module({
imports: [
// ConfigModule.forRoot({
// validationSchema: Joi.object({
// DATABASE_HOST: Joi.required(),
// DATABASE_PORT: Joi.number().default(5432)
// })
// }),
TypeOrmModule.forRootAsync({
useFactory: () => ({
type: "postgres",
host: process.env.DATABASE_HOST,
port: +process.env.DATABASE_PORT,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
autoLoadEntities: true, // 有助于自动加载模块,而不是指定实体数组
synchronize: true // 同步,确保我们的TypeORM实体在每次运行应用程序时都会与数据库同步 生产环境设置为true,开发环境设置为false
})
}),
ConfigModule.forRoot({ load: [appConfig] }),
CoffeeModule,
CoffeeRatingModule,
DatabaseModule
],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {
}

异常过滤器、管道、守卫、拦截器

  1. 异常过滤器(EXCEPTION FILTERS):异常过滤器负责处理应用程序中可能发生的未处理异常,控制任何或特定响应的确切流和内容,将其发送回客户端
  2. 管道(PIPES):管道通常用于处理两件事
  • 转换:将输入数据转换为期望的输出
  • 验证:评估输入数据,有效则通过管道,无效抛出异常
  1. 守卫(GUARDS):守卫确定给定的请求是否满足某些条件,如身份验证、授权、角色、ACL,如果满足条件,请求将被允许访问路由
  2. 拦截器(INTERCEPTORS):拦截器具有许多受面向方面变成启发的有用功能
  • 在方法执行之前或之后绑定额外的逻辑
  • 转换方法返回的结果
  • 扩展基本方法行为
  • 完全覆盖方法

例如:处理诸如"缓存响应"之类的事情

如何将上述四种构建块绑定到我们的应用程序?基本上有三种不同的绑定方式:过滤器、守卫和拦截器绑定到路由处理程序,管道特定(仅适用于管道)

嵌套构建块可以是:

  • "全局"范围
  • "控制器"范围
  • "方法"范围
  • 额外的第4个"参数"范围:仅适用于管道

在​ ​main.ts​ ​​中,通过​ ​ValidationPipe​ ​设置全局管道

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({
whitelist: true, // 参数白名单
forbidNonWhitelisted: true, // 上传白名单之外的参数,报错
transform: true,
transformOptions: {
enableImplicitConversion: true
}
}));
await app.listen(3000);
}

bootstrap();

但是在这里设置无法注入任何依赖,因此可以在​ ​app.module.ts​ ​​中通过​ ​provider​ ​进行设置并

定义一个名为​ ​APP_PIPE​ ​​provider的东西,以这种方式提供​ ​ValidationPipe​ ​​,可以在​ ​AppModule​ ​​的范围内实例化​ ​ValidationPipe​ ​并在创建后将其注册为全局管道。

每个其他构建块功能也有类似的标记

import { APP_PIPE, APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from "@nestjs/core";

如何设置非全局,例如将​ ​ValidationPipe​ ​​绑定到仅在​ ​CoffeeController​ ​中定义的每个路由处理程序

在​ ​app.controller.ts​ ​​中使用​ ​@UsePipes​ ​​装饰器绑定单个管道或用​ ​,​ ​分隔的管道列表

其他相同

UsePipes, UseFilters, UseGuards, UseInterceptors,
import {
Controller,
Get,
Post,
Body,
Param,
HttpCode,
HttpStatus,
Res,
Patch,
Delete,
Query,
UsePipes, UseFilters, UseGuards, UseInterceptors, ValidationPipe
} from "@nestjs/common";
import { CoffeeService } from "./coffee.service";
import { CreateCoffeeDto } from "./dto/create-coffee.dto";
import { UpdateCoffeeDto } from "./dto/update-coffee.dto";
import { PaginationQueryDto } from "../common/dto/pagination-query.dto";

class demo {
canActivate(context) {
return true;
}
}

// 创建命令 nest g controller coffee
interface PostHello {
name: string,
id: number | string
}

@UsePipes(ValidationPipe)
@Controller("coffee")
export class CoffeeController {

constructor(private readonly coffeesService: CoffeeService) {
}

@UseGuards(demo)
@Get()
findAll(@Query() paginationQuery: PaginationQueryDto) {
// const { limit, offset } = paginationQuery;
console.log(123);
return this.coffeesService.findAll(paginationQuery);
}

@Get(":id")
findOne(@Param("id") id: string) {
return this.coffeesService.findOne(id);
}


@Post()
create(@Body() createCoffeeDto: CreateCoffeeDto) {
return this.coffeesService.create(createCoffeeDto);
}

@Patch(":id")
update(@Param("id") id: string, @Body() updateCoffeeDto: UpdateCoffeeDto) {
return this.coffeesService.update(id, updateCoffeeDto);
}

@Delete(":id")
delete(@Param("id") id: string) {
return this.coffeesService.remove(id);
}
}

基于参数的管道

@Patch(":id")
update(@Param("id") id: string, @Body() updateCoffeeDto: UpdateCoffeeDto) {
return this.coffeesService.update(id, updateCoffeeDto);
}

查看更新函数,有两个参数,资源"id"以及更新现有实体所需的"有效负载"

如果想将Pipe绑定到请求的Body而不是id参数,可以使用基于参数的管道

通过将​ ​ValidationPipe​ ​​类引用直接传递给这里的​ ​@Body​ ​​装饰器,可以让Nest只在这个参数上执行​ ​this particular pipe​

@Patch(":id")
update(@Param("id") id: string, @Body(ValidationPipe) updateCoffeeDto: UpdateCoffeeDto) {
return this.coffeesService.update(id, updateCoffeeDto);
}

使用过滤器捕获异常

通过创建​ ​ExceptionFilter​ ​​负责捕获作为​ ​HttpException​ ​类实例的异常,并为它实现自定义相应逻辑

  • 使用​ ​Nest CLI​ ​​过滤器原理图生成过滤器类
    ​​ ​nest g filter common/filters/http-exception​
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';

@Catch()
export class HttpExceptionFilter<T> implements ExceptionFilter {
catch(exception: T, host: ArgumentsHost) {}
}
  • 顶部的​ ​@Catch()​ ​​装饰器将所需的元数据绑定到​ ​ExceptionFilter​ ​​,这个​ ​@Catch​ ​装饰器可以采用单个参数或逗号分隔的列表,如果需要,允许一次为多种类型的异常设置过滤器
  • 因为要处理所有属于​ ​HttpException​ ​​实例的异常,所以在​ ​@Catch()​ ​​中增加​ ​HttpException​ ​​,同时将​ ​T​ ​​泛型继承​ ​HttpException​
import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from "@nestjs/common";

@Catch(HttpException)
export class HttpExceptionFilter<T extends HttpException> implements ExceptionFilter {
catch(exception: T, host: ArgumentsHost) {

}
}
  • 通过上面设置,可以实现自定义响应逻辑,为此,需要访问底层平台的​ ​Response​ ​对象,以便操纵或转换它并继续发送响应。
  • 通过调用​ ​ArgumentsHost​ ​​的实例​ ​host​ ​​的​ ​switchToHttp()​ ​方法,可以能够访问到请求或响应对象。
const context = host.switchToHttp();
  • 调用​ ​context​ ​​的​ ​getResponse()​ ​方法,可以返回底层平台(默认Express)的响应。
const response = context.getResponse<Response>();
  • 使用​ ​exception​ ​​对象,提取两个参数:​ ​statusCode​ ​​和​ ​body​
const status = exception.getStatus();
const exceptionResponse = exception.getResponse();
const error =
typeof response === "string"
? { message: exceptionResponse } :
(exceptionResponse as Object);
  • 设置响应
response.status(status).json({
...error,
timestamp: new Date().toISOString()
});

完整的异常处理

import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from "@nestjs/common";
import { Response } from "express";

@Catch(HttpException)
export class HttpExceptionFilter<T extends HttpException> implements ExceptionFilter {
catch(exception: T, host: ArgumentsHost) {
const context = host.switchToHttp();
const response = context.getResponse<Response>();

const status = exception.getStatus();
const exceptionResponse = exception.getResponse();
const error =
typeof response === "string"
? { message: exceptionResponse } :
(exceptionResponse as Object);

response.status(status).json({
...error,
timestamp: new Date().toISOString()
});
}
}
  • 将全局​ ​ExceptionFilter​ ​​绑定到应用程序上
    在main.ts中,通过​​ ​app.useGlobalFilters​ ​进行绑定
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";
import { HttpExceptionFilter } from "./common/filters/http-exception.filter";

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 参数白名单
forbidNonWhitelisted: true, // 上传白名单之外的参数,报错
transform: true,
transformOptions: {
enableImplicitConversion: true
}
}));
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}

bootstrap();

路由守卫

Guards的最佳用例之一:身份验证和授权

例如:实现一个Guard,它提取和验证一个Token,并使用提取的信息来确定请求是否可以继续

本例子实现两件事:

  1. 验证​ ​API_KEY​ ​​是否存在于​ ​authorization​ ​请求头
  2. 检查正在访问的路由是否是​ ​"公共"​ ​的

通过​ ​Nest CLI​ ​生成一个Guard类

​nest g guard common/guards/api-key​

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class ApiKeyGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
return true;
}
}

​Guard​ ​​类的关键是实现​ ​canActivate()​ ​方法,返回true或者false来判断是否通过

  • 全局绑定
    在​​ ​main.ts​ ​​中通过​ ​app.useGlobalGuards(new ApiKeyGuard());​ ​进行绑定
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";
import { HttpExceptionFilter } from "./common/filters/http-exception.filter";
import { ApiKeyGuard } from "./common/guards/api-key.guard";

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 参数白名单
forbidNonWhitelisted: true, // 上传白名单之外的参数,报错
transform: true,
transformOptions: {
enableImplicitConversion: true
}
}));
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalGuards(new ApiKeyGuard());
await app.listen(3000);
}

bootstrap();

为了实现"验证每个请求中应该存在API_KEY,且仅存在非公共路由上"

  1. 在​ ​.env​ ​​文件中定义​ ​API_KEY=3f4a1a66501692601596e772d3db1c97bb22970c7b08e6b588ccb393baab7e3f​
DATABASE_USER=postgres
DATABASE_PASSWORD=pass123
DATABASE_NAME=postgres
DATABASE_PORT=5432
DATABASE_HOST=localhost
API_KEY=3f4a1a66501692601596e772d3db1c97bb22970c7b08e6b588ccb393baab7e3f
  1. 获取请求
const request = context.switchToHttp().getRequest<Request>();
  1. 获取​ ​Authorization​ ​请求头
const authHeader = request.header("Authorization");
  1. 返回对比结果
return authHeader === process.env.API_KEY;
  1. 设置完成后,只有当请求携带​ ​Authorization​ ​​请求头,且与​ ​.env​ ​中设置的值一样才可以访问

完整guard

import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { Observable } from "rxjs";
import { Request } from "express";

@Injectable()
export class ApiKeyGuard implements CanActivate {
canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
const request = context
.switchToHttp()
.getRequest<Request>();

const authHeader = request.header("Authorization");
return authHeader === process.env.API_KEY;
}
}

上述操作实现了访问路由时验证是否存在API令牌,但仍未检测正在访问的路由是否被声明为公共

通过​ ​自定义元数据​ ​可以以声明方式制定程序中哪些端点是公共的,或者希望与控制器或路由一起存储的任何数据

Nest提供了通过​ ​@SetMetadata​ ​装饰器将自定义元数据附加到路由处理程序的能力,使用方式如下:

@Get
@SetMetadata('key', 'value')
getHello(): string{
return 'Hello World!';
}

​@SetMetadata​ ​接收两个参数

  • 第一个参数是将用作查找键的元数据"键"
  • 第二个参数是可以是任何类型的元数据"值"

这是我们为这个特定键放置我们想要存储的任何值的地方

例如:在​ ​CoffeeController​ ​​中,对​ ​findAll()​ ​方法增加元数据

@SetMetadata('isPublic',true)
@Get()
findAll(@Query() paginationQuery: PaginationQueryDto) {
// const { limit, offset } = paginationQuery;
console.log(123);
return this.coffeesService.findAll(paginationQuery);
}

这是最简单的方法,但不是最佳实践。

理想情况下,应该创建自己的装饰器来实现相同的目标。

  1. 在​ ​/common/​ ​​目录下新建文件夹并命名为​ ​/decorators/​ ​,在这里存储装饰器
  2. 在该目录下新建​ ​public.decorator.ts​ ​的新文件
  3. 在该文件中,导出两个东西
  • 一是作为元数据​ ​"key"​
  • 二是新装饰器本身,称之为​ ​@Public​
  1. 文件内容
import { SetMetadata } from "@nestjs/common";

export const IS_PUBLIC_KEY = 'isPublic';

export const Public = () => SetMetadata(IS_PUBLIC_KEY,true);
  1. 在​ ​CoffeeController​ ​​中,对​ ​findAll()​ ​​装饰器进行优化,使​ ​用@Public​ ​​代替​ ​@SetMetadata('isPublic',true)​
import {Public} from "../common/decorators/public.decorator";

@UsePipes(ValidationPipe)
@Controller("coffee")
export class CoffeeController {

constructor(private readonly coffeesService: CoffeeService) {
}

@Public()
@Get()
findAll(@Query() paginationQuery: PaginationQueryDto) {
// const { limit, offset } = paginationQuery;
console.log(123);
return this.coffeesService.findAll(paginationQuery);
}

定义好公共路由后,改造Guard。

通过​ ​Reflector​ ​的实例对象可以访问当前上下文的元数据。

  1. 在​ ​api-key.guard.ts​ ​​中注入​ ​Reflector​
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { Observable } from "rxjs";
import { Request } from "express";
import { Reflector } from "@nestjs/core";

@Injectable()
export class ApiKeyGuard implements CanActivate {
constructor(private readonly reflector:Reflector) {
}

canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
const request = context
.switchToHttp()
.getRequest<Request>();

const authHeader = request.header("Authorization");
return authHeader === process.env.API_KEY;
}
}
  1. 通过​ ​reflector.get​ ​获取元数据,接收两个参数
  • key
  • 目标对象上下文,这里使用​ ​context.getHandler()​
  • 如果需要从​ ​Class Level​ ​​中检索元数据,这里调用​ ​content.getClass()​
  1. 如果是公共的直接返回
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { Observable } from "rxjs";
import { Request } from "express";
import { Reflector } from "@nestjs/core";
import { IS_PUBLIC_KEY } from "../decorators/public.decorator";
import { ConfigService } from "@nestjs/config";

@Injectable()
export class ApiKeyGuard implements CanActivate {
constructor(
private readonly reflector:Reflector,
private readonly configService:ConfigService) {
}

canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {

const isPublic = this.reflector.get(IS_PUBLIC_KEY, context.getHandler());

if(isPublic){
return true;
}

const request = context
.switchToHttp()
.getRequest<Request>();

const authHeader = request.header("Authorization");
return authHeader === this.configService.get('API_KEy');
}
}

这时候会报错

NestJS学习笔记_数据库_05

原因:在​ ​Guard​ ​​内部使用了依赖注入,并在​ ​main.ts​ ​中实例化

依赖其他类的全局守卫必须在​ ​@Module​ ​上下文中注册,解决问题并将这个守卫添加到module中

  1. 生成一个模块并称为​ ​common​ ​​ ​nest g mo common​
  2. 这将生成一个模块类,可以在其中注册将来可能制作的任何全局增强器,包括ApiKeyGuard
import { Module } from '@nestjs/common';
import { APP_GUARD } from "@nestjs/core";
import { ApiKeyGuard } from "./guards/api-key.guard";
import { ConfigModule } from "@nestjs/config";

@Module({
imports:[ConfigModule],
providers:[
{
provide: APP_GUARD,
useClass: ApiKeyGuard
}
]

})
export class CommonModule {}
  1. 删除​ ​main.ts​ ​​中的​ ​app.useGlobalGuards(new ApiKeyGuard());​
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";
import { HttpExceptionFilter } from "./common/filters/http-exception.filter";

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 参数白名单
forbidNonWhitelisted: true, // 上传白名单之外的参数,报错
transform: true,
transformOptions: {
enableImplicitConversion: true
}
}));
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}

bootstrap();

拦截器

  1. 方法执行之前或之后绑定额外的逻辑
  2. 转换从方法返回的"结果"
  3. 转换从方法抛出的"异常"
  4. 扩展基本方法行为
  5. 完全覆盖一个方法 - 取决于特定条件

例子:希望接口的响应总是位于"data" property 数据属性中,创建一个新的拦截器(WrapResponseInterceptor)处理该问题。该拦截器处理所有传入的请求,并自动包装数据

  • 通过​ ​Nest CLI​ ​​自动生成
    ​​ ​nest g interceptor common/interceptors/wrap-response​
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class WrapResponseInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle();
}
}
  • Interceptor是一个带​ ​@Injectable()​ ​装饰器的类
  • 所有拦截器都应该实现从​ ​'@nestjs/common'​ ​​导出的​ ​NestInterceptor​ ​接口
  • ​NestInterceptor​ ​​接口要求在类中提供​ ​intercept()​ ​方法
  • ​intercept()​ ​​方法应该从​ ​RxJS​ ​​库返回一个​ ​Observable​
  • ​CallHandler​ ​​接口实现了​ ​handle()​ ​方法,使用该方法在拦截器中调用路由处理程序方法
  • 如果没有在拦截方法的实现中调用​ ​handle()​ ​方法,路由处理程序不会被执行
  • ​intercept()​ ​方法有效地包装了请求、响应流,允许在执行最终路由处理程序之前和之后实现自定义逻辑
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import {tap,map} from 'rxjs/operators'

@Injectable()
export class WrapResponseInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('before')
// return next.handle().pipe(tap(data=>console.log('adter...',data)));
return next.handle().pipe(map(data=>({data})));
}
}

将这个拦截器全局绑定在应用程序上

在​ ​main.ts​ ​​中,使用​ ​app.useGlobalInterceptors(new WrapResponseInterceptor())​ ​进行绑定

import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";
import { HttpExceptionFilter } from "./common/filters/http-exception.filter";
import { WrapResponseInterceptor } from "./common/interceptors/wrap-response.interceptor";

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 参数白名单
forbidNonWhitelisted: true, // 上传白名单之外的参数,报错
transform: true,
transformOptions: {
enableImplicitConversion: true
}
}));
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalInterceptors(new WrapResponseInterceptor())
await app.listen(3000);
}

bootstrap();

在拦截器中,通过​ ​return​ ​可以返回处理之后的数据。

通过​ ​return next.handle().pipe(map(data=>({data})));​ ​​可以将所有的返回结果包装在​ ​data​ ​中

拦截器处理超时

生成拦截器

​nest g interceptor common/interceptors/timeout​

import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable,timeout } from 'rxjs';

@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(timeout(3000));
}
}

在​ ​main.ts​ ​中引入

import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";
import { HttpExceptionFilter } from "./common/filters/http-exception.filter";
import { WrapResponseInterceptor } from "./common/interceptors/wrap-response.interceptor";
import { TimeoutInterceptor } from "./common/interceptors/timeout.interceptor";

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 参数白名单
forbidNonWhitelisted: true, // 上传白名单之外的参数,报错
transform: true,
transformOptions: {
enableImplicitConversion: true
}
}));
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalInterceptors(
new WrapResponseInterceptor(),
new TimeoutInterceptor()
)
await app.listen(3000);
}

bootstrap();

在findAll()中通过​ ​setTimeout​ ​模拟时长

async findAll(paginationQuery: PaginationQueryDto) {
await new Promise(resolve => setTimeout(resolve,5000))
const { limit, offset } = paginationQuery;
return this.coffeeRepository.find({
relations: ["flavors"],
skip: offset,
take: limit
});
}

管道

  1. 转换:将输入数据转换成所需要的输出数据
  2. 验证,评估输入数据,通过则pass,否则抛出异常

管道对控制器的路由处理程序正在处理的参数进行操作

NestJS在方法被调用之前触发一个管道,管道还接收要传递给方法的参数,任何转换或者验证操作都在这时发生,之后,使用任何可能转换的参数调用路由处理程序

NestJS提供几个开箱即用的管道,全部来自于​ ​@nestjs/common​

  • ViladationPipe:参数格式化
  • ParseArrayPipe:解析和验证数组

构建自己的管道,自动将任何传入的字符串解析为整数,称之为:​ ​ParseIntPipe​

  1. 通过​ ​NestJS CLI​ ​​生成一个管道类
    ​​ ​nest g pipe common/pipes/parse-int​
import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common';

@Injectable()
export class ParseIntPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
return value;
}
}
  1. 修改内部逻辑,将​ ​string​ ​​返回​ ​整数​
import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from "@nestjs/common";

@Injectable()
export class ParseIntPipe implements PipeTransform {
transform(value: string, metadata: ArgumentMetadata) {
const val = parseInt(value,10);
if(isNaN(val)){
throw new BadRequestException(`Validation failed. "${val} is not an integer."`)
}
return val;
}
}
  1. 将管道绑定到一些​ ​@Param()​ ​​装饰器上,作为​ ​@Param​ ​的第二个参数进行传递
@Get(":id")
findOne(@Param("id", ParseIntPipe) id: string) {
console.log(id);
console.log(typeof id);
return this.coffeesService.findOne(id);
}

中间件

中间件是一个在处理路由处理程序和任何其他构建块之前调用的函数。这包括拦截器、守卫和管道。

中间件函数可以访问Request、Response,并且不专门绑定到任何方法,而是绑定到指定的路由路径。

中间件可以执行以下任务:

  1. 执行代码
  2. 更改请求和响应对象
  3. 结束请求、相应周期
  4. 在调用堆栈中调用​ ​next()​ ​中间件函数

使用中间件时,如果当前中间件函数没有结束请求、响应周期,它必须调用​ ​next()​ ​方法,该方法将控制权传递给下一个中间件函数,否则请求将被挂起,永远不会完成

创建中间件:自定义Nest中间件可以在Function和Class中实现

函数中间件是无状态的,不能注入依赖,且无权访问Nest容器

类中间件可以依赖外部依赖并注入在同一模块范围内注册的提供程序

  • 通过​ ​Nest CLI​ ​​生成一个中间件类,称之为`logging``
    ​​ ​nest g middleware common/middleware/logging​
import { Injectable, NestMiddleware } from '@nestjs/common';

@Injectable()
export class LoggingMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
next();
}
}
  • 在其中增加一句打印
import { Injectable, NestMiddleware } from '@nestjs/common';

@Injectable()
export class LoggingMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
console.log('Hi from middleware!');
next();
}
}
  • 注册新创建的中间件。该中间件没有特别绑定到任何方法,不能使用装饰器以声明方式绑定,但是可以将中间件绑定到路由路径,表示为字符串
  • 注册到​ ​LoggingMiddleware​
  • 在​ ​common.module.ts​ ​​中,让​ ​CommonModule​ ​​实现了​ ​NestModule​ ​​接口,并在​ ​configure​ ​​中调用​ ​LoggingMiddleware​ ​可以指定路由、指定请求方法或排除路由
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { LoggingMiddleware } from "./middleware/logging.middleware";

@Module({
imports:[ConfigModule],
providers:[
// {
// provide: APP_GUARD,
// useClass: ApiKeyGuard
// }
]
})
export class CommonModule implements NestModule{
configure(consumer: MiddlewareConsumer) {
// consumer.apply(LoggingMiddleware).forRoutes('*'); // 绑定到所有路由
// consumer.apply(LoggingMiddleware).forRoutes('coffee'); // 绑定到coffee路由
// consumer.apply(LoggingMiddleware).forRoutes({
// path: '*',
// method:RequestMethod.GET
// }); // 指定请求方法与路由
// consumer.apply(LoggingMiddleware).exclude('coffee'); // 排除coffee
consumer.apply(LoggingMiddleware).forRoutes('*');
}
}

更改​ ​logging.middle.ts​ ​,输出请求时常

import { Injectable, NestMiddleware } from "@nestjs/common";

@Injectable()
export class LoggingMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
console.time("Request-response time");
console.log("Hi from middleware!");

res.on("finish", () => console.timeEnd("Request-response time"));
next();
}
}

自定义参数装饰器

在​ ​common/decorators​ ​​中新建​ ​protocol.decorator.ts​

import { createParamDecorator, ExecutionContext } from "@nestjs/common";

export const Protocol = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.protocol;
}
);

在​ ​findAll()​ ​​中定义​ ​@Protocol​

@Public()
@Get()
findAll(@Protocol() protocol: string, @Query() paginationQuery: PaginationQueryDto) {
console.log(protocol);
// const { limit, offset } = paginationQuery;
// console.log(123);
return this.coffeesService.findAll(paginationQuery);
}

更新​ ​protocol.decorator.ts​ ​​与​ ​findAll()​ ​传递默认值

import { createParamDecorator, ExecutionContext } from "@nestjs/common";

export const Protocol = createParamDecorator(
(defaultValue: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.protocol;
}
);
@Public()
@Get()
findAll(@Protocol("https") protocol: string, @Query() paginationQuery: PaginationQueryDto) {
console.log(protocol);
// const { limit, offset } = paginationQuery;
// console.log(123);
return this.coffeesService.findAll(paginationQuery);
}

Swagger

记录应用程序如何工作并显示我们的API参数和返回是大多数应用程序文档的重要组成部分

公开外部软件开发工具(或SDK)尤其如此

Swagger是自动化整个过程的一个很好的工具

NesJS集成和自动生成Open API文档

使用Open API规范记录应用程序

Open API规范是一种与语言无关的定义格式,用于描述RESTful AP

Open API文档允许我们描述整个API,包括

  • 可用的操作或端点
  • 操作参数:每个操作的输入和输出
  • 认证方法
  • 联系信息、许可、使用条款和其他信息

Nest提供了一个专用模块​ ​@nestjs/swagger​ ​,可以简单地通过利用装饰器来生成Open API文档

  1. 安装依赖
    ​​ ​yarn add @nestjs/swagger swagger-ui-express​
  2. 在​ ​main.ts​ ​​中,通过生成一个基本​ ​文档​ ​​开始设置​ ​swagger​
const options = new DocumentBuilder()
.setTitle("Iluvcoffee")
.setDescription("Coffee application")
.setVersion("1.0")
.build();
  1. 调用​ ​SwaggerModule.createDocument()​ ​方法创建文档
const document = SwaggerModule.createDocument(app, options);
  1. 调用​ ​SwaggerModule.setup()​ ​方法将所有内容连接在一起,此方法接收一些参数,包括
  • 挂载Swagger UI的路由路径
  • 应用程序实例
  • 实例化的文档对象
SwaggerModule.setup("api", app, document);
  1. 启动​ ​yarn start:dev​ ​​并访问​ ​http://127.0.0.1:3000/api/#/​
  2. NestJS学习笔记_javascript_06

初始化的Swagger UI内容并不完整,例如POST请求中,没有标明参数

NestJS学习笔记_javascript_07

但是有一个专门的DOT类,代表该接口的输入参数

NestJS学习笔记_学习_08

Nest提供了一个插件来增强TypeScript编译过程,减少需要创建的样板代码数量,从而解决该问题。

推荐:在需要覆盖插件提供的基本功能的任何地方添加特定的装饰器

  1. 启用新的​ ​Swagger CLI​ ​​插件,打开​ ​nest-cli.json​ ​增加以下配置
"compilerOptions": {
"deleteOutDir": true,
"plugins": [
"@nestjs/swagger/plugin"
]
}
  1. 重启后刷新​ ​http://127.0.0.1:3000/api/#/default/CoffeeController_create​ ​有了所需要的DTO
  2. NestJS学习笔记_bootstrap_09

  3. 但是在​ ​Patch​ ​中仍无法正常显示DTO
  4. NestJS学习笔记_bootstrap_10

  5. 打开​ ​update-coffee.dto.ts​ ​​,从​ ​"@nestjs/swagger"​ ​​中导出​ ​PartialType​ ​​而不是从​ ​"@nestjs/mapped-types"​ ​中导出
import { PartialType } from "@nestjs/swagger";
import { CreateCoffeeDto } from "./create-coffee.dto";

export class UpdateCoffeeDto extends PartialType(CreateCoffeeDto) {
}
  1. 刷新​ ​http://127.0.0.1:3000/api/#/default/CoffeeController_update​ ​正常
  2. NestJS学习笔记_bootstrap_11

在Swigger UI显示的DTO中,无法很好的看出参数的含义

NestJS学习笔记_javascript_12

通过在​ ​create-coffee.dto.ts​ ​​中,通过​ ​@ApiProperty()​ ​注解给每个参数增加描述、举例等

import { IsString } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";

export class CreateCoffeeDto {
@ApiProperty({ description: "The name of a coffee" })
@IsString()
readonly name: string;

@ApiProperty({ description: "The brand of a coffee" })
@IsString()
readonly brand: string;

@ApiProperty({ description: "The flavor of a coffee", example: ["caramel", "chocolate"] })
@IsString({ each: true })
readonly flavors: string[];
}

刷新后可以显示

NestJS学习笔记_数据库_13

定义其他响应结果

通过​ ​@ApiResponse()​ ​注解,可以为路由定义不同的返回状态结果。

也可以通过专门的注解(​ ​@ApiForbiddenResponse​ ​等),返回描述信息

同样可以定义专门的装饰器进行复用,减少重复代码

// @ApiResponse({ status: 403, description: "Forbidden." })
@ApiForbiddenResponse({ description: "Forbidden." })@Public()
@Get()
findAll(@Protocol("https") protocol: string, @Query() paginationQuery: PaginationQueryDto) {
console.log(protocol);
// const { limit, offset } = paginationQuery;
// console.log(123);
return this.coffeesService.findAll(paginationQuery);
}

NestJS学习笔记_数据库_14

使用标签(Tag)对标签进行分组,可以将相关的端点、API进行分组

通过在​ ​coffee.controller.ts​ ​​中使用​ ​@ApiTags("coffee")​ ​​注解装饰​ ​CoffeeController​ ​,可以进行分组

import { ApiTags } from "@nestjs/swagger";

@ApiTags("coffee")
@Controller("coffee")
export class CoffeeController {}

NestJS学习笔记_数据库_15

Jest

  • ​yarn test​ ​:用于单元测试
  • ​yarn test:cov​ ​:用于单元测试和收集测试覆盖率
  • ​yarn test:e2e​ ​:用于端到端(End to End)测试

对于NestJS中的单元测试,通常的做法是通过将​ ​.spec.ts​ ​文件保存在与它们测试的应用程序源代码文件相同的文件夹中

每个Controller、Provicer、Service等都应该有自己的专用测试文件

测试文件扩展名必须是​ ​*.spec.ts​

端到端测试默认情况下通常位于专用的​ ​/test/​ ​目录中,端到端测试通常按照测试的"特性"或者"功能"分组到单独的文件中。

端到端测试文件扩展名必须是​ ​*.e2e-spec.ts​

单元测试侧重于单个类和函数,端到端测试适合对整个系统进行高级验证