Master NestJS - The JavaScript Node.js Framework

2. Introduction to NestJS

2.1. Installing and Using Nest CLI

npm i -g @nestjs/cli

创建新项目

# nest new 
nest new nest-events-backend

选npm
选npm

npm run start:dev

默认端口3000
默认端口3000

2.2. NestJS Project Structure




装饰器模式,注解
装饰器模式,注解

请求处理器
请求处理器

service
service

控制器的测试文件
控制器的测试文件

3. Rest API

3.1. Controllers


  @Get("/bye")
  getBye() {
    return "Bye!"
  }


3.2. Resource Controller

what is resource
what is resource

生成controller
生成controller

nest generate controller
nest g co

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

@Controller("events")
export class EventsController {
  @Get()
  findAll() {}
  @Get()
  findOne() {}
  @Post()
  create() {}
  @Patch()
  update() {}
  @Delete()
  remove() {}
}


3.3. Route Parameters

获取路由中特定参数

    @Get(':id')
    findOne(@Param('id') id) {
        return id;
    }

@Param()没有指定路由参数名称,则获取所有

    @Get(':id/:event')
    findOne(@Param() params) {
        return params;
    }


3.4. Request Body

    @Post()
    create(@Body() input) {
        return input;
    }

3.5. Responses and Status Codes

    @Get()
    findAll() {
        return [
            { id: 1, name: 'First event' },
            { id: 2, name: 'Second event' },
        ]
    }
    // localhost:3000/events/id
    @Get(':id')
    findOne(@Param('id') id) {
        return { id: 1, name: 'First event' };
    }




    @Patch(':id')
    update(@Param('id') id, @Body() input) {
        return input;
    }
    @Delete(':id')
    @HttpCode(204)
    remove(@Param('id') id) { }

3.6. Request Payload - Data Transfer Objects

export class CreateEventDto {
  name: string;
  description: string;
  when: string;
  address: string;
}

    @Post()
    create(@Body() input: CreateEventDto) {
        // DTO: data transfer object 定义了一个数据结构
        return input;
    }

3.7. The Update Payload

    @Patch(':id')
    update(@Param('id') id, @Body() input: UpdateEventDto) {
        return input;
    }

可选参数

export class UpdateEventDto {
  name?: string; // ?代表可选
  description?: string;
  when?: string;
  address?: string;
}

另一种方式

npm i --save @nestjs/mapped-types
import { PartialType } from "@nestjs/mapped-types";
import { CreateEventDto } from "./create-event.dto";

// 继承CreateEventDto的所有字段,并且是可选的
export class UpdateEventDto extends PartialType(CreateEventDto) {}

3.8. A Working API Example

export class Event {
  id: number;
  name: string;
  description: string;
  when: Date;
  address: string;
}

import {
  Controller,
  Get,
  Post,
  Delete,
  Patch,
  Param,
  Body,
  HttpCode,
} from "@nestjs/common";
import { CreateEventDto } from "./create-event.dto";
import { UpdateEventDto } from "./update-event.dto";
import { Event } from "./event.entity";

@Controller("events")
export class EventsController {
  private events: Event[] = [];
  @Get()
  findAll() {
    return this.events;
  }
  // localhost:3000/events/id
  @Get(":id")
  findOne(@Param("id") id) {
    return this.events.find((event) => event.id === parseInt(id));
  }
  @Post()
  create(@Body() input: CreateEventDto) {
    const event = {
      ...input,
      when: new Date(input.when),
      id: this.events.length + 1,
    };
    this.events.push(event);
    return event;
  }
  @Patch(":id")
  update(@Param("id") id, @Body() input: UpdateEventDto) {
    // 根据id找到要修改的
    const index = this.events.findIndex((event) => event.id === parseInt(id));
    this.events[index] = {
      ...this.events[index],
      ...input, // 后面的会覆盖前面的
      when: input.when ? new Date(input.when) : this.events[index].when,
    };

    return this.events[index];
  }
  @Delete(":id")
  @HttpCode(204)
  remove(@Param("id") id) {
    this.events = this.events.filter((event) => event.id !== parseInt(id));
  }
}

4. Database Basics

4.1. Adding Docker to the Stack



4.2. Running the Database with Docker Compose

version: "3.8"

services:
  mysql:
    image: mysql:8.0.23
    command: --default-authentication-plugin=mysql_native_password
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: example
    ports:
      - 3306:3306

  postgres:
    image: postgres:13.1
    restart: always
    environment:
      POSTGRES_PASSWORD: example
    ports:
      - 5432:5432

  adminer:
    image: adminer
    restart: always
    ports:
      - 8080:8080
# 在有docker-compose.yml文件的目录运行
docker-compose up





4.3. Introduction to ORMs

4.5. Connecting to the Database

npm i --save @nestjs/typeorm typeorm mysql
// app.module.ts
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { EventsController } from "./events/events.controller";
import { TypeOrmModule } from "@nestjs/typeorm";

@Module({
  // 导入orm框架,配置数据库连接
  imports: [
    TypeOrmModule.forRoot({
      type: "mysql",
      host: "127.0.0.1",
      port: 3307,
      username: "root",
      password: "123456",
      database: "nest-events",
    }),
  ],
  // 引入controller
  controllers: [AppController, EventsController],
  providers: [AppService],
})
export class AppModule {}

4.7. The Entity (Primary Key & Columns)

// event.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";

// name: 表名
// @Entity('event', { name: 'event' })
@Entity() // 改变name不会删旧表,而是会创建新表
export class Event {
  // @PrimaryGeneratedColumn('uuid')
  // @PrimaryGeneratedColumn('rowid')
  // @PrimaryColumn() // 如果是复合主键,就在每个主键上加
  @PrimaryGeneratedColumn() // 自动生成
  id: number;

  @Column({ length: 100 })
  name: string;

  @Column()
  description: string;

  // name指定列名
  @Column({ name: "when_date" })
  when: Date;

  @Column()
  address: string;
}
// app.module.ts
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { EventsController } from "./events/events.controller";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Event } from "./events/event.entity";

@Module({
  // 导入orm框架,配置数据库连接
  imports: [
    TypeOrmModule.forRoot({
      type: "mysql",
      host: "127.0.0.1",
      port: 3307,
      username: "root",
      password: "123456",
      database: "nest-events",
      // 使用实体需要在这里引入
      entities: [Event],
      // 自动建表,你修改 Entity 里面字段,
      // 或者 *.entity{.ts,.js } 的名字,都会自动帮你修改。
      synchronize: true,
    }),
  ],
  // 引入controller
  controllers: [AppController, EventsController],
  providers: [AppService],
})
export class AppModule {}

4.8. Repository Pattern



4.9. Repository in Practice

import {
  Controller,
  Get,
  Post,
  Delete,
  Patch,
  Param,
  Body,
  HttpCode,
} from "@nestjs/common";
import { CreateEventDto } from "./create-event.dto";
import { UpdateEventDto } from "./update-event.dto";
import { Event } from "./event.entity";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm/repository/Repository";
@Controller("events")
export class EventsController {
  // 依赖注入
  constructor(
    @InjectRepository(Event)
    private readonly respository: Repository<Event>
  ) {}

  @Get()
  async findAll() {
    return await this.respository.find();
  }
  // localhost:3000/events/id
  @Get(":id")
  async findOne(@Param("id") id) {
    return await this.respository.findOne(id);
  }
  @Post()
  async create(@Body() input: CreateEventDto) {
    return await this.respository.save({
      ...input,
      when: new Date(input.when),
    });
  }
  @Patch(":id")
  async update(@Param("id") id, @Body() input: UpdateEventDto) {
    const event = await this.respository.findOne(id);

    return await this.respository.save({
      ...event,
      ...input,
      when: input.when ? new Date(input.when) : event.when,
    });
  }
  @Delete(":id")
  @HttpCode(204)
  async remove(@Param("id") id) {
    const event = await this.respository.findOne(id);
    await this.respository.remove(event);
  }
}

4.10. Repository Querying Criteria and Options

SET NAMES utf8mb4;

INSERT INTO `event` (`id`, `description`, `when`, `address`, `name`) VALUES
(1,    'Let\'s meet together.',    '2021-02-15 21:00:00',    'Office St 120',    'Team Meetup'),
(2,    'Let\'s learn something.',    '2021-02-17 21:00:00',    'Workshop St 80',    'Workshop'),
(3,    'Let\'s meet with big bosses',    '2021-02-17 21:00:00',    'Boss St 100',    'Strategy Meeting'),
(4,    'Let\'s try to sell stuff',    '2021-02-11 21:00:00',    'Money St 34',    'Sales Pitch'),
(5,    'People meet to talk about business ideas',    '2021-02-12 21:00:00',    'Invention St 123',    'Founders Meeting');

如果请求方法不对,也会报错 You must provide selection conditions in order to find a single row

    @Get('/practice')
    async practice() {
        return await this.repository.find({
            where: { id: 3 }
        })
    }

    @Get('/practice')
    async practice() {
        return await this.repository.find({
            where: { id: MoreThan(3) }
        })
    }

    @Get('/practice')
    async practice() {
        return await this.repository.find({
            where: {
                id: MoreThan(3),
                when: MoreThan(new Date('2021-02-12T13:00:00'))
            }
        })
    }

    @Get('/practice')
    async practice() {
        return await this.repository.find({
            // 多个where是or
            where: [
                {
                    id: MoreThan(3),
                    when: MoreThan(new Date('2021-02-12T13:00:00'))
                },
                {
                    description: Like('%meet%')
                }
            ]
        })
    }

    @Get('/practice')
    async practice() {
        return await this.repository.find({
            // 多个where是or
            where: [
                {
                    id: MoreThan(3),
                    when: MoreThan(new Date('2021-02-12T13:00:00'))
                },
                {
                    description: Like('%meet%')
                }
            ],
            // 限制取几个
            take: 2
        })
    }

    @Get('/practice')
    async practice() {
        return await this.repository.find({
            // 多个where是or
            where: [
                {
                    id: MoreThan(3),
                    when: MoreThan(new Date('2021-02-12T13:00:00'))
                },
                {
                    description: Like('%meet%')
                }
            ],
            // 限制取几个
            take: 2,
            order: {
                // 倒序
                id: 'desc'
            }
        })
    }

    @Get('/practice')
    async practice() {
        return await this.repository.find({
            // 只取其中指定列
            select: ['id', 'when'],
            // 多个where是or
            where: [
                {
                    id: MoreThan(3),
                    when: MoreThan(new Date('2021-02-12T13:00:00'))
                },
                {
                    description: Like('%meet%')
                }
            ],
            // 限制取几个
            take: 2,
            order: {
                // 倒序
                id: 'desc'
            }
        })
    }

5. Data Validation

5.1. Introduction to Pipes

    // localhost:3000/events/id
    @Get(':id') // ParseIntPipe转为number类型
    async findOne(@Param('id', ParseIntPipe) id) {
        console.log(typeof id); // number
        return await this.repository.findOne(id)
    }

pipe 最有用的是用于参数校验

5.2. Input Validation

npm i --save class-validator class-transformer
    @Post() // ValidationPipe 自动校验
    async create(@Body(ValidationPipe) input: CreateEventDto) {
        return await this.repository.save({
            ...input,
            when: new Date(input.when)
        })
    }
import { Length } from "class-validator";

export class CreateEventDto {
  @Length(5, 255)
  name: string;
  description: string;
  when: string;
  address: string;
}

export class CreateEventDto {
  @Length(5, 255, { message: "the name length is wrong" })
  name: string;
  description: string;
  when: string;
  address: string;
}

export class CreateEventDto {
  @IsString()
  @Length(5, 255, { message: "the name length is wrong" })
  name: string;
  @Length(5, 255)
  description: string;
  @IsDateString()
  when: string;
  @Length(5, 255)
  address: string;
}

开启全局验证,如果有使用 Dto,并且有校验装饰器(注解),则会自动校验,无需自己加 ValidationPipe

UpdateEventDto 继承了 CreateEventDto,它的属性也会带上校验注解.

// main.ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

5.3. Validation Groups and Options

import { Length, IsString, IsDateString } from "class-validator";

export class CreateEventDto {
  @IsString()
  @Length(5, 255, { message: "the name length is wrong" })
  name: string;
  @Length(5, 255)
  description: string;
  @IsDateString()
  when: string;
  // 分组
  @Length(5, 255, { groups: ["create"] })
  @Length(10, 20, { groups: ["update"] })
  address: string;
}
    @Post() // ValidationPipe 自动校验
    async create(@Body(new ValidationPipe({ groups: ['create'] })) input: CreateEventDto) {
        return await this.repository.save({
            ...input,
            when: new Date(input.when)
        })
    }

    @Patch(':id')
    async update(
        @Param('id') id,
        @Body(new ValidationPipe({ groups: ['update'] })) input: UpdateEventDto
    ) {
        const event = await this.repository.findOne(id)

        return await this.repository.save({
            ...event, ...input,
            when: input.when ? new Date(input.when) : event.when
        })
    }

要使用分组校验,需要禁用全局校验



也可以使用此装饰器
也可以使用此装饰器

6. Modules, Providers, Dependency Injection

6.1. Introduction to Modules, Providers and Dependency Injection

nest generate module events


6.2. Creating a Custom Module

使用@Module 定义的模块是静态的,提供固定功能,而动态模块提供多个可选的功能,动态模块可以按需导入

// app.japan.service.ts
import { Injectable } from "@nestjs/common";

@Injectable()
export class AppJapanService {
  getHello(): string {
    return "ハローワールド";
  }
}



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

@Injectable()
export class AppJapanService {
  constructor(
    @Inject("APP_NAME")
    private readonly name: string
  ) {}
  getHello(): string {
    return `ハローワールド from ${this.name}`;
  }
}


// app.dummy.ts
export class AppDummy {
  public dummy(): string {
    return "dummy";
  }
}


7. Configuration, Logging, and Errors

7.1. Application Config and Environments

npm i --save @nestjs/config






7.2. Custom Configuration Files and Options

把 typeOrm 的配置放在单独的配置文件

// orm.config.ts
import { TypeOrmModuleOptions } from "@nestjs/typeorm";
import { Event } from "src/events/event.entity";
import { registerAs } from "@nestjs/config";

export default registerAs(
  "org.config",
  (): TypeOrmModuleOptions => ({
    type: "mysql",
    host: process.env.DB_HOST,
    port: Number(process.env.DB_PORT),
    username: process.env.DB_USER,
    password: process.env.DB_PASS,
    database: process.env.DB_NAME,
    // 使用实体需要在这里引入
    entities: [Event],
    // 自动建表,你修改 Entity 里面字段,
    // 或者 *.entity{.ts,.js } 的名字,都会自动帮你修改。
    synchronize: true,
  })
);
// orm.config.prod.ts
import { TypeOrmModuleOptions } from "@nestjs/typeorm";
import { Event } from "src/events/event.entity";
import { registerAs } from "@nestjs/config";

export default registerAs(
  "org.config",
  (): TypeOrmModuleOptions => ({
    type: "mysql",
    host: process.env.DB_HOST,
    port: Number(process.env.DB_PORT),
    username: process.env.DB_USER,
    password: process.env.DB_PASS,
    database: process.env.DB_NAME,
    // 使用实体需要在这里引入
    entities: [Event],
    // 自动建表,你修改 Entity 里面字段,
    // 或者 *.entity{.ts,.js } 的名字,都会自动帮你修改。
    synchronize: false,
  })
);
// app.module.ts
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { EventsModule } from "./events/events.module";
import { AppJapanService } from "./app.japan.service";
import { AppDummy } from "./app.dummy";
import { ConfigModule } from "@nestjs/config";
import ormConfig from "./config/orm.config";
import ormConfigProd from "./config/orm.config.prod";
@Module({
  // 导入orm框架,配置数据库连接
  imports: [
    // 配置这个,可以读取根目录的.env文件
    ConfigModule.forRoot({
      envFilePath: ".env",
      isGlobal: true,
      expandVariables: true,
      load: [ormConfig],
    }),
    TypeOrmModule.forRootAsync({
      useFactory:
        process.env.NODE_ENV !== "production" ? ormConfig : ormConfigProd,
    }),
    // 导入子模块
    EventsModule,
  ],
  // 引入controller
  controllers: [AppController],
  providers: [
    // 提供类
    {
      provide: AppService,
      useClass: AppJapanService,
    },
    // 提供变量
    {
      provide: "APP_NAME",
      useValue: "Nest Events Backend!",
    },
    // 提供函数调用结果
    {
      provide: "MESSAGE",
      inject: [AppDummy],
      useFactory: (app) => `${app.dummy()}`,
    },
    AppDummy,
  ],
})
export class AppModule {}

expandVariables

APP_URL=mywebsite.com
SUPPORT_EMAIL=support@${APP_URL}


7.3. Logging

    @Get()
    async findAll() {
        this.logger.log(`Hit the findAll route`)
        const events = await this.repository.find()
        this.logger.debug(`Found ${events.length} events`)
        return events
    }


main.ts
main.ts

7.4. Exception Filters

    @Get(':id')
    async findOne(@Param('id', ParseIntPipe) id: number) {
        const event = await this.repository.findOne({
            where: { id }
        })
        if (!event) {
            throw new NotFoundException()
        }
        return event
    }

8. Intermediate Database Concepts

8.1. Understanding Relations

数据库实例之间的关系1-1 1-n n-m
数据库实例之间的关系1-1 1-n n-m

8.2. One To Many Relation

// attendee.entity.ts
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { Event } from "./event.entity";

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

  @Column()
  name: string;

  @ManyToOne(() => Event, (event) => event.attendees)
  event: Event;
}
// event.entity.ts
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { Attendee } from "./attendee.entity";

@Entity() // 改变name不会删旧表,而是会创建新表
export class Event {
  // ...

  @OneToMany(() => Attendee, (attendee) => attendee.event)
  attendees: Attendee[];
}


8.3. Loading Related Entities

INSERT INTO
    `event` (`id`, `description`, `when`, `address`, `name`)
VALUES
    (
        1,
        'Let\'s meet together.',
        '2021-02-15 21:00:00',
        'Office St 120',
        'Team Meetup'
    ),
    (
        2,
        'Let\'s learn something.',
        '2021-02-17 21:00:00',
        'Workshop St 80',
        'Workshop'
    ),
    (
        3,
        'Let\'s meet with big bosses',
        '2021-02-17 21:00:00',
        'Boss St 100',
        'Strategy Meeting'
    ),
    (
        4,
        'Let\'s try to sell stuff',
        '2021-02-11 21:00:00',
        'Money St 34',
        'Sales Pitch'
    ),
    (
        5,
        'People meet to talk about business ideas',
        '2021-02-12 21:00:00',
        'Invention St 123',
        'Founders Meeting'
    );

INSERT INTO
    `attendee` (`id`, `name`, `eventId`)
VALUES
    (1, 'Piotr', 1),
    (2, 'John', 1),
    (3, 'Terry', 1),
    (4, 'Bob', 2),
    (5, 'Joe', 2),
    (6, 'Donald', 2),
    (7, 'Harry', 4);
    @Get('/practice2')
    async practice2() {
        return await this.repository.findOne({
            where: { id: 1 }
        })
    }


这样也可以查出 attendees


8.4. Associating Related Entities

    @Get('/practice2')
    async practice2() {
        const event = await this.repository.findOne({
            where: { id: 1 }
        })
        const attendee = new Attendee()
        attendee.name = 'Jetty'
        attendee.event = event

        await this.attendeeRepository.save(attendee)
        return event
    }

    @Get('/practice2')
    async practice2() {
        const event = new Event()
        event.id = 1

        const attendee = new Attendee()
        attendee.name = 'Jetty The Second'
        attendee.event = event

        await this.attendeeRepository.save(attendee)
        return event
    }

级联操作(不是数据库的级联)

    @Get('/practice2')
    async practice2() {
        const event = await this.repository.findOne({
            where: { id: 1 }, relations: ['attendees']
        })

        const attendee = new Attendee()
        attendee.name = 'Using cascade'

        event.attendees.push(attendee)

        await this.repository.save(event)
        return event
    }

如果 nullable 设置为 true

将 event 的 attendees 设为空

此时与之相关联的 attendee 的外键的值都置为 null

8.5. Many To Many Relation


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

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

  @Column()
  name: string;

  @ManyToMany(() => Teacher, (teacher) => teacher.subjects, { cascade: true })
  @JoinTable() // 会创建一个关系表(subject_teachers_teacher)
  teachers: Teacher[];
}
import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from "typeorm";
import { Subject } from "./subject.entity";

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

  @Column()
  name: string;

  @ManyToMany(() => Subject, (subject) => subject.teachers)
  subjects: Subject[];
}

可以指定关联的列的名称,和反过来的关联列
可以指定关联的列的名称,和反过来的关联列

import { Controller, Post } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { Subject } from "./subject.entity";
import { Teacher } from "./teacher.entity";

@Controller("school")
export class TrainingController {
  constructor(
    @InjectRepository(Subject)
    private readonly subjectRepository: Repository<Subject>
  ) {}

  @Post("/create")
  public async savingRelation() {
    const subject = new Subject();
    subject.name = "Math";

    const teacher1 = new Teacher();
    teacher1.name = "John Doe";

    const teacher2 = new Teacher();
    teacher2.name = "Harry Doe";

    subject.teachers = [teacher1, teacher2];

    await this.subjectRepository.save(subject);
  }

  @Post("/remove")
  public async removingRelation() {
    const subject = await this.subjectRepository.findOne({
      where: { id: 1 },
      relations: ["teachers"],
    });

    subject.teachers = subject.teachers.filter((teacher) => teacher.id !== 2);

    await this.subjectRepository.save(subject);
  }
}
// school.module.ts
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Subject } from "./subject.entity";
import { Teacher } from "./teacher.entity";
import { TrainingController } from "./training.controller";

@Module({
  imports: [TypeOrmModule.forFeature([Subject, Teacher])],
  controllers: [TrainingController],
})
export class SchoolModule {}

8.6. Query Builder Introduction

    @Get('/practice3')
    async practice3() {
        return await this.repository.createQueryBuilder('e')
            .select(['e.id', 'e.name'])
            .orderBy('e.id', 'DESC')
            .getMany()
    }

// event.service.ts
import { InjectRepository } from "@nestjs/typeorm";
import { Event } from "./event.entity";
import { Repository } from "typeorm";
import { Logger } from "@nestjs/common/services";
import { Injectable } from "@nestjs/common";

@Injectable()
export class EventsService {
  private readonly logger = new Logger(EventsService.name);
  constructor(
    @InjectRepository(Event)
    private readonly eventsRepository: Repository<Event>
  ) {}

  private getEventsBaseQuery() {
    return (
      this.eventsRepository
        .createQueryBuilder("e")
        // .select(["e.id", "e.name"])
        .orderBy("e.id", "ASC")
    );
  }

  public async getEvent(id: number): Promise<Event | undefined> {
    const query = this.getEventsBaseQuery().andWhere("e.id = :id", { id });
    this.logger.debug(await query.getSql());
    return await query.getOne();
  }
}
    // 依赖注入
    constructor(
        @InjectRepository(Event)
        private readonly repository: Repository<Event>,
        @InjectRepository(Attendee)
        private readonly attendeeRepository: Repository<Attendee>,
        private readonly eventsService: EventsService
    ) { }
    // ...
    @Get(':id')
    async findOne(@Param('id', ParseIntPipe) id: number) {
        const event = await this.eventsService.getEvent(id)

        if (!event) {
            throw new NotFoundException()
        }
        return event
    }


8.7. Joins And Aggregation with Query Builder

    public getEventsWithAttendeeCountQuery() {
        return this.getEventsBaseQuery() // 查询到所有events
            .loadRelationCountAndMap( // 给每个events的attendees注入值,值为关联的attendee的数量
                'e.attendeeCount', 'e.attendees'
            )
    }

    public async getEvent(id: number): Promise<Event | undefined> {
        const query = this.getEventsWithAttendeeCountQuery()
            .andWhere('e.id = :id', { id }) // 根据id筛选结果
        this.logger.debug(await query.getSql())
        return await query.getOne()
    }


// attendee.entity.ts
import {
  Column,
  Entity,
  JoinColumn,
  ManyToOne,
  PrimaryGeneratedColumn,
} from "typeorm";
import { Event } from "./event.entity";

// 参会意向
export enum AttendeeAnswerEnum {
  Accepted = 1,
  Maybe,
  Rejected,
}

@Entity()
export class Attendee {
  //...

  @Column("enum", {
    enum: AttendeeAnswerEnum,
    default: AttendeeAnswerEnum.Accepted,
  })
  answer: AttendeeAnswerEnum;
}
// event.entity.ts
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { Attendee } from "./attendee.entity";

@Entity()
export class Event {
  // ...

  attendeeRejected?: number;
  attendeeMaybe?: number;
  attendeeAccepted?: number;
}
// event.service.ts
import { InjectRepository } from "@nestjs/typeorm";
import { Event } from "./event.entity";
import { Repository } from "typeorm";
import { Logger } from "@nestjs/common/services";
import { Injectable } from "@nestjs/common";
import { Attendee, AttendeeAnswerEnum } from "./attendee.entity";

@Injectable()
export class EventsService {
  // ...

  public getEventsWithAttendeeCountQuery() {
    return this.getEventsBaseQuery() // 查询到所有events
      .loadRelationCountAndMap(
        // 给每个events的attendees注入值,值为关联的attendee的数量
        "e.attendeeCount",
        "e.attendees"
      )
      .loadRelationCountAndMap(
        "e.attendeeAccepted",
        "e.attendees",
        "attendee",
        (qb) =>
          qb.where("attendee.answer = :answer", {
            answer: AttendeeAnswerEnum.Accepted,
          })
      )
      .loadRelationCountAndMap(
        "e.attendeeRejected", // mapToProperty映射到哪个值
        "e.attendees", // relationName关系名称
        "attendee", // aliasName数据库名
        (qb) =>
          qb.where("attendee.answer = :answer", {
            answer: AttendeeAnswerEnum.Rejected,
          })
      )
      .loadRelationCountAndMap(
        "e.attendeeMaybe",
        "e.attendees",
        "attendee",
        (qb) =>
          qb.where("attendee.answer = :answer", {
            answer: AttendeeAnswerEnum.Maybe,
          })
      );
  }
  // ...
}

8.8. Filtering Data Using Query Builder

// list.event.ts
export class ListEvents {
  when?: WhenEventFilter = WhenEventFilter.All;
}

export enum WhenEventFilter {
  All = 1,
  Today,
  Tommorow,
  ThisWeek,
  NextWeek,
}
    @Get() // Query: ?xxx=xxx
    async findAll(@Query() filter: ListEvents) {
        this.logger.log(`Hit the findAll route`)
        const events = await this.eventsService
            .getEventsWithAttendeeCountFiltered(filter)
        this.logger.debug(`Found ${events.length} events`)
        return events
    }
    public async getEventsWithAttendeeCountFiltered(filter?: ListEvents) {
        let query = this.getEventsWithAttendeeCountQuery()

        if (!filter) {
            return await query.getMany()
        }

        if (filter.when) {
            if (filter.when == WhenEventFilter.Today) {
                query = query.andWhere(
                    `e.when >= CURDATE() AND e.when <= CURDATE() + INTERVAL 1 DAY`
                )
            }
            if (filter.when == WhenEventFilter.Tommorow) {
                query = query.andWhere(
                    `e.when >= CURDATE() + INTERVAL 1 DAY AND e.when <= CURDATE() + INTERVAL 2 DAY`
                )
            }
            if (filter.when == WhenEventFilter.ThisWeek) {
                query = query.andWhere('YEARWEEK(e.when, 1) = YEARWEEK(CURDATE(), 1)')
            }
            if (filter.when == WhenEventFilter.NextWeek) {
                query = query.andWhere('YEARWEEK(e.when, 1) = YEARWEEK(CURDATE(), 1) + 1')
            }
        }

        return await query.getMany()
    }

8.9. Pagination Using Query Builder

// paginator.ts
import { SelectQueryBuilder } from "typeorm";

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

// 可以返回数组
export interface PaginationResult<T> {
  first: number;
  last: number;
  limit: number;
  total?: number;
  data: T[];
}

export async function paginate<T>(
  qb: SelectQueryBuilder<T>,
  options: PaginateOptions = {
    limit: 10,
    currentPage: 1,
  }
): Promise<PaginationResult<T>> {
  const offset = (options.currentPage - 1) * options.limit;
  const data = await qb.limit(options.limit).offset(offset).getMany();
  return {
    first: offset + 1,
    last: offset + data.length,
    limit: options.limit,
    total: options.total ? await qb.getCount() : null,
    data,
  };
}
    @Get() // Query: ?xxx=xxx
    @UsePipes(new ValidationPipe({ transform: true })) // 没有提供值时,会填充默认值
    async findAll(@Query() filter: ListEvents) {
        const events = await this.eventsService
            .getEventsWithAttendeeCountFilteredPaginated(
                filter,
                {
                    total: true,
                    currentPage: filter.page,
                    limit: 10
                }
            )
        return events
    }
    private async getEventsWithAttendeeCountFiltered(filter?: ListEvents) {
        let query = this.getEventsWithAttendeeCountQuery()

        if (!filter) {
            return query
        }

        if (filter.when) {
            if (filter.when == WhenEventFilter.Today) {
                query = query.andWhere(
                    `e.when >= CURDATE() AND e.when <= CURDATE() + INTERVAL 1 DAY`
                )
            }
            if (filter.when == WhenEventFilter.Tommorow) {
                query = query.andWhere(
                    `e.when >= CURDATE() + INTERVAL 1 DAY AND e.when <= CURDATE() + INTERVAL 2 DAY`
                )
            }
            if (filter.when == WhenEventFilter.ThisWeek) {
                query = query.andWhere('YEARWEEK(e.when, 1) = YEARWEEK(CURDATE(), 1)')
            }
            if (filter.when == WhenEventFilter.NextWeek) {
                query = query.andWhere('YEARWEEK(e.when, 1) = YEARWEEK(CURDATE(), 1) + 1')
            }
        }

        return await query
    }

    public async getEventsWithAttendeeCountFilteredPaginated(
        filter: ListEvents,
        paginateOptions: PaginateOptions
    ) {
        return await paginate(
            await this.getEventsWithAttendeeCountFiltered(filter),
            paginateOptions
        )
    }

8.10. Updating, Deleting, Modifying Relations using QB


    @Delete(':id')
    @HttpCode(204)
    async remove(@Param('id') id) {
        const result = await this.eventsService.deleteEvent(id)
        if (result?.affected !== 1) {
            throw new NotFoundException()
        }
    }
// 把name属性改成Confidential
await this.subjectRepository
  .createQueryBuilder("e")
  .update()
  .set({ name: "Confidential" })
  .execute();

    constructor(
        @InjectRepository(Subject)
        private readonly subjectRepository: Repository<Subject>,
        @InjectRepository(Teacher)
        private readonly teacherRepository: Repository<Teacher>
    ) { }

    @Post('/create')
    public async savingRelation() {
        const subject = new Subject();
        subject.name = 'Math';

        await this.subjectRepository.save(subject)

        const teacher1 = new Teacher();
        teacher1.name = 'John Doe';

        const teacher2 = new Teacher();
        teacher2.name = 'Harry Doe';

        await this.teacherRepository.save([teacher1, teacher2]);
    }

    @Post('/create')
    public async savingRelation() {
        const subject = await this.subjectRepository.findOne({ where: { id: 3 } })

        const t1 = await this.teacherRepository.findOne({ where: { id: 3 } })
        const t2 = await this.teacherRepository.findOne({ where: { id: 4 } })
        return await this.subjectRepository
            .createQueryBuilder()
            .relation(Subject, 'teachers')
            .of(subject)
            .add([t1, t2])
    }

8.11. One to One Relation

import {
  Column,
  Entity,
  JoinColumn,
  OneToOne,
  PrimaryGeneratedColumn,
} from "typeorm";
import { Profile } from "./profile.entity";

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;
  @Column()
  username: string;
  @Column()
  password: string;
  @Column()
  email: string;
  @Column()
  firstName: string;
  @Column()
  lastName: string;
  @OneToOne(() => Profile)
  @JoinColumn()
  profile: Profile;
}
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";

@Entity()
export class Profile {
  @PrimaryGeneratedColumn()
  id: number;
  @Column()
  age: number;
}

9. Authentication, JWT, Authorization

9.1. Introduction to Authentication

9.2. Local Passport Strategy

npm i --save @nestjs/passport passport passport-local  @types/passport-local
// local.strategy.ts
import { Injectable, Logger, UnauthorizedException } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { Strategy } from "passport-local";
import { Repository } from "typeorm";
import { User } from "./user.entity";
import { InjectRepository } from "@nestjs/typeorm";

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  private readonly logger = new Logger(LocalStrategy.name);

  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) {
      this.logger.debug(`User ${username} not found!`);
      throw new UnauthorizedException();
    }

    if (password !== user.password) {
      this.logger.debug(`Invalid credentials for user ${username}`);
      throw new UnauthorizedException();
    }

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

9.3. Logging In - Passport Strategy with a Nest Guard

import { Controller, Post, Request, UseGuards } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";

@Controller("auth")
export class AuthController {
  @Post("login")
  // 路由守卫
  @UseGuards(AuthGuard("local"))
  async login(@Request() request) {
    return {
      userId: request.user.id,
      token: "the token will go here",
    };
  }
}

9.4. JWT - JSON Web Tokens Introduction

9.5. JWT - Generating Token

npm i --save @nestjs/jwt passport-jwt
npm i --save-dev @types/passport-jwt

// auth.module.ts
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { User } from "./user.entity";
import { LocalStrategy } from "./local.strategy";
import { AuthController } from "./auth.controller";
import { JwtModule } from "@nestjs/jwt";
import { AuthService } from "./auth.service";
@Module({
  imports: [
    TypeOrmModule.forFeature([User]),
    JwtModule.registerAsync({
      // 异步创建,防止环境变量在module创建时还无法读取
      useFactory: () => ({
        secret: process.env.AUTH_SECRET, // JWT密码(放到.env)
        signOptions: {
          expiresIn: "60m", // 60min后过期
        },
      }),
    }),
  ],
  providers: [LocalStrategy, AuthService],
  controllers: [AuthController],
})
export class AuthModule {}
import { Controller, Inject, Post, Request, UseGuards } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
import { AuthService } from "./auth.service";

@Controller("auth")
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post("login")
  // 路由守卫
  @UseGuards(AuthGuard("local"))
  async login(@Request() request) {
    return {
      userId: request.user.id,
      token: this.authService.getTokenForUser(request.user),
    };
  }
}
// auth.service.ts
import { Injectable } from "@nestjs/common";
import { User } from "./user.entity";
import { JwtService } from "@nestjs/jwt";

@Injectable()
export class AuthService {
  constructor(private readonly jwtService: JwtService) {}
  public getTokenForUser(user: User): string {
    return this.jwtService.sign({
      username: user.username,
      sub: user.id,
    });
  }
}

9.6. JWT - Strategy & Guard - Authenticating with JWT Token

import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { InjectRepository } from "@nestjs/typeorm";
import { ExtractJwt, Strategy } 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(),
      ignoreExpiration: false,
      secretOrKey: process.env.AUTH_SECRET,
    });
  }

  async validate(payload: any) {
    // sub是user.id
    return await this.userRepository.findOne({ where: { id: payload.sub } });
  }
}
    @Get('profile')
    // 传递的'jwt'是策略名,
    // 对应PassportStrategy(从passport-jwt导入就是jwt(默认值)),
    // 可以通过 PassportStrategy(Strategy,'xxx')指定,
    // 会自动找实现类
    @UseGuards(AuthGuard('jwt'))
    async getProfile(@Request() request) {
        return request.user
    }

问题: 会返回密码
问题: 会返回密码

9.7. Hashing Passwords with Bcrypt

npm i bcrypt
npm i --save-dev @types/bcrypt
import * as bcrypt from "bcrypt";
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  // ...

  public async hashPassword(password: string): Promise<string> {
    // 参数2是hash的轮数
    return await bcrypt.hash(password, 10);
  }
}
// ...
import * as bcrypt from "bcrypt";

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  // ...

  public async validate(username: string, password: string): Promise<any> {
    // ...

    if (!(await bcrypt.compare(password, user.password))) {
      this.logger.debug(`Invalid credentials for user ${username}`);
      throw new UnauthorizedException();
    }

    return user;
  }
}

9.8. Custom CurrentUser Decorator

// current-user.decorator.ts
import { ExecutionContext, createParamDecorator } from "@nestjs/common";

export const CurrentUser = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user ?? null;
  }
);
    @Post('login')
    // 路由守卫
    @UseGuards(AuthGuard('local'))
    async login(@CurrentUser() user: User) {
        return {
            userId: user.id,
            token: this.authService.getTokenForUser(user)
        }
    }

    @Get('profile')
    @UseGuards(AuthGuard('jwt'))
    async getProfile(@CurrentUser() user: User) {
        return user
    }

9.9. User Registration

// create.user.dto.ts
import { IsEmail, Length } from "class-validator";

export class CreateUserDto {
  @Length(5)
  username: string;
  @Length(8)
  password: string;
  @Length(8)
  retypedPassword: string;
  @Length(2)
  firstName: string;
  @Length(2)
  lastName: string;
  @IsEmail()
  email: string;
}
import { Injectable } from "@nestjs/common";
import { User } from "./user.entity";
import { JwtService } from "@nestjs/jwt";
import * as bcrypt from "bcrypt";
@Injectable()
export class AuthService {
  // ...
  public async hashPassword(password: string): Promise<string> {
    return await bcrypt.hash(password, 10);
  }
}
import { Controller, Post, Body, BadRequestException } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { CreateUserDto } from "./input/create.user.dto";
import { User } from "./user.entity";
import { Repository } from "typeorm";
import { InjectRepository } from "@nestjs/typeorm";

@Controller("users")
export class UsersController {
  constructor(
    private readonly authService: AuthService,
    @InjectRepository(User)
    private readonly userRepository: Repository<User>
  ) {}

  @Post()
  async create(@Body() createUserDto: CreateUserDto) {
    const user = new User();

    if (createUserDto.password !== createUserDto.retypedPassword) {
      throw new BadRequestException(["Passwords are not identical"]);
    }

    // 当根据email或username查找到,说明已存在,不能添加
    const existingUser = await this.userRepository.findOne({
      where: [
        { username: createUserDto.username },
        { email: createUserDto.email },
      ],
    });

    if (existingUser) {
      throw new BadRequestException(["username or email is already taken"]);
    }

    user.username = createUserDto.username;
    user.password = await this.authService.hashPassword(createUserDto.password);
    user.email = createUserDto.email;
    user.firstName = createUserDto.firstName;
    user.lastName = createUserDto.lastName;

    return {
      ...(await this.userRepository.save(user)),
      token: this.authService.getTokenForUser(user),
    };
  }
}

9.10. Only Authenticated Users Can Create Events

import {
  Column,
  Entity,
  ManyToOne,
  OneToMany,
  PrimaryGeneratedColumn,
} from "typeorm";
import { Attendee } from "./attendee.entity";
import { User } from "src/auth/user.entity";

@Entity()
export class Event {
  //

  @ManyToOne(() => User, (user) => user.organized)
  @JoinColumn({ name: "organizerId" })
  organizer: User;
  @Column({ nullable: true }) // 如果不能null,创建会报错
  organizerId: number;

  //
}
import {
  Column,
  Entity,
  JoinColumn,
  OneToMany,
  OneToOne,
  PrimaryGeneratedColumn,
} from "typeorm";
import { Profile } from "./profile.entity";
import { Event } from "src/events/event.entity";
@Entity()
export class User {
  //
  @OneToMany(() => Event, (event) => event.organizer)
  organized: Event[];
}
// event.service.ts
    public async createEvent(input: CreateEventDto, user: User): Promise<Event> {
        return await this.eventsRepository.save({
            ...input,
            organizer: user,
            when: new Date(input.when)
        })
    }
// auth-guard.local.ts
import { AuthGuard } from "@nestjs/passport";

export class AuthGuardLocal extends AuthGuard("local") {}
// auth-guard.jwt.ts
import { AuthGuard } from "@nestjs/passport";

export class AuthGuardJwt extends AuthGuard("jwt") {}
    @UseGuards(AuthGuardLocal)
    // ...
    @UseGuards(AuthGuardJwt)
    @Post()
    @UseGuards(AuthGuardJwt)
    async create(
        @Body() input: CreateEventDto,
        @CurrentUser() user: User
    ) {
        return await this.eventsService.createEvent(input, user)
    }

9.11. Only The Owners Can Edit or Delete Events

import {
  Controller,
  Get,
  Post,
  ForbiddenException,
  Delete,
  Patch,
  Param,
  Body,
  HttpCode,
  ParseIntPipe,
  ValidationPipe,
  Logger,
  NotFoundException,
  Query,
  UsePipes,
  UseGuards,
} from "@nestjs/common";
import { CreateEventDto } from "./input/create-event.dto";
import { UpdateEventDto } from "./input/update-event.dto";
import { Event } from "./event.entity";
import { InjectRepository } from "@nestjs/typeorm";
import { MoreThan } from "typeorm/find-options/operator/MoreThan";
import { Repository } from "typeorm/repository/Repository";
import { Like } from "typeorm";
import { Attendee } from "./attendee.entity";
import { EventsService } from "./event.service";
import { ListEvents } from "./input/list.event";
import { CurrentUser } from "src/auth/current-user.decorate";
import { User } from "src/auth/user.entity";
import { AuthGuardJwt } from "src/auth/auth-guard.jwt";
@Controller("events")
export class EventsController {
  // ...

  @Patch(":id")
  @UseGuards(AuthGuardJwt)
  async update(
    @Param("id") id,
    @Body() input: UpdateEventDto,
    @CurrentUser() user: User
  ) {
    const event = await this.eventsService.getEvent(id);

    if (!event) {
      throw new NotFoundException();
    }

    if (event.organizerId !== user.id) {
      throw new ForbiddenException(
        null,
        `You are not allowed to change this event`
      );
    }

    return await this.eventsService.updateEvent(event, input);
  }
  @Delete(":id")
  @UseGuards(AuthGuardJwt)
  @HttpCode(204)
  async remove(@Param("id") id, @CurrentUser() user: User) {
    const event = await this.eventsService.getEvent(id);

    if (!event) {
      throw new NotFoundException();
    }

    if (event.organizerId !== user.id) {
      throw new ForbiddenException(
        null,
        `You are not allowed to remove this event`
      );
    }

    await this.eventsService.deleteEvent(id);
  }
}
// event.service.ts
    public async updateEvent(event: Event, input: UpdateEventDto): Promise<Event> {
        return await this.eventsRepository.save({
            ...event, ...input,
            when: input.when ? new Date(input.when) : event.when
        })
    }

10. Data Serialization

10.1. Interceptors and Serialization

10.2. Serializing Data

import {
  Controller,
  Get,
  Post,
  ForbiddenException,
  Delete,
  Patch,
  Param,
  Body,
  HttpCode,
  ParseIntPipe,
  ValidationPipe,
  Logger,
  NotFoundException,
  Query,
  UsePipes,
  UseGuards,
  SerializeOptions,
  UseInterceptors,
  ClassSerializerInterceptor,
} from "@nestjs/common";
import { CreateEventDto } from "./input/create-event.dto";
import { UpdateEventDto } from "./input/update-event.dto";
import { Event } from "./event.entity";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm/repository/Repository";
import { EventsService } from "./event.service";
import { ListEvents } from "./input/list.event";
import { CurrentUser } from "src/auth/current-user.decorate";
import { User } from "src/auth/user.entity";
import { AuthGuardJwt } from "src/auth/auth-guard.jwt";
@Controller("events")
@SerializeOptions({
  strategy: "excludeAll", // 序列化时排除所有,res为空
})
export class EventsController {
  // ...
  @Get(":id")
  @UseInterceptors(ClassSerializerInterceptor)
  async findOne(@Param("id", ParseIntPipe) id: number) {
    const event = await this.eventsService.getEvent(id);

    if (!event) {
      throw new NotFoundException();
    }
    return event;
  }
  // ...
}


// user.entity.ts
import {
  Column,
  Entity,
  JoinColumn,
  OneToMany,
  OneToOne,
  PrimaryGeneratedColumn,
} from "typeorm";
import { Profile } from "./profile.entity";
import { Event } from "src/events/event.entity";
import { Expose } from "class-transformer";
@Entity()
export class User {
  @PrimaryGeneratedColumn()
  @Expose()
  id: number;

  @Column({ unique: true })
  @Expose()
  username: string;

  @Column()
  password: string;

  @Column({ unique: true })
  @Expose()
  email: string;

  @Column()
  @Expose()
  firstName: string;

  @Column()
  @Expose()
  lastName: string;

  @OneToOne(() => Profile)
  @JoinColumn()
  @Expose()
  profile: Profile;

  @OneToMany(() => Event, (event) => event.organizer)
  @Expose()
  organized: Event[];
}
// attendee.entity.ts
import {
  Column,
  Entity,
  JoinColumn,
  ManyToOne,
  PrimaryGeneratedColumn,
} from "typeorm";
import { Event } from "./event.entity";
import { Expose } from "class-transformer";

// 参会意向
export enum AttendeeAnswerEnum {
  Accepted = 1,
  Maybe,
  Rejected,
}

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

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

  @ManyToOne(() => Event, (event) => event.attendees, {
    nullable: false,
  })
  // @JoinColumn({
  //     name: 'event_id',
  //     referencedColumnName: 'secondary'
  // })
  @JoinColumn()
  event: Event;

  @Column("enum", {
    enum: AttendeeAnswerEnum,
    default: AttendeeAnswerEnum.Accepted,
  })
  @Expose()
  answer: AttendeeAnswerEnum;
}
// event.entity.ts
import {
  Column,
  Entity,
  JoinColumn,
  ManyToOne,
  OneToMany,
  PrimaryGeneratedColumn,
} from "typeorm";
import { Attendee } from "./attendee.entity";
import { User } from "src/auth/user.entity";
import { Expose } from "class-transformer";

// name: 表名
// @Entity('event', { name: 'event' })
@Entity() // 改变name不会删旧表,而是会创建新表
export class Event {
  // @PrimaryGeneratedColumn('uuid')
  // @PrimaryGeneratedColumn('rowid')
  // @PrimaryColumn() // 如果是复合主键,就在每个主键上加
  @PrimaryGeneratedColumn() // 自动生成
  @Expose() // 暴露,序列化可以返回
  id: number;

  @Column({ length: 100 })
  @Expose()
  name: string;

  @Column()
  @Expose()
  description: string;

  // name指定列名
  // @Column({ name: 'when_date' })
  @Column()
  @Expose()
  when: Date;

  @Column()
  @Expose()
  address: string;

  @OneToMany(() => Attendee, (attendee) => attendee.event, {
    // eager: true, // 联表查询
    cascade: true,
  })
  @Expose()
  attendees: Attendee[];

  @ManyToOne(() => User, (user) => user.organized)
  @JoinColumn({ name: "organizerId" })
  @Expose()
  organizer: User;
  @Column({ nullable: true })
  organizerId: number;

  @Expose()
  attendeeCount?: number; // 不会保存到数据库

  @Expose()
  attendeeRejected?: number;
  @Expose()
  attendeeMaybe?: number;
  @Expose()
  attendeeAccepted?: number;
}

auth.controller.tsf
auth.controller.tsf

验证登录状态,可以看到,没有返回password
验证登录状态,可以看到,没有返回password

!!!不知道为啥 findAll 为空{},findAll 查询结果正常,但是返回序列化出错?

10.3. Serializing Nested Objects

PaginationResult 的结果被 ts 编译,但是返回的不是 js 对象,不会被序列化处理,应该把它变成 js 对象

// ...
export class PaginationResult<T> {
  // Partial: 使 中的所有属性可选
  constructor(partial: Partial<PaginationResult<T>>) {
    // 将所有可枚举自身属性的值从一个或多个源对象复制到 目标对象。返回目标对象
    Object.assign(this, partial);
  }
  @Expose()
  first: number;
  @Expose()
  last: number;
  @Expose()
  limit: number;
  @Expose()
  total?: number;
  @Expose()
  data: T[];
}

export async function paginate<T>(
  qb: SelectQueryBuilder<T>,
  options: PaginateOptions = {
    limit: 10,
    currentPage: 1,
  }
): Promise<PaginationResult<T>> {
  const offset = (options.currentPage - 1) * options.limit;
  const data = await qb.limit(options.limit).offset(offset).getMany();
  return new PaginationResult({
    first: offset + 1,
    last: offset + data.length,
    limit: options.limit,
    total: options.total ? await qb.getCount() : null,
    data,
  });
}

破案了,要把 PaginationResult 改为 class,能被序列化,findall 才可以返回

11. (Practical) Building Full Events API

11.1. (Practical) Building Full Events API


11.2. Relations Between Entities

import {
  Column,
  Entity,
  JoinColumn,
  OneToMany,
  OneToOne,
  PrimaryGeneratedColumn,
} from "typeorm";
import { Profile } from "./profile.entity";
import { Event } from "src/events/event.entity";
import { Expose } from "class-transformer";
import { Attendee } from "src/events/attendee.entity";
@Entity()
export class User {
  // ...

  @OneToMany(() => Event, (event) => event.organizer)
  @Expose()
  organized: Event[];

  @OneToMany(() => Attendee, (attendee) => attendee.user)
  @Expose()
  attended: Attendee[];
}
import {
  Column,
  Entity,
  JoinColumn,
  ManyToOne,
  PrimaryGeneratedColumn,
} from "typeorm";
import { Event } from "./event.entity";
import { Expose } from "class-transformer";
import { User } from "src/auth/user.entity";

@Entity()
export class Attendee {
  //...

  @ManyToOne(() => Event, (event) => event.attendees, {
    nullable: true,
    onDelete: "CASCADE",
  })
  @JoinColumn()
  event: Event;

  @Column()
  eventId: number;

  @ManyToOne(() => User, (user) => user.attended)
  @Expose()
  user: User;

  @Column({ nullable: true })
  userId: number;
}
import {
  Column,
  Entity,
  JoinColumn,
  ManyToOne,
  OneToMany,
  PrimaryGeneratedColumn,
} from "typeorm";
import { Attendee } from "./attendee.entity";
import { User } from "src/auth/user.entity";
import { Expose } from "class-transformer";
import { PaginationResult } from "src/pagination/paginator";

export class Event {
  // ...
}
// 定义返回类型
export type PaginatedEvents = PaginationResult<Event>;

11.3. Getting Event Attendees

// attendee.service.ts
import { InjectRepository } from "@nestjs/typeorm";
import { Attendee } from "./attendee.entity";
import { Repository } from "typeorm";

export class AttendeesService {
  constructor(
    @InjectRepository(Attendee)
    private readonly attendeeRepository: Repository<Attendee>
  ) {}

  public async findByEventId(eventId: number): Promise<Attendee[]> {
    return await this.attendeeRepository.find({
      where: {
        event: { id: eventId },
      },
    });
  }
}
// event-attendees.controller.ts
import {
  Controller,
  Get,
  UseInterceptors,
  Param,
  SerializeOptions,
  ClassSerializerInterceptor,
} from "@nestjs/common";
import { AttendeesService } from "./attendee.service";

@Controller("events/:eventId/attendees")
@SerializeOptions({ strategy: "excludeAll" })
export class EventAttendeesController {
  constructor(private readonly attendeesService: AttendeesService) {}

  @Get()
  @UseInterceptors(ClassSerializerInterceptor)
  async findAll(@Param("eventId") eventId: number) {
    return await this.attendeesService.findByEventId(eventId);
  }
}

11.4. Getting Events Organized by User

// event.service.ts
    public async getEventsOrganizedByUserIdPaginated(
        userId: number, paginateOptions: PaginateOptions
    ): Promise<PaginatedEvents> {
        return await paginate<Event>(
            this.getEventsOrganizedByUserIdQuery(userId),
            paginateOptions
        )
    }

    private getEventsOrganizedByUserIdQuery(
        userId: number
    ) {
        return this.getEventsBaseQuery()
            .where('e.organizerId = :userId', { userId })
    }
// event-organized-by-user.controller.ts
import {
  Controller,
  Param,
  Query,
  SerializeOptions,
  Get,
  UseInterceptors,
  ClassSerializerInterceptor,
} from "@nestjs/common";
import { EventsService } from "./event.service";

@Controller("events-organized-by-user/:userId")
@SerializeOptions({ strategy: "excludeAll" })
export class EventsOrganizedByUserController {
  constructor(private readonly eventsService: EventsService) {}

  @Get()
  @UseInterceptors(ClassSerializerInterceptor)
  async findAll(@Param("userId") userId: number, @Query("page") page = 1) {
    return await this.eventsService.getEventsOrganizedByUserIdPaginated(
      userId,
      { currentPage: page, limit: 5 }
    );
  }
}
// event.service.ts
@Injectable()
export class EventsService {
  // ...
  public async getEventsOrganizedByUserIdPaginated(
    userId: number,
    paginateOptions: PaginateOptions
  ): Promise<PaginatedEvents> {
    return await paginate<Event>(
      this.getEventsOrganizedByUserIdQuery(userId),
      paginateOptions
    );
  }

  private getEventsOrganizedByUserIdQuery(userId: number) {
    return this.getEventsBaseQuery().where("e.organizerId = :userId", {
      userId,
    });
  }
}

11.5. Current User Event Attendance - the Business Logic

  // event.service.ts
public async getEventsAttendedByUserIdPaginated(
        userId: number, paginateOptions: PaginateOptions
    ): Promise<PaginatedEvents> {
        return await paginate<Event>(
            this.getEventsAttendedByUserIdQuery(userId),
            paginateOptions
        )
    }

    private getEventsAttendedByUserIdQuery(
        userId: number
    ) {
        return this.getEventsBaseQuery()
            .leftJoinAndSelect('e.attendees', 'a')
            .where('a.userId = :userId', { userId })
    }
// attendee.service.ts
@Injectable()
export class AttendeesService {
  constructor(
    @InjectRepository(Attendee)
    private readonly attendeeRepository: Repository<Attendee>
  ) {}

  // ...

  public async findOneByEventIdAndUserId(
    eventId: number,
    userId: number
  ): Promise<Attendee | undefined> {
    return await this.attendeeRepository.findOne({
      where: {
        event: { id: eventId },
        user: { id: userId },
      },
    });
  }

  public async createOrUpdate(
    input: any,
    eventId: number,
    userId: number
  ): Promise<Attendee> {
    const attendee =
      (await this.findOneByEventIdAndUserId(eventId, userId)) ?? new Attendee();
    attendee.eventId = eventId;
    attendee.userId = userId;
    // rest of input

    return await this.attendeeRepository.save(attendee);
  }
}

11.6. Current User Event Attendance - the Controller

    public async createOrUpdate(
        input: CreateAttendeeDto, eventId: number, userId: number
    ): Promise<Attendee> {
        const attendee = await this.findOneByEventIdAndUserId(eventId, userId)
            ?? new Attendee()
        attendee.eventId = eventId
        attendee.userId = userId
        attendee.answer = input.answer

        return await this.attendeeRepository.save(attendee)
    }
// create-attendee.dto.ts
import { IsEnum } from "class-validator";
import { AttendeeAnswerEnum } from "../attendee.entity";

export class CreateAttendeeDto {
  @IsEnum(AttendeeAnswerEnum)
  answer: AttendeeAnswerEnum;
}
// create-user-event-attendance.controller.ts
import {
  Controller,
  Get,
  Param,
  ParseIntPipe,
  Body,
  Query,
  Put,
  UseGuards,
  SerializeOptions,
  UseInterceptors,
  ClassSerializerInterceptor,
  NotFoundException,
} from "@nestjs/common";
import { EventsService } from "./event.service";
import { AttendeesService } from "./attendee.service";
import { CreateAttendeeDto } from "./input/create-attendee.dto";
import { CurrentUser } from "src/auth/current-user.decorate";
import { User } from "src/auth/user.entity";
import { AuthGuardJwt } from "src/auth/auth-guard.jwt";

@Controller("events-attendance")
@SerializeOptions({ strategy: "excludeAll" })
export class CurrentUserEventAttendanceController {
  constructor(
    private readonly eventsService: EventsService,
    private readonly attendeesService: AttendeesService
  ) {}

  @Get()
  @UseGuards(AuthGuardJwt)
  @UseInterceptors(ClassSerializerInterceptor)
  async findAll(@CurrentUser() user: User, @Query("page") page = 1) {
    return await this.eventsService.getEventsAttendedByUserIdPaginated(
      user.id,
      { limit: 6, currentPage: page }
    );
  }

  @Get(":/eventId")
  @UseGuards(AuthGuardJwt)
  @UseInterceptors(ClassSerializerInterceptor)
  async findOne(
    @Param("eventId", ParseIntPipe) eventId: number,
    @CurrentUser() user: User
  ) {
    const attendee = await this.attendeesService.findOneByEventIdAndUserId(
      eventId,
      user.id
    );
    if (!attendee) {
      throw new NotFoundException();
    }
    return attendee;
  }

  @Put(":/eventId")
  @UseGuards(AuthGuardJwt)
  @UseInterceptors(ClassSerializerInterceptor)
  async createOrUpdate(
    @Param("eventId", ParseIntPipe) eventId: number,
    @Body() input: CreateAttendeeDto,
    @CurrentUser() user: User
  ) {
    return this.attendeesService.createOrUpdate(input, eventId, user.id);
  }
}

11.7. Events Refactoring





保证是int类型
保证是int类型

// 每个方法都有返回值类型定义
// event.service.ts
import { Injectable } from "@nestjs/common";
import { Logger } from "@nestjs/common/services";
import { InjectRepository } from "@nestjs/typeorm";
import { User } from "src/auth/user.entity";
import { PaginateOptions, paginate } from "src/pagination/paginator";
import { DeleteResult, Repository, SelectQueryBuilder } from "typeorm";
import { AttendeeAnswerEnum } from "./attendee.entity";
import { Event, PaginatedEvents } from "./event.entity";
import { CreateEventDto } from "./input/create-event.dto";
import { ListEvents, WhenEventFilter } from "./input/list.event";
import { UpdateEventDto } from "./input/update-event.dto";

@Injectable()
export class EventsService {
  private readonly logger = new Logger(EventsService.name);
  constructor(
    @InjectRepository(Event)
    private readonly eventsRepository: Repository<Event>
  ) {}

  private getEventsBaseQuery(): SelectQueryBuilder<Event> {
    return (
      this.eventsRepository
        .createQueryBuilder("e")
        // .select(['e.id', 'e.name'])
        .orderBy("e.id", "ASC")
    );
  }

  public getEventsWithAttendeeCountQuery(): SelectQueryBuilder<Event> {
    return this.getEventsBaseQuery() // 查询到所有events
      .loadRelationCountAndMap(
        // 给每个events的attendees注入值,值为关联的attendee的数量
        "e.attendeeCount",
        "e.attendees"
      )
      .loadRelationCountAndMap(
        "e.attendeeAccepted",
        "e.attendees",
        "attendee",
        (qb) =>
          qb.where("attendee.answer = :answer", {
            answer: AttendeeAnswerEnum.Accepted,
          })
      )
      .loadRelationCountAndMap(
        "e.attendeeRejected", // mapToProperty映射到哪个值
        "e.attendees", // relationName关系名称
        "attendee", // aliasName数据库名
        (qb) =>
          qb.where("attendee.answer = :answer", {
            answer: AttendeeAnswerEnum.Rejected,
          })
      )
      .loadRelationCountAndMap(
        "e.attendeeMaybe",
        "e.attendees",
        "attendee",
        (qb) =>
          qb.where("attendee.answer = :answer", {
            answer: AttendeeAnswerEnum.Maybe,
          })
      );
  }

  private getEventsWithAttendeeCountFilteredQuery(
    filter?: ListEvents
  ): SelectQueryBuilder<Event> {
    let query = this.getEventsWithAttendeeCountQuery();

    if (!filter) {
      return query;
    }

    if (filter.when) {
      if (filter.when == WhenEventFilter.Today) {
        query = query.andWhere(
          `e.when >= CURDATE() AND e.when <= CURDATE() + INTERVAL 1 DAY`
        );
      }
      if (filter.when == WhenEventFilter.Tommorow) {
        query = query.andWhere(
          `e.when >= CURDATE() + INTERVAL 1 DAY AND e.when <= CURDATE() + INTERVAL 2 DAY`
        );
      }
      if (filter.when == WhenEventFilter.ThisWeek) {
        query = query.andWhere("YEARWEEK(e.when, 1) = YEARWEEK(CURDATE(), 1)");
      }
      if (filter.when == WhenEventFilter.NextWeek) {
        query = query.andWhere(
          "YEARWEEK(e.when, 1) = YEARWEEK(CURDATE(), 1) + 1"
        );
      }
    }

    return query;
  }

  public async getEventsWithAttendeeCountFilteredPaginated(
    filter: ListEvents,
    paginateOptions: PaginateOptions
  ): Promise<PaginatedEvents> {
    return await paginate(
      await this.getEventsWithAttendeeCountFilteredQuery(filter),
      paginateOptions
    );
  }

  public async getEventWithAttendeeCount(
    id: number
  ): Promise<Event | undefined> {
    const query = this.getEventsWithAttendeeCountQuery().andWhere(
      "e.id = :id",
      { id }
    ); // 根据id筛选结果
    this.logger.debug(await query.getSql());
    return await query.getOne();
  }

  public async findOne(id: number): Promise<Event | undefined> {
    return await this.eventsRepository.findOne({ where: { id } });
  }

  public async createEvent(input: CreateEventDto, user: User): Promise<Event> {
    return await this.eventsRepository.save({
      ...input,
      organizer: user,
      when: new Date(input.when),
    });
  }

  public async updateEvent(
    event: Event,
    input: UpdateEventDto
  ): Promise<Event> {
    return await this.eventsRepository.save({
      ...event,
      ...input,
      when: input.when ? new Date(input.when) : event.when,
    });
  }

  public async deleteEvent(id: number): Promise<DeleteResult> {
    return await this.eventsRepository
      .createQueryBuilder("e")
      .delete()
      .where("id = :id", { id })
      .execute();
  }

  public async getEventsOrganizedByUserIdPaginated(
    userId: number,
    paginateOptions: PaginateOptions
  ): Promise<PaginatedEvents> {
    return await paginate<Event>(
      this.getEventsOrganizedByUserIdQuery(userId),
      paginateOptions
    );
  }

  private getEventsOrganizedByUserIdQuery(
    userId: number
  ): SelectQueryBuilder<Event> {
    return this.getEventsBaseQuery().where("e.organizerId = :userId", {
      userId,
    });
  }

  public async getEventsAttendedByUserIdPaginated(
    userId: number,
    paginateOptions: PaginateOptions
  ): Promise<PaginatedEvents> {
    return await paginate<Event>(
      this.getEventsAttendedByUserIdQuery(userId),
      paginateOptions
    );
  }

  private getEventsAttendedByUserIdQuery(
    userId: number
  ): SelectQueryBuilder<Event> {
    return this.getEventsBaseQuery()
      .leftJoinAndSelect("e.attendees", "a")
      .where("a.userId = :userId", { userId });
  }
}

nestjs 可能出现循环依赖,写代码时要注意

如果返回的是普通对象(没有 constructer),不会被序列化,要转为 js 对象

@Entity()
export class Event {
  constructor(parial?: Partial<Event>) {
    Object.assign(this, parial);
  }

  // ...
}
// 定义返回类型
export type PaginatedEvents = PaginationResult<Event>;

定义默认值需要用到 Default…pipe

12. Testing

12.1. Manual Testing with Postman

12.2. Introduction to Automated Testing

12.3. Introduction to Jest

npm i --save-dev @nestjs/testing

安装后有的jest配置
安装后有的jest配置

// test.spec.ts
test("test is null", () => {
  const n = null;

  expect(n).toBeNull();
});
npm run test
# or
yarn test


13. Unit Testing

13.1. Basic Unit Test and Code Coverage

测试时,要把 src/目录换成相对路径,不然找不到

// event.entity.ts
import { Event } from "./event.entity";

// test("Event should be initialized through constructor", () => {
//   const event = new Event({
//     name: "Interesting event",
//     description: "That was fun",
//   });

//   expect(event).toEqual({
//     name: "Interesting event",
//     description: "That was fun",
//   });
// });

import { Event } from "./event.entity";

test("Event should be initialized through constructor", () => {
  const event = new Event({
    name: "Interesting event",
    description: "That was fun",
  });

  expect(event).toEqual({
    name: "Interesting event",
    description: "That was fun",
    id: undefined,
    when: undefined,
    address: undefined,
    attendees: undefined,
    organizer: undefined,
    organizerId: undefined,
    event: undefined,
    attendeeCount: undefined,
    attendeeRejected: undefined,
    attendeeMaybe: undefined,
    attendeeAccepted: undefined,
  });
});
npm run test:cov

测试覆盖率
测试覆盖率

13.2. Test Grouping, Spies, Mocks, Setup and Teardown

// event.controller.spec.ts
import { Repository } from "typeorm";
import { EventsService } from "./event.service";
import { EventsController } from "./events.controller";
import { Event } from "./event.entity";
import { ListEvents } from "./input/list.event";
import { User } from "../auth/user.entity";
import { NotFoundException } from "@nestjs/common";

describe("EventsController", () => {
  let eventsService: EventsService;
  let eventsController: EventsController;
  let eventsRepository: Repository<Event>;
  beforeAll(() => console.log("this logged once"));
  beforeEach(() => {
    eventsService = new EventsService(eventsRepository);
    eventsController = new EventsController(eventsService);
  });
  // it === test
  it("should return a list of events", async () => {
    const result = {
      first: 1,
      last: 1,
      limit: 10,
      data: [],
    };

    // 模拟方法,并提供方法实现(不依赖/使用外部方法,只提供模拟调用的结果,查看功能是否可用)
    // eventsService.getEventsWithAttendeeCountFilteredPaginated
    //     = jest.fn().mockImplementation((): any => result)

    // 监视并模拟方法
    const spy = jest
      .spyOn(eventsService, "getEventsWithAttendeeCountFilteredPaginated")
      .mockImplementation((): any => result);

    expect(await eventsController.findAll(new ListEvents())).toEqual(result);
    // 只想调用一次
    expect(spy).toBeCalledTimes(1);
  });

  it("should not delete an event, when it's not found", async () => {
    const deleteSpy = jest.spyOn(eventsService, "deleteEvent");

    const findSpy = jest
      .spyOn(eventsService, "findOne")
      .mockImplementation((): any => undefined);

    try {
      await eventsController.remove(1, new User());
    } catch (error) {
      expect(error).toBeInstanceOf(NotFoundException);
    }

    // 找不到就不能删除
    expect(deleteSpy).toBeCalledTimes(0);
    expect(findSpy).toBeCalledTimes(1);
  });
});

Related Issues not found

Please contact @malred to initialize the comment

Error: Comments Not Initialized
Emoji | Preview
Code -1: Request has been terminated
Possible causes: the network is offline, Origin is not allowed by Access-Control-Allow-Origin, the page is being unloaded, etc.
Powered By Valine
v1.3.4
  目录
  1. 2. Introduction to NestJS
  2. 3. Rest API
  3. 4. Database Basics
  4. 5. Data Validation
  5. 6. Modules, Providers, Dependency Injection
  6. 7. Configuration, Logging, and Errors
  7. 8. Intermediate Database Concepts
  8. 9. Authentication, JWT, Authorization
  9. 10. Data Serialization
  10. 11. (Practical) Building Full Events API
  11. 12. Testing
  12. 13. Unit Testing
    1. 13.1. Basic Unit Test and Code Coverage
    2. 13.2. Test Grouping, Spies, Mocks, Setup and Teardown
访问我的GitHub
邮件联系我
QQ联系我: 2725953379
访问我的GitHub
邮件联系我
QQ联系我: 2725953379