NestJS
0x01 概述
(1)简介
- 官网:https://nestjs.com/
- NestJS 是一个用于构建高效、可扩展的 NodeJS 服务器端应用的框架,使用渐进式 JavaScript 构建并完全支持 TypeScript,且结合了 OOP、FP、FRP 的元素
- 渐进式 JavaScript 构建:通过逐步增强用户体验和功能,构建高性能、可维护且适应不同设备和网络环境的 Web 应用程序
- OOP:面向对象编程
- FP:函数式编程
- FRP:函数式反应式编程
- NestJS 使用强大的 HTTP 服务器框架,如 Express 或 Fastify
(2)安装
NestJS 支持通过脚手架工具、克隆启动项目或安装核心和支持包来初始化新项目
版本信息:
- NodeJS v23.2.0
- npm v10.9.0
a. 脚手架工具
@nestjs/cli v11.0.5
- 使用命令
npm install -g @nestjs/cli全局安装脚手架 - 使用命令
nest new nest-app创建一个名为 nest-app 的应用- 选择包管理工具,如 npm
- 使用命令
cd nest-app进入项目目录 - 使用命令
npm run start:dev启动项目并访问 http://localhost:3000/
b. 克隆启动项目
- 使用命令
git clone https://github.com/nestjs/typescript-starter.git nest-app克隆 - 使用命令
cd nest-app进入项目目录 - 使用命令
npm install安装依赖 - 使用命令
npm run start:dev启动项目并访问 http://localhost:3000/
c. 安装核心和支持包
- 使用命令
mkdir nest-app创建项目目录 - 使用命令
cd nest-app进入项目目录 - 使用命令
npm install @nestjs/core @nestjs/common rxjs reflect-metadata @nestjs/platform-express安装核心和支持包- @nestjs/core:核心模块,用于构建、启动、管理 NestJS 应用
- @nestjs/common:包含构建 NestJS 应用的基础设施和常用装饰器
- rxjs:用于构建异步和事件驱动程序
- reflect-metadata:实现元编程,提供元数据反射 API,并且可以在运行时检查和操作对象的元数据
- @nestjs/platform-express:NestJS 的 Express 平台适配器
- 创建并编辑 main.ts
- 使用命令
npm run start:dev启动项目并访问 http://localhost:3000/
(3)项目结构
以脚手架工具创建的项目命目录为准
-
dist:打包目录
-
node_modules:NodeJS 依赖目录
-
src:代码资源目录
-
app.controller.spec.ts:控制器层测试文件
-
app.controller.ts:控制器层文件
-
app.module.ts:模块层文件
-
app.service.ts:服务层文件
-
main.ts:入口文件
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; /** * 该引导函数负责创建新的嵌套应用程序对象 * 启动服务器并监听端口 */ async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(process.env.PORT ?? 3000); } bootstrap();
-
-
test:E2E 测试目录
-
.eslintrc.js:Eslint 配置文件
-
.gitignore:Git 忽略配置文件
-
.prettierrc:Prettier 配置文件
-
nest-cli.json:NestJS 脚手架配置文件
-
package.json:NodeJS 包配置文件
-
tsconfig.json:TypeScript 配置文件
0x02 基础使用
(1)控制器
-
控制器是用
@Controller装饰器注解的类 -
控制器中有与特定路由相关联的动作,即函数方法
-
控制器的功能是在应用中,使用特定的路由与 HTTP 请求方式来创建端点
之后接收请求,并通过相关代码来处理该请求
最后返回响应到客户端
-
@Controller的参数有三种类型:类型 说明 空 顶级路由 字符串 一般路由 对象 路由配置对象 举例:app.controller.ts
import { Controller, Get } from '@nestjs/common'; @Controller() export class AppController { constructor() {} @Get() getHello(): string { return 'Hello'; } @Get('/bye') getBye(): string { return 'Bye'; } }启动项目后,使用以下方法进行测试
- GET 方法请求 http://localhost:3000/
- GET 方法请求 http://localhost:3000/bye
-
控制器中还支持使用其他注解实现更多 HTTP 请求方法
举例:
-
events.controller.ts
import { Controller, Delete, Get, Patch, Post } from '@nestjs/common'; @Controller('events') export class EventsController { @Post() create() { return 'create event'; } @Get() readOne() { return 'read-one event'; } @Get() readAll() { return 'read-all event'; } @Patch() update() { return 'update event'; } @Delete() delete() { return 'delete event'; } } -
events.module.ts
import { Module } from '@nestjs/common'; import { EventsController } from './events.controller'; @Module({ imports: [], controllers: [EventsController], providers: [], }) export class EventsModule {} -
main.ts
import { NestFactory } from '@nestjs/core'; import { EventsModule } from './events.module'; async function bootstrap() { const app = await NestFactory.create(EventsModule); await app.listen(process.env.PORT ?? 3000); } bootstrap(); -
启动项目并按控制器中的 HTTP 请求方法尝试请求 http://localhost:3000/events
-
(2)路由参数
- 路由参数有三种形式,分别对应不同的装饰器
- 各自的装饰器中传空值时,表示获取该参数类型中的所有参数
a. param 参数
举例:events.controller.ts
import { Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common';
@Controller('events')
export class EventsController {
// ...
@Get(':id')
readOne(@Param('id') id: string) {
return id;
}
// ...
}
启动项目并尝试访问 http://localhost:3000/events/123,http://localhost:3000/events/hello
b. query 参数
举例:events.controller.ts
import { Controller, Delete, Get, Patch, Post, Query } from '@nestjs/common';
@Controller('events')
export class EventsController {
// ...
@Get()
readAll(@Query() query: object) {
return query;
}
// ...
}
启动项目并尝试访问 http://localhost:3000/events?hello=world
c. body 参数
举例:events.controller.ts
import { Body, Controller, Delete, Get, Patch, Post } from '@nestjs/common';
@Controller('events')
export class EventsController {
@Post()
create(@Body() body: object) {
return body;
}
// ...
}
启动项目并尝试向 http://localhost:3000/events 发生 POST 请求以及以下 JSON 数据
{
"hello": "world"
}
(3)响应与状态码
HTTP 状态码参考《HTTP 常见状态码说明 | 博客园-SRIGT》
-
响应数据默认使用 JSON 格式
-
HTTP 状态码使用
@HttpCode装饰器注解相关动作 -
举例:events.controller.ts
import { Controller, Delete, Get, HttpCode, Patch, Post } from '@nestjs/common'; @Controller('events') export class EventsController { // ... @Get() @HttpCode(200) readOne() { return { id: 1, title: 'event title', description: 'event description', date: 'event date', }; } // ... }启动项目并尝试访问 http://localhost:3000/events
(4)请求有效负载
-
请求有效负载指通过定义数据传输对象类(Data Transfer Object class,DTO class)来控制接收到的数据类型
-
对于不同的请求方法(POST / PATCH / PUT),可以通过 @nestjs/mapped-types 包减少重复代码,实现对 DTO 类的类型继承,并且变为可选参数
- 使用命令
npm install --save @nestjs/mapped-types安装
- 使用命令
-
举例:
-
create-event.dto.ts
export class CreateEventDto { id: number; title: string; description: string; date: Date | string; } -
update-event.dto.ts
import { PartialType } from '@nestjs/mapped-types'; import { CreateEventDto } from './create-event.dto'; export class UpdateEventDto extends PartialType(CreateEventDto) {} -
events.controller.ts
import { Body, Controller, Delete, Get, Patch, Post } from '@nestjs/common'; import { CreateEventDto } from './create-event.dto'; import { UpdateEventDto } from './update-event.dto'; @Controller('events') export class EventsController { @Post() create(@Body() body: CreateEventDto) { return body; } // ... @Patch() update(@Body() body: UpdateEventDto) { return body; } // ... }
-
0x03 数据库
(1)对象关系映射 ORM
- 对象关系映射(Object Relational Mapping,ORM)用于解决面向对象编程语言与关系型数据库之间不匹配的问题
- TypeORM 是适用于 NodeJS 的 ORM 库,NestJS 模块提供了 TypeORM 的集成
- TypeORM 官网:https://typeorm.io/
- TypeORM 是适用于 NodeJS 的 ORM 库,NestJS 模块提供了 TypeORM 的集成
- 重要概念:
- 实体(Entity)是一个映射到数据库表的类,实体与表一一对应
- 仓库(Repository)提供程序对特定实体的数据访问
- 查询构建器(Query Builder)用于以面向对象的方法构建 SQL 查询
(2)连接数据库
MySQL 数据库基础参考《MySQL | 博客园-SRIGT》
-
使用命令
npm install --save @nestjs/typeorm typeorm mysql安装相关依赖 -
events.module.ts
import { Module } from '@nestjs/common'; import { EventsController } from './events.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; @Module({ imports: [ TypeOrmModule.forRoot({ type: 'mysql', // 数据库类型 host: 'localhost', // 数据库地址 port: 3306, // 数据库端口 username: 'root', // 数据库用户名 password: 'root', // 数据库密码 database: 'nest-db', // 数据库名 }), ], controllers: [EventsController], providers: [], }) export class EventsModule {} -
使用命令
npm run start:dev启动项目- 异常:TypeORM 报错
Client does not support authentication protocol requested by server; consider upgrading MySQL client - 原因:可能是因为最新的 mysql 模块并未完全支持 MySQL 8 的
caching_sha2_password加密方式 - 解决方法:使用以下 SQL 语句:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password_string';(其中password_string可以替换为自定义的密码字符串)
- 异常:TypeORM 报错
(3)实体(表)
-
实体类的文件名通常在后缀名前添加
.entity -
实体类需要使用
@Entity装饰器做注解,其中的参数类型包括:类型 说明 字符串 表名 对象 实体特定选项集合 -
在实体类中,所有属性均对应一个数据列,每个列都需要
@Column做注解@Column的参数为类的数据类型与选项对象,如@Column("tinyint", { default: 0 })
可以通过以下方法指定主键列:
@PrimaryGeneratedColumn用于指定自增主键列,其中参数可以为"uuid"、"rowid"、"increment"、"identity"@PrimaryColumn用于指定列为主键列
-
举例:
-
event.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; @Entity('event') export class Event { @PrimaryGeneratedColumn("increment") id: number; @Column() title: string; @Column() description: string; @Column() date: Date; } -
events.module.ts
import { Module } from '@nestjs/common'; import { EventsController } from './events.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Event } from './event.entity'; @Module({ imports: [ TypeOrmModule.forRoot({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'root', database: 'nest-db', entities: [Event], // 数据表实体 synchronize: true, // 是否自动同步数据库 }), ], controllers: [EventsController], providers: [], }) export class EventsModule {}
-
(4)仓库(查询语句)
-
仓库类分为两种:
- 一般仓库类包含
save()、find()、findOne()、remove()等基本方法,大多数方法可以通过一般仓库类实现save():创建或更新一个存在的实体find():查找多个实体,可以选择使用条件findOne():通过 ID 或指定条件来查找一个实体remove():移除实体
- 特定仓库类是为某个实体创建的独立类,主要用于某个查询使用了多次的情况
- 一般仓库类包含
-
举例:
-
events.controller.ts
import { Body, Controller, Delete, Get, Param, Patch, Post, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Event } from './event.entity'; import { CreateEventDto } from './create-event.dto'; import { UpdateEventDto } from './update-event.dto'; @Controller('events') export class EventsController { constructor( @InjectRepository(Event) private readonly repository: Repository<Event>, ) {} @Post() async create(@Body() body: CreateEventDto) { return await this.repository.save({ ...body, date: new Date(body.date), }); } @Get(':id') async readOne(@Param('id') id) { return await this.repository.findOne(id); } @Get() async readAll() { return await this.repository.find(); } @Patch(':id') async update(@Param('id') id, @Body() body: UpdateEventDto) { const event = await this.repository.findOne(id); return await this.repository.save({ ...event, ...body, date: body.date ? new Date(body.date) : event!.date, }); } @Delete(':id') async delete(@Param('id') id) { const event = await this.repository.findOne(id); return await this.repository.remove(event!); } } -
events.module.ts
import { Module } from '@nestjs/common'; import { EventsController } from './events.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Event } from './event.entity'; @Module({ imports: [ TypeOrmModule.forRoot({ // ... }), TypeOrmModule.forFeature([Event]), ], controllers: [EventsController], providers: [], }) export class EventsModule {}
-
-
仓库类支持多种查询条件与选项:
举例:
where条件查询:-
SELECT * FROM event WHERE event.id = 1;return await this.repository.find({ where: { id: 1 } }); -
SELECT * FROM event WHERE event.id > 1;return await this.repository.find({ where: { id: MoreThan(1) } // import { MoreThan } from 'typeorm'; });
更多方法参考《Find Options | TypeORM》
-
0x04 验证器
(1)管道
管道(Pipe)介于输入与输出之间,用于自动对数据进行处理
举例:events.controller.ts
import {
// ...
ParseIntPipe,
// ...
} from '@nestjs/common';
// ...
@Controller('events')
export class EventsController {
// ...
@Get(':id')
async readOne(@Param('id', ParseIntPipe) id) {
// ...
}
// ...
}
其他内置管道参考《Built-in pipes | NestJS Documentation》
(2)输入验证
-
使用命令
npm install --save class-validator class-transformer安装类验证器与类转换器库 -
create-event.dto.ts
import { IsDateString, IsString, Length } from 'class-validator'; export class CreateEventDto { @IsString({ message: '标题应为字符串' }) @Length(1, 255, { message: '标题应为1~255个字符' }) title: string; @IsString({ message: '描述应为字符串' }) @Length(1, 255, { message: '描述应为1~255个字符' }) description: string; @IsDateString({}, { message: '日期格式不正确' }) date: Date | string; } -
main.ts
import { NestFactory } from '@nestjs/core'; import { EventsModule } from './events.module'; import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { const app = await NestFactory.create(EventsModule); app.useGlobalPipes(new ValidationPipe()); // 全局校验管道 await app.listen(process.env.PORT ?? 3000); } bootstrap();
(3)组验证
通过分组的方式,实现区分应用在某个数据的同名类验证器注解
举例:
-
create-event.dto.ts
import { IsDateString, IsString, Length } from 'class-validator'; export class CreateEventDto { // ... @IsString({ message: '描述应为字符串' }) @Length(1, 255, { message: '描述应为1~255个字符', groups: ['create'] }) @Length(5, 255, { message: '描述应为5~255个字符', groups: ['update'] }) description: string; // ... } -
events.controller.ts
import { Body, Controller, Delete, Get, Param, Patch, Post, ValidationPipe, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Event } from './event.entity'; import { CreateEventDto } from './create-event.dto'; import { UpdateEventDto } from './update-event.dto'; @Controller('events') export class EventsController { // ... @Post() async create( @Body(new ValidationPipe({ groups: ['create'] })) body: CreateEventDto, ) { return await this.repository.save({ ...body, date: new Date(body.date), }); } // ... @Patch(':id') async update( @Param('id') id, @Body(new ValidationPipe({ groups: ['update'] })) body: UpdateEventDto, ) { const event = await this.repository.findOne(id); return await this.repository.save({ ...event, ...body, date: body.date ? new Date(body.date) : event!.date, }); } // ... } -
main.ts 中禁用全局校验管道
app.useGlobalPipes(new ValidationPipe());
0x05 模块
(1)概念
- 模块(Module)是一个拥有特定工具的类
- 控制器(Controller)由模块注册
- 提供者(Providers)是使用依赖注入的类的统称,使用
@Injectable装饰器做注解,由 NestJS 创建和管理 - 依赖注入(Dependency Injection,DI)用于将类所依赖的其他类或对象通过构造函数或属性自动注入
- 优点:代码解耦、容易测试
(2)自定义模块
-
使用命令
nest generate module events通过 NestJS 脚手架生成名为 events 的模块- 会自动创建 events 目录与 events.module.ts,并在 app.module.ts 中的
@Module注解中新增imports: [EventsModule]
- 会自动创建 events 目录与 events.module.ts,并在 app.module.ts 中的
-
将原来 events 模块相关文件复制到 events 目录中
-
create-event.dto.ts、update-event.dto.ts、event.entity.ts、events.controller.ts
-
events.module.ts
import { Module } from '@nestjs/common'; import { EventsController } from './events.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Event } from './event.entity'; @Module({ imports: [ TypeOrmModule.forFeature([Event]), ], controllers: [EventsController], providers: [], }) export class EventsModule {}
-
-
app.module.ts
import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { EventsModule } from './events/events.module'; import { TypeOrmModule } from '@nestjs/typeorm'; @Module({ imports: [ TypeOrmModule.forRoot({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'root', database: 'nest-db', entities: [Event], synchronize: true, }), EventsModule, ], controllers: [AppController], providers: [AppService], }) export class AppModule {} -
main.ts
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(process.env.PORT ?? 3000); } bootstrap();
(3)静态模块与动态模块
- 静态模块通过
@Module装饰器定义的,具有固定提供者和导入的模块 - 动态模块通过模块类中的静态方法(如
register、forRoot)返回的,允许在运行时根据条件动态配置提供者或导入的模块 - 举例:静态模块——Events,动态模块——TypeOrmModule
(4)提供者
提供者分为三种:类提供者(Class Provider)、值提供者(Value Provider)、工厂提供者(Factory Provider)
a. 类提供者
-
app.chinese.service.ts
export class AppChineseService { getHello(): string { return '你好,世界!'; } } -
app.module.ts
// ... import { AppService } from './app.service'; import { AppChineseService } from './app.chinese.service'; @Module({ // ... providers: [ { provide: AppService, useClass: AppChineseService, }, ], }) export class AppModule {} -
启动项目并访问 http://localhost:3000/
b. 值提供者
-
app.module.ts
// ... @Module({ // ... providers: [ AppService, { provide: 'APP_NAME', useValue: 'Nest App', }, ], }) export class AppModule {} -
app.service.ts
import { Inject, Injectable } from '@nestjs/common'; @Injectable() export class AppService { constructor( @Inject('APP_NAME') private readonly appName: string, ) {} getHello(): string { return `Hello World! from ${this.appName}`; } }
c. 工厂提供者
-
app.dummy.ts
export class AppDummy { public dummy(): string { return 'dummy'; } } -
app.module.ts
// ... import { AppDummy } from './app.dummy'; @Module({ // ... providers: [ AppService, { provide: 'APP_MESSAGE', inject: [AppDummy], useFactory: (appDummy) => `${appDummy.dummy()} Factory`, }, AppDummy, ], }) export class AppModule {} -
app.service.ts
import { Inject, Injectable } from '@nestjs/common'; @Injectable() export class AppService { constructor( @Inject('APP_MESSAGE') private readonly message: string, ) {} getHello(): string { return `Hello World! from ${this.message}`; } }
0x06 配置
(1)应用配置与环境
NestJS 官方提供了 config 模块用于配置管理
使用命令 npm install --save @nestjs/config 安装 config 模块
a. .env
-
在根目录新建 .env 文件
DB_TYPE=mysql DB_HOST=localhost DB_PORT=3306 DB_USERNAME=root DB_PASSWORD=root DB_DATABASE=nest-db -
app.module.ts
// ... import { ConfigModule } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; @Module({ imports: [ ConfigModule.forRoot(), TypeOrmModule.forRoot({ type: process.env.DB_TYPE as any, host: process.env.DB_HOST, port: Number(process.env.DB_PORT), username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, database: process.env.DB_DATABASE, entities: [Event], synchronize: true, }), EventsModule, ], // ... }) export class AppModule {}
b. 自定义配置文件
-
src/config/orm.config.ts
import { registerAs } from '@nestjs/config'; import { TypeOrmModuleOptions } from '@nestjs/typeorm'; import { Event } from 'src/events/event.entity'; export default registerAs( 'orm.config', (): TypeOrmModuleOptions => ({ type: process.env.DB_TYPE as any, host: process.env.DB_HOST, port: Number(process.env.DB_PORT), username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, database: process.env.DB_DATABASE, entities: [Event], synchronize: true, }), );src/config/orm.config.prod.ts
import { registerAs } from '@nestjs/config'; import { TypeOrmModuleOptions } from '@nestjs/typeorm'; import { Event } from 'src/events/event.entity'; export default registerAs( 'orm.config', (): TypeOrmModuleOptions => ({ type: process.env.DB_TYPE as any, host: process.env.DB_HOST, port: Number(process.env.DB_PORT), username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, database: process.env.DB_DATABASE, entities: [Event], synchronize: false, // 生产环境禁用数据库同步 }), ); -
app.module.ts
// ... import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule } from '@nestjs/config'; import ormConfig from './config/orm.config'; import ormConfigProd from './config/orm.config.prod'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, // 是否作为全局配置 load: [ormConfig, ormConfigProd], // 加载配置文件 }), TypeOrmModule.forRootAsync({ // 异步 useFactory: process.env.NODE_ENV !== 'production' ? ormConfig : ormConfigProd, }), // ... ], // ... }) export class AppModule {}
c. 变量展开
-
.env
APP_URL=example.com SUPPORT_EMAIL=support@${APP_URL} -
app.module.ts
// ... @Module({ imports: [ ConfigModule.forRoot({ // ... expandVariables: true, // 是否展开变量 // process.env.SUPPORT_EMAIL === "support@example.com" }), // ... ], // ... }) export class AppModule {}
(2)日志
-
events.controller.ts
import { Logger, // ... } from '@nestjs/common'; // ... @Controller('events') export class EventsController { private readonly logger = new Logger(EventsController.name); // ... @Get(':id') async readOne(@Param('id') id) { this.logger.log(`Reading event with id ${id}`); const events = await this.repository.findOne(id); this.logger.debug(`Found event: ${events}`); return events; } // ... } -
main.ts
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule, { logger: ['error', 'warn', 'debug', 'log', 'verbose'], }); await app.listen(process.env.PORT ?? 3000); } bootstrap();
0x07 关系
关系可以分为三类:
- 一对一关系:如个人与身份证号码
- 一对多关系:如文章与评论
- 多对多关系:如老师与学生
(1)一对多关系
单个事件(Event)中存在多个参与者(Attendee)
a. 创建关系
-
attendee.entity.ts
import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn, } from 'typeorm'; import { Event } from './event.entity'; @Entity('attendee') export class Attendee { @PrimaryGeneratedColumn('uuid') id: number; @Column() name: string; // 多对一注解 @ManyToOne( () => Event, // 关联的实体类型 (event) => event.attendees, // 关联的字段 { nullable: false, // 不能为空 }, ) // 连接列注解 @JoinColumn() event: Event; } -
events.entity.ts
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; import { Attendee } from './attendee.entity'; @Entity('event') export class Event { @PrimaryGeneratedColumn('increment') id: number; @Column() title: string; @Column() description: string; @Column() date: Date; // 一对多注解 @OneToMany( () => Attendee, // 关联的实体类型 (attendee) => attendee.event, // 关联的字段 { nullable: false, // 是否允许为空 }, ) attendees: Attendee[]; } -
orm.config.ts
import { registerAs } from '@nestjs/config'; import { TypeOrmModuleOptions } from '@nestjs/typeorm'; import { Attendee } from 'src/events/attendee.entity'; import { Event } from 'src/events/event.entity'; export default registerAs( 'orm.config', (): TypeOrmModuleOptions => ({ // ... entities: [Attendee, Event], synchronize: true, }), );
b. 载入关系实体
载入方法包括:立即加载(eager)、延迟加载(懒加载,lazy)
-
event.entity.ts
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; import { Attendee } from './attendee.entity'; @Entity('event') export class Event { // ... @OneToMany( () => Attendee, (attendee) => attendee.event, { eager: true, }, ) attendees: Attendee[]; } -
events.controller.ts
// ... @Controller('events') export class EventsController { // ... @Get(':id/attendees') async getAttendees(@Param('id') id) { const event = await this.repository.findOne({ where: { id }, relations: ['attendees'], }); if (!event) return null; return event.attendees; } }
c. 联合关系实体
-
events.module.ts
import { Module } from '@nestjs/common'; import { EventsController } from './events.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Attendee } from './attendee.entity'; import { Event } from './event.entity'; @Module({ imports: [TypeOrmModule.forFeature([Attendee, Event])], controllers: [EventsController], providers: [], }) export class EventsModule {} -
events.controller.ts
// ... @Controller('events') export class EventsController { // ... constructor( @InjectRepository(Attendee) private readonly attendeeRepository: Repository<Attendee>, @InjectRepository(Event) private readonly eventRepository: Repository<Event>, ) {} // ... } -
方法一:
@Get(':id/attendees') async saveAttendees(@Param('id') id) { const event = await this.eventRepository.findOne(id); if (!event) return null; const attendee = new Attendee(); attendee.name = 'Alice'; attendee.event = event; return await this.attendeeRepository.save(attendee); }方法二:
@Get(':id/attendees') async saveAttendees(@Param('id') id) { const event = new Event(); event.id = id; const attendee = new Attendee(); attendee.name = 'Alice'; attendee.event = event; return await this.attendeeRepository.save(attendee); }方法三:
-
event.entity.ts
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; import { Attendee } from './attendee.entity'; @Entity('event') export class Event { // ... @OneToMany( () => Attendee, (attendee) => attendee.event, { cascade: true, // 是否级联保存、更新、删除 }, ) attendees: Attendee[]; } -
events.controller.ts
@Get(':id/attendees') async saveAttendees(@Param('id') id) { const event = await this.repository.findOne({ where: { id }, relations: ['attendees'], }); const attendee = new Attendee(); attendee.name = 'Alice'; event.relations.push(attendee); return await this.eventRepository.save(event); }
-
(2)一对一关系
单个事件(Event)中存在单个委员会(Committee)
-
committee.entity.ts
import { Column, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn } from 'typeorm'; import { Event } from './event.entity'; @Entity('committee') export class Committee { @PrimaryGeneratedColumn('uuid') id: number; @Column() name: string; @OneToOne(() => Event) @JoinColumn() event: Event; } -
event.entity.ts
import { // ... OneToOne, } from 'typeorm'; // ... import { Committee } from './committee.entity'; @Entity('event') export class Event { // ... // 一对一注解 @OneToOne(() => Committee) committee: Committee; } -
orm.config.ts
// ... import { Committee } from 'src/events/committee.entity'; export default registerAs( 'orm.config', (): TypeOrmModuleOptions => ({ // ... entities: [Committee, Event], synchronize: true, }), );
(3)多对多关系
多个事件(Event)中存在多个主持人(Presenter)
-
presenter.entity.ts
import { Column, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn, } from 'typeorm'; import { Event } from './event.entity'; @Entity('presenter') export class Presenter { @PrimaryGeneratedColumn('uuid') id: number; @Column() name: string; // 多对多注解 @ManyToMany(() => Presenter, (presenter) => presenter.id, { cascade: true, }) // 连接表注解 @JoinTable() events: Event[]; } -
event.entity.ts
import { Column, Entity, ManyToMany, OneToMany, PrimaryGeneratedColumn, } from 'typeorm'; import { Presenter } from './presenter.entity'; // ... @Entity('event') export class Event { // ... // 多对多注解 @ManyToMany(() => Presenter, (presenter) => presenter.events) presenters: Presenter[]; } -
orm.config.ts
// ... import { Presenter } from 'src/events/presenter.entity'; export default registerAs( 'orm.config', (): TypeOrmModuleOptions => ({ // ... entities: [Attendee, Event, Presenter], synchronize: true, }), ); -
events.module.ts
// ... import { Presenter } from './presenter.entity'; @Module({ imports: [TypeOrmModule.forFeature([Attendee, Event, Presenter])], controllers: [EventsController], providers: [], }) export class EventsModule {} -
events.controller.ts
// ... import { Presenter } from './presenter.entity'; @Controller('events') export class EventsController { constructor( // ... @InjectRepository(Presenter) private readonly presenterRepository: Repository<Presenter>, ) {} // ... @Get(':id/presenters') async savePresenters(@Param('id') id) { const event = await this.eventRepository.findOne(id); if (!event) return null; const presenter1 = new Presenter(); presenter1.name = 'Bob'; const presenter2 = new Presenter(); presenter2.name = 'Carol'; event.presenters = [presenter1, presenter2]; return await this.eventRepository.save(event); } @Delete(':id/presenters/:name') async deletePresenters(@Param('id') id, @Param('name') name) { const event = await this.eventRepository.findOne(id); if (!event) return null; event.presenters = event.presenters.filter( (presenter) => presenter.name !== name, ); return await this.eventRepository.save(event); } }
(4)查询构建器
查询构建器用于使用面向对象的语法构建查询
a. 创建查询构建器
-
events.service.ts
import { Repository } from 'typeorm'; import { Event } from './event.entity'; import { InjectRepository } from '@nestjs/typeorm'; import { Injectable, Logger } from '@nestjs/common'; @Injectable() export class EventsService { private readonly logger = new Logger(EventsService.name); constructor( @InjectRepository(Event) private readonly repository: Repository<Event>, ) {} private getEventsBaseQuery() { return this.repository .createQueryBuilder('e') // 创建一个查询构建器,e 是表的别名 .orderBy('e.id', 'DESC'); // 按照 id 降序排列 } public async getEvent(id: number): Promise<Event | null> { const query = this.getEventsBaseQuery() // 获取基础查询 .andWhere('e.id = :id', { id }); // 添加一个条件,id 等于传入的 id this.logger.debug(query.getSql()); // 打印 SQL 语句 return await query.getOne(); } } -
events.controller.ts
import { NotFoundException, // ... } from '@nestjs/common'; // ... import { EventsService } from './events.service'; @Controller('events') export class EventsController { constructor( // ... private readonly eventsService: EventsService, ) {} // ... @Get(':id') async readOne(@Param('id') id) { const event = await this.eventsService.getEvent(id); if (!event) throw new NotFoundException(); return event; } // ... } -
events.module.ts
// ... import { EventsService } from './events.service'; @Module({ // ... providers: [EventsService], }) export class EventsModule {}
b. 聚合
-
event.entity.ts
// ... @Entity('event') export class Event { // ... attendeeCount?: number; } -
events.service.ts
// ... @Injectable() export class EventsService { // ... private getEventsWithAttendeeCountQuery() { return this.getEventsBaseQuery().loadRelationCountAndMap( 'e.attendeeCount', 'e.attendees', ); } public async getEvent(id: number): Promise<Event | null> { const query = this.getEventsWithAttendeeCountQuery().andWhere( 'e.id = :id', { id }, ); this.logger.debug(query.getSql()); return await query.getOne(); } }
c. 连接
-
attendee.entity.ts
// ... export enum AttendeeAnswerEnum { Accepted = 1, Maybe = 2, Rejected = 3, } @Entity('attendee') export class Attendee { // ... @Column({ type: 'enum', enum: AttendeeAnswerEnum, default: AttendeeAnswerEnum.Maybe, }) answer: AttendeeAnswerEnum; } -
events.entity.ts
// ... @Entity('event') export class Event { // ... attendeeRejected?: number; attendeeMaybe?: number; attendeeAccepted?: number; } -
events.service.ts
// ... import { AttendeeAnswerEnum } from './attendee.entity'; @Injectable() export class EventsService { // ... private getEventsWithAttendeeCountQuery() { return this.getEventsBaseQuery() .loadRelationCountAndMap( 'e.attendeeAccepted', 'e.attendees', 'attendee', (queryBuilder) => queryBuilder.where('attendee.answer = :answer', { answer: AttendeeAnswerEnum.Accepted, }), ) .loadRelationCountAndMap( 'e.attendeeAccepted', 'e.attendees', 'attendee', (queryBuilder) => queryBuilder.where('attendee.answer = :answer', { answer: AttendeeAnswerEnum.Maybe, }), ) .loadRelationCountAndMap( 'e.attendeeAccepted', 'e.attendees', 'attendee', (queryBuilder) => queryBuilder.where('attendee.answer = :answer', { answer: AttendeeAnswerEnum.Rejected, }), ); } // ... }
d. 过滤
-
list.events.ts
export enum DateEventFilter { All = 1, Today, Tomorrow, ThisWeek, NextWeek, } export class ListEvents { date?: DateEventFilter = DateEventFilter.All; } -
events.service.ts
// ... import { ListEvents, DateEventFilter } from './list.events'; @Injectable() export class EventsService { // ... public async getEventsWithAttendeeCountFiltered(filter?: ListEvents) { let query = this.getEventsWithAttendeeCountQuery(); if (!filter) return await query.getMany(); if (filter.date) { switch (filter.date) { case DateEventFilter.Today: query = query.andWhere( 'e.date >= CURDATE() and e.date <= CURDATE() + INTERVAL 1 DAY', ); break; case DateEventFilter.Tomorrow: query = query.andWhere( 'e.date >= CURDATE() + INTERVAL 1 DAY and e.date <= CURDATE() + INTERVAL 2 DAY', ); break; case DateEventFilter.ThisWeek: query = query.andWhere( 'YEARWEEK(e.date, 1) = YEARWEEK(CURDATE(), 1)', ); break; case DateEventFilter.NextWeek: query = query.andWhere( 'YEARWEEK(e.date, 1) = YEARWEEK(CURDATE(), 1) + 1', ); break; } } return await query.getMany(); } } -
events.controller.ts
import { ParseIntPipe, // ... } from '@nestjs/common'; // ... import { EventsService } from './events.service'; import { ListEvents } from './list.events'; @Controller('events') export class EventsController { constructor( // ... private readonly eventsService: EventsService, ) {} // ... @Get() async readAll(@Query(ParseIntPipe) filter: ListEvents) { return await this.eventsService.getEventsWithAttendeeCountFiltered(filter); } // ... }
e. 分页
-
src\pagination\paginator.ts
import { ObjectLiteral, SelectQueryBuilder } from 'typeorm'; /** * 分页选项 */ export interface PaginateOptions { limit: number; currentPage: number; total?: boolean; } /** * 分页结果 */ export interface PaginationResult<T> { firstPage: number; lastPage: number; limit: number; total?: number; data: T[]; } /** * 分页查询 * @param queryBuilder 查询构建器 * @param options 分页选项 * @returns 分页结果 */ export async function paginate<T extends ObjectLiteral>( queryBuilder: SelectQueryBuilder<T>, options: PaginateOptions = { limit: 10, currentPage: 1, total: false, }, ): Promise<PaginationResult<T>> { const offset = (options.currentPage - 1) * options.limit; const data = await queryBuilder.limit(options.limit).offset(offset).getMany(); return { firstPage: offset + 1, lastPage: offset + data.length, limit: options.limit, total: options.total ? await queryBuilder.getCount() : undefined, data, }; } -
list.events.ts
// ... export class ListEvents { // ... page: number = 1; } -
events.service.ts
// ... import { paginate, PaginateOptions } from 'src/pagination/paginator'; @Injectable() export class EventsService { // ... // 改为私有方法,并移除 .getMany() private async getEventsWithAttendeeCountFiltered(filter?: ListEvents) { let query = this.getEventsWithAttendeeCountQuery(); if (!filter) return query; // ... return query; } public async getEventsWithAttendeeCountFilteredPaginated( filter: ListEvents, paginateOptions: PaginateOptions, ) { return await paginate( await this.getEventsWithAttendeeCountFiltered(filter), paginateOptions, ); } } -
events.controller.ts
import { // ... UsePipes, ValidationPipe, } from '@nestjs/common'; // ... import { EventsService } from './events.service'; import { ListEvents } from './list.events'; @Controller('events') export class EventsController { // ... @Get() @UsePipes( new ValidationPipe({ transform: true, // 将查询参数转换为数字 }), ) async readAll(@Query(ParseIntPipe) filter: ListEvents) { return await this.eventsService.getEventsWithAttendeeCountFilteredPaginated( filter, { total: true, currentPage: filter.page, limit: 10, }, ); } // ... }
f. 其他操作
举例:删除实体
-
events.service.ts
import { DeleteResult, Repository } from 'typeorm'; // ... @Injectable() export class EventsService { constructor( @InjectRepository(Event) private readonly repository: Repository<Event>, ) {} // ... public async deleteEvent(id: number): Promise<DeleteResult> { return this.repository .createQueryBuilder('e') .delete() .where('id = :id', { id, }) .execute(); } } -
events.controller.ts
// ... @Controller('events') export class EventsController { // ... @Delete(':id') async delete(@Param('id') id) { const result = await this.eventsService.deleteEvent(id); if (result?.affected !== 1) throw new NotFoundException(); // affected 为 1 表示删除操作成功 return; } // ... }
修改实体:
.update().set(新数据对象).where().execute();修改关系:
.relation(目标实体, 列).of(当前实体).add(新数据列表);
0x08 认证
(1)概述
- 认证指当用户进入系统时,需要验证用户是否拥有且有效的身份凭证
- Passport 是常用的认证库
- 策略(Strategy)是一个用于校验用户凭证合法性的类,通过本地数据库校验(本地策略)或 JWT 策略来实现
- 使用命令
npm install --save @nestjs/passport passport安装适用于 NestJS 的 Passport 及其他相关依赖
- 使用命令
- 策略(Strategy)是一个用于校验用户凭证合法性的类,通过本地数据库校验(本地策略)或 JWT 策略来实现
- Bcrypt 是常用的密码哈希算法
- 对于同一字符串的加密结果相同,适用于密码加密与验证
- 使用命令
npm install bcrypt与npm install --save-dev @types/bcrypt安装 Bcrypt 及类型声明包
- 使用命令
- 对于同一字符串的加密结果相同,适用于密码加密与验证
使用命令
nest generate module auth生成 auth 模块
(2)Passport
a. 本地策略
-
使用命令
npm install --save passport-local安装 Passport 本地策略依赖 -
使用命令
npm install --save-dev @types/passport-local安装类型声明包 -
src\auth\user.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; @Entity('user') export class User { @PrimaryGeneratedColumn('uuid') id: number; @Column() username: string; @Column() password: string; } -
src\config\orm.config.ts
// ... import { User } from 'src/auth/user.entity'; export default registerAs( 'orm.config', (): TypeOrmModuleOptions => ({ // ... entities: [/* ... */ User], synchronize: true, }), ); -
src\auth\local.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { InjectRepository } from '@nestjs/typeorm'; import { Strategy } from 'passport-local'; import { User } from './user.entity'; import { Repository } from 'typeorm'; @Injectable() export class LocalStrategy extends PassportStrategy(Strategy) { constructor( @InjectRepository(User) private readonly userRepository: Repository<User>, ) { super(); } public async validate(username: string, password: string): Promise<any> { const user = await this.userRepository.findOne({ where: { username } }); if (!user || user.password !== password) throw new UnauthorizedException(); return user; } } -
src\auth\auth.controller.ts
import { Controller, Post, Request, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; @Controller('auth') export class AuthController { @Post('login') @UseGuards(AuthGuard('local')) // 使用守卫,调用策略来验证凭证('local' 字符串在 LocalStrategy 类继承时确定) async login(@Request() request) { return { userId: request.user.id, }; } } -
src\auth\auth.module.ts
import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from './user.entity'; import { AuthController } from './auth.controller'; import { LocalStrategy } from './local.strategy'; @Module({ imports: [TypeOrmModule.forFeature([User])], controllers: [AuthController], providers: [LocalStrategy], }) export class AuthModule {}
b. JWT
Json Web Token(JWT)是一种令牌,安全且轻量,优缺点参考《NodeJS 与 Express-登录鉴权-JWT | 博客园-SRIGT》
-
使用命令
npm install --save @nestjs/jwt passport-jwt安装 Passport.js JWT 策略与其他依赖 -
使用命令
npm install --save-dev @types/passport-jwt安装类型声明包 -
.env
JWT_SECRET=JSONWEBTOKENSECRET -
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, }); } } -
src\auth\jwt.strategy.ts
import { Injectable } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { InjectRepository } from '@nestjs/typeorm'; import { ExtractJwt, Strategy, StrategyOptionsWithRequest } from 'passport-jwt'; import { User } from './user.entity'; import { Repository } from 'typeorm'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor( @InjectRepository(User) private readonly userRepository: Repository<User>, ) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), // 从请求头中提取 JWT ignoreExpiration: false, // 不在此处检查 JWT 是否过期 secretOrKey: process.env.JWT_SECRET, // 用于验证 JWT 的密钥 } as StrategyOptionsWithRequest); } async validate(payload: any) { return await this.userRepository.findOne(payload.sub); } } -
src\auth\auth.module.ts
// ... import { JwtModule } from '@nestjs/jwt'; import { JwtStrategy } from './jwt.strategy'; import { AuthService } from './auth.service'; @Module({ imports: [ // ... JwtModule.registerAsync({ useFactory: () => ({ secret: process.env.JWT_SECRET, // JWT 加密密钥 signOptions: { expiresIn: '24h' }, // JWT 有效期,如 24 小时 }), }), ], // ... providers: [/* ... */, JwtStrategy AuthService], }) export class AuthModule {} -
src\auth\profile.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; @Entity('profile') export class Profile { @PrimaryGeneratedColumn() id: number; @Column() age: number; } -
src\config\orm.config.ts
// ... import { Profile } from 'src/auth/profile.entity'; import { User } from 'src/auth/user.entity'; export default registerAs( 'orm.config', (): TypeOrmModuleOptions => ({ // ... entities: [/* ... */ Profile, User], synchronize: true, }), ); -
src\auth\user.entity.ts
import { Column, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn, } from 'typeorm'; import { Profile } from './profile.entity'; @Entity('user') export class User { // ... @OneToOne(() => Profile) @JoinColumn() profile: Profile; } -
src\auth\auth.controller.ts
import { Controller, Get, Post, Request, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { AuthService } from './auth.service'; @Controller('auth') export class AuthController { // ... @Get('profile') @UseGuards(AuthGuard('jwt')) async getProfile(@Request() request) { return request.user; } }
(3)Bcrypt
a. 密码哈希加密与比较
-
auth.service.ts
// ... import * as bcrypt from 'bcrypt'; @Injectable() export class AuthService { // ... public async hashPassword(password: string): Promise<string> { return await bcrypt.hash(password, 10); } } -
local.strategy.ts
// ... import * as bcrypt from 'bcrypt'; @Injectable() export class LocalStrategy extends PassportStrategy(Strategy) { // ... public async validate(username: string, password: string): Promise<any> { const user = await this.userRepository.findOne({ where: { username } }); if (!(user && (await bcrypt.compare(password, user.password)))) throw new UnauthorizedException(); return user; } }
b. 自定义修饰器用于验证用户
-
src\auth\current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common'; export const CurrentUser = createParamDecorator( (data: unknown, context: ExecutionContext) => { const request = context.switchToHttp().getRequest(); return request.user ?? null; }, ); -
auth.controller.ts
// ... import { CurrentUser } from './current-user.decorator'; @Controller('auth') export class AuthController { constructor(private readonly authService: AuthService) {} @Post('login') @UseGuards(AuthGuard('local')) async login(@CurrentUser() user) { return { userId: user.id, token: this.authService.getTokenForUser(user), }; } @Get('profile') @UseGuards(AuthGuard('jwt')) async getProfile(@CurrentUser() user) { return user; } }
c. 用户注册
-
user.entity.ts
// ... @Entity('user') export class User { // ... @Column({ unique: true }) username: string; // ... } -
src\auth\create-user.dto.ts
import { IsString, Length } from 'class-validator'; export class CreateUserDto { @IsString({ message: '用户名应为字符串' }) @Length(1, 255, { message: '用户名应为1~255个字符' }) username: string; @IsString({ message: '密码应为字符串' }) @Length(1, 255, { message: '密码应为1~255个字符' }) password: string; @IsString({ message: '确认密码应为字符串' }) @Length(1, 255, { message: '确认密码应为1~255个字符' }) retypedPassword: string; } -
src\auth\users.controller.ts
import { BadRequestException, Body, Controller, Post } from '@nestjs/common'; import { AuthService } from './auth.service'; import { CreateUserDto } from './create-user.dto'; import { User } from './user.entity'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; @Controller('users') export class UsersController { constructor( private readonly authService: AuthService, @InjectRepository(User) private readonly userRepository: Repository<User>, ) {} @Post() async create(@Body() body: CreateUserDto) { const user = new User(); if (body.password !== body.retypedPassword) throw new BadRequestException(['密码不一致']); const existingUser = await this.userRepository.findOne({ where: { username: body.username }, }); if (existingUser) throw new BadRequestException(['用户名已存在']); user.username = body.username; user.password = await this.authService.hashPassword(body.password); return { ...(await this.userRepository.save(user)), token: this.authService.getTokenForUser(user), }; } } -
auth.module.ts
// ... import { UsersController } from './users.controller'; @Module({ // ... controllers: [/* ... */ UsersController], // ... }) export class AuthModule {}
0x09 序列化
(1)拦截器与序列化
- 拦截器(Interceptor)可以拦截并修改控制器返回的响应,由 NestInterceptor 实现,具有以下功能:
- 在方法前后绑定额外的逻辑
- 通过函数转换返回的结果与抛出的异常
- 扩展基础函数的功能
- 重写指定函数
- 序列化(Serialization)用于将一个对象从一种状态转换为另一种状态
- 如:将一个实体转换为 JSON,其中仅包含白名单中的属性
(2)序列化数据
-
使用命令
npm installl --save class-transformer安装类转换器库(在 0x04 章(2)节安装过) -
events.controller.ts
import { // ... ClassSerializerInterceptor, SerializeOptions, UseInterceptors, } from '@nestjs/common'; // ... @Controller('events') @SerializeOptions({ strategy: 'excludeAll', // 不包含所有未标记的属性 }) export class EventsController { // ... @Get(':id') @UseInterceptors(ClassSerializerInterceptor) // 序列化响应 async readOne(@Param('id') id) { // ... } // ... } -
event.entity.ts
// ... import { Expose } from 'class-transformer'; @Entity('event') export class Event { @PrimaryGeneratedColumn('increment') id: number; @Column() @Expose() title: string; @Column() @Expose() description: string; @Column() @Expose() date: Date; // ... @Expose() attendeeCount?: number; @Expose() attendeeRejected?: number; @Expose() attendeeMaybe?: number; @Expose() attendeeAccepted?: number; }
(3)序列化嵌套对象
序列化分页器处返回的对象
paginator.ts
import { Expose } from 'class-transformer';
import { ObjectLiteral, SelectQueryBuilder } from 'typeorm';
export interface PaginateOptions {
limit: number;
currentPage: number;
total?: boolean;
}
// 将接口转换为类
export class PaginationResult<T> {
constructor(partial: Partial<PaginationResult<T>>) {
Object.assign(this, partial);
}
@Expose()
firstPage: number;
@Expose()
lastPage: number;
@Expose()
limit: number;
@Expose()
total?: number;
@Expose()
data: T[];
}
export async function paginate<T extends ObjectLiteral>(
queryBuilder: SelectQueryBuilder<T>,
options: PaginateOptions = {
limit: 10,
currentPage: 1,
total: false,
},
): Promise<PaginationResult<T>> {
const offset = (options.currentPage - 1) * options.limit;
const data = await queryBuilder.limit(options.limit).offset(offset).getMany();
// 返回实例化对象
return new PaginationResult({
firstPage: offset + 1,
lastPage: offset + data.length,
limit: options.limit,
total: options.total ? await queryBuilder.getCount() : undefined,
data,
});
}
-End-

浙公网安备 33010602011771号