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 run start:dev

默认端口3000

2.2. NestJS Project Structure




装饰器模式,注解
请求处理器
service
控制器的测试文件

3. Rest API

3.1. Controllers


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


3.2. Resource Controller

what is resource
生成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

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

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[];
}


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


    @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
验证登录状态,可以看到,没有返回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类型

// 每个方法都有返回值类型定义
// 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配置

// 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);
  });
});

  目录