# nestplus3 **Repository Path**: yhding/nestplus3 ## Basic Information - **Project Name**: nestplus3 - **Description**: 根据官网教程一步步生成的项目 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-09-27 - **Last Updated**: 2024-11-22 ## Categories & Tags **Categories**: Uncategorized **Tags**: Postgresql, TypeScript, Nestjs, Pnpm ## README

Nest Logo

[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 [circleci-url]: https://circleci.com/gh/nestjs/nest

A progressive Node.js framework for building efficient and scalable server-side applications.

NPM Version Package License NPM Downloads CircleCI Coverage Discord Backers on Open Collective Sponsors on Open Collective Support us

## Description [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. ## Installation ```bash $ npm install ``` ## Running the app ```bash # development $ npm run start # watch mode $ npm run start:dev # production mode $ npm run start:prod ``` ## Test ```bash # unit tests $ npm run test # e2e tests $ npm run test:e2e # test coverage $ npm run test:cov ``` ## Support Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). ## Stay in touch - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) - Website - [https://nestjs.com](https://nestjs.com/) - Twitter - [@nestframework](https://twitter.com/nestframework) ## License Nest is [MIT licensed](LICENSE). ## DTO 数据传输对象 data transfer object 数据传输对象 aka(also known as)称作 DTO - 主要是做数据类型安全的,确保请求接口的参数形状都是类似的。 - class-validator 用来做数据验证,看官网 - @nestjs/mapped-types PartialType 会自动添加可选项装饰器。不用重复写 - new ValidationPipe({ whitelist: true }) 会将我们传入的多余字段过滤掉。 ```ts // dto class MyDto { name: string } // /api/create body {name: '1', isOk: true} // 入库是只会是 {name: '1'} ``` forbidNonWhitelisted: true,表示不能传递dto之外的数据。 - dto实例 VS DTO class transform 未开启时,dto instance 并不是 DTO class的实例 ```ts // main.ts new ValidationPipe({ ... // transform: true, ... }) // controller.ts { // body @Post() create(@Body() createCoffeeDto: CreateCoffeeDto) { console.log(createCoffeeDto instanceof CreateCoffeeDto); // false return this.coffeesService.create(createCoffeeDto); } } ``` 开启 transform: true ```ts // main.ts new ValidationPipe({ ... transform: true, ... }) // controller.ts { // body @Post() create(@Body() createCoffeeDto: CreateCoffeeDto) { console.log(createCoffeeDto instanceof CreateCoffeeDto); // true return this.coffeesService.create(createCoffeeDto); } } ``` transform 自动将类型对齐 ```ts // main.ts new ValidationPipe({ ... transform: true, ... }) // controller.ts @Get(':id') findById(@Param('id') id: number) { // transform 自动将参数转换成number console.log(typeof id); // number return this.coffeesService.findOne('' + id); } ``` ```ts // main.ts new ValidationPipe({ ... ... }) // controller.ts @Get(':id') findById(@Param('id') id: number) { console.log(typeof id); // NOTICE: string return this.coffeesService.findOne('' + id); } ``` ## docker启动postgres数据库 新建一个docker-compose.yml ```yml # docker-compose.yml # Use postgres/example user/password credentials version: '3.1' services: db: image: postgres restart: always ports: - "5432:54321" environment: POSTGRES_PASSWORD: pass123 ``` 运行如下命令 ```bash docker-compose up -d # 运行全部容器 docker-compose up db -d # 运行名字叫 db 的容器 ``` 连接到 postgres 数据库,[文档](https://docs.nestjs.com/techniques/database#multiple-databases) 出现启动docker是报错见此[issue](https://stackoverflow.com/a/66198584/9672709) ```bash $ docker compose up -d [+] Running 0/1 - Container nest01-sss-1 Starting 0.1s Error response from daemon: Ports are not available: exposing port TCP 0.0.0.0:54322 -> 0.0.0.0:0: listen tcp 0.0.0.0:54322: bind: An attempt was made to access a socket in a way forbidden by its access permissions. ``` ## 自动建立表格 每一个entity对应数据库的一张表 ```ts TypeOrmModule.forRoot({ host: 'localhost', port: 5432, username: 'postgres', password: 'pass123', database: 'postgres', synchronize: true, // 确保typeorm 实体,每次运行应用程序时都会与数据库同步,建立数据库表 autoLoadEntities: true, // 自动加载模块,而不是指定实体数组 }) ``` ## 每当与数据库进行同步时会有一个存储库(repository) 通过 repository 进行数据库交互,需将将我们的repository通过@InjectRepository() 注入到service里 ## 增删改查相关操作 [typeorm.io](https://typeorm.io/) ## 表关系 [many to many](https://typeorm.bootcss.com/many-to-many-relations) 先有两个实体 EntityA,EntityB类,对应Entitya,Entityb实例。现想建立以EntityA为Owner的多对多关系,则需要如下操作 ```ts // EntityA class EntityA { @JoinTable() // (1) @ManyToMany( () => EntityB, // (2.1) entityb => entityb.a // (2.2) ) b: EntityB[] } // EntityB class EntityB { @ManyToMany( () => EntityA, // (3.1) entitya => entitya.b, // (3.2) ) a: EntityA[] } ``` 如上代码所示: EntityA实体通过`(1)`指定当前实体对应的表格是owner端,通过`(2.1)`指定“相关”实体的类型,通过`(2.2)`指定返回的“相关”实体实例的属性,与当前实体EntityA建立反向关系(EntityB中的EntityA是哪个属性)。 EntityB实体不是表格的owner端不需要`(1)`处类似的操作,通过`(3.1)`指定了“相关”实体的类型,通过`(3.2)`指定选择的“相关”实体的属性,与当前实体EntityB建立反向关系(EntityA中EntityB是哪个属性)。 对应的表关系,见官方文档。 ## 级联查询 通过 多对多关系的建立,我们需要建立新建时自动新建对应表格的记录。[saving-many-to-many-relations](https://typeorm.io/many-to-many-relations#saving-many-to-many-relations) ## 分页 所有的入参都要通过 dto 做类型定义,通过controller进行query获取,通过service进行数据获取。 ## 事务 transaction,为了保证连续操作后,数据库一致 多项都操作成功才是成功。例如,我们想在更新咖啡时进行消息通知,存在两个动作,只有两个动作都起作用时才能真正入库。文档 [transactions](https://docs.nestjs.com/techniques/database#transactions) ```ts // service.ts @Injectable() export class UsersService { constructor(private dataSource: DataSource) {} async createMany(users: User[]) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { await queryRunner.manager.save(users[0]); await queryRunner.manager.save(users[1]); await queryRunner.commitTransaction(); } catch (err) { // since we have errors lets rollback the changes we made await queryRunner.rollbackTransaction(); } finally { // you need to release a queryRunner which was manually instantiated await queryRunner.release(); } } } ``` ## 模块间相互引用 需要再一个模块导出 service,在另一个模块引入该模块。 ```ts // moduleA.ts @Module({ controllers: [ModuleAController], providers: [ModuleAService], exports: [ModuleAService], }) // moduleB.ts @Module({ imports: [moduleA], controllers: [ModuleBController], providers: [ModuleBService], }) // moduleB.service.ts @Injectable() export class ModuleBService { constructor(private readonly moduleAService: ModuleAService) {} } ``` ## nestjs实现依赖注入的方式 - [依赖注入(Dependency injection)模式](https://v6.angular.cn/guide/dependency-injection-pattern),依赖注入是一种技术,我们将依赖的实例化委托给“IOC控制反转”容器,这个IOC容器就是nestjs运行时系统。 - @Injectable 装饰的class将会被标记为 提供者 provider,由Nest 容器管理的类。一般称为service。当然还有别的也会使用该装饰器,例如:拦截器,路由守卫,管道等 - controller构造函数里的service,都是Nest将该"提供者"注入到我们的控制器中。 在Module中向nest的控制反转容器,注册了一个Provider。 具体的实现: - nest容器实例化CoffeesController时会先检查(构造函数注入的)依赖,例如:CoffeesService。 ```ts @Module({ imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])], controllers: [CoffeesController], providers: [ CoffeesService, { provide: COFFEES_BRANDS, useValue: ['buddy brew', 'nescafe'] }, ], exports: [CoffeesService], }) ``` - 当CoffeesController初始化时,检查到有依赖CoffeesService时,通过nest容器找到该CoffeesService,并用该CoffeesService的“token(CoffeesService)”查找,从而返回该CoffeesService类。 - 返回CoffeesService实例前将,会将其缓存再返回。 - 当前CoffeesService也需要一些依赖,也需要被解决。自下而上 > 最终找到 私有provider(自己在module注入的), 公共provider(数据库), 内置provider(pipe,拦截器,路由守卫,过滤器(报错filter)) ```ts @Injectable() export class CoffeesService { constructor( @InjectRepository(Coffee) private readonly coffeeRepository: Repository, @InjectRepository(Flavor) private readonly flavorRepository: Repository, // 事物 https://docs.nestjs.com/techniques/database#transactions private readonly dataSource: DataSource, @Inject(COFFEES_BRANDS) coffeeBrands: string[], ) { console.log('Inject', COFFEES_BRANDS, coffeeBrands); } } ``` ## service端Injectable 指定实例化方式 nodejs 的请求并不是按照多线程模型。 ```ts // coffees.service.ts @Injectable({ // scope: Scope.DEFAULT, // a // scope: Scope.TRANSIENT, // b scope: Scope.REQUEST, // c }) export class CoffeesService { constructor() { console.log('initial') // console.log(COFFEES_BRANDS); // console.log('Inject', COFFEES_BRANDS, coffeeBrands); } } ``` 标记`a` 处是默认值,只会整个程序生命周期内只实例化一次。 标记`b` 处是特定值,只会在整个项目引入的地方实例化一次。 标记`c` 处是特定值,表示会在每次请求时都会实例化一次。 注意:如果controller隐式依赖了一个service,那么这个controller也会隐式变为 REQUEST scope ```ts @Controller('coffees') export class CoffeesController { constructor( // 隐式依赖了scope: Scope.REQUEST 的CoffeesService private readonly coffeesService: CoffeesService, @Inject(REQUEST) private readonly request: Request, ) { // 这里将会被隐式转换为 scope: Scope.REQUEST 类型的class console.log('CoffeeController created'); console.log(request); } ``` ## 如何读取配置文件 目的是不要将配置放到本地。例如数据库配置,本地调试可以尝试把`env.demo`改成`.env`。 正常的配置逻辑应该写在docker的环境变量里。 ```ts ... ConfigModule.forRoot({ // envFilePath: '.environment', // ignoreEnvFile: true, }), ... ``` 需要配合安装: ```bash pnpm add @hapi/joi pnpm add -D @types/hapi__joi ``` 使用 joi 对配置文件进行校验, ```ts import * as Joi from '@hapi/joi'; @Module({ imports: [ ConfigModule.forRoot({ // envFilePath: '.environment', // ignoreEnvFile: true, validationSchema: Joi.object({ DB_HOST: Joi.required(), DB_PORT: Joi.number().default(5432), }), }), ... ``` 使用ConfigModule读取配置文件,首先导入ConfigModule ```ts // module.ts @Module({ imports: [ConfigModule], ... ``` 其次,service构造函数中引用该ConfigService ```ts // service.ts @Injectable({ scope: Scope.DEFAULT, // scope: Scope.TRANSIENT, // scope: Scope.REQUEST, }) export class CoffeesService { constructor( private readonly configService: ConfigService, ) { // 注意这里都是字符串类型,configService并不会做类型转换 const dbHost = this.configService.get('DB_HOST'); // const dbHost = this.configService.get('DB_HOST', 'defaultVal'); console.log(dbHost); } ``` 注:this.configService.get到的配置都是字符串 ```ts // app.config.ts export default () => ({ database: { host: 'localhost', }, }) // module.ts ... imports: [ // load 加载配置 ConfigModule.forRoot({ load: [appConfig] }), ] ... // service.ts ... constructor(private readonly configService: ConfigService) { configService.get('database.host') } ... ``` 如何注入局部环境变量,而不是都是在全局上注册。 ```ts // coffees.config.ts import { registerAs } from '@nestjs/config'; export default registerAs('coffees', () => ({ foo: 'bar', })); // coffees.module.ts ... ConfigModule.forFeature(coffeesConfig), ... // coffees.service.ts @Injectable({ scope: Scope.DEFAULT, // scope: Scope.TRANSIENT, // scope: Scope.REQUEST, }) export class CoffeesService { constructor( @Inject(COFFEES_BRANDS) coffeeBrands: string[], private readonly configService: ConfigService, ) { console.log('initial'); // 注意这里都是字符串类型,configService并不会做类型转换 const dbHost = this.configService.get('DB_HOST'); console.log(dbHost); // const dbHost2 = this.configService.get('database.host'); // console.log(dbHost2); const coffeesConfig = this.configService.get('coffees'); console.log('coffeesConfig', coffeesConfig); } ``` 这样子做其实并不完美,没有类型定义。我们可以尝试像`COFFEES_BRANDS`一样,注入类型。 ```ts // coffees.config.ts import { registerAs } from '@nestjs/config'; export const KEY = 'COFFEES_CONFIG'; export default registerAs('coffees', () => ({ foo: 'bar', })); // coffees.module.ts @Module({ imports: [ // 注入配置 ConfigModule.forFeature(coffeesConfig), ], controllers: [CoffeesController], providers: [ { provide: coffeesConfig.KEY, useFactory: (configService: ConfigService) => { return configService.get('coffees'); }, inject: [ConfigService], }, ], }) // coffees.service.ts @Injectable({ scope: Scope.DEFAULT, // scope: Scope.TRANSIENT, // scope: Scope.REQUEST, }) export class CoffeesService { constructor( @Inject(coffeesConfig.KEY) injectedCoffeesConfig: ConfigType, ) { console.log('initial'); // 注意这里都是字符串类型,configService并不会做类型转换 const dbHost = this.configService.get('DB_HOST'); console.log(dbHost); // const dbHost2 = this.configService.get('database.host'); // console.log(dbHost2); const coffeeC = this.configService.get('coffees'); console.log('coffeeC', coffeeC); console.log('injectedCoffeesConfig', injectedCoffeesConfig); // console.log('Scope.TRANSIENT ', Scope.TRANSIENT); // console.log('Scope.REQUEST', Scope.REQUEST); // console.log(COFFEES_BRANDS); console.log('Inject', COFFEES_BRANDS, coffeeBrands); } } ``` ## pipes filters interceptors guards https://docs.nestjs.com/pipes#global-scoped-pipes 使用方式1,整个app粒度的使用 ```ts // main.ts ... app.useGlobalPipes(); app.useGlobalFilters(); app.useGlobalGuards(); app.useGlobalInterceptors(); app.useGlobalPipes( new ValidationPipe({ whitelist: true, transform: true, forbidNonWhitelisted: true, transformOptions: { // 开启隐式转换将不再需要@Type类型 enableImplicitConversion: true, }, }), ); ... ``` 使用方式2,整个Module粒度使用 ```ts import { APP_PIPE } from '@nestjs/core'; @Module({ imports: [], controllers: [AppController], providers: [ AppService, { // NOTICE: 这里导出的APP_PIPE,及其他几种选择都是可以注入进去的。 provide: APP_PIPE, // APP_FILTER, APP_GUARD, APP_INTERCEPTOR useClass: ValidationPipe, }, ], }) export class AppModule {} ``` 使用方式3,整个controller使用 ```ts import { UseFilters, UseGuards, UseInterceptors, UsePipes, ValidationPipe, } from '@nestjs/common'; @UsePipes(ValidationPipe) @UseFilters() @UseGuards() @UseInterceptors() @Controller('coffees') export class CoffeesController { ... @UsePipes(ValidationPipe) @Get() findAll(@Query() paginationQuery: PaginationQueryDto) { return this.coffeesService.findAll(paginationQuery); } ... } ``` 使用方式4,某个路由粒度使用,见上例findAll方法 使用方式5,某个body参数验证,目前只支持 Pipes ```ts @Put(':id') update( @Param('id') id: number, @Body(ValidationPipe) updateCoffeeDto: UpdateCoffeeDto, ) { return this.coffeesService.update(id, updateCoffeeDto); } ``` 如果想要传递参数进去,这里可以考虑直接 new 出来一个实例 ```ts @UsePipes(new ValidationPipe({ ... })) export class CoffeesController {} ``` ### 使用自定义报错filter ```bash nest g filter common/filters/http-exception ``` ```ts // http-exception.filter.ts import { ArgumentsHost, Catch, ExceptionFilter, HttpException, } from '@nestjs/common'; import { Response } from 'express'; @Catch(HttpException) export class HttpExceptionFilter implements ExceptionFilter { catch(exception: T, host: ArgumentsHost) { const ctx = host.switchToHttp(); const res = ctx.getResponse(); const status = exception.getStatus(); const exceptionRes = exception.getResponse(); const error = typeof res === 'string' ? { message: exceptionRes } : typeof exceptionRes === 'string' ? { message: exceptionRes } : exceptionRes; console.log(status); res.status(status).json({ ...error, timestamp: new Date().toISOString(), }); } } // main.ts app.useGlobalFilters(new HttpExceptionFilter()); ``` ### 使用自定义守卫 guard 实现一个全部路由都得授权才能访问的守卫 ```bash nest g guard common/guards/api-key ``` ```ts // api-key.guard.ts import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { Request } from 'express'; import { Observable } from 'rxjs'; @Injectable() export class ApiKeyGuard implements CanActivate { canActivate( context: ExecutionContext, ): boolean | Promise | Observable { // 为什么要switchToHttp? 原因是对接不用的应用时需要切换成不同的类型,例如: graphql, ws, rpc, http等 // https://docs.nestjs.com/fundamentals/execution-context const req = context.switchToHttp().getRequest(); const authHeader = req.header('Authorization'); // 验证请求头里带上 { Authorization: 'API_KEY' } return authHeader === process.env.API_KEY; } } // main.ts app.useGlobalGuards(new ApiKeyGuard()); // filter比guard先初始化 ``` 接着实现一些公共路由 方式1: ```ts // coffees.controller.ts @SetMetaData('isPublic', true) @Get() findAll() {} ``` 这种方式不是很优雅,需要自定义 decorator 进行类似的操作 方式2: ```ts // common/decorator/public.decorator.ts import { SetMetadata } from '@nestjs/common'; export const IS_PUBLIC_KEY = 'isPublic'; export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); // coffees.controller.ts ... // @SetMetadata('isPublic', true) @Public() @Get() findAll(@Query() paginationQuery: PaginationQueryDto) { return this.coffeesService.findAll(paginationQuery); } ... // api-key.guard.ts import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Reflector } from '@nestjs/core'; import { Request } from 'express'; import { Observable } from 'rxjs'; import { IS_PUBLIC_KEY } from '../decorator/public.decorator'; @Injectable() export class ApiKeyGuard implements CanActivate { constructor( private readonly reflector: Reflector, private readonly configService: ConfigService, ) {} canActivate( context: ExecutionContext, ): boolean | Promise | Observable { // const isPublic = this.reflector.get(IS_PUBLIC_KEY, context.getClass()); // https://docs.nestjs.com/fundamentals/execution-context#reflection-and-metadata const isPublic = this.reflector.get(IS_PUBLIC_KEY, context.getHandler()); if (isPublic) { return true; } console.log('guard'); const req = context.switchToHttp().getRequest(); const authHeader = req.header('Authorization'); return authHeader === this.configService.get('API_KEY'); } } // common.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { APP_GUARD } from '@nestjs/core'; import { ApiKeyGuard } from './guards/api-key.guard'; @Module({ imports: [ConfigModule], providers: [ { provide: APP_GUARD, useClass: ApiKeyGuard, }, ], }) export class CommonModule {} // app.module.ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { CoffeesModule } from './coffees/coffees.module'; import { CoffeeRatingModule } from './coffee-rating/coffee-rating.module'; import { DatabaseModule } from './database/database.module'; import { ConfigModule } from '@nestjs/config'; import { CommonModule } from './common/common.module'; import * as Joi from '@hapi/joi'; import appConfig from './config/app.config'; @Module({ imports: [ CommonModule, ], controllers: [AppController], providers: [AppService], }) export class AppModule {} ``` [更多 metadata](https://docs.nestjs.com/fundamentals/execution-context#reflection-and-metadata) ### 拦截器 Rxjs 是一个promise和callback的强大替代品。 ```bash nest g interceptor common/interceptor/wrap-response ``` ```ts // wrap-response.interceptor.ts import { CallHandler, ExecutionContext, Injectable, NestInterceptor, } from '@nestjs/common'; import { map, Observable, tap } from 'rxjs'; @Injectable() // 可以作为 provider 引入 export class WrapResponseInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable { console.log('请求前'); return next.handle().pipe( // tap(data => { // console.log('之后', data) // }) map((data) => { console.log('之后', data); return { data }; }), ); } } // main.ts ... app.useGlobalInterceptors(new WrapResponseInterceptor()); ... ``` 增加超时拦截器 ```bash nest g interceptor common/interceptor/timeout ``` ```ts // timeout.interceptor.ts import { CallHandler, ExecutionContext, Injectable, NestInterceptor, RequestTimeoutException, } from '@nestjs/common'; import { catchError, Observable, throwError, timeout, TimeoutError, } from 'rxjs'; @Injectable() export class TimeoutInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable { return next.handle().pipe( timeout(3e3), catchError((e) => { if (e instanceof TimeoutError) { return throwError(() => new RequestTimeoutException()); } return throwError(() => e); }), ); } } // main.ts ... app.useGlobalInterceptors( new WrapResponseInterceptor(), new TimeoutInterceptor(), ); ... ``` ### 自定义管道 ```bash nest g pipe common/pipes/parse-int ``` ```ts // common/pipes/parse-int.pipe.ts import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform, } from '@nestjs/common'; @Injectable() export class ParseIntPipe implements PipeTransform { /** * 一些默认值可以自动设置进去 */ transform(value: string, metadata: ArgumentMetadata) { const val = parseInt(value, 10); if (isNaN(val)) { throw new BadRequestException( `Validation failed . "${val}" is not an integer.`, ); } return val; } } // coffees.controller.ts ... @Get(':id') findById(@Param('id', ParseIntPipe) id: number) { console.log(id); return this.coffeesService.findOne(id); } ... ``` ## 中间件 作用如下:可以访问到请求响应对象,不用绑定到任何方法上,而是绑定到指定路由路径上。 - 执行代码 - 修改请求响应对象 - 请求响应结束生命周期 - 调用堆栈中调用`next()`中间件函数 当请求或响应还没有结束时必须要调用next才能将请求或者响应交给下一个中间件函数。 - function 中间件 无状态的,不会被nestjs控制反转容器注入依赖,并且无权访问反转容器。 - class 中间件 类中间件可以依赖外部依赖并注入,并且可以在模块范围内注册。