WangShuXian6 / blog

FE-BLOG
https://wangshuxian6.github.io/blog/
MIT License
46 stars 10 forks source link

精通 NestJS – Node.js 框架 Master NestJS 9 - Node.js Framework #187

Open WangShuXian6 opened 9 months ago

WangShuXian6 commented 9 months ago

精通 NestJS – Node.js 框架 Master NestJS 9 - Node.js Framework

https://www.udemy.com/course/master-nestjs-the-javascript-nodejs-framework/

学习 Nest 9 和 Node、GraphQL、REST、单元测试、E2E 测试、Type ORM 3、使用 TypeScript 进行 API 开发等等!

开发健壮的 REST API 理解和创建 GraphQL API 单元测试和端到端测试 使用 Docker 的稳健开发工作流程 使用现代数据库抽象 (TypeORM) 了解模块、供应商和服务! 学习身份验证和授权(使用 Passport) 了解 JWT 令牌的工作原理 了解如何配置应用程序以及如何保留日志 了解查询生成器 – 了解如何有效地构建查询 了解如何验证和序列化数据 学习使用 Nest CLI 了解代码设计模式,例如存储库或服务

需要VSCode

VSCode插件: JavaScript and TypeScript Nightly JavaScript and TypeScript Nightly - Visual Studio Marketplace

TypeScript Importer https://marketplace.visualstudio.com/items?itemName=pmneo.tsimporter

postman https://www.postman.com/downloads/

项目源码 https://github.com/piotr-jura-udemy/master-nest-js

WangShuXian6 commented 9 months ago

NestJS 简介

安装

使用 Nest CLI

npm i -g @nestjs/cli

nest  --help

使用模板库

https://github.com/nestjs/typescript-starter

git clone https://github.com/nestjs/typescript-starter.git project
cd project
npm install
npm run start:dev

在windows上,本地开发,修改代码,热更新后,会出现3000端口被占用的错误。

需要结束node

taskkill /F /IM node.exe

创建新的nest项目

项目名为 nest-events-backend nest new nest-events-backend

启动开发服务器

cd nest-events-backend

npm run start:dev

通过 localhost:3000 访问后端服务 默认返回 hello world!

003 NestJS Project Structure NestJS 项目结构

image image

Nest应用程序由模块组成,默认情况下总是至少有一个主模块,称为应用程序模块。 该模块由控制器、服务、实体和其他较小的构建块组成。 模型被定义在他们自己的模块文件中,类装饰器模块被用来描述它们。

主入口函数 src\main.ts

bootstrap函数负责创建新的nest应用程序对象 并启动服务器,使其在指定的位置监听,默认情况下是3000。

Module

Module 模块是一个抽象的概念。包含一组功能。 模块是一个用模块装饰器注释的类。

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Controller

控制器定义API和端点

Service

服务是一个类,应用程序的业务逻辑放在其中, 业务逻辑不是直接连接到处理一个请求或发送一个响应中。

app.controller.spec.ts

控制器测试放在控制器旁边

WangShuXian6 commented 9 months ago

03 - Controllers, Routing, Requests 控制器、路由、请求

简介

路由基本上是定义了路径,URL,HTTP动词,状态码。

002 Controllers

控制器是一个带有控制器装饰器注释的类。 image 控制器的工作是使用特定的HTTP动词在你的应用程序中创建端点。 然后接受请求,将该请求的处理传递给你项目中的其他代码. 然后返回,将响应返回给客户。 它控制着我们处理请求的过程。

你需要告诉应用程序如何将一个路径转化为控制器和它们的动作。

/events 路径 方式1:@Controller('/events')

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller('/events')
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

方式2:

@Controller({
  path: '/events',
})

空路由表示根路由

@Controller() 空路由 / @Get() 空路由 /

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

getHello() 表示根路径的请求响应

访问 localhost:3000 返回 ·this.appService.getHello();· 即 ·Hello World!· image

路由

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }

  @Get('/bye')
  getbye(): string {
    return 'Bye';
  }
}

@Get('/bye')
get 请求localhost:3000/bye 返回 Bye

003 Resource Controller 围绕资源组织控制器和请求动作

image

创建事件资源控制器 EventsController

nest genarate controller nset g co

src/events.controller.ts

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

@Controller('/events')
export class EventsController {
  @Get()
  findAll() {}

  @Get()
  findOne() {}

  @Post()
  create() {}

  @Patch()
  update() {}

  @Delete()
  remove() {}
}

向 nest 注册控制器 EventsController

controllers: [AppController, EventsController]

src/app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { EventsController } from './events.controller';

@Module({
  imports: [],
  controllers: [AppController, EventsController],
  providers: [AppService],
})
export class AppModule {}

使用各请求动作访问 localhost:3000/events 都将响应 200

围绕着资源建立的控制器,尽量保持简短,并且最多拥有五个基本的操作, 如果你觉得有必要增加更多的操作,也许你只是需要一个新的资源。 用描述性的方式来调用你的操作是一个好主意。

资源并不总是指我们的数据库表,它可以是更抽象的东西。

004 Route Parameters 路由参数

这需要定义动态路由 image

通过参数装饰器 @Param 将参数传递给请求

单独获取 id 参数

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

如果参数过多,可以获取所有参数

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

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

@Controller('/events')
export class EventsController {
  @Get()
  findAll() {
    return 'all';
  }

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

  @Post()
  create() {}

  @Patch(':id')
  update(@Param('id') id) {}

  @Delete(':id')
  remove(@Param('id') id) {}
}

005 Request Body 请求体

json请求体通过@Body装饰器转换程js对象 input

  @Post()
  create(@Body() input) {
    // json请求体通过@Body转换程js对象 input
    return input;
  }

src\events.controller.ts

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

@Controller('/events')
export class EventsController {
  @Get()
  findAll() {
    return 'all';
  }

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

  @Post()
  create(@Body() input) {
    // json请求体通过@Body转换程js对象 input
    return input;
  }

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

  @Delete(':id')
  remove(@Param('id') id) {}
}

image

006 Responses and Status Codes 响应和状态码

响应

响应有两种情况

1-返回一个原始值,如字符串。数字或布尔,字面上的东西被返回 006 Responses-2x

2-返回一个数组或一个对象,它被序列化为json

src\events.controller.ts

 @Get()
  findAll() {
    return [
      { id: 1, name: 'First event' },
      { id: 2, name: 'Second event' },
    ];
  }
import {
  Controller,
  Delete,
  Get,
  Param,
  Patch,
  Post,
  Body,
} from '@nestjs/common';

@Controller('/events')
export class EventsController {
  @Get()
  findAll() {
    return [
      { id: 1, name: 'First event' },
      { id: 2, name: 'Second event' },
    ];
  }

  @Get(':id')
  findOne(@Param('id') id) {
    return { id: 1, name: 'First event' };
  }

  @Post()
  create(@Body() input) {
    // json请求体通过@Body转换程js对象 input
    return input;
  }

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

  @Delete(':id')
  remove(@Param('id') id) {}
}

image

响应 Content-Typeapplication/json; charset=utf-8 image

默认get成功的的响应状态码为200 get localhost:3000/events image

创建资源成功状态码为201 post localhost:3000/events

  @Post()
  create(@Body() input) {
    // json请求体通过@Body转换程js对象 input
    return input;
  }

image

更新资源成功状态码为200 patch localhost:3000/events/1 image

良好的rest API实践是返回刚刚创建或更新的结果。 删除该资源时,不应该返回被删除的资源或任何类型的消息说它被删除了。 最好的做法是不返回任何东西。 删除资源成功状态码为204 @HttpCode(204) src\events.controller.ts

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

@Controller('/events')
export class EventsController {
  @Get()
  findAll() {
    return [
      { id: 1, name: 'First event' },
      { id: 2, name: 'Second event' },
    ];
  }

  @Get(':id')
  findOne(@Param('id') id) {
    return { id: 1, name: 'First event' };
  }

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

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

  @Delete(':id')
  @HttpCode(204)
  remove(@Param('id') id) {}
}

image

状态码 006 Status-Codes-2x

007 Request Payload - Data Transfer Objects 请求携带的有效负载-数据传输对象 DTO

数据传输对象 DTO :定义请求体的属性和这个属性的类型 create-event.dto.ts

create-event.dto.ts

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

将DTO类型声明添加到创建动作的输入参数中

src\events.controller.ts

import { CreateEventDto } from './create-event.dto';

  @Post()
  create(@Body() input: CreateEventDto) {
    return input;
  }
import {
  Body,
  Controller,
  Delete,
  Get,
  HttpCode,
  Param,
  Patch,
  Post,
} from '@nestjs/common';
import { CreateEventDto } from './create-event.dto';

@Controller('/events')
export class EventsController {
  @Get()
  findAll() {
    return [
      { id: 1, name: 'First event' },
      { id: 2, name: 'Second event' },
    ];
  }

  @Get(':id')
  findOne(@Param('id') id) {
    return { id: 1, name: 'First event' };
  }

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

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

  @Delete(':id')
  @HttpCode(204)
  remove(@Param('id') id) {}
}

008 The Update Payload 更新请求的有效载荷

改变一个事件的一些属性,但不发送不需要改变的属性。

继承并将创建资源的DTO的属性改为可选类型

npm i -S @nestjs/mapped-types

src/update-event.dto.ts

import { PartialType } from '@nestjs/mapped-types';
import { CreateEventDto } from './create-event.dto';

export class UpdateEventDto extends PartialType(CreateEventDto) {}

src/events.controller.ts

  @Patch(':id')
  update(@Param('id') id, @Body() input: UpdateEventDto) {
    return input;
  }
import {
  Body,
  Controller,
  Delete,
  Get,
  HttpCode,
  Param,
  Patch,
  Post,
} from '@nestjs/common';
import { CreateEventDto } from './create-event.dto';
import { UpdateEventDto } from './update-event.dto';

@Controller('/events')
export class EventsController {
  @Get()
  findAll() {
    return [
      { id: 1, name: 'First event' },
      { id: 2, name: 'Second event' },
    ];
  }

  @Get(':id')
  findOne(@Param('id') id) {
    return { id: 1, name: 'First event' };
  }

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

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

  @Delete(':id')
  @HttpCode(204)
  remove(@Param('id') id) {}
}

009 A Working API Example api示例

完整的事件控制器,它能实际工作的API,可以返回事件和创建、编辑和删除资源。

实体类

src/event.entity.ts

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

控制器

src/events.controller.ts

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

@Controller('/events')
export class EventsController {
  private events: Event[] = [];

  @Get()
  findAll() {
    return this.events;
  }

  @Get(':id')
  findOne(@Param('id') id) {
    const event = this.events.find((event) => event.id === parseInt(id));

    return event;
  }

  @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) {
    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));
  }
}
WangShuXian6 commented 9 months ago

04 - Database Basics

001 Database Basics - Section Introduction 数据库简介

002 Adding Docker to the Stack

002 Docker-2x

003 Running the Database with Docker Compose

docker-compose.yml

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:
    #将主机的端口3307映射到容器的端口3306
      - 3307: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 up -d

adminer 数据库管理平台

打开 localhost:8080 创建mysql类型数据库 默认 localhost

系统:MySQL 服务器: 127.0.0.1:3307 用户名:root 密码:example 数据库:留空

服务器为 hostname[:port] image

adminer 存在bug,在windows上测试无法使用,无法连接任意种类的数据库。

可以使用 navicat 连接数据库。

连接数据库

mysql

服务器:localhost:3307 用户名:root 密码:example image

PostgreSQL 服务器:localhost:5432 初始数据库:postgres 用户名:postgres 密码:example image

创建mysql数据库 nest-events

字符集:utf8mb4 image

创建PostgreSQL数据库 nest-events

编码:UTF8

004 Introduction to ORMs ORM 简介

004 TypeORM-2x

ORM代表对象关系映射,它背后的概念是,在面向对象的编程语言中,可以用对象来代表数据库表。 这是一个存在于所有编程语言中的共同概念,它使数据库的工作变得更加容易。 因为你在大多数时候不必手动编写数据库查询。 只在需要非常高效的查询时才手动编写查询语句。

每个Entity 实体类 映射到数据库的一个表。

Repository 存储库是一种编程模式,它为特定的实体提供数据访问,你将使用存储库来加载和存储数据库中的数据。

Query Builder 查询生成器用于构造数据库查询,通过调用特定的方法,以面向对象的方式,创建可重复使用的查询。

006 Connecting to the Database 使用typeorm连接到数据库

npm i -S @nestjs/typeorm typeorm mysql

app 使用 typeorm 模块并传递数据库连接参数

src/app.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { EventsController } from './events.controller';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: '127.0.0.1', //在外部运行该程序时使用'127.0.0.1',在容器内运行程序时为 mysql 容器名
      port: 3307,
      username: 'root',
      password: 'example',
      database: 'nest-events',
    }),
  ],
  controllers: [AppController, EventsController],
  providers: [AppService],
})
export class AppModule {}

TypeOrmModule 模块初始化成功,表示已连接到数据库

[Nest] 12164  - 2024/02/21 20:47:57     LOG [InstanceLoader] TypeOrmCoreModule dependencies initialized +10ms

008 The Entity (Primary Key & Columns) 实体(主键和列)

定义实体 @Entity()

@Entity() 会依据类名生成默认的表名

自定义表名: @Entity('event')

@Entity('event', { name: 'event' })

每个实体都必须有一个主键 @PrimaryGeneratedColumn() 自动生成id 在MySQL中是自增整数 ,在postcrisis中是serial,在Oracle中是sequence。 image @PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn("rowid")

  @PrimaryGeneratedColumn()
  id: number;

或者使用已存在的id列作为主键,例如信用卡号,身份证号,手机号 @PrimaryColumn()

或者使用多个@PrimaryColumn() 创建复合主键

src/event.entity.ts

  @PrimaryColumn()
  id: number;

  @PrimaryColumn()
  name: string;

其他列必须使用列装饰器 @Column() 默认情况下,会尝试根据属性类型来猜测列的正确类型,也可以明确指定类型。 但它不会是一个抽象的类型,如数字或字符串。 它必须是数据库引擎的特定类型。 @Column('varchar')

不使用 使用列装饰器 @Column() 的属性将不会存储到数据库中。例如特殊属性。

src/event.entity.ts

import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Event {
  // 每个实体都必须有一个主键
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column()
  description: string;

  @Column()
  when: Date;

  @Column()
  address: string;
}

导入TypeOrmModule模块时,配置实体类

src/app.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { Event } from './event.entity';
import { EventsController } from './events.controller';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: '127.0.0.1', //在外部运行该程序时使用'127.0.0.1',在容器内运行程序时为 mysql 容器名
      port: 3307,
      username: 'root',
      password: 'example',
      database: 'nest-events',
      entities: [Event],
      synchronize: true,
    }),
  ],
  controllers: [AppController, EventsController],
  providers: [AppService],
})
export class AppModule {}

synchronize: true 同步模式,true表示将使用定义的实体自动更新数据库的模式,主要用于本地开发。

如果列的名称或属性更新,数据库的列将直接更新。保留原数据。

如果表名更新,数据库将创建新的表,原表将保留。

009 Repository Pattern 存储库模式

009 Repository-2x 实体Entity代表数据库中的行 Repository 存储库 代表数据库中的表,管理表的所有实体。 TypeOrmModule 使用泛型存储库管理表的所有实体的增删改查。

TypeOrmModule 使用特殊存储库定制更复杂的通用查询。

011 Repository in Practice 存储库实战

app 中导入 实体类Event

src/app.module.ts

TypeOrmModule.forFeature([Event])
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { Event } from './event.entity';
import { EventsController } from './events.controller';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: '127.0.0.1', //在外部运行该程序时使用'127.0.0.1',在容器内运行程序时为 mysql 容器名
      port: 3307,
      username: 'root',
      password: 'example',
      database: 'nest-events',
      entities: [Event],
      synchronize: true,
    }),
    TypeOrmModule.forFeature([Event]),
  ],
  controllers: [AppController, EventsController],
  providers: [AppService],
})
export class AppModule {}

通过依赖注入将实体类Event注入到当前类中作为Event存储类使用

所有的数据库操作都需要使用 repository 操作,应用到数据库中。

  constructor(
    // 通过依赖注入将实体类Event注入到当前类中作为Event存储类使用
    @InjectRepository(Event)
    private readonly repository: Repository<Event>,
  ) {}

src/events.controller.ts

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

@Controller('/events')
export class EventsController {
  constructor(
    // 通过依赖注入将实体类Event注入到当前类中作为Event存储类使用
    @InjectRepository(Event)
    private readonly repository: Repository<Event>,
  ) {}

  @Get()
  async findAll() {
    return await this.repository.find();
  }

  @Get(':id')
  async findOne(@Param('id') id) {
    return await this.repository.findOne({
      where: {
        id,
      },
    });
  }

  @Post()
  async create(@Body() input: CreateEventDto) {
    return await this.repository.save({
      ...input,
      when: new Date(input.when),
    });
  }

  @Patch(':id')
  async update(@Param('id') id, @Body() input: UpdateEventDto) {
    const event = await this.repository.findOne({
      where: {
        id,
      },
    });

    return await this.repository.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.repository.findOne({
      where: {
        id,
      },
    });
    await this.repository.remove(event);
  }
}

012 Repository Querying Criteria and Options 存储库查询条件和选项

在 nest-events 数据库的 event 表中插入数据用以测试

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');

image image

src\events.controller.ts

import { Like, MoreThan, Repository } from 'typeorm';

  @Get('/practice')
  async practice() {
    return await this.repository.find({
      select: ['id', 'when'],
      where: [
        {
          id: MoreThan(3),
          when: MoreThan(new Date('2021-02-12T13:00:00')),
        },
        {
          description: Like('%meet%'),
        },
      ],
      take: 2,
      order: {
        id: 'DESC',
      },
    });
  }

get localhost:3000/events/practice image

WangShuXian6 commented 9 months ago

05 - Data Validation 数据验证

https://docs.nestjs.com/pipes

001 Data Validation - Section Introduction 简介

Nest中的验证器管道是一组预定义的验证器。 每个验证器都是检查一件事的函数。 它可能会检查一个电话号码是否正确,它可能会检查一些文本的长度,诸如此类。

002 Introduction to Pipes 管道

002 Pipes-2x

管道对数据进行验证,转换,等等。

ParseIntPipe 将数据格式化为整数

ParseIntPipe 将请求传入的参数字符串 id 格式化为整数类型 可以使用都好添加多个管道,可以使用 new ParseIntPipe() 为管道添加参数 src\events.controller.ts

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

  @Get(':id')
  async findOne(@Param('id', ParseIntPipe) id) {
    return await this.repository.findOne({
      where: {
        id,
      },
    });
  }

使用 ParseIntPipe 管道后,可以将id安全的声明为整数

  @Get(':id')
  async findOne(@Param('id', ParseIntPipe) id: number) {
    return await this.repository.findOne({
      where: {
        id,
      },
    });
  }

003 Input Validation 验证输入

安装验证器和转换器

npm i -S class-validator class-transformer

创建控制器单独使用 @Body(ValidationPipe) 验证请求体

src/events.controller.ts

import {
  ValidationPipe,
} from '@nestjs/common';

  @Post()
  async create(@Body(ValidationPipe) input: CreateEventDto) {
    return await this.repository.save({
      ...input,
      when: new Date(input.when),
    });
  }

CreateEventDto DTO中对每个请求参数定义验证规则

src/create-event.dto.ts

import { IsDateString, IsString, Length } 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)
  address: string;
}

app 全局启用验证器

这将对所有控制器进行验证,如果DTO定义了验证装饰器

src/main.ts

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

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // Remove line below to enable local ValidationPipe settings
  // 全局启用验证器
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

create 不再需要 ValidationPipe

 async create(@Body() input: CreateEventDto) {

004 Validation Groups and Options 验证分组和选项

验证分组 类似标签

验证分组需要禁用全局验证管道

删除 src\main.tsapp.useGlobalPipes(new ValidationPipe());

create-event.dto 为地址参数添加验证分组名 create,update

src/create-event.dto.ts

import { IsDateString, IsString, Length } 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;
}

控制器中 创建路由使用 带 create 标签的规则验证,更新使用带 update 标签的规则验证

src\events.controller.ts

创建路由使用 带 create 标签的规则验证

  @Post()
  async create(
    @Body(new ValidationPipe({ groups: ['create'] })) input: CreateEventDto,
  ) {
    return await this.repository.save({
      ...input,
      when: new Date(input.when),
    });
  }

更新使用带 update 标签的规则验证


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

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

使用管道装饰器在指定方法上或指定类上应用验证管道

src\events.controller.ts

import {
  UsePipes,
} from '@nestjs/common';

  @UsePipes(new ValidationPipe({ groups: ['create'] }))
  @Post()
  async create(@Body() input: CreateEventDto) {
    return await this.repository.save({
      ...input,
      when: new Date(input.when),
    });
  }
WangShuXian6 commented 9 months ago

06 - Modules, Providers, Dependency Injection 模块,提供者,依赖注入

001 Modules, Providers, Dependency Injection - Section Introduction

在抽象支付服务中,需要处理支付,然后发送支付结果邮件。

应将具体的支付服务例如微信支付,支付宝支付作为独立的服务注入到支付服务中。 具体的微信支付等只需要公开通用支付接口即可。例如Pay().由支付服务调用Pay。 抽象支付服务不需要直到各个具体支付服务的细节。

将发送邮件服务也作为作为独立的服务注入到支付服务中。

image

这是代码模块化,易于测试。

且具体支付服务可以mock。只测试核心支付服务。

模块

每个模块应当只处理特定的任务。 例如订单模块,电子邮件模块。 各模块有自己的数据库实体,控制器。 各模块输出特定的提供者供其他模块使用。

002 Introduction to Modules, Providers and Dependency Injection

002 Module-DI-Service-2x

模块的提供者将注入到其它类中使用。

003 Creating a Custom Module 创建自定义模块

创建事件模块

nest generate module events

src\events\events.module.ts

import { Module } from '@nestjs/common';

@Module({})
export class EventsModule {}

这回自动将 EventsModule 导入到app中 src\app.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { Event } from './event.entity';
import { EventsController } from './events.controller';
import { EventsModule } from './events/events.module';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: '127.0.0.1', //在外部运行该程序时使用'127.0.0.1',在容器内运行程序时为 mysql 容器名
      port: 3307,
      username: 'root',
      password: 'example',
      database: 'nest-events',
      entities: [Event],
      synchronize: true,
    }),
    TypeOrmModule.forFeature([Event]),
    EventsModule,
  ],
  controllers: [AppController, EventsController],
  providers: [AppService],
})
export class AppModule {}

整理文件,将属于 EventsModule 的文件放入 src\events\文件夹下

image

TypeOrmModule.forFeature([Event]), EventsController 从 app模块移入events模块

全局模块只需要连接数据库 src\app.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { Event } from './events/event.entity';
import { EventsModule } from './events/events.module';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: '127.0.0.1',
      port: 3307,
      username: 'root',
      password: 'example',
      database: 'nest-events',
      entities: [Event],
      synchronize: true,
    }),
    EventsModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

事件模块定义自己的数据库实体和控制器 src\events\events.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Event } from './event.entity';
import { EventsController } from './events.controller';

@Module({
  imports: [TypeOrmModule.forFeature([Event])],
  controllers: [EventsController],
})
export class EventsModule {}

004 Static Modules and Dynamic Modules 静态模块与动态模块

每个模块只在自己的上下文中可用。 [TypeOrmModule.forFeature([Event]) 只在 事件模块中可用 因为事件模块不需要其他功能的数据库实体。只需要自身相关的数据库实体。

@Module({
  imports: [TypeOrmModule.forFeature([Event])],
  controllers: [EventsController],
})
export class EventsModule {}

静态模块功能不变,不需要使用额外参数。例如事件模块。

@Module({
  imports: [
    EventsModule,
  ]
})
export class AppModule {}

动态模块需要使用参数来确定运行时功能,例如TypeOrmModule在运行时需要数据库连接参数. 这将创建一个模块实例。

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: '127.0.0.1',
      port: 3307,
      username: 'root',
      password: 'example',
      database: 'nest-events',
      entities: [Event],
      synchronize: true,
    }),
  ],

})
export class AppModule {}

005 Standard & Custom Providers 标准和 自定义提供者

标准提供者

使 AppService 可注入到app中

@Module({
  providers: [AppService],
})
export class AppModule {}

自定义类提供者


@Module({
  providers: [
    {
      provide: AppService,
      useClass: AppService,
    },
  ],
})
export class AppModule {}

根据情况动态使用不同的服务作为提供者 例如使用中文提供者,请求根路由将返回中文而非英文

src\app.chinese.service.ts

export class AppChineseService {
  constructor() {}

  getHello(): string {
    return `你好 世界! `;
  }
}

app中使用 app.chinese.service 作为提供者的实际类 src\app.module.ts

import { AppChineseService } from './app.chinese.service';

@Module({
  providers: [
    {
      provide: AppService,
      //useClass: AppService,
      useClass: AppChineseService,
    },
  ],
})
export class AppModule {}

控制器中调用 this.appService.getHello() 将返回中文版本响应文字

src\app.controller.ts

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    console.log('hello');
    return this.appService.getHello();
  }

  @Get('/bye')
  getbye(): string {
    return 'Bye';
  }
}

get localhost:3000 image

只要提供接口 getHello ,任意语言都可以替换提供者。

自定义值提供者

例如将程序名称和消息的值作为提供者

  providers: [
    {
      provide: AppService,
      //useClass: AppService,
      useClass: AppChineseService,
    },
    {
      provide: 'APP_NAME',
      useValue: 'Nest Events Backend!',
    },
  ],

src\app.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { Event } from './events/event.entity';
import { EventsModule } from './events/events.module';
import { AppChineseService } from './app.chinese.service';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: '127.0.0.1',
      port: 3307,
      username: 'root',
      password: 'example',
      database: 'nest-events',
      entities: [Event],
      synchronize: true,
    }),
    EventsModule,
  ],
  controllers: [AppController],
  providers: [
    {
      provide: AppService,
      //useClass: AppService,
      useClass: AppChineseService,
    },
    {
      provide: 'APP_NAME',
      useValue: 'Nest Events Backend!',
    },
  ],
})
export class AppModule {}

使用 APP_NAME 提供者的值 APP_NAME 的值注入给私有变量 name

src\app.chinese.service.ts

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

@Injectable()
export class AppChineseService {
  constructor(
    @Inject('APP_NAME')
    private readonly name: string,
  ) {}

  getHello(): string {
    return `你好 世界! from ${this.name}`;
  }
}

image

自定义工厂提供者

工厂函数提供者返回一个值

待注入的类 src\app.dummy.ts

export class AppDummy {
  public dummy(): string {
    return 'dummy';
  }
}

将AppDummy 作为提供者 src\app.module.ts

  providers: [
    {
      provide: 'MESSAGE',
      inject: [AppDummy], //将标准提供者AppDummy注入工厂函数
      useFactory: (app) => `${app.dummy()} Factory!`, //注入的提供者将成为工厂函数的参数 app = AppDummy
    },
    AppDummy, //成为标准提供者
  ],
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { Event } from './events/event.entity';
import { EventsModule } from './events/events.module';
import { AppChineseService } from './app.chinese.service';
import { AppDummy } from './app.dummy';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: '127.0.0.1',
      port: 3307,
      username: 'root',
      password: 'example',
      database: 'nest-events',
      entities: [Event],
      synchronize: true,
    }),
    EventsModule,
  ],
  controllers: [AppController],
  providers: [
    {
      provide: AppService,
      //useClass: AppService,
      useClass: AppChineseService,
    },
    {
      provide: 'APP_NAME',
      useValue: 'Nest Events Backend!',
    },
    {
      provide: 'MESSAGE',
      inject: [AppDummy], //将标准提供者AppDummy注入工厂函数
      useFactory: (app) => `${app.dummy()} Factory!`, //注入的提供者将成为工厂函数的参数 app = AppDummy
    },
    AppDummy, //成为标准提供者
  ],
})
export class AppModule {}

使用 工厂函数提供者 MESSAGE src\app.chinese.service.ts

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

@Injectable()
export class AppChineseService {
  constructor(
    @Inject('APP_NAME')
    private readonly name: string,
    @Inject('MESSAGE')
    private readonly message: string,
  ) {}

  getHello(): string {
    return `你好 世界! from ${this.name}, ${this.message}`;
  }
}

image

WangShuXian6 commented 9 months ago

07 - Configuration, Logging, and Errors 配置,日志

001 Application Config and Environments 应用程序配置和环境变量

001 Config-Env-2x

不同的环境将使用不同的数据库等等。 每个环境需要独立的配置。

安装配置模块

npm i -S @nestjs/config

app 模块中注册配置模块

仅在当前模块下有效 ConfigModule.forRoot() 将寻找根目录下 的 .env ,将其中的变量加载到进程中。

src/app.module.ts

import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot(),
  ],
})
export class AppModule {}
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { Event } from './events/event.entity';
import { EventsModule } from './events/events.module';
import { AppChineseService } from './app.chinese.service';
import { AppDummy } from './app.dummy';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot(),
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: '127.0.0.1',
      port: 3307,
      username: 'root',
      password: 'example',
      database: 'nest-events',
      entities: [Event],
      synchronize: true,
    }),
    EventsModule,
  ],
  controllers: [AppController],
  providers: [
    {
      provide: AppService,
      //useClass: AppService,
      useClass: AppChineseService,
    },
    {
      provide: 'APP_NAME',
      useValue: 'Nest Events Backend!',
    },
    {
      provide: 'MESSAGE',
      inject: [AppDummy], //将标准提供者AppDummy注入工厂函数
      useFactory: (app) => `${app.dummy()} Factory!`, //注入的提供者将成为工厂函数的参数 app = AppDummy
    },
    AppDummy, //成为标准提供者
  ],
})
export class AppModule {}

每个环境的shell都有自己环境变量,变量可以在node中通过 process.env 读取。

例如: console.log(process.env);

通过 .env 文件配置环境变量

不同的环境应当有自己独立的env文件

.env

DB_HOST=localhost
DB_PORT=3307
DB_USER=root
DB_PASSWORD=example
DB_NAME=nest-events

需要重启应用载入新环境变量env文件

读取 环境变量 中的 数据库地址

src\app.chinese.service.ts

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

@Injectable()
export class AppChineseService {
  constructor(
    @Inject('APP_NAME')
    private readonly name: string,
    @Inject('MESSAGE')
    private readonly message: string,
  ) {}

  getHello(): string {
    console.log(process.env.DB_HOST);
    return `你好 世界! from ${this.name}, ${this.message}`;
  }
}

使用环境变量中的数据库配置

src\app.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { Event } from './events/event.entity';
import { EventsModule } from './events/events.module';
import { AppChineseService } from './app.chinese.service';
import { AppDummy } from './app.dummy';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot(),
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: process.env.DB_HOST,
      port: Number(process.env.DB_PORT),
      username: process.env.DB_USER,
      password: process.env.DB_PASSWORD,
      database: process.env.DB_NAME,
      entities: [Event],
      synchronize: true,
    }),
    EventsModule,
  ],
  controllers: [AppController],
  providers: [
    {
      provide: AppService,
      //useClass: AppService,
      useClass: AppChineseService,
    },
    {
      provide: 'APP_NAME',
      useValue: 'Nest Events Backend!',
    },
    {
      provide: 'MESSAGE',
      inject: [AppDummy], //将标准提供者AppDummy注入工厂函数
      useFactory: (app) => `${app.dummy()} Factory!`, //注入的提供者将成为工厂函数的参数 app = AppDummy
    },
    AppDummy, //成为标准提供者
  ],
})
export class AppModule {}

002 Custom Configuration Files and Options 自定义配置文件和选项

使配置模块全局有效

src\app.module.ts

    ConfigModule.forRoot({
      isGlobal: true, //全局有效
      ignoreEnvFile: false, // 是否加载 env 环境配置文件 例如,如果需要通过docker配置环境变量则true
      envFilePath: '.env', //指定将要加载二点环境变量配置文件
    }),

如果模块众多,需要为每个模块分别加载配置文件,可以将各配置文件放到单独文件夹中

自定义配置导出工厂函数 registerAs

src/config/orm.config.ts

import { registerAs } from '@nestjs/config';
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { Event } from './../events/event.entity';

export default registerAs(
  'orm.config',
  (): TypeOrmModuleOptions => ({
    type: 'mysql',
    host: process.env.DB_HOST,
    port: Number(process.env.DB_PORT),
    username: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME,
    entities: [Event],
    synchronize: true,
  }),
);

生产环境中停用同步模式 synchronize src/config/orm.config.prod.ts

import { registerAs } from '@nestjs/config';
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { Event } from './../events/event.entity';

export default registerAs(
  'orm.config',
  (): TypeOrmModuleOptions => ({
    type: 'mysql',
    host: process.env.DB_HOST,
    port: Number(process.env.DB_PORT),
    username: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME,
    entities: [Event],
    synchronize: false,
  }),
);

.env

DB_HOST=localhost
DB_PORT=3307
DB_USER=root
DB_PASSWORD=example
DB_NAME=nest-events

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

加载环境配置

src\app.module.ts

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppDummy } from './app.dummy';
import { AppChineseService } from './app.chinese.service';
import { AppService } from './app.service';
import ormConfig from './config/orm.config';
import ormConfigProd from './config/orm.config.prod';
import { EventsModule } from './events/events.module';

@Module({
  imports: [
    // 使用 ormConfig 作为通用环境配置
    ConfigModule.forRoot({
      isGlobal: true,
      load: [ormConfig], //通过配置服务读取配置
      expandVariables: true, //启用可扩展环境变量 以支持字符串变量  ${APP_URL}
    }),
    // 异步加载工厂函数配置 分别用以数据库配置
    TypeOrmModule.forRootAsync({
      useFactory:
        process.env.NODE_ENV !== 'production' ? ormConfig : ormConfigProd,
    }),
    EventsModule,
  ],
  controllers: [AppController],
  providers: [
    {
      provide: AppService,
      useClass: AppChineseService,
    },
    {
      provide: 'APP_NAME',
      useValue: 'Nest Events Backend!',
    },
    {
      provide: 'MESSAGE',
      inject: [AppDummy],
      useFactory: (app) => `${app.dummy()} Factory!`,
    },
    AppDummy,
  ],
})
export class AppModule {}

003 Logging 日志

src\events\events.controller.ts

import {
  Logger
} from '@nestjs/common';

@Controller('/events')
export class EventsController {
  private readonly logger = new Logger(EventsController.name);

  constructor(
    // 通过依赖注入将实体类Event注入到当前类中作为Event存储类使用
    @InjectRepository(Event)
    private readonly repository: Repository<Event>,
  ) {}

  @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;
  }

}

image

配置启用的日志级别

程序只输出 'error', 'warn', 'debug' 级别的日志,普通级别日志将被忽略。 不需要再手动为生产环境删除日志代码

src/main.ts

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

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    logger: ['error', 'warn', 'debug'],
  });
  // Remove line below to enable local ValidationPipe settings
  // 全局启用验证器
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

004 Exception Filters 异常过滤器

004 Exception-Filters-2x

自定义 资源不存在异常

查找,更新,删除资源时,如果资源不存在。则返回异常

src/events/events.controller.ts

import {
  Body,
  Controller,
  Delete,
  Get,
  HttpCode,
  Param,
  Patch,
  Post,
  ParseIntPipe,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Like, MoreThan, Repository } from 'typeorm';
import { CreateEventDto } from './create-event.dto';
import { Event } from './event.entity';
import { UpdateEventDto } from './update-event.dto';

@Controller('/events')
export class EventsController {
  private readonly logger = new Logger(EventsController.name);

  constructor(
    // 通过依赖注入将实体类Event注入到当前类中作为Event存储类使用
    @InjectRepository(Event)
    private readonly repository: Repository<Event>,
  ) {}

  @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;
  }

  @Get('/practice')
  async practice() {
    return await this.repository.find({
      select: ['id', 'when'],
      where: [
        {
          id: MoreThan(3),
          when: MoreThan(new Date('2021-02-12T13:00:00')),
        },
        {
          description: Like('%meet%'),
        },
      ],
      take: 2,
      order: {
        id: 'DESC',
      },
    });
  }

  @Get(':id')
  async findOne(@Param('id', ParseIntPipe) id: number) {
    const event = await this.repository.findOne({
      where: {
        id,
      },
    });

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

    return event;
  }

  //@UsePipes(new ValidationPipe({ groups: ['create'] }))
  @Post()
  async create(@Body() input: CreateEventDto) {
    return await this.repository.save({
      ...input,
      when: new Date(input.when),
    });
  }

  @Patch(':id')
  async update(@Param('id') id, @Body() input: UpdateEventDto) {
    const event = await this.repository.findOne({
      where: {
        id,
      },
    });

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

    return await this.repository.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.repository.findOne({
      where: {
        id,
      },
    });

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

    await this.repository.remove(event);
  }
}

get localhost:3000/events/10

{
    "message": "Not Found",
    "statusCode": 404
}

image

在HTTP通信中,状态码是响应的一部分,用于表示服务器处理请求的结果。当你使用NestJS或任何其他后端框架时,浏览器或客户端发出GET请求后,服务器会返回一个响应,该响应包含状态码、响应头和响应体。

状态码200: 当请求成功,且服务器找到了请求的资源时,状态码通常是200(OK)。在这种情况下,服务器的响应体通常包含请求的资源内容,如JSON数据、HTML页面等。浏览器或客户端通常不在响应体中显示状态码,因为状态码是响应的一部分,而非响应体的内容。客户端或开发者工具(如Chrome的开发者工具)会在网络请求的概览中显示这个状态码,而不是在响应体中。

状态码404: 当服务器无法找到请求的资源时,会返回404(Not Found)状态码。在某些情况下,尤其是API开发中,开发者可能会选择在响应体中也包含状态码和错误消息,以提供更多关于错误的上下文信息。这就是为什么你可能会在响应体中看到一个statusCode字段,其中包含404状态码,以及可能的message字段来描述错误。

这种行为差异主要是由于开发者决定响应格式的方式。在成功的请求中(状态码200),通常期望的是直接获取到所请求的资源,而无需额外的状态码信息。但在错误处理的情况下(例如404错误),提供额外的错误信息(包括状态码)在响应体中对调试和错误处理是有帮助的。

在NestJS中,你可以通过异常过滤器(Exception Filters)来自定义错误响应的结构,包括决定是否在响应体中包含状态码。这提供了灵活性,允许开发者根据需要调整错误处理和成功响应的表现形式。

将状态码包含在响应体中,特别是在发生异常时,可能看起来有些重复,因为HTTP响应已经通过其状态行包含了状态码。但是,这种做法在API设计中还是有其优点和适用场景的,以下是一些理由:

增强的客户端解析能力:在响应体中包含状态码和可能的错误消息可以让客户端应用更容易地解析和处理错误。这对于那些可能不直接访问HTTP状态码的客户端尤其有用,例如某些前端框架或库。

一致性和明确性:在API响应中明确包含状态码和错误详情可以提供更一致和明确的错误处理机制。这对于维护一个大型的、由多个开发者或团队协作的API尤为重要。

自定义错误处理:在某些情况下,API可能需要返回更详细的错误信息,比如自定义错误代码或额外的错误上下文信息。将这些信息与HTTP状态码一起包含在响应体中,可以提供更丰富的错误反馈。

兼容性和灵活性:在某些复杂的系统或微服务架构中,中间件或代理可能会修改原始的HTTP状态码。在响应体中包含原始的状态码和错误信息可以确保终端用户获得准确的错误上下文。

尽管如此,最佳实践是保持API的响应尽可能清晰和简洁。如果决定在响应体中包含状态码和错误信息,应确保这样做能为API的使用者带来明确的好处。此外,应在API文档中清晰地说明这种设计选择,包括状态码的含义和可能的错误代码。

在NestJS中,你可以利用异常过滤器(Exception Filters)和拦截器(Interceptors)来灵活地处理异常和响应格式,以遵循你的API设计原则和最佳实践。例如,你可以创建一个自定义异常过滤器来格式化所有异常响应,包括状态码、错误消息和任何其他相关信息,以确保API的一致性和易用性。

WangShuXian6 commented 9 months ago

08 - Intermediate Database Concepts 中间数据库概念

001 Understanding Relations 理解实体之间的数据库关系

001 DB-Relations-2x

002 One To Many Relation 一对多关系

一个出席者对应一个会议事件 一个会议事件对应多个出席者

事件的出席者实体

出席者的 event 关联到事件的 event.attendees

@ManyToOne 默认为 event 生成列 eventId

可以手动指定列名 @JoinColumn({ name: 'event_id' })

默认将指向 event 表的 id 主键。 也可以手动指定指向 event 表的 其他 主键 referencedColumnName: 'id'

src/events/attendee.entity.ts

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

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

  @Column()
  name: string;

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

事件实体中添加出席者

事件的 attendees 关联到 出席者的 attendee.event src/events/event.entity.ts

import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { Attendee } from './attendee.entity';

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

  @Column()
  name: string;

  @Column()
  description: string;

  @Column()
  when: Date;

  @Column()
  address: string;

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

配置出席者实体

entities: [Event, Attendee],

src\config\orm.config.ts

import { registerAs } from '@nestjs/config';
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { Attendee } from 'src/events/attendee.entity';
import { Event } from './../events/event.entity';

export default registerAs(
  'orm.config',
  (): TypeOrmModuleOptions => ({
    type: 'mysql',
    host: process.env.DB_HOST,
    port: Number(process.env.DB_PORT),
    username: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME,
    entities: [Event, Attendee],
    synchronize: true,
  }),
);

src\config\orm.config.prod.ts

import { registerAs } from '@nestjs/config';
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { Event } from './../events/event.entity';
import { Attendee } from 'src/events/attendee.entity';

export default registerAs(
  'orm.config',
  (): TypeOrmModuleOptions => ({
    type: 'mysql',
    host: process.env.DB_HOST,
    port: Number(process.env.DB_PORT),
    username: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME,
    entities: [Event, Attendee],
    synchronize: false,
  }),
);

image

003 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);

同步加载当前实体的关联实体 eager: true

在加载当前实体后,将硬加载其关联的实体,性能消耗更大,不推荐。

src\events\event.entity.ts

import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { Attendee } from './attendee.entity';

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

  @Column()
  name: string;

  @Column()
  description: string;

  @Column()
  when: Date;

  @Column()
  address: string;

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

src\events\events.controller.ts

  @Get('practice2')
  async practice2() {
    return await this.repository.findOne({
      where: {
        id: 1,
      },
    });
  }

这将把事件的关联实体出席者attendees的全部数据返回

image

在当前实体配置了全局同步加载关联实体后,在具体的方法中取消同步加载关联实体 loadEagerRelations: false

src\events\events.controller.ts

  @Get('practice2')
  async practice2() {
    return await this.repository.findOne({
      where: {
        id: 1,
      },
      loadEagerRelations: false,
    });
  }

将不加载关联的 attendees 表和字段 image

当前实体全局 eager 不同步加载关联实体时,也可以在具体的方法中强制同步加载关联实体 relations: ['attendees']

src\events\event.entity.ts

import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { Attendee } from './attendee.entity';

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

  @Column()
  name: string;

  @Column()
  description: string;

  @Column()
  when: Date;

  @Column()
  address: string;

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

指定要加载的关联实体在当前表的字段 src\events\events.controller.ts

  @Get('practice2')
  async practice2() {
    return await this.repository.findOne({
      where: {
        id: 1,
      },
      //loadEagerRelations: false,
      relations: ['attendees'],
    });
  }

image

懒加载当前实体的关联实体

在不指定任何同步加载参数时,默认为懒加载关联实体。

image

004 Associating Related Entities 手动关联相关实体

将关联实体导入当前事件模块

src/events/events.module.ts

import { Attendee } from './attendee.entity';

@Module({
  imports: [TypeOrmModule.forFeature([Event, Attendee])],
  controllers: [EventsController],
})
export class EventsModule {}

需要将关联的实体存储库Repository注入到当前控制器

src/events/events.controller.ts

import { Attendee } from './attendee.entity';

  constructor(
    // 通过依赖注入将实体类Event注入到当前类中作为Event存储类使用
    @InjectRepository(Event)
    private readonly repository: Repository<Event>,
    @InjectRepository(Attendee)
    private readonly attendeeRepository: Repository<Attendee>,
  ) {}

当会议事件没有关联的出席者时

src\events\events.controller.ts

  @Get('practice2')
  async practice2() {
    const attendee = new Attendee();
    attendee.name = 'Potter';
    attendee.event = event;

    await this.attendeeRepository.save(attendee);

    return event;
  }

image image

如果已经知道会议事件的数据

src\events\events.controller.ts

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

    const attendee = new Attendee();
    attendee.name = 'Potter2';
    attendee.event = event;

    await this.attendeeRepository.save(attendee);

    return event;
  }

image image

使用关系级联选项 cascade: true

关系级联选项 cascade: true 启用后,在保存当前事件时,将同步保存关联的出席者数据。默认不会保存关联实体数据。 但这不是数据库级别的保存。 这只是 TypeOrm 在保存事件时,自动执行了保存出席者操作。

全部有效

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

只对插入和更新有效

  @OneToMany(() => Attendee, (attendee) => attendee.event, {
    cascade: ['insert', 'update'],
  })
  attendees: Attendee[];

src\events\event.entity.ts

import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { Attendee } from './attendee.entity';

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

  @Column()
  name: string;

  @Column()
  description: string;

  @Column()
  when: Date;

  @Column()
  address: string;

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

获取事件关联的原出席者数据,再为其加入新的出席者,然后保存事件,这将同步保存新的出席者。

e.event = event; // 级联将导致循环引用错误 所以此行需要删除,保存时出席者attendee将自动关联eventId

src\events\events.controller.ts


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

    const attendee = new Attendee();
    attendee.name = 'Potter3';
    // attendee.event = event; // 级联将导致循环引用错误

    event.attendees.push(attendee);

    await this.repository.save(event);

    return event;
  }

image image

级联默认对增删改查全部有效,需要非常小心关联操作

attendees表 中 eventId 可以为空时 nullable: true, 如果事件中将事件的出席者清空 event.attendees=[],将导致attendees表中所有关联该事件的出席者的eventId全部为NULL

image

005 Many To Many Relation 多对多关系

https://typeorm.bootcss.com/many-to-many-relations https://www.typeorm.org/many-to-many-relations

使用联合装饰器@JoinTable()为多对对关系创建中间表 @JoinTable() 只能用在多对多的实体其中之一上。

@JoinTable()是@ManyToMany关系所必需的。 你必须把@JoinTable放在关系的一个(拥有)方面 image

@JoinTable({name:'demo'}) 可以定制中间表名称

学校模块中 老师与学科的多对多关系

src\school\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 {}

拥有方启用级联 src\school\subject.entity.ts

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()
  teachers: Teacher[];
}

src\school\teacher.entity.ts

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

src\school\training.controller.ts

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,
      },
      //loadEagerRelations: false,
      relations: ['teachers'],
    });

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

    await this.subjectRepository.save(subject);
  }
}

app 导入学校模块

src\app.module.ts

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppDummy } from './app.dummy';
import { AppChineseService } from './app.chinese.service';
import { AppService } from './app.service';
import ormConfig from './config/orm.config';
import ormConfigProd from './config/orm.config.prod';
import { EventsModule } from './events/events.module';
import { SchoolModule } from './school/school.module';

@Module({
  imports: [
    // 使用 ormConfig 作为通用环境配置
    ConfigModule.forRoot({
      isGlobal: true,
      load: [ormConfig], //通过配置服务读取配置
      expandVariables: true, //启用可扩展环境变量 以支持字符串变量  ${APP_URL}
    }),
    // 异步加载工厂函数配置 分别用以数据库配置
    TypeOrmModule.forRootAsync({
      useFactory:
        process.env.NODE_ENV !== 'production' ? ormConfig : ormConfigProd,
    }),
    EventsModule,
    SchoolModule,
  ],
  controllers: [AppController],
  providers: [
    {
      provide: AppService,
      useClass: AppChineseService,
    },
    {
      provide: 'APP_NAME',
      useValue: 'Nest Events Backend!',
    },
    {
      provide: 'MESSAGE',
      inject: [AppDummy],
      useFactory: (app) => `${app.dummy()} Factory!`,
    },
    AppDummy,
  ],
})
export class AppModule {}

配置中 导入 老师,学科实体

src\config\orm.config.ts

import { registerAs } from '@nestjs/config';
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { Attendee } from 'src/events/attendee.entity';
import { Event } from './../events/event.entity';
import { Subject } from './../school/subject.entity';
import { Teacher } from './../school/teacher.entity';

export default registerAs(
  'orm.config',
  (): TypeOrmModuleOptions => ({
    type: 'mysql',
    host: process.env.DB_HOST,
    port: Number(process.env.DB_PORT),
    username: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME,
    entities: [Event, Attendee, Subject, Teacher],
    synchronize: true,
  }),
);

joinColumn 手动指定拥有方即学科与中间表的关联id

src\school\subject.entity.ts

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 表示当前实体为拥有方
  @JoinTable({
    joinColumn: {
      name: 'subjectId', //中间表的列名
      referencedColumnName: 'id', //当前表的列名 id 关联到中间表的 subjectId
    },
  })
  teachers: Teacher[];
}

inverseJoinColumn 手动指定拥有方关联的老师与中间表的关联id

src\school\subject.entity.ts

``

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 表示当前实体为拥有方
  @JoinTable({
    joinColumn: {
      name: 'subjectId', //中间表的列名
      referencedColumnName: 'id', //当前表的列名 id 关联到中间表的 subjectId
    },
    // inverseJoinColumn 对关联的另一个老师表手动指定中间表id
    inverseJoinColumn: {
      name: 'teacherId', //中间表的老师id列名
      referencedColumnName: 'id', //老师表的id 关联到中间表的 teacherId
    },
  })
  teachers: Teacher[];
}

中间表

image

post localhost:3000/school/create

创建一个学科,2个老师

subject 表 image

teacher 表 image

中间表subject_teachers_teacher image

006 Query Builder Introduction 查询生成器简介

事件自定义查询语句

src\events\events.controller.ts

  @Get('practice2')
  async practice2() {
    return await this.repository
      .createQueryBuilder('e')
      .select(['e.id', 'e.name'])
      .orderBy('e.id', 'ASC')
      .take(3)
      .getMany();
  }

image

创建事件服务,重复使用查询生成器

事件服务中存放业务逻辑

src/events/events.service.ts

import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Event } from './event.entity';

@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')
      .orderBy('e.id', 'DESC');
  }

  public async getEvent(id: number): Promise<Event | undefined> {
    const query = this.getEventsBaseQuery().andWhere('e.id = :id', { id });

    this.logger.debug(query.getSql()); //查询生成器 可获取实际sql语句

    return await query.getOne();
  }
}

将事件服务作为提供者

src\events\events.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Attendee } from './attendee.entity';
import { Event } from './event.entity';
import { EventsController } from './events.controller';
import { EventsService } from './events.service';

@Module({
  imports: [TypeOrmModule.forFeature([Event, Attendee])],
  controllers: [EventsController],
  providers: [EventsService],
})
export class EventsModule {}

将事件服务注入事件控制器

src\events\events.controller.ts

import { EventsService } from './events.service';

@Controller('/events')
export class EventsController {
  private readonly logger = new Logger(EventsController.name);

  constructor(
    // 通过依赖注入将实体类Event注入到当前类中作为Event存储类使用
    @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.repository.findOne({
    //   where: {
    //     id,
    //   },
    // });

    const event = await this.eventsService.getEvent(id);

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

    return event;
  }

}

image

实际sql

SELECT `e`.`id` AS `e_id`, `e`.`name` AS `e_name`, `e`.`description` AS `e_description`, `e`.`when` AS `e_when`, `e`.`address` AS `e_address` FROM `event` `e` WHERE `e`.`id` = ? ORDER BY `e`.`id` DESC

007 Joins And Aggregation with Query Builder 查询生成器的联接和聚合

查询事件数量

src/events/events.service.ts 方法1:getCount()

import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Event } from './event.entity';

@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')
      .orderBy('e.id', 'DESC');
  }

  public getEventsWithAttendeeCountQuery() {
    return this.getEventsBaseQuery().getCount();
  }

  public async getEvent(id: number): Promise<Event | undefined> {
    const query = this.getEventsBaseQuery().andWhere('e.id = :id', { id });

    this.logger.debug(query.getSql()); //查询生成器 可获取实际sql语句

    return await query.getOne();
  }
}

查询事件数量,同时也获取关联的出席者attendees数量 loadRelationCountAndMap

将 attendees 属性数量映射到事件实体的 attendeeCount 【attendeeCount 不存在于数据库中】

事件实体添加 attendeeCount 虚拟属性 出席者数量,不会保存到数据库

src/events/event.entity.ts

import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { Attendee } from './attendee.entity';

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

  @Column()
  name: string;

  @Column()
  description: string;

  @Column()
  when: Date;

  @Column()
  address: string;

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

  attendeeCount?: number;
  //attendeeRejected?: number;
  //attendeeMaybe?: number;
  //attendeeAccepted?: number;
}

事件服务查询事件数量,同时也获取关联的出席者attendees数量

src/events/events.service.ts

import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Event } from './event.entity';

@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')
      .orderBy('e.id', 'DESC');
  }

  public getEventsWithAttendeeCountQuery() {
    return this.getEventsBaseQuery().loadRelationCountAndMap(
      'e.attendeeCount',
      'e.attendees',
    );
  }

  public async getEvent(id: number): Promise<Event | undefined> {
    const query = this.getEventsWithAttendeeCountQuery().andWhere(
      'e.id = :id',
      { id },
    );

    this.logger.debug(query.getSql()); //查询生成器 可获取实际sql语句

    return await query.getOne();
  }
}

image

为出席者添加分类属性 answer 表示同意,也许,拒绝出席

src/events/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 {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

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

  @Column('enum', {
    enum: AttendeeAnswerEnum,
    default: AttendeeAnswerEnum.Accepted,
  })
  answer: AttendeeAnswerEnum;
}

为事件实体添加虚拟属性表示 同意,也许,拒绝出席的数量

src/events/event.entity.ts

import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { Attendee } from './attendee.entity';

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

  @Column()
  name: string;

  @Column()
  description: string;

  @Column()
  when: Date;

  @Column()
  address: string;

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

  attendeeCount?: number;
  attendeeRejected?: number;
  attendeeMaybe?: number;
  attendeeAccepted?: number;
}

事件服务中,查询同意,也许,拒绝 出席者的数量,为每类出席者再添加一个查询生成器

src\events\events.service.ts

import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AttendeeAnswerEnum } from './attendee.entity';
import { Event } from './event.entity';

@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')
      .orderBy('e.id', 'DESC');
  }

  public getEventsWithAttendeeCountQuery() {
    return this.getEventsBaseQuery()
      .loadRelationCountAndMap('e.attendeeCount', 'e.attendees')
      .loadRelationCountAndMap(
        'e.attendeeAccepted',
        'e.attendees',
        'attendee',
        (qb) =>
          qb.where('attendee.answer = :answer', {
            answer: AttendeeAnswerEnum.Accepted,
          }),
      )
      .loadRelationCountAndMap(
        'e.attendeeMaybe',
        'e.attendees',
        'attendee',
        (qb) =>
          qb.where('attendee.answer = :answer', {
            answer: AttendeeAnswerEnum.Maybe,
          }),
      )
      .loadRelationCountAndMap(
        'e.attendeeRejected',
        'e.attendees',
        'attendee',
        (qb) =>
          qb.where('attendee.answer = :answer', {
            answer: AttendeeAnswerEnum.Rejected,
          }),
      );
  }

  public async getEvent(id: number): Promise<Event | undefined> {
    const query = this.getEventsWithAttendeeCountQuery().andWhere(
      'e.id = :id',
      { id },
    );

    this.logger.debug(query.getSql());

    return await query.getOne();
  }
}

image

008 Filtering Data Using Query Builder 使用查询生成器筛选数据

根据事件发生时间过滤数据

将DTO文件归类到单独的input 文件夹中

src\events\input\create-event.dto.ts src\events\input\update-event.dto.ts

image

创建事件过滤器

src/events/input/list.events.ts

export class ListEvents {
  when?: WhenEventFilter = WhenEventFilter.All; //枚举类型 默认为全部事件
}

export enum WhenEventFilter {
  All = 1,
  Today,
  Tommorow,
  ThisWeek,
  NextWeek,
}

事件服务添加过滤查询 getEventsWithAttendeeCountFiltered

src\events\events.service.ts

import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AttendeeAnswerEnum } from './attendee.entity';
import { Event } from './event.entity';
import { ListEvents, WhenEventFilter } from './input/list.events';

@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')
      .orderBy('e.id', 'DESC');
  }

  public getEventsWithAttendeeCountQuery() {
    return this.getEventsBaseQuery()
      .loadRelationCountAndMap('e.attendeeCount', 'e.attendees')
      .loadRelationCountAndMap(
        'e.attendeeAccepted',
        'e.attendees',
        'attendee',
        (qb) =>
          qb.where('attendee.answer = :answer', {
            answer: AttendeeAnswerEnum.Accepted,
          }),
      )
      .loadRelationCountAndMap(
        'e.attendeeMaybe',
        'e.attendees',
        'attendee',
        (qb) =>
          qb.where('attendee.answer = :answer', {
            answer: AttendeeAnswerEnum.Maybe,
          }),
      )
      .loadRelationCountAndMap(
        'e.attendeeRejected',
        'e.attendees',
        'attendee',
        (qb) =>
          qb.where('attendee.answer = :answer', {
            answer: AttendeeAnswerEnum.Rejected,
          }),
      );
  }

  public async getEventsWithAttendeeCountFiltered(filter?: ListEvents) {
    let query = this.getEventsWithAttendeeCountQuery();

    if (!filter) {
      return 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();
  }

  public async getEvent(id: number): Promise<Event | undefined> {
    const query = this.getEventsWithAttendeeCountQuery().andWhere(
      'e.id = :id',
      { id },
    );

    this.logger.debug(query.getSql());

    return await query.getOne();
  }
}

异步方法getEventsWithAttendeeCountFiltered,它的作用是根据提供的过滤条件(filter)来获取事件及其参加者的数量。下面是对这个方法的详细解释:

方法结构 public async getEventsWithAttendeeCountFiltered(filter?: ListEvents):这是一个公共的异步方法,可接受一个可选的参数filter,其类型为之前定义的ListEvents类。这意味着filter可以包含一个when属性,用于指定筛选条件。 查询逻辑 初始化查询:

let query = this.getEventsWithAttendeeCountQuery();:首先,通过调用getEventsWithAttendeeCountQuery方法初始化查询。这个方法可能是在当前类中定义的,用于获取包含参加者数量的事件列表的基础查询。 无筛选条件时的快速返回:

if (!filter) { return query.getMany(); }:如果没有提供filter参数,即没有筛选条件,方法将直接执行查询并返回结果。getMany()可能是一个ORM方法,用于执行查询并获取多条记录。 应用筛选条件:

如果filter参数存在,并且其when属性有值,则根据when的值修改查询以应用相应的筛选条件。 filter.when == WhenEventFilter.Today:如果筛选条件是今天的事件,则通过andWhere方法添加SQL条件来筛选e.when字段(假设代表事件日期)在今天的事件。 filter.when == WhenEventFilter.Tommorow:如果筛选条件是明天的事件,类似地修改查询以筛选明天的事件。 filter.when == WhenEventFilter.ThisWeek:如果筛选条件是本周的事件,使用YEARWEEK函数来比较年和周,筛选本周内的事件。 filter.when == WhenEventFilter.NextWeek:如果筛选条件是下周的事件,同样使用YEARWEEK函数,并将当前周数加1来筛选下周的事件。 结果返回 最后,无论是否应用了筛选条件,都通过await query.getMany();执行最终的查询并返回结果。由于这是一个异步操作,使用await确保在返回结果前查询完成。 SQL和ORM 注意,代码中的SQL语句(如CURDATE()、INTERVAL、YEARWEEK())是针对特定SQL方言(可能是MySQL)编写的。确保这些函数在使用的数据库中是有效的。 andWhere方法看起来是用于构建查询的ORM(对象关系映射)方法,允许动态地向查询添加更多的条件。 这个方法的设计展示了如何在NestJS服务中结合使用TypeScript类和枚举,以及如何使用ORM方法来构建灵活的数据库查询。此外,通过可选参数和条件语句,这种设计还提供了高度的灵活性,使得可以根据不同的筛选条件重用同一查询逻辑。

query = query.andWhere('YEARWEEK(e.when, 1) = YEARWEEK(CURDATE(), 1)'); 是一个添加到SQL查询中的条件表达式,它使用YEARWEEK函数来筛选出在特定周的事件。下面是对这个表达式的详细解释:

YEARWEEK 函数 YEARWEEK(date, mode) 是一个SQL函数,常见于MySQL数据库,用于返回给定日期的年份和周数。这个函数可以接受两个参数: 第一个参数是日期值,用于计算其所属的年份和周数。 第二个参数是模式(mode),它是可选的,用于确定周的起始日和周的计算方式。在你的代码中,模式被设置为1。 参数 mode mode 参数决定了周是如何被计算的,特别是一周的起始日(通常是星期日或星期一)以及第一周是如何定义的。1 作为mode值通常意味着周是从星期一开始,且第一周是包含该年第一个星期四的那周。这是ISO周日期系统的一部分,它是许多国际标准(如ISO 8601)中使用的周计算方式。 表达式解释 YEARWEEK(e.when, 1) 计算e.when日期所在的年份和周数。这里e.when假设是数据库中某个表(例如事件表,这里用e表示)的一个字段,该字段存储了事件的日期。

YEARWEEK(CURDATE(), 1) 计算当前日期(CURDATE() 返回当前日期)所在的年份和周数。同样使用模式1,意味着采用ISO周日期系统。

YEARWEEK(e.when, 1) = YEARWEEK(CURDATE(), 1) 这个条件比较了事件日期和当前日期所在的年份和周数。如果它们在同一周内,这个条件就为真。因此,这个andWhere条件用于筛选出当前周内发生的所有事件。

使用场景 这种类型的查询在需要基于周来过滤或聚合数据时非常有用,比如在制作日历视图、报告或任何需要按周组织数据的场景中。使用YEARWEEK函数可以直接在数据库层面上完成这种复杂的日期逻辑,而不需要在应用程序代码中进行额外的日期处理,这有助于提高效率和性能。

事件控制器中添加过滤装饰器

src/events/events.controller.ts

  @Get()
  async findAll(@Query() filter: ListEvents) {
    this.logger.debug(filter);
    this.logger.log(`Hit the findAll route`);
    const events =
      await this.eventsService.getEventsWithAttendeeCountFiltered(filter);
    this.logger.debug(`Found ${events.length} events`);
    return events;
  }
import {
  Body,
  Controller,
  Delete,
  Get,
  HttpCode,
  Logger,
  NotFoundException,
  Param,
  ParseIntPipe,
  Patch,
  Post,
  Query,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Like, MoreThan, Repository } from 'typeorm';
import { Attendee } from './attendee.entity';
import { Event } from './event.entity';
import { EventsService } from './events.service';
import { CreateEventDto } from './input/create-event.dto';
import { ListEvents } from './input/list.events';
import { UpdateEventDto } from './input/update-event.dto';

@Controller('/events')
export class EventsController {
  private readonly logger = new Logger(EventsController.name);

  constructor(
    // 通过依赖注入将实体类Event注入到当前类中作为Event存储类使用
    @InjectRepository(Event)
    private readonly repository: Repository<Event>,
    @InjectRepository(Attendee)
    private readonly attendeeRepository: Repository<Attendee>,
    private readonly eventsService: EventsService,
  ) {}

  @Get()
  async findAll(@Query() filter: ListEvents) {
    this.logger.debug(filter);
    this.logger.log(`Hit the findAll route`);
    const events =
      await this.eventsService.getEventsWithAttendeeCountFiltered(filter);
    this.logger.debug(`Found ${events.length} events`);
    return events;
  }

  @Get('/practice')
  async practice() {
    return await this.repository.find({
      select: ['id', 'when'],
      where: [
        {
          id: MoreThan(3),
          when: MoreThan(new Date('2021-02-12T13:00:00')),
        },
        {
          description: Like('%meet%'),
        },
      ],
      take: 2,
      order: {
        id: 'DESC',
      },
    });
  }

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

    // const attendee = new Attendee();
    // attendee.name = 'Potter3';
    // // attendee.event = event; // 级联将导致循环引用错误

    // event.attendees.push(attendee);

    // await this.repository.save(event);

    // return event;

    return await this.repository
      .createQueryBuilder('e')
      .select(['e.id', 'e.name'])
      .orderBy('e.id', 'ASC')
      .take(3)
      .getMany();
  }

  @Get(':id')
  async findOne(@Param('id', ParseIntPipe) id: number) {
    // const event = await this.repository.findOne({
    //   where: {
    //     id,
    //   },
    // });

    const event = await this.eventsService.getEvent(id);

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

    return event;
  }

  //@UsePipes(new ValidationPipe({ groups: ['create'] }))
  @Post()
  async create(@Body() input: CreateEventDto) {
    return await this.repository.save({
      ...input,
      when: new Date(input.when),
    });
  }

  @Patch(':id')
  async update(@Param('id') id, @Body() input: UpdateEventDto) {
    const event = await this.repository.findOne({
      where: {
        id,
      },
    });

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

    return await this.repository.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.repository.findOne({
      where: {
        id,
      },
    });

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

    await this.repository.remove(event);
  }
}

image

009 Pagination Using Query Builder 使用查询生成器分页

通用分页

src/pagination/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,
  };
}

过滤器添加页码参数 page

src/events/input/list.events.ts

export class ListEvents {
  when?: WhenEventFilter = WhenEventFilter.All; //枚举类型 默认为全部事件
  page: number = 1;
}

export enum WhenEventFilter {
  All = 1,
  Today,
  Tommorow,
  ThisWeek,
  NextWeek,
}

事件服务增加分页逻辑

src/events/events.service.ts

import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { paginate, PaginateOptions } from 'src/pagination/paginator';
import { Repository } from 'typeorm';
import { AttendeeAnswerEnum } from './attendee.entity';
import { Event } from './event.entity';
import { ListEvents, WhenEventFilter } from './input/list.events';

@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')
      .orderBy('e.id', 'DESC');
  }

  public getEventsWithAttendeeCountQuery() {
    return this.getEventsBaseQuery()
      .loadRelationCountAndMap('e.attendeeCount', 'e.attendees')
      .loadRelationCountAndMap(
        'e.attendeeAccepted',
        'e.attendees',
        'attendee',
        (qb) =>
          qb.where('attendee.answer = :answer', {
            answer: AttendeeAnswerEnum.Accepted,
          }),
      )
      .loadRelationCountAndMap(
        'e.attendeeMaybe',
        'e.attendees',
        'attendee',
        (qb) =>
          qb.where('attendee.answer = :answer', {
            answer: AttendeeAnswerEnum.Maybe,
          }),
      )
      .loadRelationCountAndMap(
        'e.attendeeRejected',
        'e.attendees',
        'attendee',
        (qb) =>
          qb.where('attendee.answer = :answer', {
            answer: AttendeeAnswerEnum.Rejected,
          }),
      );
  }

  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 query;
  }

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

  public async getEvent(id: number): Promise<Event | undefined> {
    const query = this.getEventsWithAttendeeCountQuery().andWhere(
      'e.id = :id',
      { id },
    );

    this.logger.debug(query.getSql());

    return await query.getOne();
  }
}

控制器使用分页服务 getEventsWithAttendeeCountFilteredPaginated

使用 @UsePipes(new ValidationPipe({ transform: true })) 填充默认 filter.page

src/events/events.controller.ts

import {
  Body,
  Controller,
  Delete,
  Get,
  HttpCode,
  Logger,
  NotFoundException,
  Param,
  ParseIntPipe,
  Patch,
  Post,
  Query,
  UsePipes,
  ValidationPipe,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Like, MoreThan, Repository } from 'typeorm';
import { Attendee } from './attendee.entity';
import { Event } from './event.entity';
import { EventsService } from './events.service';
import { CreateEventDto } from './input/create-event.dto';
import { ListEvents } from './input/list.events';
import { UpdateEventDto } from './input/update-event.dto';

@Controller('/events')
export class EventsController {
  private readonly logger = new Logger(EventsController.name);

  constructor(
    // 通过依赖注入将实体类Event注入到当前类中作为Event存储类使用
    @InjectRepository(Event)
    private readonly repository: Repository<Event>,
    @InjectRepository(Attendee)
    private readonly attendeeRepository: Repository<Attendee>,
    private readonly eventsService: EventsService,
  ) {}

  @Get()
  @UsePipes(new ValidationPipe({ transform: true }))
  async findAll(@Query() filter: ListEvents) {
    const events =
      await this.eventsService.getEventsWithAttendeeCountFilteredPaginated(
        filter,
        {
          total: true,
          currentPage: filter.page,
          limit: 2,
        },
      );
    return events;
  }

  @Get('/practice')
  async practice() {
    return await this.repository.find({
      select: ['id', 'when'],
      where: [
        {
          id: MoreThan(3),
          when: MoreThan(new Date('2021-02-12T13:00:00')),
        },
        {
          description: Like('%meet%'),
        },
      ],
      take: 2,
      order: {
        id: 'DESC',
      },
    });
  }

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

    // const attendee = new Attendee();
    // attendee.name = 'Potter3';
    // // attendee.event = event; // 级联将导致循环引用错误

    // event.attendees.push(attendee);

    // await this.repository.save(event);

    // return event;

    return await this.repository
      .createQueryBuilder('e')
      .select(['e.id', 'e.name'])
      .orderBy('e.id', 'ASC')
      .take(3)
      .getMany();
  }

  @Get(':id')
  async findOne(@Param('id', ParseIntPipe) id: number) {
    // const event = await this.repository.findOne({
    //   where: {
    //     id,
    //   },
    // });

    const event = await this.eventsService.getEvent(id);

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

    return event;
  }

  //@UsePipes(new ValidationPipe({ groups: ['create'] }))
  @Post()
  async create(@Body() input: CreateEventDto) {
    return await this.repository.save({
      ...input,
      when: new Date(input.when),
    });
  }

  @Patch(':id')
  async update(@Param('id') id, @Body() input: UpdateEventDto) {
    const event = await this.repository.findOne({
      where: {
        id,
      },
    });

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

    return await this.repository.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.repository.findOne({
      where: {
        id,
      },
    });

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

    await this.repository.remove(event);
  }
}

数据转换:当设置为true时,transform选项会告诉ValidationPipe自动将接收到的请求数据(通常是字符串或其他基本类型)转换为声明的DTO(数据传输对象)类的实例。在这个例子中,DTO类是ListEvents。

类型转换:除了将普通的对象字面量转换为DTO类的实例之外,transform选项还能进行类型转换。例如,如果ListEvents类中有数字类型的属性,而从客户端接收到的是字符串类型的数字,这个选项会尝试将这些字符串转换为数字类型。

嵌套对象转换:当DTO中包含嵌套对象或类实例时,transform也会尝试递归地转换这些嵌套的对象。

当transform: true设置在ValidationPipe中,并且在DTO(Data Transfer Object)中存在带有默认值的属性时,如果客户端在请求中没有提供对应的参数,ValidationPipe会将这个属性设置为其在DTO中定义的默认值。

在您提供的ListEvents类中,page属性被定义并初始化为1。

image

010 Updating, Deleting, Modifying Relations using QB 使用查询生成器更新、删除、修改关联数据

使用 查询生成器 优化对大量数据和多次查询操作

多次删除操作

src/events/events.service.ts

import { DeleteResult, Repository } from "typeorm";

@Injectable()
export class EventsService {
  private readonly logger = new Logger(EventsService.name);

  constructor(
    @InjectRepository(Event)
    private readonly eventsRepository: Repository<Event>,
  ) {}

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

使用删除服务

src/events/events.controller.ts


@Controller('/events')
export class EventsController {
  private readonly logger = new Logger(EventsController.name);

  constructor(
    // 通过依赖注入将实体类Event注入到当前类中作为Event存储类使用
    @InjectRepository(Event)
    private readonly repository: Repository<Event>,
    @InjectRepository(Attendee)
    private readonly attendeeRepository: Repository<Attendee>,
    private readonly eventsService: EventsService,
  ) {}

  @Delete(':id')
  @HttpCode(204)
  async remove(@Param('id') id) {
    const result = await this.eventsService.deleteEvent(id);

    if (result?.affected !== 1) {
      throw new NotFoundException();
    }
  }
}

更新课程

不应该将课程的所有属性全部加载到内存中然后再去查询,更新 应当使用查询生成器的set只更新需要更新的属性。

将所有课程名字都改为 Confidential

src\school\training.controller.ts

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('/remove')
  public async removingRelation() {
    // const subject = await this.subjectRepository.findOne({
    //   where: {
    //     id: 1,
    //   },
    //   //loadEagerRelations: false,
    //   relations: ['teachers'],
    // });

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

    // await this.subjectRepository.save(subject);
    await this.subjectRepository
      .createQueryBuilder('s')
      .update()
      .set({ name: 'Confidential' })
      .execute();
  }
}

使用查询生成器优化关联操作的性能

查询到课程后,获取到对应的老师,再将老师全部更新,关联到课程。

src\school\training.controller.ts

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>,
    @InjectRepository(Teacher)
    private readonly teacherRepository: Repository<Teacher>,
  ) {}
  @Post('/create')
  public async savingRelation() {
    // const subject = new Subject();
    // subject.name = 'Math';

    const subject = await this.subjectRepository.findOne({ where: { id: 3 } });

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

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

    // subject.teachers = [teacher1, teacher2];
    // await this.teacherRepository.save([teacher1, teacher2]);

    const teacher1 = await this.teacherRepository.findOne({ where: { id: 5 } });
    const teacher2 = await this.teacherRepository.findOne({ where: { id: 6 } });

    return await this.subjectRepository
      .createQueryBuilder()
      .relation(Subject, 'teachers') // 关联  Subject 类 的 teachers 键
      .of(subject)
      .add([teacher1, teacher2]);
  }

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

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

    // await this.subjectRepository.save(subject);
    await this.subjectRepository
      .createQueryBuilder('s')
      .update()
      .set({ name: 'Confidential' })
      .execute();
  }
}

代码段中的savingRelation方法是TrainingController类的一部分,这个类被装饰为一个NestJS控制器,负责处理与school路由相关的请求。savingRelation方法特别是用来处理与/school/create路由相关的POST请求。其主要功能是在Subject实体和Teacher实体之间建立关联。下面是对这个方法的详细解析:

方法概览 首先,方法通过this.subjectRepository.findOne({ where: { id: 3 } })获取ID为3的Subject实体实例。这里假设Subject实体代表了学科,例如数学、科学等。 接着,方法通过两次调用this.teacherRepository.findOne(),分别获取ID为5和6的Teacher实体实例。这里Teacher实体代表教师。 最后,方法使用TypeORM的createQueryBuilder()和relation()方法,将找到的两个教师实体添加到之前找到的学科实体的teachers关联中。这表明这两个教师教授这个学科。 关键代码解析 TypeORM存储库注入:@InjectRepository(Subject)和@InjectRepository(Teacher)装饰器用于注入对应的TypeORM存储库。这允许你在控制器中使用存储库API来执行数据库操作。

查找实体:this.subjectRepository.findOne({ where: { id: 3 } })和this.teacherRepository.findOne({ where: { id: x } })分别用于查找特定ID的学科和教师实体。这些操作通常返回一个Promise,需要使用await来解析结果。

建立关联:

this.subjectRepository.createQueryBuilder()初始化一个QueryBuilder,用于构建和执行SQL查询。 .relation(Subject, 'teachers')指定我们要操作的关联。这里'teachers'应该是Subject实体中定义的一个关联属性,表示一个学科可以有多个教师。 .of(subject)指定了我们要添加关联的目标实体,即之前找到的特定学科实体。 .add([teacher1, teacher2])将两个教师实体添加到学科的teachers关联中。这意味着在数据库中,会建立这两个教师与该学科之间的关系。 方法的作用 这个方法展示了如何在NestJS应用程序中使用TypeORM来操作实体间的关系。具体到这个例子中,它演示了如何将教师与学科关联起来,这在实现诸如学校管理系统这样的应用时非常常见。通过这种方式,可以灵活地添加或修改实体间的关联,而无需直接操作关联表。

清空老师,课程中间表,使用数据库中存在的相关老师,课程id测试。

post localhost:3000/school/create 将生成关联老师,课程的中间表数据 image

优化了关联数据的操作。

011 One to One Relation 一对一关系

创建用户档案模块

用户和用户档案时一对一关系

src/auth/profile.entity.ts

import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

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

  @Column()
  age: number;
}

src/auth/user.entity.ts

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

配置中添加 用户和档案实体 到TypeOrm 模块的 entities ,用以同步到数据库

src/config/orm.config.ts

import { registerAs } from '@nestjs/config';
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { Profile } from 'src/auth/profile.entity';
import { User } from 'src/auth/user.entity';
import { Attendee } from 'src/events/attendee.entity';
import { Event } from './../events/event.entity';
import { Subject } from './../school/subject.entity';
import { Teacher } from './../school/teacher.entity';

export default registerAs(
  'orm.config',
  (): TypeOrmModuleOptions => ({
    type: 'mysql',
    host: process.env.DB_HOST,
    port: Number(process.env.DB_PORT),
    username: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME,
    entities: [Event, Attendee, Subject, Teacher, User, Profile],
    synchronize: true,
  }),
);

使用方式

    // const user = new User();
    // const profile = new Profile();

    // user.profile = profile;
    // user.profile = null;
    // Save the user here
WangShuXian6 commented 9 months ago

09 - Authentication, JWT, Authorization 身份验证、JWT、授权

001 Introduction to Authentication

001 Passport-2x

002 Local Passport Strategy 本地密码策略

使用用户名和密码进行验证

安装密码库

npm i -S @nestjs/passport passport passport-local

npm i -D @types/passport-local

创建auth验证模块

src/auth/auth.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';

@Module({
  imports: [],
  providers: [],
})
export class AuthModule {}

app中添加验证模块

src/app.module.ts

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppDummy } from './app.dummy';
import { AppChineseService } from './app.chinese.service';
import { AppService } from './app.service';
import ormConfig from './config/orm.config';
import ormConfigProd from './config/orm.config.prod';
import { EventsModule } from './events/events.module';
import { SchoolModule } from './school/school.module';
import { AuthModule } from './auth/auth.module';

@Module({
  imports: [
    // 使用 ormConfig 作为通用环境配置
    ConfigModule.forRoot({
      isGlobal: true,
      load: [ormConfig], //通过配置服务读取配置
      expandVariables: true, //启用可扩展环境变量 以支持字符串变量  ${APP_URL}
    }),
    // 异步加载工厂函数配置 分别用以数据库配置
    TypeOrmModule.forRootAsync({
      useFactory:
        process.env.NODE_ENV !== 'production' ? ormConfig : ormConfigProd,
    }),
    AuthModule,
    EventsModule,
    SchoolModule,
  ],
  controllers: [AppController],
  providers: [
    {
      provide: AppService,
      useClass: AppChineseService,
    },
    {
      provide: 'APP_NAME',
      useValue: 'Nest Events Backend!',
    },
    {
      provide: 'MESSAGE',
      inject: [AppDummy],
      useFactory: (app) => `${app.dummy()} Factory!`,
    },
    AppDummy,
  ],
})
export class AppModule {}

创建本地验证策略,使其可注入

src/auth/local.strategy.ts

import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { InjectRepository } from '@nestjs/typeorm';
import { Strategy } from 'passport-local';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@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验证模块 导入用户实体和本地策略

src/auth/auth.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { LocalStrategy } from './local.strategy';
import { User } from './user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  providers: [LocalStrategy],
})
export class AuthModule {}

003 Logging In - Passport Strategy with a Nest Guard 登录-带Nest Guard的Passport策略

验证控制器

src/auth/auth.controller.ts

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

@Controller('auth')
export class AuthController {

}

注册验证控制器

src/auth/auth.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthController } from './auth.controller';
import { LocalStrategy } from './local.strategy';
import { User } from './user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  providers: [LocalStrategy],
  controllers: [AuthController],
})
export class AuthModule {}

控制器中添加登录路由,使用验证守卫和默认的本地策略

src/auth/auth.controller.ts

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

@Controller('auth')
export class AuthController {
  @Post('login')
  @UseGuards(AuthGuard('local')) //本地验证策略的默认名称为 local
  async login(@Request() request) {
    return {
      userId: request.user.id,
      token: 'the token will go here',
    };
  }
}

AuthGuard('local') 默认从请求体获取 username 和 password 来验证 post localhost:3000/auth/login

image

004 JWT - JSON Web Tokens Introduction JWT-JSON Web令牌简介

004 JWT-2x 令牌 不要包含密码等敏感数据。

令牌不需要数据库查询,且负载应尽量小。

005 JWT - Generating Token JWT-生成令牌

jwt库

npm i -S @nestjs/jwt passport-jwt

npm i -D @types/passport-jwt

auth模块中注册jwt模块

src/auth/auth.module.ts

import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthController } from './auth.controller';
import { LocalStrategy } from './local.strategy';
import { User } from './user.entity';

@Module({
  imports: [
    TypeOrmModule.forFeature([User]),
    JwtModule.registerAsync({
      useFactory: () => ({
        secret: process.env.AUTH_SECRET,
        signOptions: {
          expiresIn: '60m',
        },
      }),
    }),
  ],
  providers: [LocalStrategy],
  controllers: [AuthController],
})
export class AuthModule {}

env 环境变量添加jwt密钥

.env

DB_HOST=localhost
DB_PORT=3307
DB_USER=root
DB_PASSWORD=example
DB_NAME=nest-events

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

AUTH_SECRET=secret123

验证服务

src/auth/auth.service.ts

import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { User } from './user.entity';

@Injectable()
export class AuthService {
  constructor(private readonly jwtService: JwtService) {}

  public getTokenForUser(user: User): string {
    return this.jwtService.sign({
      username: user.username,
      sub: user.id,
    });
  }
}

验证模块中注册验证服务为提供者

环境变量在创建验证模块时不可用,需要使用工厂函数和异步配置jwt模块

src/auth/auth.module.ts

import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { User } from './user.entity';

@Module({
  imports: [
    TypeOrmModule.forFeature([User]),
    JwtModule.registerAsync({
      useFactory: () => ({
        secret: process.env.AUTH_SECRET,
        signOptions: {
          expiresIn: '60m',
        },
      }),
    }),
  ],
  providers: [LocalStrategy, AuthService],
  controllers: [AuthController],
})
export class AuthModule {}

控制器中生成jwt

src/auth/auth.controller.ts

import { Controller, 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) {
    console.log(process.env.AUTH_SECRET);
    return {
      userId: request.user.id,
      token: this.authService.getTokenForUser(request.user),
    };
  }
}

image

006 JWT - Strategy & Guard - Authenticating with JWT Token JWT-策略与保护-使用JWT令牌进行身份验证

创建策略使用jwt令牌进行验证

src/auth/jwt.strategy.ts

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { InjectRepository } from '@nestjs/typeorm';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
  ) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false, //令牌过期后是否忽略,false-过期则需要重新登陆
      secretOrKey: process.env.AUTH_SECRET,
    });
  }

  async validate(payload: any) {
    return await this.userRepository.findOne({ where: { id: payload.sub } });//返回的{user:...}数据将被填充到request中供后续路由使用
  }
}

控制器添加获取用户资料getProfile路由,使用jwt令牌策略验证

src/auth/auth.controller.ts

import { Controller, Get, Post, Request, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';

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

  @Post('login')
  @UseGuards(AuthGuard('local'))
  async login(@Request() request) {
    console.log(process.env.AUTH_SECRET);
    return {
      userId: request.user.id,
      token: this.authService.getTokenForUser(request.user),
    };
  }

  @Get('profile')
  @UseGuards(AuthGuard('jwt'))
  async getProfile(@Request() request) {
    return request.user;
  }
}

验证模块注册jwt策略为提供者

src/auth/auth.module.ts

import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';
import { LocalStrategy } from './local.strategy';
import { User } from './user.entity';

@Module({
  imports: [
    TypeOrmModule.forFeature([User]),
    JwtModule.registerAsync({
      useFactory: () => ({
        secret: process.env.AUTH_SECRET,
        signOptions: {
          expiresIn: '60m',
        },
      }),
    }),
  ],
  providers: [LocalStrategy, JwtStrategy, AuthService],
  controllers: [AuthController],
})
export class AuthModule {}

请求 profile

添加请求头 Authorization 值为 Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRvbSIsInN1YiI6MSwiaWF0IjoxNzA4NjE4NjE0LCJleHAiOjE3MDg2MjIyMTR9.cRhlY2yPuUN2ShrMqGvp9x8D8R6taiXO8p4JImtpgKE

image

007 Hashing Passwords with Bcrypt 使用Bcrypt哈希密码

Bcrypt

npm i  -S  bcrypt

npm i -D @types/bcrypt

验证服务添加加密功能

src/auth/auth.service.ts

import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { User } from './user.entity';

@Injectable()
export class AuthService {
  constructor(private readonly jwtService: JwtService) {}

  public getTokenForUser(user: User): string {
    return this.jwtService.sign({
      username: user.username,
      sub: user.id,
    });
  }

  public async hashPassword(password: string): Promise<string> {
    return await bcrypt.hash(password, 10);
  }
}

本地密码策略更新使用加密比较密码

src/auth/local.strategy.ts

import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { InjectRepository } from '@nestjs/typeorm';
import * as bcrypt from 'bcrypt';
import { Strategy } from 'passport-local';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@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 (!(await bcrypt.compare(password, user.password))) {
      this.logger.debug(`Invalid credentials for user ${username}`);
      throw new UnauthorizedException();
    }

    return user;
  }
}

008 Custom CurrentUser Decorator 自定义CurrentUser装饰器

使用自定义参数对用户进行验证

创建用户自定义参数装饰器 CurrentUser

src/auth/current-user.decorator.ts

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

export const CurrentUser = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user ?? null;
  },
);

使用CurrentUser 装饰器

src/auth/auth.controller.ts

import { Controller, Get, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { CurrentUser } from './current-user.decorator';
import { User } from './user.entity';

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

  @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;
  }
}

009 User Registration 用户注册

更新用户实体

src/auth/user.entity.ts

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

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

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

  @Column()
  password: string;

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

  @Column()
  firstName: string;

  @Column()
  lastName: string;

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

添加用户DTO

src/auth/input/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;
}

添加用户控制器

src/auth/users.controller.ts

import { BadRequestException, Body, Controller, Post } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AuthService } from './auth.service';
import { CreateUserDto } from './input/create.user.dto';
import { User } from './user.entity';

@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']);
    }

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

验证模块注册用户控制器

src/auth/auth.module.ts

import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';
import { LocalStrategy } from './local.strategy';
import { User } from './user.entity';
import { UsersController } from './users.controller';

@Module({
  imports: [
    TypeOrmModule.forFeature([User]),
    JwtModule.registerAsync({
      useFactory: () => ({
        secret: process.env.AUTH_SECRET,
        signOptions: {
          expiresIn: '60m',
        },
      }),
    }),
  ],
  providers: [LocalStrategy, JwtStrategy, AuthService],
  controllers: [AuthController, UsersController],
})
export class AuthModule {}

请求注册路由

post localhost:3000/auth/create

010 Only Authenticated Users Can Create Events 只有经过身份验证的用户才能创建事件

image

在事件和用户之间建立联系 organizer

一个组织者对应多个事件

src/events/event.entity.ts

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

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

  @Column()
  name: string;

  @Column()
  description: string;

  @Column()
  when: Date;

  @Column()
  address: string;

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

  @ManyToOne(() => User, (user) => user.organized)
  organizer: User;

  @Column({ nullable: true })
  organizerId: number;

  attendeeCount?: number;
  attendeeRejected?: number;
  attendeeMaybe?: number;
  attendeeAccepted?: number;
}

src/auth/user.entity.ts

import {
  Column,
  Entity,
  JoinColumn,
  OneToMany,
  OneToOne,
  PrimaryGeneratedColumn,
} from 'typeorm';
import { Event } from './../events/event.entity';
import { Profile } from './profile.entity';

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

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

  @Column()
  password: string;

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

  @Column()
  firstName: string;

  @Column()
  lastName: string;

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

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

事件服务添加创建事件 createEvent

src/events/events.service.ts

import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from 'src/auth/user.entity';
import { paginate, PaginateOptions } from 'src/pagination/paginator';
import { DeleteResult, Repository } from 'typeorm';
import { AttendeeAnswerEnum } from './attendee.entity';
import { Event } from './event.entity';
import { CreateEventDto } from './input/create-event.dto';
import { ListEvents, WhenEventFilter } from './input/list.events';

@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')
      .orderBy('e.id', 'DESC');
  }

  public getEventsWithAttendeeCountQuery() {
    return this.getEventsBaseQuery()
      .loadRelationCountAndMap('e.attendeeCount', 'e.attendees')
      .loadRelationCountAndMap(
        'e.attendeeAccepted',
        'e.attendees',
        'attendee',
        (qb) =>
          qb.where('attendee.answer = :answer', {
            answer: AttendeeAnswerEnum.Accepted,
          }),
      )
      .loadRelationCountAndMap(
        'e.attendeeMaybe',
        'e.attendees',
        'attendee',
        (qb) =>
          qb.where('attendee.answer = :answer', {
            answer: AttendeeAnswerEnum.Maybe,
          }),
      )
      .loadRelationCountAndMap(
        'e.attendeeRejected',
        'e.attendees',
        'attendee',
        (qb) =>
          qb.where('attendee.answer = :answer', {
            answer: AttendeeAnswerEnum.Rejected,
          }),
      );
  }

  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 query;
  }

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

  public async getEvent(id: number): Promise<Event | undefined> {
    const query = this.getEventsWithAttendeeCountQuery().andWhere(
      'e.id = :id',
      { id },
    );

    this.logger.debug(query.getSql());

    return await query.getOne();
  }

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

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

事件控制器创建事件

src/events/events.controller.ts

import { User } from 'src/auth/user.entity';

  // You can also use the @UsePipes decorator to enable pipes.
  // It can be done per method, or for every method when you
  // add it at the controller level.
  @Post()
  async create(@Body() input: CreateEventDto, @CurrentUser() user: User) {
    return await this.eventsService.createEvent(input, user);
  }

优化验证模块 创建独立的验证守卫

src/auth/auth-guard.jwt.ts

import { AuthGuard } from '@nestjs/passport';

export class AuthGuardJwt extends AuthGuard('jwt') {}

src/auth/auth-guard.local.ts

import { AuthGuard } from '@nestjs/passport';

export class AuthGuardLocal extends AuthGuard('local') {}

使用新的验证守卫

src/auth/auth.controller.ts

import { Controller, Get, Post, UseGuards } from '@nestjs/common';
import { AuthGuardJwt } from './auth-guard.jwt';
import { AuthGuardLocal } from './auth-guard.local';
import { AuthService } from './auth.service';
import { CurrentUser } from './current-user.decorator';
import { User } from './user.entity';

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

  @Post('login')
  @UseGuards(AuthGuardLocal)
  async login(@CurrentUser() user: User) {
    return {
      userId: user.id,
      token: this.authService.getTokenForUser(user),
    };
  }

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

事件创建使用jwt验证守卫

src/events/events.controller.ts

import { AuthGuardJwt } from 'src/auth/auth-guard.jwt';
import { User } from 'src/auth/user.entity';

  // You can also use the @UsePipes decorator to enable pipes.
  // It can be done per method, or for every method when you
  // add it at the controller level.
  @Post()
  @UseGuards(AuthGuardJwt)
  async create(@Body() input: CreateEventDto, @CurrentUser() user: User) {
    return await this.eventsService.createEvent(input, user);
  }

image

011 Only The Owners Can Edit or Delete Events 只有所有者才能编辑或删除事件

更新方法提取到服务中

src/events/events.service.ts

import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from 'src/auth/user.entity';
import { paginate, PaginateOptions } from 'src/pagination/paginator';
import { DeleteResult, Repository } from 'typeorm';
import { AttendeeAnswerEnum } from './attendee.entity';
import { Event } from './event.entity';
import { CreateEventDto } from './input/create-event.dto';
import { ListEvents, WhenEventFilter } from './input/list.events';
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() {
    return this.eventsRepository
      .createQueryBuilder('e')
      .orderBy('e.id', 'DESC');
  }

  public getEventsWithAttendeeCountQuery() {
    return this.getEventsBaseQuery()
      .loadRelationCountAndMap('e.attendeeCount', 'e.attendees')
      .loadRelationCountAndMap(
        'e.attendeeAccepted',
        'e.attendees',
        'attendee',
        (qb) =>
          qb.where('attendee.answer = :answer', {
            answer: AttendeeAnswerEnum.Accepted,
          }),
      )
      .loadRelationCountAndMap(
        'e.attendeeMaybe',
        'e.attendees',
        'attendee',
        (qb) =>
          qb.where('attendee.answer = :answer', {
            answer: AttendeeAnswerEnum.Maybe,
          }),
      )
      .loadRelationCountAndMap(
        'e.attendeeRejected',
        'e.attendees',
        'attendee',
        (qb) =>
          qb.where('attendee.answer = :answer', {
            answer: AttendeeAnswerEnum.Rejected,
          }),
      );
  }

  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 query;
  }

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

  public async getEvent(id: number): Promise<Event | undefined> {
    const query = this.getEventsWithAttendeeCountQuery().andWhere(
      'e.id = :id',
      { id },
    );

    this.logger.debug(query.getSql());

    return await query.getOne();
  }

  public async createEvent(input: CreateEventDto, user: User): Promise<Event> {
    console.log(user);
    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();
  }
}

控制器不再需要注入存储库 repository,为更新和删除添加验证,必须是组织者可以操作事件

src/events/events.controller.ts

import {
  Body,
  Controller,
  Delete,
  ForbiddenException,
  Get,
  HttpCode,
  Logger,
  NotFoundException,
  Param,
  ParseIntPipe,
  Patch,
  Post,
  Query,
  UseGuards,
  UsePipes,
  ValidationPipe,
} from '@nestjs/common';
import { AuthGuardJwt } from 'src/auth/auth-guard.jwt';
import { CurrentUser } from 'src/auth/current-user.decorator';
import { User } from 'src/auth/user.entity';
import { EventsService } from './events.service';
import { CreateEventDto } from './input/create-event.dto';
import { ListEvents } from './input/list.events';
import { UpdateEventDto } from './input/update-event.dto';

@Controller('/events')
export class EventsController {
  private readonly logger = new Logger(EventsController.name);

  constructor(private readonly eventsService: EventsService) {}

  @Get()
  @UsePipes(new ValidationPipe({ transform: true }))
  async findAll(@Query() filter: ListEvents) {
    const events =
      await this.eventsService.getEventsWithAttendeeCountFilteredPaginated(
        filter,
        {
          total: true,
          currentPage: filter.page,
          limit: 2,
        },
      );
    return events;
  }

  @Get('/practice')
  async practice() {
    // return await this.repository.find({
    //   select: ['id', 'when'],
    //   where: [
    //     {
    //       id: MoreThan(3),
    //       when: MoreThan(new Date('2021-02-12T13:00:00')),
    //     },
    //     {
    //       description: Like('%meet%'),
    //     },
    //   ],
    //   take: 2,
    //   order: {
    //     id: 'DESC',
    //   },
    // });
  }

  @Get('practice2')
  async practice2() {
    // const event = await this.repository.findOne({
    //   where: {
    //     id: 1,
    //   },
    //   //loadEagerRelations: false,
    //   relations: ['attendees'],
    // });
    // const attendee = new Attendee();
    // attendee.name = 'Potter3';
    // // attendee.event = event; // 级联将导致循环引用错误
    // event.attendees.push(attendee);
    // await this.repository.save(event);
    // return event;
    // return await this.repository
    //   .createQueryBuilder('e')
    //   .select(['e.id', 'e.name'])
    //   .orderBy('e.id', 'ASC')
    //   .take(3)
    //   .getMany();
  }

  @Get(':id')
  async findOne(@Param('id', ParseIntPipe) id: number) {
    // const event = await this.repository.findOne({
    //   where: {
    //     id,
    //   },
    // });

    const event = await this.eventsService.getEvent(id);

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

    return event;
  }

  // You can also use the @UsePipes decorator to enable pipes.
  // It can be done per method, or for every method when you
  // add it at the controller level.
  @Post()
  @UseGuards(AuthGuardJwt)
  async create(@Body() input: CreateEventDto, @CurrentUser() user: User) {
    return await this.eventsService.createEvent(input, user);
  }

  // Create new ValidationPipe to specify validation group inside @Body
  // new ValidationPipe({ groups: ['update'] })
  @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 authorized 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 authorized to remove this event`,
      );
    }

    await this.eventsService.deleteEvent(id);
  }
}
WangShuXian6 commented 9 months ago

10 - Data Serialization 数据序列化

001 Interceptors and Serialization 拦截器和序列化

001 Serialization-2x

拦截器可以转换数据,转换异常,增加缓存功能。可以修改控制器返回的数据。

序列化转换实体的json数据,只响应需要的数据给用户。

002 Serializing Data 序列化

序列化 有两种策略,排除所有实体参数或公开所有实体参数

事件控制器添加序列化装饰器 @SerializeOptions({ strategy: 'excludeAll' })

src/events/events.controller.ts

import {
  Body,
  ClassSerializerInterceptor,
  Controller,
  Delete,
  ForbiddenException,
  Get,
  HttpCode,
  Logger,
  NotFoundException,
  Param,
  ParseIntPipe,
  Patch,
  Post,
  Query,
  SerializeOptions,
  UseGuards,
  UseInterceptors,
  UsePipes,
  ValidationPipe,
} from '@nestjs/common';
import { AuthGuardJwt } from 'src/auth/auth-guard.jwt';
import { CurrentUser } from 'src/auth/current-user.decorator';
import { User } from 'src/auth/user.entity';
import { EventsService } from './events.service';
import { CreateEventDto } from './input/create-event.dto';
import { ListEvents } from './input/list.events';
import { UpdateEventDto } from './input/update-event.dto';

@Controller('/events')
@SerializeOptions({ strategy: 'excludeAll' })
export class EventsController {

获取路由添加序列化拦截装饰器 @UseInterceptors(ClassSerializerInterceptor)

  @Get(':id')
  @UseInterceptors(ClassSerializerInterceptor)
  async findOne(@Param('id', ParseIntPipe) id: number) {
    // const event = await this.repository.findOne({
    //   where: {
    //     id,
    //   },
    // });

    const event = await this.eventsService.getEvent(id);

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

    return event;
  }

实体中定义序列化时应该公开那些参数 @Expose()

公开 的参数才会响应给用户

src/events/event.entity.ts

import { Expose } from 'class-transformer';
import { User } from 'src/auth/user.entity';
import {
  Column,
  Entity,
  ManyToOne,
  OneToMany,
  PrimaryGeneratedColumn,
} from 'typeorm';
import { Attendee } from './attendee.entity';

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

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

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

  @Column()
  @Expose()
  when: Date;

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

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

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

  @Column({ nullable: true })
  organizerId: number;

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

src/events/attendee.entity.ts

import { Expose } from 'class-transformer';
import {
  Column,
  Entity,
  JoinColumn,
  ManyToOne,
  PrimaryGeneratedColumn,
} from 'typeorm';
import { Event } from './event.entity';

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: true,
  })
  @JoinColumn()
  event: Event;

  @Column('enum', {
    enum: AttendeeAnswerEnum,
    default: AttendeeAnswerEnum.Accepted,
  })
  @Expose()
  answer: AttendeeAnswerEnum;
}

src/auth/user.entity.ts

import { Expose } from 'class-transformer';
import {
  Column,
  Entity,
  JoinColumn,
  OneToMany,
  OneToOne,
  PrimaryGeneratedColumn,
} from 'typeorm';
import { Event } from './../events/event.entity';
import { Profile } from './profile.entity';

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

验证控制器添加序列化和拦截器

src/auth/auth.controller.ts

import {
  ClassSerializerInterceptor,
  Controller,
  Get,
  Post,
  SerializeOptions,
  UseGuards,
  UseInterceptors,
} from '@nestjs/common';
import { AuthGuardJwt } from './auth-guard.jwt';
import { AuthGuardLocal } from './auth-guard.local';
import { AuthService } from './auth.service';
import { CurrentUser } from './current-user.decorator';
import { User } from './user.entity';

@Controller('auth')
@SerializeOptions({ strategy: 'excludeAll' })
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('login')
  @UseGuards(AuthGuardLocal)
  async login(@CurrentUser() user: User) {
    return {
      userId: user.id,
      token: this.authService.getTokenForUser(user),
    };
  }

  @Get('profile')
  @UseGuards(AuthGuardJwt)
  @UseInterceptors(ClassSerializerInterceptor)
  async getProfile(@CurrentUser() user: User) {
    return user;
  }
}

事件控制器补全拦截器

src/events/events.controller.ts

import {
  Body,
  ClassSerializerInterceptor,
  Controller,
  Delete,
  ForbiddenException,
  Get,
  HttpCode,
  Logger,
  NotFoundException,
  Param,
  ParseIntPipe,
  Patch,
  Post,
  Query,
  SerializeOptions,
  UseGuards,
  UseInterceptors,
  UsePipes,
  ValidationPipe,
} from '@nestjs/common';
import { AuthGuardJwt } from 'src/auth/auth-guard.jwt';
import { CurrentUser } from 'src/auth/current-user.decorator';
import { User } from 'src/auth/user.entity';
import { EventsService } from './events.service';
import { CreateEventDto } from './input/create-event.dto';
import { ListEvents } from './input/list.events';
import { UpdateEventDto } from './input/update-event.dto';

@Controller('/events')
@SerializeOptions({ strategy: 'excludeAll' })
export class EventsController {
  private readonly logger = new Logger(EventsController.name);

  constructor(private readonly eventsService: EventsService) {}

  @Get()
  @UsePipes(new ValidationPipe({ transform: true }))
  @UseInterceptors(ClassSerializerInterceptor)
  async findAll(@Query() filter: ListEvents) {
    const events =
      await this.eventsService.getEventsWithAttendeeCountFilteredPaginated(
        filter,
        {
          total: true,
          currentPage: filter.page,
          limit: 2,
        },
      );
    return events;
  }

  @Get('/practice')
  async practice() {
    // return await this.repository.find({
    //   select: ['id', 'when'],
    //   where: [
    //     {
    //       id: MoreThan(3),
    //       when: MoreThan(new Date('2021-02-12T13:00:00')),
    //     },
    //     {
    //       description: Like('%meet%'),
    //     },
    //   ],
    //   take: 2,
    //   order: {
    //     id: 'DESC',
    //   },
    // });
  }

  @Get('practice2')
  async practice2() {
    // const event = await this.repository.findOne({
    //   where: {
    //     id: 1,
    //   },
    //   //loadEagerRelations: false,
    //   relations: ['attendees'],
    // });
    // const attendee = new Attendee();
    // attendee.name = 'Potter3';
    // // attendee.event = event; // 级联将导致循环引用错误
    // event.attendees.push(attendee);
    // await this.repository.save(event);
    // return event;
    // return await this.repository
    //   .createQueryBuilder('e')
    //   .select(['e.id', 'e.name'])
    //   .orderBy('e.id', 'ASC')
    //   .take(3)
    //   .getMany();
  }

  @Get(':id')
  @UseInterceptors(ClassSerializerInterceptor)
  async findOne(@Param('id', ParseIntPipe) id: number) {
    // const event = await this.repository.findOne({
    //   where: {
    //     id,
    //   },
    // });

    const event = await this.eventsService.getEvent(id);

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

    return event;
  }

  // You can also use the @UsePipes decorator to enable pipes.
  // It can be done per method, or for every method when you
  // add it at the controller level.
  @Post()
  @UseGuards(AuthGuardJwt)
  @UseInterceptors(ClassSerializerInterceptor)
  async create(@Body() input: CreateEventDto, @CurrentUser() user: User) {
    return await this.eventsService.createEvent(input, user);
  }

  // Create new ValidationPipe to specify validation group inside @Body
  // new ValidationPipe({ groups: ['update'] })
  @Patch(':id')
  @UseGuards(AuthGuardJwt)
  @UseInterceptors(ClassSerializerInterceptor)
  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 authorized 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 authorized to remove this event`,
      );
    }

    await this.eventsService.deleteEvent(id);
  }
}

由于 findAll 返回的对象不是实例化的类:ClassSerializerInterceptor依赖类的元数据来应用序列化规则。如果返回的数据不是通过类构造函数创建的实例,而是简单的对象字面量,那么可能不会应用任何序列化规则。 所以 findAll 将不会返回数据。

003 Serializing Nested Objects 序列化嵌套对象

序列化的目标必须是实体的类的实例。分页返回类的实例 PaginationResult

序列化时,需要通过实体的 @Expose() 参数决定是否返回 如果序列化的是一个普通对象,将返回空。 序列化的目标必须是实体的类的实例。

src/pagination/paginator.ts

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

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

export class PaginationResult<T> {
  constructor(partial: Partial<PaginationResult<T>>) {
    Object.assign(this, partial);
  }

  @Expose()
  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,
  });
}

现在 findAll 可以返回数据 image

WangShuXian6 commented 9 months ago

11 - (Practical) Building Full Events API 11-(实用)构建完整事件API

001 (Practical) Building Full Events API

001 Events-Application-2x

002 Relations Between Entities 实体之间的关系

002 Simple-Entity-Relation-2x

截断所有表

由于开发阶段在不断个更新列名,旧数据的新列名将为空,发生错误,需要清空数据表 truncate-all-tables.sql

SET
    foreign_key_checks = 0;

TRUNCATE TABLE `attendee`;

TRUNCATE TABLE `event`;

TRUNCATE TABLE `profile`;

TRUNCATE TABLE `subject`;

TRUNCATE TABLE `subject_teachers_teacher`;

TRUNCATE TABLE `teacher`;

TRUNCATE TABLE `user`;

SET
    foreign_key_checks = 1;

这段SQL语句用于清空多个表的内容,并在此过程中暂时禁用外键约束检查,以避免因外键依赖而导致的删除错误。下面是对这个SQL语句的逐步解析:

禁用外键约束检查

SET foreign_key_checks = 0;

清空(截断)表

接下来的几条TRUNCATE TABLE语句用于清空指定的表:

TRUNCATE TABLE `attendee`;
TRUNCATE TABLE `event`;
TRUNCATE TABLE `profile`;
TRUNCATE TABLE `subject`;
TRUNCATE TABLE `subject_teachers_teacher`;
TRUNCATE TABLE `teacher`;
TRUNCATE TABLE `user`;

重新启用外键约束检查

SET foreign_key_checks = 1;

使用场景

这样的操作通常用于开发或测试环境,在需要快速清空数据库内容并从头开始时非常有用。但是,在生产环境中直接使用这种方法需要非常谨慎,因为它会永久删除所有指定表中的数据,并且不能恢复。

总的来说,这段SQL语句是一种快速清理数据库内容的方法,通过暂时禁用外键约束来避免在清空表时遇到依赖问题。在执行这类操作时,建议确保已经做好了适当的数据备份,以防不测。

用户与出席者 一对多关系

src/auth/user.entity.ts

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

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

  @OneToMany(() => Attendee, (attendee) => attendee.user)
  attended: Attendee[];
}

src/events/attendee.entity.ts

import { Expose } from 'class-transformer';
import { User } from 'src/auth/user.entity';
import {
  Column,
  Entity,
  JoinColumn,
  ManyToOne,
  PrimaryGeneratedColumn,
} from 'typeorm';
import { Event } from './event.entity';

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: true,
  })
  @JoinColumn()
  event: Event;

  @Column()
  eventId: number;

  @Column('enum', {
    enum: AttendeeAnswerEnum,
    default: AttendeeAnswerEnum.Accepted,
  })
  @Expose()
  answer: AttendeeAnswerEnum;

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

  @Column()
  userId: number;
}

事件实体定义通用分页类型 PaginatedEvents

src/events/event.entity.ts

import { Expose } from 'class-transformer';
import { User } from 'src/auth/user.entity';
import { PaginationResult } from 'src/pagination/paginator';
import {
  Column,
  Entity,
  ManyToOne,
  OneToMany,
  PrimaryGeneratedColumn,
} from 'typeorm';
import { Attendee } from './attendee.entity';

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

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

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

  @Column()
  @Expose()
  when: Date;

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

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

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

  @Column({ nullable: true })
  organizerId: number;

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

export type PaginatedEvents = PaginationResult<Event>;

003 Getting Event Attendees 获取活动参与者

控制器只应该获取用户输入,然后响应。 业务逻辑应放在服务中。

创建出席者服务

获取所有参与指定事件的出席者

这里Attendee实体中有一个名为event的关联属性,该属性链接到一个事件实体,而这个事件实体有一个id字段。

src/events/attendees.service.ts

import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Attendee } from './attendee.entity';

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

会议事件出席者控制器

src/events/event-attendees.controller.ts

import {
  ClassSerializerInterceptor,
  Controller,
  Get,
  Param,
  SerializeOptions,
  UseInterceptors,
} from '@nestjs/common';
import { AttendeesService } from './attendees.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);
  }
}

004 Getting Events Organized by User 获取按用户组织的事件

src/events/events.service.ts

import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from 'src/auth/user.entity';
import { paginate, PaginateOptions } from 'src/pagination/paginator';
import { DeleteResult, Repository } 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.events';
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() {
    return this.eventsRepository
      .createQueryBuilder('e')
      .orderBy('e.id', 'DESC');
  }

  public getEventsWithAttendeeCountQuery() {
    return this.getEventsBaseQuery()
      .loadRelationCountAndMap('e.attendeeCount', 'e.attendees')
      .loadRelationCountAndMap(
        'e.attendeeAccepted',
        'e.attendees',
        'attendee',
        (qb) =>
          qb.where('attendee.answer = :answer', {
            answer: AttendeeAnswerEnum.Accepted,
          }),
      )
      .loadRelationCountAndMap(
        'e.attendeeMaybe',
        'e.attendees',
        'attendee',
        (qb) =>
          qb.where('attendee.answer = :answer', {
            answer: AttendeeAnswerEnum.Maybe,
          }),
      )
      .loadRelationCountAndMap(
        'e.attendeeRejected',
        'e.attendees',
        'attendee',
        (qb) =>
          qb.where('attendee.answer = :answer', {
            answer: AttendeeAnswerEnum.Rejected,
          }),
      );
  }

  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 query;
  }

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

  public async getEvent(id: number): Promise<Event | undefined> {
    const query = this.getEventsWithAttendeeCountQuery().andWhere(
      'e.id = :id',
      { id },
    );

    this.logger.debug(query.getSql());

    return await query.getOne();
  }

  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) {
    return this.getEventsBaseQuery().where('e.organizerId = :userId', {
      userId,
    });
  }
}

创建事件组织者控制器

src/events/events-organized-by-user.controller.ts

import {
  ClassSerializerInterceptor,
  Controller,
  Get,
  Param,
  Query,
  SerializeOptions,
  UseInterceptors,
} from '@nestjs/common';
import { EventsService } from './events.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 },
    );
  }
}

事件模块注册 出席者服务为提供者

src/events/events.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Attendee } from './attendee.entity';
import { AttendeesService } from './attendees.service';
import { Event } from './event.entity';
import { EventsController } from './events.controller';
import { EventsService } from './events.service';

@Module({
  imports: [TypeOrmModule.forFeature([Event, Attendee])],
  controllers: [EventsController],
  providers: [EventsService, AttendeesService],
})
export class EventsModule {}

005 Current User Event Attendance - the Business Logic 当前用户事件出席者-业务逻辑

DTO

src/events/input/create-attendee.dto.ts

import { IsEnum } from 'class-validator';
import { AttendeeAnswerEnum } from './../attendee.entity';

export class CreateAttendeeDto {
  @IsEnum(AttendeeAnswerEnum)
  answer: AttendeeAnswerEnum;
}

服务

src/events/attendees.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Attendee } from './attendee.entity';
import { CreateAttendeeDto } from './input/create-attendee.dto';

@Injectable()
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 },
      },
    });
  }

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

src/events/events.service.ts

import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from 'src/auth/user.entity';
import { paginate, PaginateOptions } from 'src/pagination/paginator';
import { DeleteResult, Repository } 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.events';
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() {
    return this.eventsRepository
      .createQueryBuilder('e')
      .orderBy('e.id', 'DESC');
  }

  public getEventsWithAttendeeCountQuery() {
    return this.getEventsBaseQuery()
      .loadRelationCountAndMap('e.attendeeCount', 'e.attendees')
      .loadRelationCountAndMap(
        'e.attendeeAccepted',
        'e.attendees',
        'attendee',
        (qb) =>
          qb.where('attendee.answer = :answer', {
            answer: AttendeeAnswerEnum.Accepted,
          }),
      )
      .loadRelationCountAndMap(
        'e.attendeeMaybe',
        'e.attendees',
        'attendee',
        (qb) =>
          qb.where('attendee.answer = :answer', {
            answer: AttendeeAnswerEnum.Maybe,
          }),
      )
      .loadRelationCountAndMap(
        'e.attendeeRejected',
        'e.attendees',
        'attendee',
        (qb) =>
          qb.where('attendee.answer = :answer', {
            answer: AttendeeAnswerEnum.Rejected,
          }),
      );
  }

  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 query;
  }

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

  public async getEvent(id: number): Promise<Event | undefined> {
    const query = this.getEventsWithAttendeeCountQuery().andWhere(
      'e.id = :id',
      { id },
    );

    this.logger.debug(query.getSql());

    return await query.getOne();
  }

  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) {
    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) {
    return this.getEventsBaseQuery()
      .leftJoinAndSelect('e.attendees', 'a')
      .where('a.userId = :userId', { userId });
  }
}

006 Current User Event Attendance - the Controller

src/events/current-user-event-attendance.controller.ts

import {
  Body,
  ClassSerializerInterceptor,
  Controller,
  Get,
  NotFoundException,
  Param,
  ParseIntPipe,
  Put,
  Query,
  SerializeOptions,
  UseGuards,
  UseInterceptors,
} from '@nestjs/common';
import { CurrentUser } from 'src/auth/current-user.decorator';
import { User } from 'src/auth/user.entity';
import { AuthGuardJwt } from './../auth/auth-guard.jwt';
import { AttendeesService } from './attendees.service';
import { EventsService } from './events.service';
import { CreateAttendeeDto } from './input/create-attendee.dto';

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

007 Events Refactoring

用户和出席者不能全部公开,否则会导致循环引用。

只公开出席者中的user. 不公开用户中的attended

src/events/current-user-event-attendance.controller.ts

import {
  Body,
  ClassSerializerInterceptor,
  Controller,
  DefaultValuePipe,
  Get,
  NotFoundException,
  Param,
  ParseIntPipe,
  Put,
  Query,
  SerializeOptions,
  UseGuards,
  UseInterceptors,
} from '@nestjs/common';
import { CurrentUser } from 'src/auth/current-user.decorator';
import { User } from 'src/auth/user.entity';
import { AuthGuardJwt } from './../auth/auth-guard.jwt';
import { AttendeesService } from './attendees.service';
import { EventsService } from './events.service';
import { CreateAttendeeDto } from './input/create-attendee.dto';

@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', new DefaultValuePipe(1), ParseIntPipe) 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);
  }
}

src/events/event.entity.ts

import { Expose } from 'class-transformer';
import { User } from 'src/auth/user.entity';
import { PaginationResult } from 'src/pagination/paginator';
import {
  Column,
  Entity,
  ManyToOne,
  OneToMany,
  PrimaryGeneratedColumn,
} from 'typeorm';
import { Attendee } from './attendee.entity';

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

  @PrimaryGeneratedColumn()
  @Expose()
  id: number;

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

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

  @Column()
  @Expose()
  when: Date;

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

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

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

  @Column({ nullable: true })
  organizerId: number;

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

export type PaginatedEvents = PaginationResult<Event>;

src/events/events-organized-by-user.controller.ts

import {
  ClassSerializerInterceptor,
  Controller,
  DefaultValuePipe,
  Get,
  Param,
  ParseIntPipe,
  Query,
  SerializeOptions,
  UseInterceptors,
} from '@nestjs/common';
import { EventsService } from './events.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', ParseIntPipe) userId: number,
    @Query('page', new DefaultValuePipe(1), ParseIntPipe) page = 1,
  ) {
    return await this.eventsService.getEventsOrganizedByUserIdPaginated(
      userId,
      { currentPage: page, limit: 5 },
    );
  }
}

src/events/events.service.ts

import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from 'src/auth/user.entity';
import { paginate, PaginateOptions } 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.events';
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')
      .orderBy('e.id', 'DESC');
  }

  public getEventsWithAttendeeCountQuery(): SelectQueryBuilder<Event> {
    return this.getEventsBaseQuery()
      .loadRelationCountAndMap('e.attendeeCount', 'e.attendees')
      .loadRelationCountAndMap(
        'e.attendeeAccepted',
        'e.attendees',
        'attendee',
        (qb) =>
          qb.where('attendee.answer = :answer', {
            answer: AttendeeAnswerEnum.Accepted,
          }),
      )
      .loadRelationCountAndMap(
        'e.attendeeMaybe',
        'e.attendees',
        'attendee',
        (qb) =>
          qb.where('attendee.answer = :answer', {
            answer: AttendeeAnswerEnum.Maybe,
          }),
      )
      .loadRelationCountAndMap(
        'e.attendeeRejected',
        'e.attendees',
        'attendee',
        (qb) =>
          qb.where('attendee.answer = :answer', {
            answer: AttendeeAnswerEnum.Rejected,
          }),
      );
  }

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

    this.logger.debug(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(
      new Event({
        ...input,
        organizer: user,
        when: new Date(input.when),
      }),
    );
  }

  public async updateEvent(
    event: Event,
    input: UpdateEventDto,
  ): Promise<Event> {
    return await this.eventsRepository.save(
      new Event({
        ...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 });
  }
}

src/events/events.controller.ts

import {
  Body,
  ClassSerializerInterceptor,
  Controller,
  Delete,
  ForbiddenException,
  Get,
  HttpCode,
  Logger,
  NotFoundException,
  Param,
  ParseIntPipe,
  Patch,
  Post,
  Query,
  SerializeOptions,
  UseGuards,
  UseInterceptors,
  UsePipes,
  ValidationPipe,
} from '@nestjs/common';
import { AuthGuardJwt } from 'src/auth/auth-guard.jwt';
import { CurrentUser } from 'src/auth/current-user.decorator';
import { User } from 'src/auth/user.entity';
import { EventsService } from './events.service';
import { CreateEventDto } from './input/create-event.dto';
import { ListEvents } from './input/list.events';
import { UpdateEventDto } from './input/update-event.dto';

@Controller('/events')
@SerializeOptions({ strategy: 'excludeAll' })
export class EventsController {
  private readonly logger = new Logger(EventsController.name);

  constructor(private readonly eventsService: EventsService) {}

  @Get()
  @UsePipes(new ValidationPipe({ transform: true }))
  @UseInterceptors(ClassSerializerInterceptor)
  async findAll(@Query() filter: ListEvents) {
    const events =
      await this.eventsService.getEventsWithAttendeeCountFilteredPaginated(
        filter,
        {
          total: true,
          currentPage: filter.page,
          limit: 2,
        },
      );
    return events;
  }

  // @Get('/practice')
  // async practice() {
  //   // return await this.repository.find({
  //   //   select: ['id', 'when'],
  //   //   where: [{
  //   //     id: MoreThan(3),
  //   //     when: MoreThan(new Date('2021-02-12T13:00:00'))
  //   //   }, {
  //   //     description: Like('%meet%')
  //   //   }],
  //   //   take: 2,
  //   //   order: {
  //   //     id: 'DESC'
  //   //   }
  //   // });
  // }

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

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

  //   // event.attendees.push(attendee);
  //   // // event.attendees = [];

  //   // // await this.attendeeRepository.save(attendee);
  //   // await this.repository.save(event);

  //   // return event;

  //   // return await this.repository.createQueryBuilder('e')
  //   //   .select(['e.id', 'e.name'])
  //   //   .orderBy('e.id', 'ASC')
  //   //   .take(3)
  //   //   .getMany();
  // }

  @Get(':id')
  @UseInterceptors(ClassSerializerInterceptor)
  async findOne(@Param('id', ParseIntPipe) id: number) {
    // console.log(typeof id);
    const event = await this.eventsService.getEventWithAttendeeCount(id);

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

    return event;
  }

  // You can also use the @UsePipes decorator to enable pipes.
  // It can be done per method, or for every method when you
  // add it at the controller level.
  @Post()
  @UseGuards(AuthGuardJwt)
  @UseInterceptors(ClassSerializerInterceptor)
  async create(@Body() input: CreateEventDto, @CurrentUser() user: User) {
    return await this.eventsService.createEvent(input, user);
  }

  // Create new ValidationPipe to specify validation group inside @Body
  // new ValidationPipe({ groups: ['update'] })
  @Patch(':id')
  @UseGuards(AuthGuardJwt)
  @UseInterceptors(ClassSerializerInterceptor)
  async update(
    @Param('id', ParseIntPipe) id,
    @Body() input: UpdateEventDto,
    @CurrentUser() user: User,
  ) {
    const event = await this.eventsService.findOne(id);

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

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

    return await this.eventsService.updateEvent(event, input);
  }

  @Delete(':id')
  @UseGuards(AuthGuardJwt)
  @HttpCode(204)
  async remove(@Param('id', ParseIntPipe) id, @CurrentUser() user: User) {
    const event = await this.eventsService.findOne(id);

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

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

    await this.eventsService.deleteEvent(id);
  }
}

src/events/events.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Attendee } from './attendee.entity';
import { AttendeesService } from './attendees.service';
import { CurrentUserEventAttendanceController } from './current-user-event-attendance.controller';
import { EventAttendeesController } from './event-attendees.controller';
import { Event } from './event.entity';
import { EventsOrganizedByUserController } from './events-organized-by-user.controller';
import { EventsController } from './events.controller';
import { EventsService } from './events.service';

@Module({
  imports: [TypeOrmModule.forFeature([Event, Attendee])],
  controllers: [
    EventsController,
    CurrentUserEventAttendanceController,
    EventAttendeesController,
    EventsOrganizedByUserController,
  ],
  providers: [EventsService, AttendeesService],
})
export class EventsModule {}
WangShuXian6 commented 9 months ago

12 - Introduction to Testing (ManualAutomatic) 测试简介(手动-自动)