原文链接 http://blog.poetries.top/2022...
Nest (NestJS) 是一个用于构建高效、可扩大的 Node.js 服务器端应用程序的开发框架。它利用 JavaScript 的渐进加强的能力,应用并齐全反对 TypeScript (依然容许开发者应用纯 JavaScript 进行开发),并联合了 OOP (面向对象编程)、FP (函数式编程)和 FRP (函数响应式编程)。
- 在底层,Nest 构建在弱小的 HTTP 服务器框架上,例如 Express (默认),并且还能够通过配置从而应用 Fastify !
- Nest 在这些常见的 Node.js 框架 (Express/Fastify) 之上进步了一个形象级别,但依然向开发者间接裸露了底层框架的 API。这使得开发者能够自在地应用实用于底层平台的有数的第三方模块。
本文基于nest8演示
根底
创立我的项目
$ npm i -g @nestjs/cli
nest new project-name
创立一个我的项目
$ tree.├── README.md├── nest-cli.json├── package.json├── src│ ├── app.controller.spec.ts│ ├── app.controller.ts│ ├── app.module.ts│ ├── app.service.ts│ └── main.ts├── test│ ├── app.e2e-spec.ts│ └── jest-e2e.json├── tsconfig.build.json└── tsconfig.json2 directories, 12 files
以下是这些外围文件的简要概述:
app.controller.ts
带有单个路由的根本控制器示例。app.module.ts
应用程序的根模块。main.ts
应用程序入口文件。它应用 NestFactory 用来创立 Nest 利用实例。
main.ts
蕴含一个异步函数,它负责疏导咱们的应用程序:
import { NestFactory } from '@nestjs/core';import { ApplicationModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(ApplicationModule); await app.listen(3000);}bootstrap();
NestFactory
裸露了一些静态方法用于创立利用实例create()
办法返回一个实现INestApplication
接口的对象, 并提供一组可用的办法
nest
有两个反对开箱即用的 HTTP 平台:express
和fastify
。 您能够抉择最适宜您需要的产品
platform-express
Express 是一个家喻户晓的 node.js 简洁 Web 框架。 这是一个通过实战考验,实用于生产的库,领有大量社区资源。 默认状况下应用@nestjs/platform-express
包。 许多用户都能够应用Express
,并且无需采取任何操作即可启用它。platform-fastify
Fastify
是一个高性能,低开销的框架,专一于提供最高的效率和速度。
Nest控制器
Nest中的控制器层负责解决传入的申请, 并返回对客户端的响应。
控制器的目标是接管利用的特定申请。路由机制管制哪个控制器接收哪些申请。通常,每个控制器有多个路由,不同的路由能够执行不同的操作
通过NestCLi创立控制器:
nest -h
能够看到nest
反对的命令
常用命令:
- 创立控制器:
nest g co user module
- 创立服务:
nest g s user module
- 创立模块:
nest g mo user module
- 默认以src为根门路生成
nest g controller posts
示意创立posts的控制器,这个时候会在src目录上面生成一个posts的文件夹,这个外面就是posts的控制器,代码如下
import { Controller } from '@nestjs/common';@Controller('posts')export class PostsController {}
创立好控制器后,nestjs
会主动的在 app.module.ts
中引入PostsController
,代码如下
// src/app.module.tsimport { Module } from '@nestjs/common';import { AppController } from './app.controller';import { AppService } from './app.service';import { PostsController } from './posts/posts.controller' @Module({ imports: [], controllers: [AppController, PostsController], providers: [AppService],})export class AppModule {}
nest配置路由申请数据
Nestjs提供了其余HTTP申请办法的装璜器@Get()
@Post()
@Put()
、@Delete()
、@Patch()
、@Options()
、@Head()
和@All()
在Nestjs中获取Get
传值或者Post提
交的数据的话咱们能够应用Nestjs中的装璜器来获取。
@Request() req@Response() res@Next() next@Session() req.session@Param(key?: string) req.params / req.params[key]@Body(key?: string) req.body / req.body[key]@Query(key?: string) req.query / req.query[key]@Headers(name?: string) req.headers / req.headers[name]
示例
@Controller('posts')export class PostsController { constructor(private readonly postsService: PostsService) {} @Post('create') create(@Body() createPostDto: CreatePostDto) { return this.postsService.create(createPostDto); } @Get('list') findAll(@Query() query) { return this.postsService.findAll(query); } @Get(':id') findById(@Param('id') id: string) { return this.postsService.findById(id); } @Put(':id') update( @Param('id') id: string, @Body() updatePostDto: UpdatePostDto, ) { return this.postsService.update(id, updatePostDto); } @Delete(':id') remove(@Param('id') id: string) { return this.postsService.remove(id); }}
留神
对于nest的return
: 当申请处理程序返回 JavaScript 对象或数组时,它将主动序列化为 JSON。然而,当它返回一个字符串时,Nest 将只发送一个字符串而不是序列化它
Nest服务
Nestjs中的服务能够是service
也能够是provider
。他们都能够通过 constructor 注入依赖关系
。服务实质上就是通过@Injectable()
装璜器注解的类。在Nestjs中服务相当于MVC
的Model
创立服务
nest g service posts
创立好服务后就能够在服务中定义对应的办法
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';import { InjectRepository } from '@nestjs/typeorm';import { Repository, Not, Between, Equal, Like, In } from 'typeorm';import * as dayjs from 'dayjs';import { CreatePostDto } from './dto/create-post.dto';import { UpdatePostDto } from './dto/update-post.dto';import { PostsEntity } from './entities/post.entity';import { PostsRo } from './interfaces/posts.interface';@Injectable()export class PostsService { constructor( @InjectRepository(PostsEntity) private readonly postsRepository: Repository<PostsEntity>, ) {} async create(post: CreatePostDto) { const { title } = post; const doc = await this.postsRepository.findOne({ where: { title } }); console.log('doc', doc); if (doc) { throw new HttpException('文章题目已存在', HttpStatus.BAD_REQUEST); } return { data: await this.postsRepository.save(post), message: '创立胜利', }; } // 分页查问列表 async findAll(query = {} as any) { let { pageSize, pageNum, orderBy, sort, ...params } = query; orderBy = query.orderBy || 'create_time'; sort = query.sort || 'DESC'; pageSize = Number(query.pageSize || 10); pageNum = Number(query.pageNum || 1); console.log('query', query); const queryParams = {} as any; Object.keys(params).forEach((key) => { if (params[key]) { queryParams[key] = Like(`%${params[key]}%`); // 所有字段反对含糊查问、%%之间不能有空格 } }); const qb = await this.postsRepository.createQueryBuilder('post'); // qb.where({ status: In([2, 3]) }); qb.where(queryParams); // qb.select(['post.title', 'post.content']); // 查问局部字段返回 qb.orderBy(`post.${orderBy}`, sort); qb.skip(pageSize * (pageNum - 1)); qb.take(pageSize); return { list: await qb.getMany(), totalNum: await qb.getCount(), // 按条件查问的数量 total: await this.postsRepository.count(), // 总的数量 pageSize, pageNum, }; } // 依据ID查问详情 async findById(id: string): Promise<PostsEntity> { return await this.postsRepository.findOne({ where: { id } }); } // 更新 async update(id: string, updatePostDto: UpdatePostDto) { const existRecord = await this.postsRepository.findOne({ where: { id } }); if (!existRecord) { throw new HttpException(`id为${id}的文章不存在`, HttpStatus.BAD_REQUEST); } // updatePostDto笼罩existRecord 合并,能够更新单个字段 const updatePost = this.postsRepository.merge(existRecord, { ...updatePostDto, update_time: dayjs().format('YYYY-MM-DD HH:mm:ss'), }); return { data: await this.postsRepository.save(updatePost), message: '更新胜利', }; } // 删除 async remove(id: string) { const existPost = await this.postsRepository.findOne({ where: { id } }); if (!existPost) { throw new HttpException(`文章ID ${id} 不存在`, HttpStatus.BAD_REQUEST); } await this.postsRepository.remove(existPost); return { data: { id }, message: '删除胜利', }; }}
Nest模块
模块是具备@Module()
装璜器的类。@Module()
装璜器提供了元数据,Nest 用它来组织应用程序构造
每个 Nest 应用程序至多有一个模块,即根模块。根模块是 Nest 开始安顿应用程序树的中央。事实上,根模块可能是应用程序中惟一的模块,特地是当应用程序很小时,然而对于大型程序来说这是没有意义的。在大多数状况下,您将领有多个模块,每个模块都有一组严密相干的性能。
@module() 装璜器承受一个形容模块属性的对象:
providers
由 Nest 注入器实例化的提供者,并且能够至多在整个模块中共享controllers
必须创立的一组控制器imports
导入模块的列表,这些模块导出了此模块中所需提供者exports
由本模块提供并应在其余模块中可用的提供者的子集
// 创立模块 postsnest g module posts
Nestjs中的共享模块
每个模块都是一个共享模块。一旦创立就能被任意模块重复使用。假如咱们将在几个模块之间共享 PostsService 实例。 咱们须要把 PostsService 放到 exports 数组中:
// posts.modules.tsimport { Module } from '@nestjs/common';import { PostsController } from './posts.controller';import { PostsService } from './posts.service';@Module({ controllers: [PostsController], providers: [PostsService], exports: [PostsService] // 共享模块导出})export class PostsModule {}
能够应用 nest g res posts
一键创立以上须要的各个模块
配置动态资源
NestJS中配置动态资源目录残缺代码
npm i @nestjs/platform-express -S
import { NestExpressApplication } from '@nestjs/platform-express';// main.tsasync function bootstrap() { // 创立实例 const app = await NestFactory.create<NestExpressApplication>(AppModule); //应用形式一 app.useStaticAssets('public') //配置动态资源目录 // 应用形式二:配置前缀目录 设置动态资源目录 app.useStaticAssets(join(__dirname, '../public'), { // 配置虚拟目录,比方咱们想通过 http://localhost:3000/static/1.jpg 来拜访public目录外面的文件 prefix: '/static/', // 设置虚构门路 }); // 启动端口 const PORT = process.env.PORT || 9000; await app.listen(PORT, () => Logger.log(`服务曾经启动 http://localhost:${PORT}`), );}bootstrap();
配置模板引擎
npm i ejs --save
配置模板引擎
// main.tsimport { NestFactory } from '@nestjs/core';import { AppModule } from './app.module';import {join} from 'path';async function bootstrap() { const app = await NestFactory.create(AppModule); app.setBaseViewsDir(join(__dirname, '..', 'views')) // 放视图的文件 app.setViewEngine('ejs'); //模板渲染引擎 await app.listen(9000);}bootstrap();
我的项目根目录新建views
目录而后新建根目录 -> views -> default -> index.ejs
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title></head><body> <h3>模板引擎</h3> <%=message%></body></html>
渲染页面
Nestjs中 Render
装璜器能够渲染模板,应用路由匹配渲染引擎
mport { Controller, Get, Render } from '@nestjs/common';import { AppService } from './app.service';@Controller()export class AppController { @Get() @Render('default/index') //应用render渲染模板引擎,参数就是文件门路:default文件夹下的index.ejs getUser(): any { return {message: "hello word"} //只有返回参数在模板能力获取,如果不传递参数,必须返回一个空对象 }}
Cookie的应用
cookie和session的应用依赖于以后应用的平台,如:express和fastify
两种的应用形式不同,这里次要记录基于express平台的用法
cookie能够用来存储用户信息,存储购物车等信息,在理论我的项目中用的十分多
npm instlal cookie-parser --save npm i -D @types/cookie-parser --save
引入注册
// main.tsimport { AppModule } from './app.module';import { NestExpressApplication } from '@nestjs/platform-express';import * as cookieParser from 'cookie-parser'async function bootstrap() { const app = await NestFactory.create<NestExpressApplication>(AppModule); //注册cookie app.use(cookieParser('dafgafa')); //加密明码 await app.listen(3000);}bootstrap();
接口中设置cookie 应用response
申请该接口,响应一个cookie
@Get()index(@Response() res){ //设置cookie, signed:true加密 //参数:1:key, 2:value, 3:配置 res.cookie('username', 'poetry', {maxAge: 1000 * 60 * 10, httpOnly: true, signed:true}) //留神: //应用res后,返回数据必须应用res //如果是用了render模板渲染,还是应用return res.send({xxx})}
cookie相干配置参数
domain
String 指定域名下无效expires
Date 过期工夫(秒),设置在某个工夫点后会在该cookoe
后生效httpOnly
Boolean 默认为false
如果为true
示意不容许客户端(通过js
来获取cookie
)maxAge
String 最大生效工夫(毫秒),设置在多少工夫后生效path
String 示意cookie
影响到的门路,如:path=/
如果门路不能匹配的时候,浏览器则不发送这个cookie
secure
Boolean 当secure
值为true
时,cookie
在 HTTP 中是有效,在HTTPS
中才无效signed
Boolean 示意是否签名cookie
,如果设置为true
的时候示意对这个cookie
签名了,这样就须要用res.signedCookies()
获取值cookie
不是应用res.cookies()
了
获取cookie
@Get()index(@Request() req){ console.log(req.cookies.username) //加密的cookie获取形式 console.log(req.signedCookies.username) return req.cookies.username}
Cookie加密
// 配置中间件的时候须要传参app.use(cookieParser('123456'));// 设置cookie的时候配置signed属性res.cookie('userinfo','hahaha',{domain:'.ccc.com',maxAge:900000,httpOnly:true,signed:true});// signedCookies调用设置的cookieconsole.log(req.signedCookies);
Session的应用
session
是另一种记录客户状态的机制,不同的是Cookie保留在客户端浏览器中,而session
保留在服务器上- 当浏览器拜访服务器并发送第一次申请时,服务器端会创立一个session对象,生成一个相似于key,value的键值对, 而后将key(cookie)返回到浏览器(客户)端,浏览器下次再拜访时,携带key(cookie),找到对应的session(value)。 客户的信息都保留在session中
装置 express-session
npm i express-session --savenpm i -D @types/express-session --save
// main.tsimport { AppModule } from './app.module';import { NestExpressApplication } from '@nestjs/platform-express';import * as session from 'express-seesion'async function bootstrap() { const app = await NestFactory.create<NestExpressApplication>(AppModule); //配置session app.use(session({ secret: 'dmyxs', cookie: { maxAge: 10000, httpOnly: true }, //以cookie存储到客户端 rolling: true //每次从新申请时,从新设置cookie })) await app.listen(3000);}bootstrap();
session相干配置参数
secret
String 生成session
签名的密钥name
String 客户端的cookie
的名称,默认为connect.sid
, 可本人设置resave
Boolean 强制保留session
即便它并没有变动, 默认为true
, 倡议设置成false
saveUninitalized
Boolean 强制将未初始化的session
存储。当新建了一个session
且未设定属性或值时,它就处于 未初始化状态。在设定一个cookie
前,这对于登陆验证,加重服务端存储压力,权限管制是有帮忙的。默认:true
, 倡议手动增加cookie
Object 设置返回到前端cookie
属性,默认值为{ path: ‘/’, httpOnly: true, secure: false, maxAge: null }
。rolling
Boolean 在每次申请时强行设置cookie
,这将重置cookie
过期工夫, 默认为false
接口中设置session
@Get() index(@Request() req){ //设置session req.session.username = 'poetry'}
获取session
@Get('/session') session(@Request() req, @Session() session ){ //获取session:两种形式 console.log(req.session.username) console.log(session.username) return 'hello session'}
跨域,前缀门路、网站平安、申请限速
跨域,门路前缀,网络安全
yarn add helmet csurf
// main.tsimport { NestFactory } from '@nestjs/core';import { Logger, ValidationPipe } from '@nestjs/common';import * as helmet from 'helmet';import * as csurf from 'csurf';import { AppModule } from './app.module';const PORT = process.env.PORT || 8000;async function bootstrap() { const app = await NestFactory.create(AppModule); // 门路前缀:如:http://www.test.com/api/v1/user app.setGlobalPrefix('api/v1'); //cors:跨域资源共享,形式一:容许跨站拜访 app.enableCors(); // 形式二:const app = await NestFactory.create(AppModule, { cors: true }); //避免跨站脚本攻打 app.use(helmet()); //CSRF爱护:跨站点申请伪造 app.use(csurf()); await app.listen(PORT, () => { Logger.log( `服务曾经启动,接口请拜访:localhost:${PORT}${PREFIX}`, ) });}bootstrap();
限速:限度客户端在肯定工夫内的申请次数
yarn add @nestjs/throttler
在须要应用的模块引入应用,这里是全局应用,在app.module.ts
中引入。这里设置的是:1分钟内只能申请10次,超过则报status为429的谬误
app.module.tsimport { APP_GUARD } from '@nestjs/core';import { Module } from '@nestjs/common';import { UserModule } from './modules/user/user.module';//引入import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';@Module({ imports: [ UserModule, ThrottlerModule.forRoot({ ttl: 60, //1分钟 limit: 10, //申请10次 }), ], providers: [ //全局应用 { provide: APP_GUARD, useClass: ThrottlerGuard, }, ],})export class AppModule { }
管道、守卫、拦截器、过滤器、中间件
- 管道:数据处理与转换,数据验证
- 守卫:验证用户登陆,爱护路由
- 拦截器:对申请响应进行拦挡,对立响应内容
- 过滤器:异样捕捉
- 中间件:日志打印
执行程序(机会)
从客户端发送一个post申请,门路为:/user/login
,申请参数为:{userinfo: ‘xx’,password: ‘xx’}
,到服务器接管申请内容,触发绑定的函数并且执行相干逻辑结束,而后返回内容给客户端的整个过程大体上要通过如下几个步骤:
全局应用: 管道 - 守卫 - 拦截器 - 过滤器 - 中间件。对立在main.ts文件中应用,全局失效
import { NestFactory } from '@nestjs/core';import { ParseIntPipe } from '@nestjs/common';import { AppModule } from './app.module';import { HttpExceptionFilter } from './common/filters/http-exception.filter';import { LoggerMiddleware } from './common/middleware/logger.middleware';import { AuthGuard } from './common/guard/auth.guard';import { AuthInterceptor } from './common/interceptors/auth.interceptor';async function bootstrap() { const app = await NestFactory.create(AppModule); //全局应用管道:这里应用的是内置,也能够应用自定义管道,在下文 app.useGlobalPipes(new ParseIntPipe()); //全局应用中间件 app.use(LoggerMiddleware) //全局应用过滤器 //这里应用的是自定义过滤器,先别管,先学会怎么在全局应用 app.useGlobalFilters(new HttpExceptionFilter()); //全局应用守卫 app.useGlobalGuards(new AuthGuard()); //全局应用拦截器 app.useGlobalInterceptors(new AuthInterceptor()); await app.listen(3000);}bootstrap();
管道
罕用内置管道,从@nestjs/common
导出
ParseIntPipe
:将字符串数字转数字ValidationPipe
:验证管道
部分应用管道
- 匹配整个门路,应用UsePipes
- 只匹配某个接口,应用UsePipes
- 在获取参数时匹配,个别应用内置管道
import { Controller, Get, Put, Body, Param, UsePipes, ParseIntPipe} from '@nestjs/common';import { myPipe } from '../../common/pipes/user.pipe';@Controller('user')@UsePipes(new myPipe()) //部分形式1:匹配整个/user, get申请和put申请都会命中export class UserController { @Get(':id') getUserById(@Param('id', new ParseIntPipe()) id) { //部分形式3:只匹配/user的get申请,应用的是内置管道 console.log('user', typeof id); return id; } @Put(':id') @UsePipes(new myPipe()) //部分形式2:只匹配/user的put申请 updateUser(@Body() user, @Param('id') id) { return { user, id, }; }}
自定义管道
应用快捷命令生成:nest g pi myPipe common/pipes
import { ArgumentMetadata, Injectable, PipeTransform, BadRequestException,} from '@nestjs/common';//自定义管道必须实现自PipeTransform,固定写法,该接口有一个transform办法//transform参数://value:应用myPipe时所传递的值,能够是param传递的的查问门路参数,能够是body的申请体//metadata:元数据,能够用它判断是来自param或body或query@Injectable()export class myPipe implements PipeTransform<string> { transform(value: string, metadata: ArgumentMetadata) { if (metadata.type === 'body') { console.log('来自申请体', value); } if (metadata.type === 'param') { console.log('来自查问门路', value); const val = parseInt(value, 10); //如果不是传递一个数字,抛出谬误 if (isNaN(val)) { throw new BadRequestException('Validation failed'); } return val; } return value; }}
守卫
自定义守卫
应用快捷命令生成:nest g gu myGuard common/guards
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';import { Reflector } from '@nestjs/core'; //反射器,作用与自定义装璜器桥接,获取数据//自定义守卫必须CanActivate,固定写法,该接口只有一个canActivate办法//canActivate参数://context:申请的(Response/Request)的援用//通过守卫返回true,否则返回false,返回403状态码@Injectable()export class AuthGuard implements CanActivate { constructor(private readonly reflector: Reflector) { } // 白名单数组 private whiteUrlList: string[] = ['/user']; // 验证该次申请是否为白名单内的路由 private isWhiteUrl(urlList: string[], url: string): boolean { if (urlList.includes(url)) { return true; } return false; } canActivate(context: ExecutionContext): boolean { // 获取申请对象 const request = context.switchToHttp().getRequest(); //console.log('request', request.headers); //console.log('request', request.params); //console.log('request', request.query); //console.log('request', request.url); // 用法一:验证是否是白名单内的路由 if (this.isWhiteUrl(this.whiteUrlList, request.url)) { return true; } else { return false; } // 用法二:应用反射器,配合装璜器应用,获取装璜器传递过去的数据 const roles = this.reflector.get<string[]>('roles', context.getHandler()); //console.log(roles); // [ 'admin' ] //http://localhost:3000/user/9?user=admin,如果与装璜器传递过去的值匹配则通过,否则不通过 //实在开发中可能从cookie或token中获取值 const { user } = request.query; if (roles.includes(user)) { return true; } else { return false; } // 其余用法 // 获取申请头中的token字段 const token = context.switchToRpc().getData().headers.token; // console.log('token', token); // 获取session const userinfo = context.switchToHttp().getRequest().session; // console.log('session', userinfo); return true; }}
部分应用守卫
import { Controller, Get, Delete, Param, UsePipes, UseGuards, ParseIntPipe,} from '@nestjs/common';import { AuthGuard } from '../../common/guard/auth.guard';import { Role } from '../../common/decorator/role.decorator'; //自定义装璜器@UseGuards(AuthGuard) //部分应用守卫,守卫整个user门路@Controller('user')export class UserController { @Get(':id') getUserById(@Param('id', new ParseIntPipe()) id) { console.log('user', typeof id); return id; } @Delete(':id') @Role('admin') //应用自定义装璜器,传入角色,必须是admin能力删除 removeUser(@Param('id') id) { return id; }}
装璜器
自定义守卫中应用到了自定义装璜器
nest g d role common/decorator
//这是快捷生成的代码import { SetMetadata } from '@nestjs/common';//SetMetadata作用:将获取到的值,设置到元数据中,而后守卫通过反射器能力获取到值export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
拦截器
应用快捷命令生成:nest g in auth common/intercepters
import { CallHandler, ExecutionContext, Injectable, NestInterceptor,} from '@nestjs/common';import { map } from 'rxjs/operators';import { Observable } from 'rxjs';//自定义拦截器必须实现自NestInterceptor,固定写法,该接口只有一个intercept办法//intercept参数://context:申请上下文,能够拿到的Response和Request@Injectable()export class AuthInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const request = context.switchToHttp().getRequest(); console.log('拦截器', request.url); return next.handle().pipe( map((data) => { console.log('全局响应拦截器办法返回内容后...'); return { status: 200, timestamp: new Date().toISOString(), path: request.url, message: '申请胜利', data: data, }; }), ); }}
过滤器
部分应用过滤器
import { Controller, Get, UseFilters, HttpException, HttpStatus,} from '@nestjs/common';import { HttpExceptionFilter } from '../../common/filters/http-exception.filter';//部分应用过滤器@UseFilters(new HttpExceptionFilter())@Controller('/user')export class ExceptionController { @Get() getUserById(@Query() { id }): string { if (!id) { throw new HttpException( { status: HttpStatus.BAD_REQUEST, message: '申请参数id 必传', error: 'id is required', }, HttpStatus.BAD_REQUEST, ); } return 'hello error'; }}
自定义过滤器
应用快捷命令生成:nest g f myFilter common/filters
import { ArgumentsHost, Catch, ExceptionFilter, HttpException,} from '@nestjs/common';//必须实现至ExceptionFilter,固定写法,该接口只有一个catch办法//catch办法参数://exception:以后正在解决的异样对象//host:传递给原始处理程序的参数的一个包装(Response/Request)的援用@Catch(HttpException)export class HttpExceptionFilter implements ExceptionFilter<HttpException> { catch(exception: HttpException, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); const status = exception.getStatus(); //获取状态码 const exceptionRes: any = exception.getResponse(); //获取响应对象 const { error, message } = exceptionRes; //自定义的异样响应内容 const msgLog = { status, timestamp: new Date().toISOString(), path: request.url, error, message, }; response.status(status).json(msgLog); }}
中间件
部分应用中间件
import { Module, MiddlewareConsumer, RequestMethod } from '@nestjs/common';import { LoggerMiddleware } from './common/middleware/logger.middlerware';import { UserModule } from './modules/user/user.module';@Module({ imports:[ UserModule ]})export class AppModule { configure(consumer: MiddlewareConsumer) { consumer .apply(LoggerMiddleware) //利用中间件 .exclude({ path: 'user', method: RequestMethod.POST }) //排除user的post办法 .forRoutes('user'); //监听门路 参数:路径名或*,*是匹配所以的路由 // .forRoutes({ path: 'user', method: RequestMethod.POST }, { path: 'album', method: RequestMethod.ALL }); //多个 // .apply(UserMiddleware) //反对多个中间件 // .forRoutes('user') }}
自定义中间件
nest g mi logger common/middleware
import { Injectable, NestMiddleware } from '@nestjs/common';import { Request, Response } from 'express';@Injectable()export class LoggerMiddleware implements NestMiddleware { //req:申请参数 //res:响应参数 //next:执行下一个中件间 use(req: Request, res: Response, next: () => void) { const { method, path } = req; console.log(`${method} ${path}`); next(); }}
函数式中间件
// 函数式中间件-利用于全局export function logger(req, res, next) { next();}// main.tsasync function bootstrap() { // 创立实例 const app = await NestFactory.create<NestExpressApplication>(AppModule); // 设置全局日志函数中间件 app.use(logger);}bootstrap();
一例看懂中间件、守卫、管道、异样过滤器、拦截器
从客户端发送一个post申请,门路为:/user/login
,申请参数为:{userinfo: ‘xx’,password: ‘xx’}
,到服务器接管申请内容,触发绑定的函数并且执行相干逻辑结束,而后返回内容给客户端的整个过程大体上要通过如下几个步骤:`
我的项目须要包反对:
npm install --save rxjs xml2js class-validator class-transformer
rxjs
针对JavaScript的反应式扩大,反对更多的转换运算xml2js
转换xml内容变成json格局class-validator
、class-transformer
管道验证包和转换器
建设user模块:模块内容构造:
nest g res user
user.controller.ts文件
import { Controller, Post, Body} from '@nestjs/common';import { UserService } from './user.service';import { UserLoginDTO } from './dto/user.login.dto';@Controller('user')export class UserController { constructor(private readonly userService: UserService) {} @Post('test') loginIn(@Body() userlogindto: UserLoginDTO) { return userlogindto; }}
user.module.ts文件
import { Module } from '@nestjs/common';import { UserController } from './user.controller';import { UserService } from './user.service';@Module({ controllers: [UserController], providers: [UserService],})export class UserModule {}
user.service.ts文件
import { Injectable } from '@nestjs/common';@Injectable()export class UserService {}
user.login.dto.ts文件
// user / dto / user.login.dto.tsimport { IsNotIn, MinLength } from 'class-validator';export class UserLoginDTO{ /* * 账号 */ @IsNotIn(['',undefined,null],{message: '账号不能为空'}) username: string; /* * 明码 */ @MinLength(6,{ message: '明码长度不能小于6位数' }) password: string;}
app.module.ts文件
import { Module } from '@nestjs/common';// 子模块加载import { UserModule } from './user/user.module'@Module({ imports: [ UserModule ]})export class AppModule {}
新建common文件夹外面别离建设对应的文件夹以及文件:
中间件(middleware) — xml.middleware.ts
守卫(guard) — auth.guard.ts
管道(pipe) — validation.pipe.ts
异样过滤器(filters) — http-exception.filter.ts
拦截器(interceptor) — response.interceptor.ts
// main.tsimport { NestFactory } from '@nestjs/core';import { AppModule } from './app.module';import { ValidationPipe } from './common/pipe/validation.pipe';import { HttpExceptionFilter } from './common/filters/http-exception.filter';import { XMLMiddleware } from './common/middleware/xml.middleware';import { AuthGuard } from './common/guard/auth.guard';import { ResponseInterceptor } from './common/interceptor/response.interceptor';async function bootstrap() { const app = await NestFactory.create(AppModule); // 全局注册通用验证管道ValidationPipe app.useGlobalPipes(new ValidationPipe()); // 全局注册通用异样过滤器HttpExceptionFilter app.useGlobalFilters(new HttpExceptionFilter()); // 全局注册xml反对中间件(这里必须调用.use才可能注册) app.use(new XMLMiddleware().use); // 全局注册权限验证守卫 app.useGlobalGuards(new AuthGuard()); // 全局注册响应拦截器 app.useGlobalInterceptors(new ResponseInterceptor()); await app.listen(3001);}bootstrap();
中间件是申请的第一道关卡
- 执行任何代码。
- 对申请和响应对象进行更改。
- 完结申请-响应周期。
- 调用堆栈中的下一个中间件函数。
- 如果以后的中间件函数没有完结申请-响应周期, 它必须调用 next() 将管制传递给下一个中间件函数。否则, 申请将被挂起
本例中:应用中间件让express反对xml申请并且将xml内容转换为json数组
// common/middleware/xml.middleware.tsimport { Injectable, NestMiddleware } from '@nestjs/common';import { Request, Response } from 'express';const xml2js = require('xml2js');const parser = new xml2js.Parser();@Injectable()export class XMLMiddleware implements NestMiddleware { // 参数是固定的Request/Response/next, // Request/Response/next对应申请体和响应体和下一步函数 use(req: Request, res: Response, next: Function) { console.log('进入全局xml中间件...'); // 获取express原生申请对象req,找到其申请头内容,如果蕴含application/xml,则执行转换 if(req.headers['content-type'] && req.headers['content-type'].includes('application/xml')){ // 监听data办法获取到对应的参数数据(这里的办法是express的原生办法) req.on('data', mreq => { // 应用xml2js对xml数据进行转换 parser.parseString(mreq,function(err,result){ // 将转换后的数据放入到申请对象的req中 console.log('parseString转换后的数据',result); // 这里之后能够依据须要对result做一些补充欠缺 req['body']= result; }) }) } // 调用next办法进入到下一个中间件或者路由 next(); }}
注册形式
- 全局注册:在
main.ts
中导入须要的中间件模块如:XMLMiddleware而后应用app.use(new XMLMiddleware().use)
即可 - 模块注册:在对应的模块中注册如:
user.module.ts
同一路由注册多个中间件的执行程序为,先是全局中间件执行,而后是模块中间件执行,模块中的中间件程序依照.apply
中注册的程序执行
守卫是第二道关卡
守卫管制一些权限内容,如:一些接口须要带上token标记,才可能调用,守卫则是对这个标记进行验证操作的。
本例中代码如下:
// common/guard/auth.guard.tsimport {Injectable,CanActivate,HttpException,HttpStatus,ExecutionContext,} from '@nestjs/common';@Injectable()export class AuthGuard implements CanActivate { // context 申请的(Response/Request)的援用 async canActivate(context: ExecutionContext): Promise<boolean> { console.log('进入全局权限守卫...'); // 获取申请对象 const request = context.switchToHttp().getRequest(); // 获取申请头中的token字段 const token = context.switchToRpc().getData().headers.token; // 如果白名单内的路由就不拦挡间接通过 if (this.hasUrl(this.urlList, request.url)) { return true; } // 验证token的合理性以及依据token做出相应的操作 if (token) { try { // 这里能够增加验证逻辑 return true; } catch (e) { throw new HttpException( '没有受权拜访,请先登录', HttpStatus.UNAUTHORIZED, ); } } else { throw new HttpException( '没有受权拜访,请先登录', HttpStatus.UNAUTHORIZED, ); } }; // 白名单数组 private urlList: string[] = [ '/user/login' ]; // 验证该次申请是否为白名单内的路由 private hasUrl(urlList: string[], url: string): boolean { let flag: boolean = false; if (urlList.indexOf(url) >= 0) { flag = true; } return flag; }};
注册形式
- 全局注册:在
main.ts
中导入须要的守卫模块如:AuthGuard
。而后应用app.useGlobalGuards(new AuthGuard())
即可 - 模块注册:在须要注册的
controller
控制器中导入AuthGuard
。而后从@nestjs/common
中导UseGuards
装璜器。最初间接搁置在对应的@Controller()
或者@Post/@Get…
等装璜器之下即可
同一路由注册多个守卫的执行程序为,先是全局守卫执行,而后是模块中守卫执行
拦截器是第三道关卡
想到自定义返回内容如
{ "statusCode": 400, "timestamp": "2022-05-14T08:06:45.265Z", "path": "/user/login", "message": "申请失败", "data": { "isNotIn": "账号不能为空" }}
这个时候就能够应用拦截器来做一下解决了。
拦截器作用:
- 在函数执行之前/之后绑定额定的逻辑
- 转换从函数返回的后果
- 转换从函数抛出的异样
- 扩大根本函数行为
- 依据所选条件齐全重写函数 (例如, 缓存目标)
拦截器的执行程序分为两个局部:
- 第一个局部在管道和自定义逻辑(next.handle()办法)之前。
- 第二个局部在管道和自定义逻辑(next.handle()办法)之后。
// common/interceptor/response.interceptor.ts/* * 全局响应拦截器,对立返回体内容 **/import { Injectable, NestInterceptor, CallHandler, ExecutionContext,} from '@nestjs/common';import { map } from 'rxjs/operators';import { Observable } from 'rxjs';// 返回体构造interface Response<T> { data: T;}@Injectable()export class ResponseInterceptor<T> implements NestInterceptor<T, Response<T>> { intercept( context: ExecutionContext, next: CallHandler<T>, ): Observable<Response<T>> { // 解析ExecutionContext的数据内容获取到申请体 const ctx = context.switchToHttp(); const request = ctx.getRequest(); // 实现数据的遍历与转变 console.log('进入全局响应拦截器...'); return next.handle().pipe( map(data => { console.log('全局响应拦截器办法返回内容后...'); return { statusCode: 0, timestamp: new Date().toISOString(), path: request.url, message: '申请胜利', data:data }; }), ); }}
两头多了个全局管道以及自定义逻辑,即只有路由绑定的函数有正确的返回值之后才会有next.handle()
之后的内容
注册形式
- 全局注册:在
main.ts
中导入须要的模块如:ResponseInterceptor
。而后应用app.useGlobalInterceptors(new ResponseInterceptor())
即可 - 模块注册:在须要注册的
controller
控制器中导入ResponseInterceptor
。而后从@nestjs/common
中导入UseInterceptors
装璜器。最初间接搁置在对应的@Controller()
或者@Post/@Get
…等装璜器之下即可
同一路由注册多个拦截器时候,优先执行模块中绑定的拦截器,而后其拦截器转换的内容将作为全局拦截器的内容,即包裹两次返回内容如:
{ // 全局拦截器成果 "statusCode": 0, "timestamp": "2022-05-14T08:20:06.159Z", "path": "/user/login", "message": "申请胜利", "data": { "pagenum": 1, // 模块中拦截器包裹成果 “pageSize": 10 "list": [] }}
管道是第四道关卡
- 管道是申请过程中的第四个内容,次要用于对申请参数的验证和转换操作。
- 我的项目中应用
class-validator
class-transformer
进行配合验证相干的输出操作内容
意识官网的三个内置管道
ValidationPipe
:基于class-validator
和class-transformer
这两个npm包编写的一个惯例的验证管道,能够从class-validator
导入配置规定,而后间接应用验证(以后不须要理解ValidationPipe
的原理,只须要晓得从class-validator
引规定,设定到对应字段,而后应用ValidationPipe
即可)ParseIntPipe
:转换传入的参数为数字
如:传递过去的是/test?id=‘123’"这里会将字符串‘123’转换成数字123
- ParseUUIDPipe:验证字符串是否是 UUID(通用惟一识别码)
如:传递过去的是/test?id=‘xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx’"这里会验证格局是否正确,不正确则抛出谬误,否则调用findOne办法
本例中管道应用如下:
// common/pipe/validation.pipe.ts/* * 全局dto验证管道 **/import { validate } from 'class-validator';import { plainToClass } from 'class-transformer';import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';@Injectable()export class ValidationPipe implements PipeTransform<any>{ // value 是以后解决的参数,而 metatype 是属性的元类型 async transform(value: any, { metatype }: ArgumentMetadata) { console.log('进入全局管道...'); if (!metatype || !this.toValidate(metatype)) { return value; } // plainToClass办法将一般的javascript对象转换为特定类的实例 const object = plainToClass(metatype, value); // 验证该对象返回出错的数组 const errors = await validate(object); if (errors.length > 0) { // 将错误信息数组中的第一个内容返回给异样过滤器 let errormsg = errors.shift().constraints; throw new BadRequestException(errormsg); } return value; } // 验证属性值的元类型是否是String, Boolean, Number, Array, Object中的一种 private toValidate(metatype: any): boolean { const types: Function[] = [String, Boolean, Number, Array, Object]; return !types.includes(metatype); }}
注册形式
- 全局注册:在
main.ts
中导入须要的模块如:ValidationPipe
;而后应用app.useGlobalPipes(new ValidationPipe())
即可 - 模块注册:在须要注册的
controller
控制器中导入ValidationPipe
;而后从@nestjs/common
中导入UsePipes
装璜器;最初间接搁置在对应的@Controller()
或者@Post/@Get…
等装璜器之下即可,管道还容许注册在相干的参数上如:@Body/@Query…
等
留神:同一路由注册多个管道的时候,优先执行全局管道,而后再执行模块管道:
- 异样过滤器是所有抛出的异样的对立解决计划
- 简略来讲就是捕捉零碎抛出的所有异样,而后自定义批改异样内容,抛出敌对的提醒。
内置异样类
零碎提供了不少内置的零碎异样类,须要的时候间接应用throw new XXX(形容,状态)这样的形式即可抛出对应的异样,一旦抛出异样,以后申请将会终止。
留神每个异样抛出的状态码有所不同。如:
BadRequestException — 400UnauthorizedException — 401ForbiddenException — 403NotFoundException — 404NotAcceptableException — 406RequestTimeoutException — 408ConflictException — 409GoneException — 410PayloadTooLargeException — 413UnsupportedMediaTypeException — 415UnprocessableEntityException — 422InternalServerErrorException — 500NotImplementedException — 501BadGatewayException — 502ServiceUnavailableException — 503GatewayTimeoutException — 504
本例中应用的是自定义的异样类,代码如下:
// common/filters/http-exception.filter.tsimport { ExceptionFilter, Catch, ArgumentsHost, HttpException,Logger,HttpStatus } from '@nestjs/common';import { Request, Response } from 'express';@Catch()export class HttpExceptionFilter implements ExceptionFilter { // exception 以后正在解决的异样对象 // host 是传递给原始处理程序的参数的一个包装(Response/Request)的援用 catch(exception: HttpException, host: ArgumentsHost) { console.log('进入全局异样过滤器...'); const ctx = host.switchToHttp(); const response = ctx.getResponse<Response>(); const request = ctx.getRequest<Request>(); // HttpException 属于根底异样类,可自定义内容 // 如果是自定义的异样类则抛出自定义的status // 否则就是内置HTTP异样类,而后抛出其对应的内置Status内容 const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; // 抛出错误信息 const message = exception.message || exception.message.message || exception.message.error || null; let msgLog = { statusCode: status, // 零碎谬误状态 timestamp: new Date().toISOString(), // 谬误日期 path: request.url, // 谬误路由 message: '申请失败', data: message // 谬误音讯内容体(争取和拦截器中定义的响应体一样) } // 打印谬误综合日志 Logger.error( '错误信息', JSON.stringify(msgLog), 'HttpExceptionFilter', ); response .status(status) .json(msgLog); }}
注册形式
- 全局注册:在
main.ts
中导入须要的模块如:HttpExceptionFilter
而后应用app.useGlobalFilters(new HttpExceptionFilter())
即可 - 模块注册:在须要注册的
controller
控制器中导入HttpExceptionFilter
而后从@nestjs/common
中导入UseFilters
装璜器;最初间接搁置在对应的@Controller()
或者@Post/@Get…
等装璜器之下即可
留神: 同一路由注册多个管道的时候,只会执行一个异样过滤器,优先执行模块中绑定的异样过滤器,如果模块中无绑定异样过滤则执行全局异样过滤器
数据验证
如何 限度 和 验证 前端传递过去的数据?
罕用:dto
(data transfer object数据传输对象) + class-validator
,自定义提醒内容,还能集成swagger
class-validator的验证项装璜器
https://github.com/typestack/...
@IsOptional() //可选的@IsNotEmpty({ message: ‘不能为空’ })@MinLength(6, {message: ‘明码长度不能小于6位’})@MaxLength(20, {message: ‘明码长度不能超过20位’})
@IsEmail({}, { message: ‘邮箱格局谬误’ }) //邮箱@IsMobilePhone(‘zh-CN’, {}, { message: ‘手机号码格局谬误’ }) //手机号码@IsEnum([0, 1], {message: ‘只能传入数字0或1’}) //枚举
@ValidateIf(o => o.username === ‘admin’) //条件判断,条件满足才验证,如:这里是传入的username是admin才验证
yarn add class-validator class-transformer
全局应用内置管道ValidationPipe
,不然会报错,无奈起作用
import { NestFactory } from '@nestjs/core';import { Logger, ValidationPipe } from '@nestjs/common';import { AppModule } from './app.module';async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalPipes(new ValidationPipe()); //全局内置管道 await app.listen(3000);}bootstrap();
编写dto
,应用class-validator
的校验项验证
创立DTO:只须要用户名,明码即可,两种都不能为空
能够应用nest g res user
一键创立带有dto的接口模块
import { IsNotEmpty, MinLength, MaxLength } from 'class-validator';export class CreateUserDto { @IsNotEmpty({ message: '用户名不能为空' }) username: string; @IsNotEmpty({ message: '明码不能为空' }) @MinLength(6, { message: '明码长度不能小于6位', }) @MaxLength(20, { message: '明码长度不能超过20位', }) password: string;}
批改DTO:用户名,明码,手机号码,邮箱,性别,状态,都是可选的
import { IsEnum, MinLength, MaxLength, IsOptional, IsEmail, IsMobilePhone,} from 'class-validator';import { Type } from 'class-transformer';export class UpdateUserDto { @IsOptional() username: string; @IsOptional() @MinLength(6, { message: '明码长度不能小于6位', }) @MaxLength(20, { message: '明码长度不能超过20位', }) password: string; @IsOptional() @IsEmail({}, { message: '邮箱格局谬误' }) email: string; @IsOptional() @IsMobilePhone('zh-CN', {}, { message: '手机号码格局谬误' }) mobile: string; @IsOptional() @IsEnum(['male', 'female'], { message: 'gender只能传入字符串male或female', }) gender: string; @IsOptional() @IsEnum({ 禁用: 0, 可用: 1 },{ message: 'status只能传入数字0或1', }) @Type(() => Number) //如果传递的是string类型,不报错,主动转成number类型 status: number;}
controller
和service
一起应用
// user.controller.tsimport { Controller, Post, Body, HttpCode, HttpStatus,} from '@nestjs/common';import { UserService } from './user.service';import { CreateUserDto } from './dto/create-user.dto';@Controller('user')export class UserController { constructor(private readonly userService: UserService) { } @Post() @HttpCode(HttpStatus.OK) async create(@Body() user: CreateUserDto) { //应用创立dto return await this.userService.create(user); } @Patch(':id') async update(@Param('id') id: string, @Body() user: UpdateUserDto) { //应用更新dto return await this.userService.update(id, user); } }
// user.service.tsimport { Injectable } from '@nestjs/common';import { Repository } from 'typeorm';import { InjectRepository } from '@nestjs/typeorm';import { UsersEntity } from './entities/user.entity';import { ToolsService } from '../../utils/tools.service';import { CreateUserDto } from './dto/create-user.dto';@Injectable()export class UserService { constructor( @InjectRepository(UsersEntity) private readonly usersRepository: Repository<UsersEntity>, ) { } async create(user: CreateUserDto) { //应用dto do some thing.... }}
进阶
配置抽离
yarn add nestjs-config
app.module.tsimport * as path from 'path';import { Module } from '@nestjs/common';//数据库import { TypeOrmModule } from '@nestjs/typeorm';//全局配置import { ConfigModule, ConfigService } from 'nestjs-config';@Module({ imports: [ //1.配置config目录 ConfigModule.load(path.resolve(__dirname, 'config', '**/!(*.d).{ts,js}')), //2.读取配置,这里读取的是数据库配置 TypeOrmModule.forRootAsync({ useFactory: (config: ConfigService) => config.get('database'), inject: [ConfigService], // 获取服务注入 }) ], controllers: [AppController], providers: [AppService],})export class AppModule {}
配置数据库
src -> config -> databaseimport { join } from 'path';export default { type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'your password', database: 'test', entities: [join(__dirname, '../', '**/**.entity{.ts,.js}')], synchronize: true,};
环境配置
yarn add cross-env
cross-env的作用是兼容window零碎和mac零碎来设置环境变量
在package.json中配置
"scripts": { "start:dev": "cross-env NODE_ENV=development nest start --watch", "start:debug": "nest start --debug --watch", "start:prod": "cross-env NODE_ENV=production node dist/main", },
dotenv的应用
yarn add dotenv
根目录创立 env.parse.ts
import * as fs from 'fs';import * as path from 'path';import * as dotenv from 'dotenv';const isProd = process.env.NODE_ENV === 'production';const localEnv = path.resolve('.env.local');const prodEnv = path.resolve('.env.prod');const filePath = isProd && fs.existsSync(prodEnv) ? prodEnv : localEnv;// 配置 通过process.env.xx读取变量dotenv.config({ path: filePath });
导入环境
// main.tsimport '../env.parse'; // 导入环境变量
.env.local
PORT=9000MYSQL_HOST=127.0.0.1MYSQL_PORT=3306MYSQL_USER=rootMYSQL_PASSWORD=123MYSQL_DATABASE=test
.env.prod
PORT=9000MYSQL_HOST=127.0.0.1MYSQL_PORT=3306MYSQL_USER=rootMYSQL_PASSWORD=1234MYSQL_DATABASE=test
读取环境变量 process.env.MYSQL_HOST
模式
文件上传与下载
yarn add @nestjs/platform-express compressingcompressing 文件下载依赖,提供流的形式
配置文件的目录地址,以及文件的名字格局
// src/config/file.ts 上传文件配置import { join } from 'path';import { diskStorage } from 'multer';/** * 上传文件配置 */export default { root: join(__dirname, '../../assets/uploads'), storage: diskStorage({ destination: join( __dirname, `../../assets/uploads/${new Date().toLocaleDateString()}`, ), filename: (req, file, cb) => { const filename = `${new Date().getTime()}.${file.mimetype.split('/')[1]}`; return cb(null, filename); }, }),};
// app.module.tsimport { ConfigModule, ConfigService } from 'nestjs-config';@Module({ imports: [ // 加载配置文件目录 src/config ConfigModule.load(resolve(__dirname, 'config', '**/!(*.d).{ts,js}')), ], controllers: [], providers: [],})export class AppModule implements NestModule {}
// upload.controller.tsimport { Controller, Get, Post, UseInterceptors, UploadedFile, UploadedFiles, Body, Res,} from '@nestjs/common';import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';import { FileUploadDto } from './dto/upload-file.dto';import { UploadService } from './upload.service';import { Response } from 'express';@Controller('common')export class UploadController { constructor(private readonly uploadService: UploadService) {} @Post('upload') @UseInterceptors(FileInterceptor('file')) uploadFile(@UploadedFile() file) { this.uploadService.uploadSingleFile(file); return true; } // 多文件上传 @Post('uploads') @UseInterceptors(FilesInterceptor('file')) uploadMuliFile(@UploadedFiles() files, @Body() body) { this.uploadService.UploadMuliFile(files, body); return true; } @Get('export') async downloadAll(@Res() res: Response) { const { filename, tarStream } = await this.uploadService.downloadAll(); res.setHeader('Content-Type', 'application/octet-stream'); res.setHeader('Content-Disposition', `attachment; filename=${filename}`); tarStream.pipe(res); }}
// upload.service.tsimport { Injectable, HttpException, HttpStatus } from '@nestjs/common';import { join } from 'path';import { createWriteStream } from 'fs';import { tar } from 'compressing';import { ConfigService } from 'nestjs-config';@Injectable()export class UploadService { constructor(private readonly configService: ConfigService) {} uploadSingleFile(file: any) { console.log('file', file); } UploadMuliFile(files: any, body: any) { console.log('files', files); } async downloadAll() { const uploadDir = this.configService.get('file').root; const tarStream = new tar.Stream(); await tarStream.addEntry(uploadDir); return { filename: 'download.tar', tarStream }; }}
// upload.module.tsimport { Module } from '@nestjs/common';import { MulterModule } from '@nestjs/platform-express';import { ConfigService } from 'nestjs-config';import { UploadService } from './upload.service';import { UploadController } from './upload.controller';@Module({ imports: [ MulterModule.registerAsync({ useFactory: (config: ConfigService) => config.get('file'), inject: [ConfigService], }), ], controllers: [UploadController], providers: [UploadService],})export class UploadModule {}
实现图片随机验证码
nest如何实现图片随机验证码?
这里应用的是svg-captcha这个库,你也能够应用其余的库
yarn add svg-captcha
封装,以便屡次调用
src -> utils -> tools.service.tsimport { Injectable } from '@nestjs/common';import * as svgCaptcha from 'svg-captcha';@Injectable()export class ToolsService { async captche(size = 4) { const captcha = svgCaptcha.create({ //可配置返回的图片信息 size, //生成几个验证码 fontSize: 50, //文字大小 width: 100, //宽度 height: 34, //高度 background: '#cc9966', //背景色彩 }); return captcha; }}
在应用的module中引入
import { Module } from '@nestjs/common';import { UserController } from './user.controller';import { UserService } from './user.service';import { ToolsService } from '../../utils/tools.service';@Module({ controllers: [UserController], providers: [UserService, ToolsService],})export class UserModule { }
应用
import { Controller, Get, Post,Body } from '@nestjs/common';import { EmailService } from './email.service';@Controller('user')export class UserController{ constructor(private readonly toolsService: ToolsService,) {} //注入服务 @Get('authcode') //当申请该接口时,返回一张随机图片验证码 async getCode(@Req() req, @Res() res) { const svgCaptcha = await this.toolsService.captche(); //创立验证码 req.session.code = svgCaptcha.text; //应用session保留验证,用于登陆时验证 console.log(req.session.code); res.type('image/svg+xml'); //指定返回的类型 res.send(svgCaptcha.data); //给页面返回一张图片 } @Post('/login') login(@Body() body, @Session() session) { //验证验证码,由前端传递过去 const { code } = body; if(code?.toUpperCase() === session.code?.toUpperCase()){ console.log(‘验证码通过’) } return 'hello authcode'; }}
前端简略代码
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> form { display: flex; } .input { width: 80px; height: 32px; } .verify_img { margin: 0px 5px; } </style></head><body> <h2>随机验证码</h2> <form action="/user/login" method="post" enctype="application/x-www-form-urlencoded"> <input type="text" name='code' class="input" /> <img class="verify_img" src="/user/code" title="看不清?点击刷新" onclick="javascript:this.src='/user/code?t='+Math.random()"> //点击再次生成新的验证码 <button type="submit">提交</button> </form></body></html>
邮件服务
邮件服务应用文档 https://nest-modules.github.i...
// 邮件服务配置// app.module.tsimport { MailerModule } from '@nestjs-modules/mailer';import { resolve, join } from 'path';import { ConfigModule, ConfigService } from 'nestjs-config';@Module({ imports: [ // 加载配置文件目录 src/config ConfigModule.load(resolve(__dirname, 'config', '**/!(*.d).{ts,js}')), // 邮件服务配置 MailerModule.forRootAsync({ useFactory: (config: ConfigService) => config.get('email'), inject: [ConfigService], }), ], controllers: [], providers: [],})export class AppModule implements NestModule {}// src/config/email.ts 邮件服务配置import { join } from 'path';// npm i ejs -Simport { EjsAdapter } from '@nestjs-modules/mailer/dist/adapters/ejs.adapter';export default { transport: { host: 'smtp.qq.com', secureConnection: true, // use SSL secure: true, port: 465, ignoreTLS: false, auth: { user: '123@test.com', pass: 'dfafew1', }, }, defaults: { from: '"nestjs" <123@test.com>', }, // preview: true, // 发送邮件前预览 template: { dir: join(__dirname, '../templates/email'), // 邮件模板 adapter: new EjsAdapter(), options: { strict: true, }, },};
邮件服务应用
// email.services.tsimport { Injectable } from '@nestjs/common';import { MailerService } from '@nestjs-modules/mailer';@Injectable()export class EmailService { // 邮件服务注入 constructor(private mailerService: MailerService) {} async sendEmail() { console.log('发送邮件'); await this.mailerService.sendMail({ to: 'test@qq.com', // 收件人 from: '123@test.com', // 发件人 // subject: '副标题', text: 'welcome', // plaintext body html: '<h1>hello</h1>', // HTML body content // template: 'email', // 邮件模板 // context: { // 传入邮件模板的data // email: 'test@qq.com', // }, }); return '发送胜利'; }}
nest基于possport + jwt做登陆验证
形式与逻辑
- 基于possport的本地策略和jwt策略
- 本地策略次要是验证账号和明码是否存在,如果存在就登陆,返回token
- jwt策略则是验证用户登陆时附带的token是否匹配和无效,如果不匹配和有效则返回401状态码
yarn add @nestjs/jwt @nestjs/passport passport-jwt passport-local passportyarn add -D @types/passport @types/passport-jwt @types/passport-local
jwt策略 jwt.strategy.ts
// src/modules/auth/jwt.strategy.tsimport { Strategy, ExtractJwt, StrategyOptions } from 'passport-jwt';import { Injectable } from '@nestjs/common';import { PassportStrategy } from '@nestjs/passport';import { jwtConstants } from './constants';@Injectable()export class JwtStrategy extends PassportStrategy(Strategy) { constructor() { super({ jwtFromRequest: ExtractJwt.fromHeader('token'), ignoreExpiration: false, secretOrKey: jwtConstants.secret, // 应用密钥解析 } as StrategyOptions); } //token验证, payload是super中曾经解析好的token信息 async validate(payload: any) { return { userId: payload.userId, username: payload.username }; }}
本地策略 local.strategy.ts
// src/modules/auth/local.strategy.tsimport { Strategy, IStrategyOptions } from 'passport-local';import { Injectable, HttpException, HttpStatus } from '@nestjs/common';import { PassportStrategy } from '@nestjs/passport';import { AuthService } from './auth.service';//本地策略//PassportStrategy承受两个参数://第一个:Strategy,你要用的策略,这里是passport-local,本地策略//第二个:别名,可选,默认是passport-local的local,用于接口时传递的字符串@Injectable()export class LocalStrategy extends PassportStrategy(Strategy) { constructor(private authService: AuthService) { super({ usernameField: 'username', passwordField: 'password', } as IStrategyOptions); } // validate是LocalStrategy的内置办法 async validate(username: string, password: string): Promise<any> { //查询数据库,验证账号密码,并最终返回用户 return await this.authService.validateUser({ username, password }); }}
constants.ts
// src/modules/auth/constants.tsexport const jwtConstants = { secret: 'secretKey',};
应用守卫 auth.controller.ts
// src/modules/auth/auth.controller.tsimport { 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) {} // 登录测试 无需token @UseGuards(AuthGuard('local')) //本地策略,传递local,执行local外面的validate办法 @Post('login') async login(@Request() req) { //通过req能够获取到validate办法返回的user,传递给login,登陆 return this.authService.login(req.user); } // 在须要的中央应用守卫,须要带token才可拜访 @UseGuards(AuthGuard('jwt'))//jwt策略,身份鉴权 @Get('userInfo') getUserInfo(@Request() req) {//通过req获取到被验证后的user,也能够应用装璜器 return req.user; }}
在module引入jwt配置和数据库查问的实体 auth.module.ts
// src/modules/auth/auth.module.tsimport { LocalStrategy } from './local.strategy';import { jwtConstants } from './constants';import { Module } from '@nestjs/common';import { PassportModule } from '@nestjs/passport';import { JwtModule } from '@nestjs/jwt';import { AuthService } from './auth.service';import { AuthController } from './auth.controller';import { JwtStrategy } from './jwt.strategy';import { UsersEntity } from '../user/entities/user.entity';import { TypeOrmModule } from '@nestjs/typeorm';@Module({ imports: [ TypeOrmModule.forFeature([UsersEntity]), PassportModule, JwtModule.register({ secret: jwtConstants.secret, signOptions: { expiresIn: '10d' }, }), ], controllers: [AuthController], providers: [AuthService, LocalStrategy, JwtStrategy], exports: [AuthService],})export class AuthModule {}
auth.service.ts
// src/modules/auth/auth.service.tsimport { Injectable } from '@nestjs/common';import { JwtService } from '@nestjs/jwt';import { compareSync } from 'bcryptjs';@Injectable()export class AuthService { constructor( @InjectRepository(UsersEntity), private readonly usersRepository: Repository<UsersEntity>, private jwtService: JwtService ) {} validateUser(username: string, password: string) { const user = await this.usersRepository.findOne({ where: { username }, select: ['username', 'password'], }); if (!user) ToolsService.fail('用户名或明码不正确'); //应用bcryptjs验证明码 if (!compareSync(password, user.password)) { ToolsService.fail('用户名或明码不正确'); } return user; } login(user: any) { const payload = { username: user.username }; // 把信息存在token return { token: this.jwtService.sign(payload), }; }}
最初在app.module.ts中导入即可测试
// app.modules.tsimport { AuthModule } from './modules/auth/auth.module';@Module({ imports: [ ... AuthModule, // 导入模块 ], controllers: [AppController], providers: [],})export class AppModule implements NestModule {}
应用postman测试
对数据库的明码加密:md5和bcryptjs
明码加密
个别开发中,是不会有人间接将明码明文间接放到数据库当中的。因为这种做法是十分不平安的,须要对明码进行加密解决。
益处:
- 预防外部网站经营人员晓得用户的明码
- 预防内部的攻打,尽可能爱护用户的隐衷
加密形式
- 应用
md5
:每次生成的值是一样的,一些网站能够破解,因为每次存储的都是一样的值 - 应用
bcryptjs
:每次生成的值是不一样的
yarn add md5
加密
import * as md5 from 'md5';const passwrod = '123456';const transP = md5(passwrod); // 固定值:e10adc3949ba59abbe56e057f20f883e
给明码加点"盐":目标是混同明码,其实还是失去固定的值
const passwrod = '123456';const salt = 'dmxys'const transP = md5(passwrod + salt); // 固定值:4e6a2881e83262a72f6c70f48f3e8022
验证明码:先加密,再验证
const passwrod = '123456';const databasePassword = 'e10adc3949ba59abbe56e057f20f883e'if (md5(passwrod) === databasePassword ) { console.log('明码通过');}
应用bcryptjs
yarn add bcryptjsyarn add -D @types/bcryptjs
同一明码,每次生成不一样的值
import { compareSync, hashSync } from 'bcryptjs';const passwrod = '123456';const transformPass = hashSync(passwrod); $2a$10$HgTA1GX8uxbocSQlbQ42/.Y2XnIL7FyfKzn6IC69IXveD6F9LiULSconst transformPass2 = hashSync(passwrod); $2a$10$mynd130vI1vkz4OQ3C.6FeYXGEq24KLUt1CsKN2WZqVsv0tPrtOcWconst transformPass3 = hashSync(passwrod); $2a$10$bOHdFQ4TKBrtcNgmduzD8esds04BoXc0JcrLme68rTeik7U96KBvu
验证明码:应用不同的值 匹配 明码123456,都能通过
const password = '123456';const databasePassword1 = '$2a$10$HgTA1GX8uxbocSQlbQ42/.Y2XnIL7FyfKzn6IC69IXveD6F9LiULS'const databasePassword2 = '$2a$10$mynd130vI1vkz4OQ3C.6FeYXGEq24KLUt1CsKN2WZqVsv0tPrtOcW'const databasePassword3 = '$2a$10$bOHdFQ4TKBrtcNgmduzD8esds04BoXc0JcrLme68rTeik7U96KBvu'if (compareSync(password, databasePassword3)) { console.log('明码通过');}
举荐应用bcryptjs
,算法要比md5
高级
角色权限
RBAC
- RBAC是基于角色的权限访问控制(Role-Based Access Control)一种数据库设计思维,依据设计数据库设计方案,实现我的项目的权限治理
- 在RBAC中,有3个根底组成部分,别离是:
用户
、角色
和权限
,权限与角色相关联,用户通过成为适当角色而失去这些角色的权限
- 权限:具备操作某个事务的能力
- 角色:一系列权限的汇合
如:个别的管理系统中:
销售人员:仅仅能够查看商品信息
经营人员:能够查看,批改商品信息
管理人员:能够查看,批改,删除,以及批改员工权限等等
管理人员只有为每个员工账号调配对应的角色,登陆操作时就只能执行对应的权限或看到对应的页面
权限类型
- 展现(菜单),如:显示用户列表,显示删除按钮等等…
- 操作(性能),如:增删改查,上传下载,发布公告,发动流动等等…
数据库设计
数据库设计:可简略,可简单,几个人应用的零碎和几千人应用的零碎是不一样的
小型我的项目:用户表,权限表
中型我的项目:用户表,角色表,权限表
大型项目:用户表,用户分组表,角色表,权限表,菜单表…
没有角色的设计
只有用户表,菜单表,两者是多对多关系,有一个关联表
毛病:
- 新建一个用户时,在用户表中增加一条数据
- 新建一个用户时,在关联表中增加N条数据
- 每次新建一个用户须要增加1+N(关联几个)条数据
- 如果有100个用户,每个用户100个权限,那须要增加10000条数据
基于RBAC的设计
用户表和角色表的关系设计:
如果你心愿一个用户能够有多个角色,如:一个人即是销售总监,也是人事管理,就设计多对多关系
如果你心愿一个用户只能有一个角色,就设计一对多,多对一关系
角色表和权限表的关系设计:
一个角色能够领有多个权限,一个权限被多个角色应用,设计多对多关系
多对多关系设计
用户表与角色表是多对多关系,角色表与菜单表是多对多关系
更加简单的设计
实现流程
- 数据表设计
- 实现角色的增删改查
- 实现用户的增删改查,减少和批改用户的时候须要抉择角色
- 实现权限的增删改查
- 实现角色与受权的关联
- 判断以后登录的用户是否有拜访菜单的权限
- 依据以后登录账户的角色信息动态显示左侧菜单(前端)
代码实现
这里将实现一个用户,部门,角色,权限的例子:
用户通过成为部门的一员,则领有部门一般角色的权限,还能够独自给用户设置角色,通过角色,获取权限。
权限模块包含,模块,菜单,操作,通过type辨别类型,这里就不再拆分。
关系总览:
- 用户 - 部门:一对多关系,这里设计用户只能退出一个部门,如果设计能够退出多个部门,设计为多对多关系
- 用户 - 角色:多对多关系,能够给用户设置多个角色
- 角色 - 部门:多对多关系,一个部门多个角色
- 角色 - 权限:多对多关系,一个角色领有多个权限,一个权限被多个角色应用
数据库实体设计
用户
import { Column, Entity, ManyToMany, ManyToOne, JoinColumn, JoinTable, PrimaryGeneratedColumn,} from 'typeorm';import { RoleEntity } from '../../role/entities/role.entity';import { DepartmentEntity } from '../../department/entities/department.entity';@Entity({ name: 'user' })export class UsersEntity { @PrimaryGeneratedColumn() id: number; @Column({ type: 'varchar', length: 30, nullable: false, unique: true, }) username: string; @Column({ type: 'varchar', name: 'password', length: 100, nullable: false, select: false, comment: '明码', }) password: string; @ManyToMany(() => RoleEntity, (role) => role.users) @JoinTable({ name: 'user_role' }) roles: RoleEntity[]; @ManyToOne(() => DepartmentEntity, (department) => department.users) @JoinColumn({ name: 'department_id' }) department: DepartmentEntity;}
角色
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable,} from 'typeorm';import { UsersEntity } from '../../user/entities/user.entity';import { DepartmentEntity } from '../../department/entities/department.entity';import { AccessEntity } from '../../access/entities/access.entity';@Entity({ name: 'role' })export class RoleEntity { @PrimaryGeneratedColumn() id: number; @Column({ type: 'varchar', length: 30 }) rolename: string; @ManyToMany(() => UsersEntity, (user) => user.roles) users: UsersEntity[]; @ManyToMany(() => DepartmentEntity, (department) => department.roles) department: DepartmentEntity[]; @ManyToMany(() => AccessEntity, (access) => access.roles) @JoinTable({ name: 'role_access' }) access: AccessEntity[];}
部门
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, OneToMany, JoinTable,} from 'typeorm';import { UsersEntity } from '../../user/entities/user.entity';import { RoleEntity } from '../../role/entities/role.entity';@Entity({ name: 'department' })export class DepartmentEntity { @PrimaryGeneratedColumn() id: number; @Column({ type: 'varchar', length: 30 }) departmentname: string; @OneToMany(() => UsersEntity, (user) => user.department) users: UsersEntity[]; @ManyToMany(() => RoleEntity, (role) => role.department) @JoinTable({ name: 'department_role' }) roles: RoleEntity[];}
权限
import { Entity, PrimaryGeneratedColumn, Column, Tree, TreeChildren, TreeParent, ManyToMany,} from 'typeorm';import { RoleEntity } from '../../role/entities/role.entity';@Entity({ name: 'access' })@Tree('closure-table')export class AccessEntity { @PrimaryGeneratedColumn() id: number; @Column({ type: 'varchar', length: 30, comment: '模块' }) module_name: string; @Column({ type: 'varchar', length: 30, nullable: true, comment: '操作' }) action_name: string; @Column({ type: 'tinyint', comment: '类型:1:模块,2:菜单,3:操作' }) type: number; @Column({ type: 'text', nullable: true, comment: '操作地址' }) url: string; @TreeParent() parentCategory: AccessEntity; @TreeChildren() childCategorys: AccessEntity[]; @ManyToMany(() => RoleEntity, (role) => role.access) roles: RoleEntity[];}
接口实现
因为要实现很多接口,这里只阐明一部分,其实都是数据库的操作,所有接口如下:
依据用户的id获取信息:id,用户名,部门名,角色,这些信息在做用户登陆时传递到token中。
这里设计的是:创立用户时,增加部门,就会成为部门的一般角色,也可独自设置角色,但不是每个用户都有独自的角色。
async getUserinfoByUid(uid: number) { 获取用户 const user = await this.usersRepository.findOne( { id: uid }, { relations: ['roles'] }, ); if (!user) ToolsService.fail('用户ID不存在'); const sql = ` select user.id as user_id, user.username, user.department_id, department.departmentname, role.id as role_id, rolename from user, department, role, department_role as dr where user.department_id = department.id and department.id = dr.departmentId and role.id = dr.roleId and user.id = ${uid}`; const result = await this.usersRepository.query(sql); const userinfo = result[0]; const userObj = { user_id: userinfo.user_id, username: userinfo.username, department_id: userinfo.department_id, departmentname: userinfo.departmentname, roles: [{ id: userinfo.role_id, rolename: userinfo.rolename }], }; // 如果用户的角色roles有值,证实独自设置了角色,所以须要拼接起来 if (user.roles.length > 0) { const _user = JSON.parse(JSON.stringify(user)); userObj.roles = [...userObj.roles, ..._user.roles]; } return userObj;}// 接口申请后果:{ "status": 200, "message": "申请胜利", "data": { "user_id": 1, "username": "admin", "department_id": 1, "departmentname": "销售部", "roles": [ { "id": 1, "rolename": "销售部员工" }, { "id": 5, "rolename": "admin" } ] }}
联合possport + jwt 做用户登陆受权验证
在验证账户明码通过后,possport 返回用户,而后依据用户id获取用户信息,存储token,用于路由守卫,还能够应用redis存储,以作他用。
async login(user: any): Promise<any> { const { id } = user; const userResult = await this.userService.getUserinfoByUid(id); const access_token = this.jwtService.sign(userResult); await this.redisService.set(`user-token-${id}`, access_token, 60 * 60 * 24); return { access_token };}{ "status": 200, "message": "申请胜利", "data": { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZGVwYXJ0bWVudF9pZCI6MSwiZGVwYXJ0bWVudG5hbWUiOiLplIDllK7pg6giLCJyb2xlcyI6W3siaWQiOjEsInJvbGVuYW1lIjoi6ZSA5ZSu6YOo5ZGY5belIn0seyJpZCI6NSwicm9sZW5hbWUiOiJhZG1pbiJ9XSwiaWF0IjoxNjIxNjA1Nzg5LCJleHAiOjE2MjE2OTIxODl9.VIp0MdzSPM13eq1Bn8bB9Iu_SLKy4yoMU2N4uwgWDls" }}
后端的权限拜访
应用守卫,装璜器,联合token,验证拜访权限
逻辑:
- 第一步:在
controller
应用自定义守卫装璜接口门路,在申请该接口门路时,全副进入守卫逻辑 - 第二步:应用自定义装璜器装璜特定接口,传递角色,自定义守卫会应用反射器获取该值,以判断该用户是否有权限
如下:findOne
接口应用了自定义装璜器装璜接口,意思是只能admin
来拜访
import { Controller, Get, Body, Patch, Post, Param, Delete, UseGuards, ParseIntPipe,} from '@nestjs/common';import { UserService } from './user.service';import { CreateUserDto } from './dto/create-user.dto';import { UpdateUserDto } from './dto/update-user.dto';import { AuthGuard } from '../../common/guard/auth.guard';import { Roles } from '../../common/decorator/role.decorator';@UseGuards(AuthGuard) // 自定义守卫@Controller('user')export class UserController { constructor(private readonly userService: UserService) { } @Get() async findAll() { const [data, count] = await this.userService.findAll(); return { count, data }; } @Get(':id') @Roles('admin') // 自定义装璜器 async findOne(@Param('id', new ParseIntPipe()) id: number) { return await this.userService.findOne(id); }}
装璜器
import { SetMetadata } from '@nestjs/common';// SetMetadata作用:将获取到的值,设置到元数据中,而后守卫通过反射器能力获取到值export const Roles = (...args: string[]) => SetMetadata('roles', args);
自定义守卫
返回true
则有拜访权限,返回false
则间接报403
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';import { JwtService } from '@nestjs/jwt';import { Reflector } from '@nestjs/core'; // 反射器,作用与自定义装璜器桥接import { ToolsService } from '../../utils/tools.service';@Injectable()export class AuthGuard implements CanActivate { constructor( private readonly reflector: Reflector, private readonly jwtService: JwtService, ) { } // 白名单数组 private whiteUrlList: string[] = []; // 验证该次申请是否为白名单内的路由 private isWhiteUrl(urlList: string[], url: string): boolean { if (urlList.includes(url)) { return true; } return false; } canActivate(context: ExecutionContext): boolean { // 获取申请对象 const request = context.switchToHttp().getRequest(); // 验证是否是白名单内的路由 if (this.isWhiteUrl(this.whiteUrlList, request.url)) return true; // 获取申请头中的token字段,解析获取存储在token的用户信息 const token = context.switchToRpc().getData().headers.token; const user: any = this.jwtService.decode(token); if (!user) ToolsService.fail('token获取失败,请传递token或书写正确'); // 应用反射器,配合装璜器应用,获取装璜器传递过去的数据 const authRoles = this.reflector.get<string[]>( 'roles', context.getHandler(), ); // 如果没有应用roles装璜,就获取不到值,就不鉴权,等于白名单 if (!authRoles) return true; // 如果用户的所属角色与装璜器传递过去的值匹配则通过,否则不通过 const userRoles = user.roles; for (let i = 0; i < userRoles.length; i++) { if (authRoles.includes(userRoles[i].rolename)) { return true; } } return false; }}
简略测试
两个用户,别离对应不同的角色,别离申请user的findOne接口
用户1:销售部员工和admin
用户2:人事部员工
用户1:销售部员工和admin{ "status": 200, "message": "申请胜利", "data": { "user_id": 1, "username": "admin", "department_id": 1, "departmentname": "销售部", "roles": [ { "id": 1, "rolename": "销售部员工" }, { "id": 5, "rolename": "admin" } ] }}用户2:人事部员工{ "status": 200, "message": "申请胜利", "data": { "user_id": 2, "username": "admin2", "department_id": 2, "departmentname": "人事部", "roles": [ { "id": 3, "rolename": "人事部员工" } ] }}不出意外的话:2号用户的申请后果{ "status": 403, "message": "Forbidden resource", "error": "Forbidden", "path": "/user/1", "timestamp": "2021-05-21T14:44:04.954Z"}
前端的权限拜访则是通过权限表url和type来解决
定时工作
nest如何开启定时工作?
定时工作场景
每天定时更新,定时发送邮件
没有controller,因为定时工作是主动实现的
yarn add @nestjs/schedule
// src/tasks/task.module.tsimport { Module } from '@nestjs/common';import { TasksService } from './tasks.service';@Module({ providers: [TasksService],})export class TasksModule {}
在这里编写你的定时工作
// src/tasks/task.service.tsimport { Injectable, Logger } from '@nestjs/common';import { Cron, Interval, Timeout } from '@nestjs/schedule';@Injectable()export class TasksService { private readonly logger = new Logger(TasksService.name); @Cron('45 * * * * *') 每隔45秒执行一次 handleCron() { this.logger.debug('Called when the second is 45'); } @Interval(10000) 每隔10秒执行一次 handleInterval() { this.logger.debug('Called every 10 seconds'); } @Timeout(5000) 5秒只执行一次 handleTimeout() { this.logger.debug('Called once after 5 seconds'); }}
自定义定时工夫
* * * * * * 别离对应的意思:第1个星:秒第2个星:分钟第3个星:小时第4个星:一个月中的第几天第5个星:月第6个星:一个星期中的第几天如:45 * * * * *:每隔45秒执行一次
挂载-应用
// app.module.tsimport { TasksModule } from './tasks/task.module';import { ScheduleModule } from '@nestjs/schedule';imports: [ ConfigModule.load(path.resolve(__dirname, 'config', '**/!(*.d).{ts,js}')), ScheduleModule.forRoot(), TasksModule, ],
接入Swagger接口文档
- 长处:不必写接口文档,在线生成,主动生成,可操作数据库,完满配合
dto
- 毛病:多一些代码,显得有点乱,习惯就好
yarn add @nestjs/swagger swagger-ui-express -D
// main.tsimport { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';async function bootstrap() { // 创立实例 const app = await NestFactory.create<NestExpressApplication>(AppModule); // 创立接口文档服务 const options = new DocumentBuilder() .addBearerAuth() // token认证,输出token才能够拜访文档 .setTitle('接口文档') .setDescription('接口文档介绍') // 文档介绍 .addServer('http://localhost:9000', '开发环境') .addServer('https://test.com/release', '正式环境') .setVersion('1.0.0') // 文档版本 .setContact('poetry', '', 'test@qq.com') .build(); // 为了创立残缺的文档(具备定义的HTTP路由),咱们应用类的createDocument()办法SwaggerModule。此办法带有两个参数,别离是应用程序实例和根本Swagger选项。 const document = SwaggerModule.createDocument(app, options, { extraModels: [], // 这里导入模型 }); // 启动swagger SwaggerModule.setup('api-docs', app, document); // 拜访门路 http://localhost:9000/api-docs // 启动端口 const PORT = process.env.PORT || 9000; await app.listen(PORT, () => Logger.log(`服务曾经启动 http://localhost:${PORT}`), );}bootstrap();
swagger装璜器
https://swagger.io/
@ApiTags('user') // 设置模块接口的分类,不设置默认调配到default@ApiOperation({ summary: '题目', description: '详细描述'}) // 单个接口形容// 传参@ApiQuery({ name: 'limit', required: true}) // query参数@ApiQuery({ name: 'role', enum: UserRole }) // query参数@ApiParam({ name: 'id' }) // parma参数@ApiBody({ type: UserCreateDTO, description: '输出用户名和明码' }) // 申请体// 响应@ApiResponse({ status: 200, description: '胜利返回200,失败返回400', type: UserCreateDTO,})// 验证@ApiProperty({ example: 'Kitty', description: 'The name of the Cat' })name: string;
在controller
引入@nestjs/swagger
, 并配置@ApiBody()
和 @ApiParam()
不写也是能够的
user.controller.tsimport { Controller, Get, Post, Body, Patch, Query, Param, Delete, HttpCode, HttpStatus, ParseIntPipe,} from '@nestjs/common';import { ApiOperation, ApiTags, ApiQuery, ApiBody, ApiResponse,} from '@nestjs/swagger';import { UserService } from './user.service';import { CreateUserDto } from './dto/create-user.dto';import { UpdateUserDto } from './dto/update-user.dto';@Controller('user')@ApiTags('user') // 设置分类export class UserController { constructor(private readonly userService: UserService) { } @Post() @ApiOperation({ summary: '创立用户', description: '创立用户' }) // 该接口 @HttpCode(HttpStatus.OK) async create(@Body() user: CreateUserDto) { return await this.userService.create(user); } @Get() @ApiOperation({ summary: '查找全副用户', description: '创立用户' }) @ApiQuery({ name: 'limit', required: true }) 申请参数 @ApiQuery({ name: 'offset', required: true }) 申请参数 async findAll(@Query() query) { console.log(query); const [data, count] = await this.userService.findAll(query); return { count, data }; } @Get(':id') @ApiOperation({ summary: '依据ID查找用户' }) async findOne(@Param('id', new ParseIntPipe()) id: number) { return await this.userService.findOne(id); } @Patch(':id') @ApiOperation({ summary: '更新用户' }) @ApiBody({ type: UpdateUserDto, description: '参数可选' }) 申请体 @ApiResponse({ 响应示例 status: 200, description: '胜利返回200,失败返回400', type: UpdateUserDto, }) async update( @Param('id', new ParseIntPipe()) id: number, @Body() user: UpdateUserDto, ) { return await this.userService.update(id, user); } @Delete(':id') @ApiOperation({ summary: '删除用户' }) async remove(@Param('id', new ParseIntPipe()) id: number) { return await this.userService.remove(id); }}
编写dto,引入@nestjs/swagger
创立
import { IsNotEmpty, MinLength, MaxLength } from 'class-validator';import { ApiProperty } from '@nestjs/swagger';export class CreateUserDto { @ApiProperty({ example: 'kitty', description: '用户名' }) 增加这里即可 @IsNotEmpty({ message: '用户名不能为空' }) username: string; @ApiProperty({ example: '12345678', description: '明码' }) @IsNotEmpty({ message: '明码不能为空' }) @MinLength(6, { message: '明码长度不能小于6位', }) @MaxLength(20, { message: '明码长度不能超过20位', }) password: string;}
更新
import { IsEnum, MinLength, MaxLength, IsOptional, ValidateIf, IsEmail, IsMobilePhone,} from 'class-validator';import { ApiProperty } from '@nestjs/swagger';import { Type } from 'class-transformer';export class UpdateUserDto { @ApiProperty({ description: '用户名', example: 'kitty', required: false }) 不是必选的 @IsOptional() username: string; @ApiProperty({ description: '明码', example: '12345678', required: false }) @IsOptional() @MinLength(6, { message: '明码长度不能小于6位', }) @MaxLength(20, { message: '明码长度不能超过20位', }) password: string; @ApiProperty({ description: '邮箱', example: 'llovenest@163.com', required: false, }) @IsOptional() @IsEmail({}, { message: '邮箱格局谬误' }) @ValidateIf((o) => o.username === 'admin') email: string; @ApiProperty({ description: '手机号码', example: '13866668888', required: false, }) @IsOptional() @IsMobilePhone('zh-CN', {}, { message: '手机号码格局谬误' }) mobile: string; @ApiProperty({ description: '性别', example: 'female', required: false, enum: ['male', 'female'], }) @IsOptional() @IsEnum(['male', 'female'], { message: 'gender只能传入字符串male或female', }) gender: string; @ApiProperty({ description: '状态', example: 1, required: false, enum: [0, 1], }) @IsOptional() @IsEnum( { 禁用: 0, 可用: 1 }, { message: 'status只能传入数字0或1', }, ) @Type(() => Number) status: number;}
关上:localhost:3000/api-docs,开始测试接口
数据库
nest连贯Mongodb
mac中,间接应用brew install mongodb-community
装置MongoDB,而后启动服务brew services start mongodb-community
查看服务曾经启动ps aux | grep mongo
Nestjs中操作Mongodb数据库能够应用Nodejs封装的DB库,也能够应用Mongoose。
// https://docs.nestjs.com/techniques/mongodbnpm install --save @nestjs/mongoose mongoosenpm install --save-dev @types/mongoose
在app.module.ts中配置数据库连贯
// app.module.tsimport { ConfigModule, ConfigService } from 'nestjs-config';import { MongooseModule } from '@nestjs/mongoose';import { MongodbModule } from '../examples/mongodb/mongodb.module';@Module({ imports: [ // 加载配置文件目录 ConfigModule.load(resolve(__dirname, 'config', '**/!(*.d).{ts,js}')), // mongodb MongooseModule.forRootAsync({ useFactory: async (configService: ConfigService) => configService.get('mongodb'), inject: [ConfigService], }), MongodbModule, ], controllers: [], providers: [],})export class AppModule implements NestModule {}
// mongodb配置// src/config/mongodb.tsexport default { uri: 'mongodb://localhost:27017/nest', // 指定nest数据库};
配置Schema
// article.schemaimport * as mongoose from 'mongoose';export const ArticleSchema = new mongoose.Schema({ title: String, content:String, author: String, status: Number,});
在控制器对应的Module中配置Model
// mongodb.module.tsimport { Module } from '@nestjs/common';import { MongodbService } from './mongodb.service';import { MongodbController } from './mongodb.controller';import { ArticleSchema } from './schemas/article.schema';import { MongooseModule } from '@nestjs/mongoose';@Module({ imports: [ MongooseModule.forFeature([ { name: 'Article', // schema名称对应 schema: ArticleSchema, // 引入的schema collection: 'article', // 数据库名称 }, ]), ], controllers: [MongodbController], providers: [MongodbService],})export class MongodbModule {}
在服务外面应用@InjectModel 获取数据库Model实现操作数据库
// mongodb.service.tsimport { Injectable } from '@nestjs/common';import { InjectModel } from '@nestjs/mongoose';@Injectable()export class MongodbService { // 注入模型 constructor(@InjectModel('Article') private readonly articleModel) {} async findAll() { return await this.articleModel.find().exec(); } async findById(id) { return await this.articleModel.findById(id); } async create(body) { return await this.articleModel.create(body); } async update(body) { const { id, ...params } = body; return await this.articleModel.findByIdAndUpdate(id, params); } async delete(id) { return await this.articleModel.findByIdAndDelete(id); }}
浏览器测试 http://localhost:9000/api/mon...
typeORM操作Mysql数据库
mac中,间接应用brew install mysql
装置mysql,而后启动服务brew services start mysql
查看服务曾经启动ps aux | grep mysql
Nest 操作Mysql官网文档:https://docs.nestjs.com/techn...
npm install --save @nestjs/typeorm typeorm mysql
配置数据库连贯地址
// src/config/typeorm.tsconst { MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE } = process.env;const config = { type: 'mysql', host: MYSQL_HOST, port: MYSQL_PORT, username: MYSQL_USER, password: MYSQL_PASSWORD, database: MYSQL_DATABASE, synchronize: process.env.NODE_ENV !== 'production', // 生产环境不要开启 autoLoadEntities: true, // 如果为true,将主动加载实体(默认:false) keepConnectionAlive: true, // 如果为true,在应用程序敞开后连贯不会敞开(默认:false) retryDelay: 3000, // 两次重试连贯的距离(ms)(默认:3000) retryAttempts: 10, // 重试连贯数据库的次数(默认:10) dateStrings: 'DATETIME', // 转化为工夫 timezone: '+0800', // +HHMM -HHMM // 主动须要导入模型 entities: ['dist/**/*.entity{.ts,.js}'],};export default config;
// app.module.ts中配置import { resolve, join } from 'path';import { ConfigModule, ConfigService } from 'nestjs-config';import { TypeOrmModule } from '@nestjs/typeorm';@Module({ imports: [ // 加载配置文件目录 ConfigModule.load(resolve(__dirname, 'config', '**/!(*.d).{ts,js}')), // 连贯mysql数据库 TypeOrmModule.forRootAsync({ useFactory: (config: ConfigService) => config.get('typeorm'), inject: [ConfigService], }), ], controllers: [], providers: [],})export class AppModule implements NestModule {}
配置实体entity
// photo.entity.tsimport { Column, Entity, ManyToMany, OneToMany, PrimaryGeneratedColumn,} from 'typeorm';import { PostsEntity } from './post.entity';@Entity('photo')export class PhotoEntity { // @PrimaryGeneratedColumn() // id: number; // 标记为主列,值主动生成 @PrimaryGeneratedColumn('uuid') id: string; // 该值将应用uuid主动生成 @Column({ length: 50 }) url: string; // 多对一关系,多个图片对应一篇文章 @ManyToMany(() => PostsEntity, (post) => post.photos) posts: PostsEntity;}import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';import { PhotoEntity } from './photo.entity';export type UserRoleType = 'admin' | 'editor' | 'ghost';export type postStatus = 1 | 2 | 3;// mysql的列类型: type/** * int, tinyint, smallint, mediumint, bigint, float, double, dec, decimal, * numeric, date, datetime, timestamp, time, year, char, varchar, nvarchar, * text, tinytext, mediumtext, blob, longtext, tinyblob, mediumblob, longblob, enum, * json, binary, geometry, point, linestring, polygon, multipoint, multilinestring, * multipolygon, geometrycollection *//** * ColumnOptions中可用选项列表: * length: number - 列类型的长度。 例如,如果要创立varchar(150)类型,请指定列类型和长度选项。 width: number - 列类型的显示范畴。 仅用于MySQL integer types(opens new window) onUpdate: string - ON UPDATE触发器。 仅用于 MySQL (opens new window). nullable: boolean - 在数据库中使列NULL或NOT NULL。 默认状况下,列是nullable:false。 update: boolean - 批示"save"操作是否更新列值。如果为false,则只能在第一次插入对象时编写该值。 默认值为"true"。 select: boolean - 定义在进行查问时是否默认暗藏此列。 设置为false时,列数据不会显示规范查问。 默认状况下,列是select:true default: string - 增加数据库级列的DEFAULT值。 primary: boolean - 将列标记为次要列。 应用形式和@ PrimaryColumn雷同。 unique: boolean - 将列标记为惟一列(创立惟一束缚)。 comment: string - 数据库列备注,并非所有数据库类型都反对。 precision: number - 十进制(准确数字)列的精度(仅实用于十进制列),这是为值存储的最大位数。仅用于某些列类型。 scale: number - 十进制(准确数字)列的比例(仅实用于十进制列),示意小数点右侧的位数,且不得大于精度。 仅用于某些列类型。 zerofill: boolean - 将ZEROFILL属性设置为数字列。 仅在 MySQL 中应用。 如果是true,MySQL 会主动将UNSIGNED属性增加到此列。 unsigned: boolean - 将UNSIGNED属性设置为数字列。 仅在 MySQL 中应用。 charset: string - 定义列字符集。 并非所有数据库类型都反对。 collation: string - 定义列排序规定。 enum: string[]|AnyEnum - 在enum列类型中应用,以指定容许的枚举值列表。 你也能够指定数组或指定枚举类。 asExpression: string - 生成的列表达式。 仅在MySQL (opens new window)中应用。 generatedType: "VIRTUAL"|"STORED" - 生成的列类型。 仅在MySQL (opens new window)中应用。 hstoreType: "object"|"string" -返回HSTORE列类型。 以字符串或对象的模式返回值。 仅在Postgres中应用。 array: boolean - 用于能够是数组的 postgres 列类型(例如 int []) transformer: { from(value: DatabaseType): EntityType, to(value: EntityType): DatabaseType } - 用于将任意类型EntityType的属性编组为数据库反对的类型DatabaseType。 留神:大多数列选项都是特定于 RDBMS 的,并且在MongoDB中不可用 */@Entity('posts')export class PostsEntity { // @PrimaryGeneratedColumn() // id: number; // 标记为主列,值主动生成 @PrimaryGeneratedColumn('uuid') id: string; // 该值将应用uuid主动生成 @Column({ length: 50 }) title: string; @Column({ length: 18 }) author: string; @Column({ type: 'longtext', default: null }) content: string; @Column({ default: null }) cover_url: string; @Column({ default: 0 }) type: number; @Column({ type: 'text', default: null }) remark: string; @Column({ type: 'enum', enum: [1, 2, 3], default: 1, }) status: postStatus; @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) create_time: Date; @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', }) update_time: Date; @Column({ type: 'enum', enum: ['admin', 'editor', 'ghost'], default: 'ghost', select: false, // 定义在进行查问时是否默认暗藏此列 }) role: UserRoleType; // 一对多关系,一篇文章对应多个图片 // 在service中查问应用 .find({relations: ['photos]}) 查问文章对应的图片 @OneToMany(() => PhotoEntity, (photo) => photo.posts) photos: [];}
参数校验
Nest 与 class-validator 配合得很好。这个优良的库容许您应用基于装璜器的验证。装璜器的性能十分弱小,尤其是与 Nest 的 Pipe 性能相结合应用时,因为咱们能够通过拜访 metatype
信息做很多事件,在开始之前须要装置一些依赖。
npm i --save class-validator class-transformer
// posts.dto.tsimport { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';import { IsNotEmpty, IsNumber, IsString } from 'class-validator';export class CreatePostDto { @IsNotEmpty({ message: '文章题目必填' }) readonly title: string; @IsNotEmpty({ message: '短少作者信息' }) readonly author: string; readonly content: string; readonly cover_url: string; @IsNotEmpty({ message: '短少文章类型' }) readonly type: number; readonly remark: string;}
在控制器对应的Module中配置Model
import { Module } from '@nestjs/common';import { TypeOrmModule } from '@nestjs/typeorm';import { PostsService } from './posts.service';import { PostsController } from './posts.controller';import { PostsEntity } from './entities/post.entity';@Module({ imports: [TypeOrmModule.forFeature([PostsEntity])], controllers: [PostsController], providers: [PostsService],})export class PostsModule {}
在服务外面应用@InjectRepository获取数据库Model实现操作数据库
// posts.services.tsimport { HttpException, HttpStatus, Injectable } from '@nestjs/common';import { InjectRepository } from '@nestjs/typeorm';import { Repository, Not, Between, Equal, Like, In } from 'typeorm';import * as dayjs from 'dayjs';import { CreatePostDto } from './dto/create-post.dto';import { UpdatePostDto } from './dto/update-post.dto';import { PostsEntity } from './entities/post.entity';import { PostsRo } from './interfaces/posts.interface';@Injectable()export class PostsService { constructor( @InjectRepository(PostsEntity) private readonly postsRepository: Repository<PostsEntity>, ) {} async create(post: CreatePostDto) { const { title } = post; const doc = await this.postsRepository.findOne({ where: { title } }); console.log('doc', doc); if (doc) { throw new HttpException('文章题目已存在', HttpStatus.BAD_REQUEST); } return { data: await this.postsRepository.save(post), message: '创立胜利', }; } // 分页查问列表 async findAll(query = {} as any) { // eslint-disable-next-line prefer-const let { pageSize, pageNum, orderBy, sort, ...params } = query; orderBy = query.orderBy || 'create_time'; sort = query.sort || 'DESC'; pageSize = Number(query.pageSize || 10); pageNum = Number(query.pageNum || 1); console.log('query', query); const queryParams = {} as any; Object.keys(params).forEach((key) => { if (params[key]) { queryParams[key] = Like(`%${params[key]}%`); // 所有字段反对含糊查问、%%之间不能有空格 } }); const qb = await this.postsRepository.createQueryBuilder('post'); // qb.where({ status: In([2, 3]) }); qb.where(queryParams); // qb.select(['post.title', 'post.content']); // 查问局部字段返回 qb.orderBy(`post.${orderBy}`, sort); qb.skip(pageSize * (pageNum - 1)); qb.take(pageSize); return { list: await qb.getMany(), totalNum: await qb.getCount(), // 按条件查问的数量 total: await this.postsRepository.count(), // 总的数量 pageSize, pageNum, }; } // 依据ID查问详情 async findById(id: string): Promise<PostsEntity> { return await this.postsRepository.findOne({ where: { id } }); } // 更新 async update(id: string, updatePostDto: UpdatePostDto) { const existRecord = await this.postsRepository.findOne({ where: { id } }); if (!existRecord) { throw new HttpException(`id为${id}的文章不存在`, HttpStatus.BAD_REQUEST); } // updatePostDto笼罩existRecord 合并,能够更新单个字段 const updatePost = this.postsRepository.merge(existRecord, { ...updatePostDto, update_time: dayjs().format('YYYY-MM-DD HH:mm:ss'), }); return { data: await this.postsRepository.save(updatePost), message: '更新胜利', }; } // 删除 async remove(id: string) { const existPost = await this.postsRepository.findOne({ where: { id } }); if (!existPost) { throw new HttpException(`文章ID ${id} 不存在`, HttpStatus.BAD_REQUEST); } await this.postsRepository.remove(existPost); return { data: { id }, message: '删除胜利', }; }}
nest对立解决数据库操作的查问后果
操作数据库时,如何做异样处异样? 比方id不存在,用户名曾经存在?如何对立解决申请失败和申请胜利?
解决形式:
- 在nest中,个别是在service中解决异样,如果有异样,间接抛出谬误,由过滤器捕捉,对立格局返回,如果胜利,service把后果返回,controller间接return后果即可,由拦截器捕捉,对立格局返回
- 失败:过滤器对立解决
- 胜利:拦截器对立解决
- 当然你也能够在
controller
解决
// user.controller.tsimport { Controller, Get, Post, Body, HttpCode, HttpStatus,} from '@nestjs/common';import { UserService } from './user.service';@Controller('user')export class UserController { constructor(private readonly userService: UserService) { } @Post() @HttpCode(HttpStatus.OK) //创立胜利返回的是201状态码,这里重置为200,须要用到的能够应用HttpCode设置 async create(@Body() user) { return await this.userService.create(user); } @Get(':id') async findOne(@Param('id') id: string) { return await this.userService.findOne(id); }}
// user.service.tsimport { Injectable, HttpException, HttpStatus } from '@nestjs/common';import { Repository } from 'typeorm';import { InjectRepository } from '@nestjs/typeorm';import { UsersEntity } from './entities/user.entity';@Injectable()export class UserService { constructor( @InjectRepository(UsersEntity) private readonly usersRepository: Repository<UsersEntity> ) { } async create(user) { const { username } = user; const result = await this.usersRepository.findOne({ username }); if (result) { //如果用户名曾经存在,抛出谬误 throw new HttpException( { message: '申请失败', error: '用户名已存在' }, HttpStatus.BAD_REQUEST, ); } return await this.usersRepository.save(user); } async findOne(id: string) { const result = await this.usersRepository.findOne(id); if (!result) { //如果用户id不存在,抛出谬误 throw new HttpException( { message: '申请失败', error: '用户id不存在' }, HttpStatus.BAD_REQUEST, ); } return result; }}
能够将HttpException
再简略封装一下,或者应用继承,这样代码更简洁一些
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';@Injectable()export class ToolsService { static fail(error, status = HttpStatus.BAD_REQUEST) { throw new HttpException( { message: '申请失败', error: error, }, status, ); }}
简洁代码
// user.service.tsimport { Injectable, HttpException, HttpStatus } from '@nestjs/common';import { Repository } from 'typeorm';import { InjectRepository } from '@nestjs/typeorm';import { UsersEntity } from './entities/user.entity';import { ToolsService } from '../../utils/tools.service';@Injectable()export class UserService { constructor( @InjectRepository(UsersEntity) private readonly usersRepository: Repository<UsersEntity> ) { } async create(user) { const { username } = user; const result = await this.usersRepository.findOne({ username }); if (result) ToolsService.fail('用户名已存在'); return await this.usersRepository.save(user); } async findOne(id: string) { const result = await this.usersRepository.findOne(id); if (!result) ToolsService.fail('用户id不存在'); return result; }}
全局应用filter过滤器
// src/common/filters/http-execption.tsimport { ArgumentsHost, Catch, ExceptionFilter, HttpException,} from '@nestjs/common';@Catch()export class HttpExceptionFilter implements ExceptionFilter { catch(exception: HttpException, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); const status = exception.getStatus(); const exceptionRes: any = exception.getResponse(); const { error, message } = exceptionRes; const msgLog = { status, message, error, path: request.url, timestamp: new Date().toISOString(), }; response.status(status).json(msgLog); }}
全局应用interceptor拦截器
// src/common/inteptors/transform.interceptor.tsimport { CallHandler, ExecutionContext, Injectable, NestInterceptor,} from '@nestjs/common';import { map } from 'rxjs/operators';import { Observable } from 'rxjs';@Injectable()export class AuthInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { return next.handle().pipe( map((data) => { return { status: 200, message: '申请胜利', data: data, }; }), ); }}
// main.tsimport { HttpExceptionFilter } from './common/filters/http-exception.filter';import { TransformInterceptor } from './common/interceptors/transform.interceptor';async function bootstrap() { // 创立实例 const app = await NestFactory.create<NestExpressApplication>(AppModule); // 全局过滤器 app.useGlobalFilters(new HttpExceptionFilter()); // 全局拦截器 app.useGlobalInterceptors(new TransformInterceptor()); // 启动端口 const PORT = process.env.PORT || 9000; await app.listen(PORT, () => Logger.log(`服务曾经启动 http://localhost:${PORT}`), );}bootstrap();
失败
胜利
数据库实体设计与操作
typeorm的数据库实体如何编写?
数据库实体的监听装璜器如何应用?
实体设计
简略例子:上面解说
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn} from "typeorm";@Entity({ name: 'users' })export class User { @PrimaryGeneratedColumn() id: number; // 默认是int(11)类型 @Column() username: string; // 默认是varchar(255)类型 @Column() password: string; @Column() status: boolean; @CreateDateColumn() created_at:date; @UpdateDateColumn() updated_at:date; @DeleteDateColumn() deleted_at:date;}
装璜器阐明
Entity
实体申明,程序运行时,主动创立的数据库表,@Entity({ name: 'users' })
,name
则是给该表命名,否则主动命名PrimaryColumn
设置主键,没有自增PrimaryGeneratedColumn
设置主键和自增,个别是id
Column
设置数据库列字段,在上面阐明CreateDateColumn
创立工夫,主动填写UpdateDateColumn
更新工夫,主动填写DeleteDateColumn
删除工夫,主动填写
列字段参数
// 写法:@Column("int")@Column("varchar", { length: 200 })@Column({ type: "int", length: 200 }) // 个别采纳这种// 罕用选项参数:@Column({ type: 'varchar', // 列的数据类型,参考mysql name: 'password', // 数据库表中的列名,string,如果和装璜的字段是一样的能够不指定 length: 30, // 列类型的长度,number nullable: false, // 是否容许为空,boolean,默认值是false select:false, // 查询数据库时是否显示该字段,boolean,默认值是true,明码个别应用false comment: '明码' // 数据库正文,stirng})password:string;@Column({ type:'varchar', unique: true, // 将列标记为惟一列,惟一束缚,比方账号不能有雷同的})username:string;@Column({ type:'tinyint', default: () => 1, // 默认值,创立时主动填写的值 comment: '0:禁用,1:可用'})status:number;@Column({ type: 'enum', enum: ['male', 'female'], // 枚举类型,只能是数组中的值 default: 'male' 默认值 })gender:string;
残缺例子
import { Column, Entity, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn,} from 'typeorm';@Entity({ name: 'users' })export class UsersEntity { @PrimaryGeneratedColumn() id: number; @Column({ type: 'varchar', length: 30, nullable: false, unique: true, }) username: string; @Column({ type: 'varchar', name: 'password', length: 100, nullable: false, select: false, comment: '明码', }) password: string; @Column({ type: 'varchar', length: 11, select: false, nullable: true, comment: '手机号码', }) mobile: string; @Column({ type: 'varchar', length: 50, select: false, nullable: true, comment: '邮箱', }) email: string; @Column({ type: 'enum', enum: ['male', 'female'], default: 'male', }) gender: string; @Column({ type: 'tinyint', default: () => 1, comment: '0:禁用,1:可用', }) status: number; @CreateDateColumn({ type: 'timestamp', nullable: false, name: 'created_at', comment: '创立工夫', }) createdAt: Date; @UpdateDateColumn({ type: 'timestamp', nullable: false, name: 'updated_at', comment: '更新工夫', }) updatedAt: Date; @DeleteDateColumn({ type: 'timestamp', nullable: true, name: 'deleted_at', comment: '删除工夫', }) deletedAt: Date;}
抽离局部反复的字段:应用继承
baseEntity
:将id,创立工夫,更新工夫,删除工夫抽离成BaseEntity
import { Entity, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, DeleteDateColumn,} from 'typeorm';@Entity()export class BaseEntity { @PrimaryGeneratedColumn() id: number; @CreateDateColumn({ type: 'timestamp', nullable: false, name: 'created_at', comment: '创立工夫', }) createdAt: Date; @UpdateDateColumn({ type: 'timestamp', nullable: false, name: 'updated_at', comment: '更新工夫', }) updatedAt: Date; @DeleteDateColumn({ type: 'timestamp', nullable: false, name: 'deleted_at', comment: '删除工夫', }) deletedAt: Date;}
users
表继承自baseEntity
,就不须要写创立工夫,批改工夫,自增ID
等反复字段了。其余的表也能够继承自baseEntity
,缩小反复代码
import { Column,Entity } from 'typeorm';import { BaseEntity } from './user.baseEntity';@Entity({ name: 'users' })export class UsersEntity extends BaseEntity { // 继承 @Column({ type: 'varchar', length: 30, nullable: false, unique: true, }) username: string; @Column({ type: 'varchar', name: 'password', length: 100, nullable: false, select: false, comment: '明码', }) password: string; @Column({ type: 'varchar', length: 11, select: false, nullable: true, comment: '手机号码', }) mobile: string; @Column({ type: 'varchar', length: 50, select: false, nullable: true, comment: '邮箱', }) email: string; @Column({ type: 'enum', enum: ['male', 'female'], default: 'male', }) gender: string; @Column({ type: 'tinyint', default: () => 1, comment: '0:禁用,1:可用', }) status: number;}
实体监听装璜器
- 其实是typeorm在操作数据库时的生命周期,能够更不便的操作数据
- 查找后:
@AfterLoad
- 插入前:
@BeforeInsert
- 插入后:
@AfterInsert
- 更新前:
@BeforeUpdate
- 更新后:
@AfterUpdate
- 删除前:
@BeforeRemove
AfterLoad
例子:其余的装璜器是一样的用法
import { Column, Entity, AfterLoad,} from 'typeorm';@Entity({ name: 'users' })export class UsersEntity extends BaseEntity { // 查找后,如果age小于20,让age = 20 @AfterLoad() // 装璜器固定写 load() { // 函数名字随你定义 console.log('this', this); if (this.age < 20) { this.age = 20; } } @Column() username: string; @Column() password: string; @Column({ type: 'tinyint', default: () => 18, }) age: number;}// 应用生命周期前是18,查找后就变成了20{ "status": 200, "message": "申请胜利", "data": { "id": 1, "username": "admin", "age": 20, }}
typeorm增删改查操作
拜访数据库的形式有哪些?
typeorm增删改查操作的形式有哪些?
多种拜访数据库的形式
第一种:Connection
import { Injectable } from '@nestjs/common';import { Connection } from 'typeorm';import { UsersEntity } from './entities/user.entity';@Injectable()export class UserService { constructor( private readonly connection: Connection, ) { } async test() { // 应用封装好办法: return await this.connection .getRepository(UsersEntity) .findOne({ where: { id: 1 } }); // 应用createQueryBuilder: return await this.connection .createQueryBuilder() .select('user') .from(UsersEntity, 'user') .where('user.id = :id', { id: 1 }) .getOne(); }}
第二种:Repository
,须要@nestjs/typeorm
的InjectRepository
来注入实体
import { Injectable } from '@nestjs/common';import { Repository } from 'typeorm';import { UsersEntity } from './entities/user.entity';import { InjectRepository } from '@nestjs/typeorm';@Injectable()export class UserService { constructor( @InjectRepository(UsersEntity) 注入实体 private readonly usersRepository: Repository<UsersEntity>, ) { } async test() { // 应用封装好办法: return await this.usersRepository.find({ where: { id: 1 } }); // 应用createQueryBuilder: return await this.usersRepository .createQueryBuilder('user') .where('id = :id', { id: 1 }) .getOne(); }}
第三种:getConnection()
:语法糖,是Connection
类型
import { Injectable } from '@nestjs/common';import { getConnection } from 'typeorm';import { UsersEntity } from './entities/user.entity';@Injectable()export class UserService { async test() { // 应用封装好办法: return await getConnection() .getRepository(UsersEntity) .find({ where: { id: 1 } }); // 应用createQueryBuilder: return await getConnection() .createQueryBuilder() .select('user') .from(UsersEntity, 'user') .where('user.id = :id', { id: 1 }) .getOne(); }}
第四种:getRepository
:语法糖
import { Injectable } from '@nestjs/common';import { getRepository } from 'typeorm';import { UsersEntity } from './entities/user.entity';@Injectable()export class UserService { async test() { // 应用封装好办法: return await getRepository(UsersEntity).find({ where: { id: 1 } }); // 应用createQueryBuilder: return await getRepository(UsersEntity) .createQueryBuilder('user') .where('user.id = :id', { id: 1 }) .getOne(); }}
第五种:getManager
import { Injectable } from '@nestjs/common';import { getManager } from 'typeorm';import { UsersEntity } from './entities/user.entity';@Injectable()export class UserService { async test() { // 应用封装好办法: return await getManager().find(UsersEntity, { where: { id: 1 } }); // 应用createQueryBuilder: return await getManager() .createQueryBuilder(UsersEntity, 'user') .where('user.id = :id', { id: 1 }) .getOne(); }}
简略总结
应用的形式太多,倡议应用:2,4
,比拟不便
Connection外围类:
connection
等于getConnection
connection.manager
等于getManager
, 等于getConnection.manager
connection.getRepository
等于getRepository
, 等于getManager.getRepository
connection.createQueryBuilder
应用QueryBuilder
connection.createQueryRunner
开启事务
EntityManager
和Repository
都封装了操作数据的办法,留神:两者的应用形式是不一样的,(切实不明确搞这么多办法做什么,学得头大)getManager
是EntityManager
的类型,getRepository
是Repository
的类型- 都能够应用
createQueryBuilder
,但应用的形式略有不同
增删改查的三种形式
- 第一种:应用sql语句,实用于sql语句纯熟的同学
- 第二种:
typeorm
封装好的办法,增删改 + 简略查问 - 第三种:
QueryBuilder
查问生成器,实用于关系查问,多表查问,简单查问 - 其实底层最终都会生成
sql
语句,只是封装了几种形式而已,不便人们应用。
第一种:sql语句
export class UserService { constructor( @InjectRepository(UsersEntity) private readonly usersRepository: Repository<UsersEntity>, ) { } async findAll() { return await this.usersRepository.query('select * from users'); // 在query中填写sql语句 }}
第二种:typeorm封装好的api办法
这里应用第二种拜访数据库的形式
export class UserService { constructor( @InjectRepository(UsersEntity) private readonly usersRepository: Repository<UsersEntity>, ) { } async findAll() { return await this.usersRepository.findAndCount(); // 封装好的办法 }}
api办法
增save(user) 创立:返回该数据的所有字段insert(user) 疾速插入一条数据,插入胜利:返回插入实体,与save办法不同的是,它不执行级联、关系和其余操作。删remove(user) 删除:返回该数据的可见字段softRemove(user); 拉黑:返回该数据的可见字段,该删除实体必须领有@DeleteDateColumn()字段,被拉黑的用户还存在数据库中,但无奈被find查找到,会在@DeleteDateColumn()字段中增加删除工夫,可应用recover复原改update(id, user) 更新:返回更新实体,不是该数据的字段复原recover({ id }) 复原:返回id,将被softRemove删除(拉黑)的用户复原,复原胜利后能够被find查找到查找全副find()find({id:9}) 条件查找,写法一,找不到返回空对象find({where:{id:10}}) 条件查找,写法二,找不到返回空对象findAndCount() 返回数据和总的条数查找一个findOne(id); 依据ID查找,找不到返回undefinedfindOne({ where: { username } }); 条件查找,找不到返回undefined依据ID查找一个或多个findByIds([1,2,3]); 查找n个,全副查找不到返回空数组,找到就返回找到的其余hasId(new UsersEntity()) 检测实体是否有合成ID,返回布尔值getId(new UsersEntity()) 获取实体的合成ID,获取不到返回undefinedcreate({username: 'admin12345', password: '123456',}) 创立一个实体,须要调用save保留count({ status: 1 }) 计数,返回数量,无返回0increment({ id }, 'age', 2); 减少,给条件为id的数据的age字段减少2,胜利返回扭转实体decrement({ id }, 'age', 2) 缩小,给条件为id的数据的age字段减少2,胜利返回扭转实体谨用findOneOrFail(id) 找不到间接报500谬误,无奈应用过滤器拦挡谬误,不要应用clear() 清空该数据表,谨用!!!
find更多参数
this.userRepository.find({ select: ["firstName", "lastName"], 要的字段 relations: ["photos", "videos"], 关系查问 where: { 条件查问 firstName: "Timber", lastName: "Saw" }, where: [{ username: "li" }, { username: "joy" }], 多个条件or, 等于:where username = 'li' or username = 'joy' order: { 排序 name: "ASC", id: "DESC" }, skip: 5, 偏移量 take: 10, 每页条数 cache: 60000 启用缓存:1分钟});
find进阶选项
TypeORM 提供了许多内置运算符,可用于创立更简单的查问
import { Not, Between, In } from "typeorm";return await this.usersRepository.find({ username: Not('admin'),});将执行以下查问:SELECT * FROM "users" WHERE "username" != 'admin'return await this.usersRepository.find({ likes: Between(1, 10)});SELECT * FROM "users" WHERE "likes" BETWEEN 1 AND 10return await this.usersRepository.find({ username: In(['admin', 'admin2']),});SELECT * FROM "users" WHERE "title" IN ('admin', 'admin2')
更多查看官网
第三种:QueryBuilder
查问生成器
应用链式操作
QueryBuilder增,删,改
// 减少return await this.usersRepository .createQueryBuilder() .insert() 申明插入操作 .into(UsersEntity) 插入的实体 .values([ 插入的值,可插入多个 { username: 'Timber', password: '123456' }, { username: 'Timber2', password: '123456' }, ]) .execute(); 执行// 批改return this.usersRepository .createQueryBuilder() .update(UsersEntity) .set({ username: 'admin22' }) .where('id = :id', { id: 2 }) .execute();// 删除return this.usersRepository .createQueryBuilder() .delete() .from(UsersEntity) .where('id = :id', { id: 8 }) .execute();// 解决异样:申请胜利会返回一个对象, 如果raw.affectedRows != 0 就是胜利"raw": { "fieldCount": 0, "affectedRows": 2, "insertId": 13, "serverStatus": 2, "warningCount": 0, "message": "&Records: 2 Duplicates: 0 Warnings: 0", "protocol41": true, "changedRows": 0}
查问
简略例子
export class UserService { constructor( @InjectRepository(UsersEntity) private readonly usersRepository: Repository<UsersEntity>, ) { } async findAll() { return await this.usersRepository .createQueryBuilder('user') 创立生成器,参数:别名 .where('user.id = :id', { id: id }) 条件 .innerJoinAndSelect('user.avatar', 'avatar') 关系查问 .addSelect('user.password') 增加显示字段 .getOne(); 获取一条数据 }}
QueryBuilder查问生成器阐明
查问单表
拜访数据库的形式不同:形式一:没有指定实体,须要应用from指定实体return await getConnection() .createQueryBuilder() .select('user.username') ‘user’:全副字段,‘user.username’:只获取username .from(UsersEntity, 'user') 参1:连贯的实体, 参2:别名 .where('user.id = :id', { id: 1 }) .getOne();形式二:指定实体:默认获取全副字段return await getConnection() .createQueryBuilder(UsersEntity, 'user') 指定实体 .where('user.id = :id', { id: 1 }) .getOne();形式三: 曾经在拜访时指定了实体:默认获取全副字段return await this.usersRepository .createQueryBuilder('user') 别名 .where('user.id = :id', { id: 1 }) .getOne();
获取后果
.getSql(); 获取理论执行的sql语句,用于开发时查看问题.getOne(); 获取一条数据(通过typeorm的字段解决).getMany(); 获取多条数据.getRawOne(); 获取一条原数据(没有通过typeorm的字段解决).getRawMany(); 获取多条原数据.stream(); 返回流数据如:通过typeorm的字段解决,获取到的就是实体设计时的字段{ "status": 200, "message": "申请胜利", "data": { "id": 1, "username": "admin", "gender": "male", "age": 18, "status": 1, "createdAt": "2021-04-26T09:58:54.469Z", "updatedAt": "2021-04-28T14:47:36.000Z", "deletedAt": null }}如:没有通过typeorm的字段解决,将数据库的字段原生不动的显示进去{ "status": 200, "message": "申请胜利", "data": { "user_id": 1, "user_username": "admin", "user_gender": "male", "user_age": 18, "user_status": 1, "user_created_at": "2021-04-26T09:58:54.469Z", "user_updated_at": "2021-04-28T14:47:36.000Z", "user_deleted_at": null }}
查问局部字段
.select(["user.id", "user.name"])理论执行的sql语句:SELECT user.id, user.name FROM users user;增加暗藏字段:实体中设置select为false时,是不显示字段,应用addSelect会将字段显示进去.addSelect('user.password')
where
条件
.where("user.name = :name", { name: "joy" })等于.where("user.name = :name").setParameter("name", "Timber")理论执行的sql语句:SELECT * FROM users user WHERE user.name = 'joy'多个条件.where("user.firstName = :firstName", { firstName: "Timber" }).andWhere("user.lastName = :lastName", { lastName: "Saw" });理论执行的sql语句:SELECT * FROM users user WHERE user.firstName = 'Timber' AND user.lastName = 'Saw'in.where("user.name IN (:...names)", { names: [ "Timber", "Cristal", "Lina" ] })理论执行的sql语句:SELECT * FROM users user WHERE user.name IN ('Timber', 'Cristal', 'Lina')or.where("user.firstName = :firstName", { firstName: "Timber" }).orWhere("user.lastName = :lastName", { lastName: "Saw" });理论执行的sql语句:SELECT * FROM users user WHERE user.firstName = 'Timber' OR user.lastName = 'Saw'子句const posts = await connection .getRepository(Post) .createQueryBuilder("post") .where(qb => { const subQuery = qb .subQuery() .select("user.name") .from(User, "user") .where("user.registered = :registered") .getQuery(); return "post.title IN " + subQuery; }) .setParameter("registered", true) .getMany();理论执行的sql语句:select * from post where post.title in (select name from user where registered = true)
having
筛选
.having("user.firstName = :firstName", { firstName: "Timber" }).andHaving("user.lastName = :lastName", { lastName: "Saw" });理论执行的sql语句:SELECT ... FROM users user HAVING user.firstName = 'Timber' AND user.lastName = 'Saw'
orderBy
排序
.orderBy("user.name", "DESC").addOrderBy("user.id", "asc");等于.orderBy({ "user.name": "ASC", "user.id": "DESC"});理论执行的sql语句:SELECT * FROM users user order by user.name asc, user.id desc;
group
分组
.groupBy("user.name").addGroupBy("user.id");
关系查问(多表)
1参:你要加载的关系,2参:可选,你为此表调配的别名,3参:可选,查问条件左关联查问.leftJoinAndSelect("user.profile", "profile") 右关联查问.rightJoinAndSelect("user.profile", "profile") 内联查问.innerJoinAndSelect("user.photos", "photo", "photo.isRemoved = :isRemoved", { isRemoved: false }) 例子:const result = await this.usersRepository .createQueryBuilder('user') .leftJoinAndSelect("user.photos", "photo") .where("user.name = :name", { name: "joy" }) .andWhere("photo.isRemoved = :isRemoved", { isRemoved: false }) .getOne();理论执行的sql语句:SELECT user.*, photo.* FROM users userLEFT JOIN photos photo ON photo.user = user.idWHERE user.name = 'joy' AND photo.isRemoved = FALSE;const result = await this.usersRepository .innerJoinAndSelect("user.photos", "photo", "photo.isRemoved = :isRemoved", { isRemoved: false }) .where("user.name = :name", { name: "Timber" }) .getOne();理论执行的sql语句:SELECT user.*, photo.* FROM users userINNER JOIN photos photo ON photo.user = user.id AND photo.isRemoved = FALSEWHERE user.name = 'Timber';多个关联const result = await this.usersRepository .createQueryBuilder("user") .leftJoinAndSelect("user.profile", "profile") .leftJoinAndSelect("user.photos", "photo") .leftJoinAndSelect("user.videos", "video") .getOne();
typeorm应用事务的3种形式
typeorm
应用事务的形式有哪些?如何应用?
事务
- 在操作多个表时,或者多个操作时,如果有一个操作失败,所有的操作都失败,要么全副胜利,要么全副失
- 解决问题:在多表操作时,因为各种异样导致一个胜利,一个失败的数据谬误。
例子:银行转账
如果用户1向用户2转了100元,但因为各种起因,用户2没有收到,如果没有事务处理,用户1扣除的100元就凭空隐没了
如果有事务处理,只有用户2收到100元,用户1才会扣除100元,如果没有收到,则不会扣除。
利用场景
多表的增,删,改操作
nest-typrorm事务的应用形式
- 应用装璜器,在
controller
中编写,传递给service
应用 - 应用
getManager
或getConnection
,在service
中编写与应用 - 应用
connection
或getConnection
,开启queryRunner
,在service
中编写与应用
形式一:应用装璜器
controller
import { Controller, Post, Body, Param, ParseIntPipe,} from '@nestjs/common';import { Transaction, TransactionManager, EntityManager } from 'typeorm'; 开启事务第一步:引入import { UserService } from './user.service-oto';@Controller('user')export class UserController { constructor(private readonly userService: UserService) { } @Post(':id') @Transaction() 开启事务第二步:装璜接口 async create( @Param('id', new ParseIntPipe()) id: number, @TransactionManager() maneger: EntityManager, 开启事务第三步:获取事务管理器 ) { return await this.userService.create(id, maneger); 开启事务第四步:传递给service,应用数据库时调用 }}
service
- 这里解决的是1对1关系:保留头像地址到
avatar
表,同时关联保留用户的id
- 如果你不会1对1关系,请先去学习对应的常识
import { Injectable } from '@nestjs/common';import { Repository, EntityManager } from 'typeorm';import { InjectRepository } from '@nestjs/typeorm';import { UsersEntity } from './entities/user.entity';import { AvatarEntity } from './entities/avatar.entity';import { ToolsService } from '../../utils/tools.service';@Injectable()export class UserService { constructor( @InjectRepository(UsersEntity) private readonly usersRepository: Repository<UsersEntity>, @InjectRepository(AvatarEntity) private readonly avatarRepository: Repository<AvatarEntity>, ) { } async create(id: number, manager: EntityManager) { const urlObj = { url: `http://www.dmyxs.com/images/${id}.png`, }; const user = await this.usersRepository.findOne({ id }); 先查找用户,因为要保留用户的id if (!user) ToolsService.fail('用户id不存在'); 找不到用户抛出异样 const avatarEntity = this.avatarRepository.create({ url: urlObj.url }); 创立头像地址的实体 const avatarUrl = await manager.save(avatarEntity); 应用事务保留副表 user.avatar = avatarUrl; 主表和副表建设关系 await manager.save(user); 应用事务保留主表 return '新增胜利'; 如果过程出错,不会保留 }}
形式二:应用getManager 或 getConnection
service
import { Injectable } from '@nestjs/common';import { Connection, Repository, getManager } from 'typeorm';import { InjectRepository } from '@nestjs/typeorm';import { UsersEntity } from './entities/user.entity';import { AvatarEntity } from './entities/avatar.entity';import { ToolsService } from '../../utils/tools.service';@Injectable()export class UserService { constructor( @InjectRepository(UsersEntity) private readonly usersRepository: Repository<UsersEntity>, private readonly connection: Connection, ) { } async test(id: string) { const urlObj = { url: `http://www.dmyxs.com/images/${id}.png`, }; const user = await this.usersRepository.findOne(id); 先查找用户 if (!user) ToolsService.fail('用户id不存在'); 找不到用户抛出异样 //getConnection的形式:await getConnection().transaction(manager=> {}); //getManager的形式: const result = await getManager().transaction(async (manager) => { const avatarEntity = manager.create(AvatarEntity, { url: urlObj.url }); 创立头像地址的实体 const avatarUrl = await manager.save(AvatarEntity, avatarEntity); 应用事务保留副表 user.avatar = avatarUrl; 创立关联 return await manager.save(UsersEntity, user); 应用事务保留主表,并返回后果 }); return result; }}{ "status": 200, "message": "申请胜利", "data": { "id": 1, "createdAt": "2021-04-26T09:58:54.469Z", "updatedAt": "2021-04-28T14:47:36.000Z", "deletedAt": null, "username": "admin", "gender": "male", "age": 18, "status": 1, "avatar": { "url": "http://www.dmyxs.com/images/1.png", "id": 52 } }}
形式三:应用 connection 或 getConnection
service
import { Injectable } from '@nestjs/common';import { Connection, Repository, getManager } from 'typeorm';import { InjectRepository } from '@nestjs/typeorm';import { UsersEntity } from './entities/user.entity';import { AvatarEntity } from './entities/avatar.entity';@Injectable()export class UserService { constructor( @InjectRepository(UsersEntity) private readonly usersRepository: Repository<UsersEntity>, private readonly connection: Connection, ) { } async test(id: string) { const urlObj = { url: `http://www.test.com/images/${id}.png`, }; const user = await this.usersRepository.findOne(id); 先查找用户 if (!user) ToolsService.fail('用户id不存在'); 找不到用户抛出异样 const queryRunner = this.connection.createQueryRunner(); 获取连贯并创立新的queryRunner await queryRunner.connect(); 应用咱们的新queryRunner建设真正的数据库连 await queryRunner.startTransaction(); 开始事务 const avatarEntity = new AvatarEntity(); 创立实体:要保留的数据 avatarEntity.url = urlObj.url; try { const result = await queryRunner.manager 应用事务保留到副表 .getRepository(AvatarEntity) .save(avatarEntity); user.avatar = result; 主表和副表建设连贯 const userResult = await queryRunner.manager 应用事务保留到副表 .getRepository(UsersEntity) .save(user); await queryRunner.commitTransaction(); 提交事务 return userResult; 返回后果 } catch (error) { console.log('创立失败,勾销事务'); await queryRunner.rollbackTransaction(); 出错回滚 } finally { await queryRunner.release(); 开释 } }}
typeorm 一对一关系设计与增删改查
实体如何设计一对一关系?如何增删改查?
一对一关系
- 定义:一对一是一种 A 只蕴含一个 B ,而 B 只蕴含一个 A 的关系
- 其实就是要设计两个表:一张是主表,一张是副表,查找主表时,关联查找副表
- 有外键的表称之为副表,不带外键的表称之为主表
- 如:一个账户对应一个用户信息,主表是账户,副表是用户信息
- 如:一个用户对应一张用户头像图片,主表是用户信息,副表是头像地址
一对一实体设计
主表:
- 应用
@OneToOne()
来建设关系- 第一个参数:
() => AvatarEntity
, 和谁建设关系? 和AvatarEntity
建设关系- 第二个参数:
(avatar) => avatar.user)
,和哪个字段联立关系?avatar
就是AvatarEntity
的别名,可轻易写,和AvatarEntity
的userinfo
字段建设关系- 第三个参数:
RelationOptions
关系选项
import { Column, Entity, PrimaryGeneratedColumn, OneToOne,} from 'typeorm';import { AvatarEntity } from './avatar.entity';@Entity({ name: 'users' })export class UsersEntity { @PrimaryGeneratedColumn() id: number; @Column() username: string; @Column() password: string; @OneToOne(() => AvatarEntity, (avatar) => avatar.userinfo) avatar: AvatarEntity;}
副表
参数:同主表一样
次要:依据@JoinColumn({ name: ‘user_id’ })
来分辨副表,name
是设置数据库的外键名字,如果不设置是userId
import { Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn,} from 'typeorm';import { UsersEntity } from './user.entity';@Entity({ name: 'avatar' })export class AvatarEntity { @PrimaryGeneratedColumn() id: number; @Column({ type: 'varchar' }) url: string; @OneToOne(() => UsersEntity, (user) => user.avatar) @JoinColumn({ name: 'userinfo_id' }) userinfo: UsersEntity;}
一对一增删改查
- 留神:只有波及两种表操作的,就须要开启事务:同时失败或同时胜利,防止数据不对立
- 在这里:创立,批改,删除都开启了事务
- 留神:所有数据应该是由前端传递过去的,这里为了不便,间接硬编码了(写死)
// user.controller.tsimport { Controller, Get, Post, Body, Patch, Query, Param, Delete, HttpCode, HttpStatus, ParseIntPipe,} from '@nestjs/common';import { Transaction, TransactionManager, EntityManager } from 'typeorm'; 开启事务第一步:引入import { UserService } from './user.service-oto';@Controller('user')export class UserController { constructor(private readonly userService: UserService) { } @Get() async findAll() { const [data, count] = await this.userService.findAll(); return { count, data }; } @Get(':id') async findOne(@Param('id', new ParseIntPipe()) id: number) { return await this.userService.findOne(id); } @Post(':id') @HttpCode(HttpStatus.OK) @Transaction() 开启事务第二步:装璜接口 async create( @Param('id', new ParseIntPipe()) id: number, @TransactionManager() maneger: EntityManager, 开启事务第三步:获取事务管理器 ) { return await this.userService.create(id, maneger); 开启事务第四步:传递给service,应用数据库时调用 } @Patch(':id') @Transaction() async update( @Param('id', new ParseIntPipe()) id: number, @TransactionManager() maneger: EntityManager, ) { return await this.userService.update(id, maneger); } @Delete(':id') @Transaction() async remove( @Param('id', new ParseIntPipe()) id: number, @TransactionManager() maneger: EntityManager, ) { return await this.userService.remove(id, maneger); }}
// user.service.tsimport { Injectable } from '@nestjs/common';import { Repository, Connection, UpdateResult, EntityManager } from 'typeorm';import { InjectRepository } from '@nestjs/typeorm';import { UsersEntity } from './entities/user.entity';import { AvatarEntity } from './entities/avatar.entity';import { ToolsService } from '../../utils/tools.service';@Injectable()export class UserService { constructor( @InjectRepository(UsersEntity) private readonly usersRepository: Repository<UsersEntity>, @InjectRepository(AvatarEntity) private readonly avatarRepository: Repository<AvatarEntity>, private connection: Connection, ) { } 一对一增删改查 查找全副 async findAll() { 应用封装好的形式 // return await this.usersRepository.findAndCount({ relations: ['avatar'] }); 应用QueryBuilder的形式 const list = await this.usersRepository .createQueryBuilder('UsersEntity') .leftJoinAndSelect('UsersEntity.avatar', 'AvatarEntity.userinfo') .getManyAndCount(); return list; } 依据主表id查找一对一 async findOne(id: number) { const result = await this.usersRepository.findOne(id, { relations: ['avatar'], }); if (!result) ToolsService.fail('用户id不存在'); return result; } 依据主表id创立一对一 async create(id: number, manager: EntityManager) { const urlObj = { url: `http://www.dmyxs.com/images/${id}.png`, }; const user = await this.usersRepository.findOne({ id }); 先查找用户 if (!user) ToolsService.fail('用户id不存在'); 如果没找到,抛出谬误,由过滤器捕捉谬误 创立实体的两种形式:new 和 create,new的形式不便条件判断 创立实体形式一: const avatarEntity = this.avatarRepository.create({ url: urlObj.url }); 创立实体 创立实体形式二: //const avatarEntity = new AvatarEntity(); //avatarEntity.url = urlObj.url; const avatarUrl = await manager.save(avatarEntity); 应用事务保留副表 user.avatar = avatarUrl; 主表和副表建设关系 await manager.save(user); 应用事务保留主表 return '新增胜利'; 如果过程出错,不会保留 } 依据主表id更改一对一 要更改的副表id,会从前端传递过去 async update(id: number, manager: EntityManager) { const urlObj = { id: 18, url: `http://www.dmyxs.com/images/${id}-update.jpg`, }; const user = await this.usersRepository.findOne( { id } ); 先查找用户 if (!user) ToolsService.fail('用户id不存在'); 如果没找到id抛出谬误,由过滤器捕捉谬误 const avatarEntity = this.avatarRepository.create({ url: urlObj.url }); 创立要批改的实体 应用事务更新办法:1参:要批改的表,2参:要批改的id, 3参:要更新的数据 await manager.update(AvatarEntity, urlObj.id, avatarEntity); return '更新胜利'; } 依据主表id删除一对一 async remove(id: number, manager: EntityManager): Promise<any> { const user = await this.usersRepository.findOne(id); if (!user) ToolsService.fail('用户id不存在'); 只删副表的关联数据 await manager.delete(AvatarEntity, { user: id }); 如果连主表用户一起删,加上面这行代码 //await manager.delete(UsersEntity, id); return '删除胜利'; }}
typeorm 一对多和多对一关系设计与增删改查
实体如何设计一对多与多对一关系,如何关联查问
一对多关系,多对一关系
定义:一对多是一种一个 A 蕴含多个 B ,而多个B只属于一个 A 的关系
其实就是要设计两个表:一张是主表(一对多),一张是副表(多对一),查找主表时,关联查找副表
有外键的表称之为副表,不带外键的表称之为主表
如:一个用户领有多个宠物,多个宠物只属于一个用户的(每个宠物只能有一个客人)
如:一个用户领有多张照片,多张照片只属于一个用户的
如:一个角色领有多个用户,多个用户只属于一个角色的(每个用户只能有一个角色)
一对多和多对一实体设计
一对多
应用@OneToMany()
来建设一对多关系
第一个参数:() => PhotoEntity
, 和谁建设关系? 和PhotoEntity
建设关系
第二个参数:(user) => user.photo
,和哪个字段联立关系?user
就是PhotoEntity
的别名,可轻易写,和PhotoEntity
的userinfo
字段建设关系
第三个参数:RelationOptions
关系选项
import { Column, Entity, PrimaryGeneratedColumn, OneToOne,} from 'typeorm';import { AvatarEntity } from './avatar.entity';@Entity({ name: 'users' })export class UsersEntity { @PrimaryGeneratedColumn() id: number; @Column() username: string; @Column() password: string; @OneToMany(() => PhotoEntity, (avatar) => avatar.userinfo) photos: PhotoEntity;}
多对一
应用@ManyToOne()
来建设多对一关系,参数如同上
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm';import { UsersEntity } from './user.entity';@Entity({ name: 'photo' })export class PhotoEntity { @PrimaryGeneratedColumn() id: number; @Column({ type: 'varchar' }) url: string; @ManyToOne(() => UsersEntity, (user) => user.photos) @JoinColumn({ name: 'userinfo_id' }) userinfo: UsersEntity;}
一对多和多对一增删改查
只有波及两种表操作的,就须要开启事务:同时失败或同时胜利,防止数据不对立
留神:所有数据应该是由前端传递过去的,这里为了不便,间接硬编码了(写死)
比较复杂的是更新操作
user.controller.ts
import { Controller, Get, Post, Body, Patch, Query, Param, Delete, HttpCode, HttpStatus, ParseIntPipe,} from '@nestjs/common';import { Transaction, TransactionManager, EntityManager } from 'typeorm'; 开启事务第一步:引入import { UserService } from './user.service-oto';@Controller('user')export class UserController { constructor(private readonly userService: UserService) { } @Get() async findAll() { const [data, count] = await this.userService.findAll(); return { count, data }; } @Get(':id') async findOne(@Param('id', new ParseIntPipe()) id: number) { return await this.userService.findOne(id); } @Post(':id') @HttpCode(HttpStatus.OK) @Transaction() 开启事务第二步:装璜接口 async create( @Param('id', new ParseIntPipe()) id: number, @TransactionManager() maneger: EntityManager, 开启事务第三步:获取事务管理器 ) { return await this.userService.create(id, maneger); 开启事务第四步:传递给service,应用数据库时调用 } @Patch(':id') @Transaction() async update( @Param('id', new ParseIntPipe()) id: number, @TransactionManager() maneger: EntityManager, ) { return await this.userService.update(id, maneger); } @Delete(':id') @Transaction() async remove( @Param('id', new ParseIntPipe()) id: number, @TransactionManager() maneger: EntityManager, ) { return await this.userService.remove(id, maneger); }}
user.service.ts
令人头大的中央:建设关系和查找应用实体,删除应用实体的id,感觉设计得不是很正当,违反人的常识
import { Injectable } from '@nestjs/common';import { Repository, EntityManager } from 'typeorm';import { InjectRepository } from '@nestjs/typeorm';import { UsersEntity } from './entities/user.entity';import { PhotoEntity } from './entities/photo.entity';import { ToolsService } from '../../utils/tools.service';@Injectable()export class UserService { constructor( @InjectRepository(UsersEntity) private readonly usersRepository: Repository<UsersEntity>, @InjectRepository(PhotoEntity) private readonly photoRepository: Repository<PhotoEntity>, ) { } 一对多增删改查 async findAll() { // return await this.usersRepository.findAndCount({ relations: ['photos'] }); const list = await this.usersRepository .createQueryBuilder('UsersEntity') .leftJoinAndSelect('UsersEntity.photos', 'PhotoEntity.userinfo') .getManyAndCount(); return list; } 依据主表id查找一对多 async findOne(id: number) { 查问一个用户有多少张照片(一对多) const result = await this.usersRepository.findOne(id, { relations: ['photos'], }); if (!result) ToolsService.fail('用户id不存在'); return result; 查问这张照片属于谁(多对一) // const result = await this.photoRepository.findOne(id, { // relations: ['userinfo'], // }); // if (!result) ToolsService.fail('图片id不存在'); // return result; } 依据主表id创立一对多 async create(id: number, manager: EntityManager) { const urlList = [ { url: `http://www.dmyxs.com/images/${id}.png`, }, { url: `http://www.dmyxs.com/images/${id}.jpg`, }, ]; const user = await this.usersRepository.findOne({ id }); if (!user) ToolsService.fail('用户id不存在'); 遍历传递过去的数据 if (urlList.length !== 0) { for (let i = 0; i < urlList.length; i++) { 创立实体的两种形式:new 和 create,new的形式不便条件判断 // const photo = new PhotoEntity(); // photo.url = urlList[i].url; // photo.user = user; // await manager.save(PhotoEntity, photo); const photoEntity = this.photoRepository.create({ url: urlList[i].url, userinfo: user, 留神:这里是应用实体建设关系,而不是实体id }); await manager.save(photoEntity); } } return '新增胜利'; } 依据主表id更改一对多 示例:删除一张,批改一张(批改的有id),新增一张 先应用创立,创立两张photo async update(id: number, manager: EntityManager) { const urlList = [ { id: 22, url: `http://www.dmyxs.com/images/${id}-update.png`, }, { url: `http://www.dmyxs.com/images/${id}-create.jpeg`, }, ]; const user = await this.usersRepository.findOne({ id }); if (!user) ToolsService.fail('用户id不存在'); 如果要批改主表,先批改主表用户信息,后批改副表图片信息 批改主表 const userEntity = this.usersRepository.create({ id, username: 'admin7', password: '123456', }); await manager.save(userEntity); 批改副表 如果前端附带了图片list if (urlList.length !== 0) { 查询数据库曾经有的图片 const databasePhotos = await manager.find(PhotoEntity, { userinfo: user }); 如果有数据,则进行循环判断,先删除多余的数据 if (databasePhotos.length >= 1) { for (let i = 0; i < databasePhotos.length; i++) { 以用户传递的图片为基准,数据库的图片id是否在用户传递过去的表里,如果不在,就是要删除的数据 const exist = urlList.find((item) => item.id === databasePhotos[i].id); if (!exist) { await manager.delete(PhotoEntity, { id: databasePhotos[i].id }); } } } 否则就是新增和更改的数据 for (let i = 0; i < urlList.length; i++) { const photoEntity = new PhotoEntity(); photoEntity.url = urlList[i].url; 如果有id则是批改操作,因为前端传递的数据是从服务端获取的,会附带id,新增的没有 if (!!urlList[i].id) { 批改则让id关联即可 photoEntity.id = urlList[i].id; await manager.save(PhotoEntity, photoEntity); } else { 否则是新增操作,关联用户实体 photoEntity.userinfo = user; await manager.save(PhotoEntity, photoEntity); } } } else { 如果前端把图片全副删除,删除所有关联的图片 await manager.delete(PhotoEntity, { userinfo: id }); } return '更新胜利'; } 依据主表id删除一对多 async remove(id: number, manager: EntityManager): Promise<any> { const user = await this.usersRepository.findOne(id); if (!user) ToolsService.fail('用户id不存在'); 只删副表的关联数据 await manager.delete(PhotoEntity, { userinfo: id }); 如果连主表用户一起删,加上面这行代码 //await manager.delete(UsersEntity, id); return '删除胜利'; }}
typeorm 多对多关系设计与增删改查
实体如何设计多对多关系?如何增删改查?
多对多关系
定义:多对多是一种 A 蕴含多个 B,而 B 蕴含多个 A 的关系
如:一个粉丝能够关注多个主播,一个主播能够有多个粉丝
如:一篇文章属于多个分类,一个分类下有多篇文章
比方这篇文章,能够放在nest目录,也能够放在typeorm目录或者mysql目录
实现形式
第一种:建设两张表,应用装璜器@ManyToMany
建设关系,typeorm
会主动生成三张表
第二种:手动建设3张表
这里应用第一种
实体设计
这里将设计一个用户(粉丝) 与 明星的 多对多关系
用户(粉丝)能够被动关注明星,让users
变为主表,退出@JoinTable()
应用@ManyToMany()
来建设多对多关系
第一个参数:() => StarEntity
, 和谁建设关系? 和StarEntity
建设关系
第二个参数:(star) => star.photo
,和哪个字段联立关系?star
就是StarEntity
的别名,可轻易写,和PhotoEntity
的followers
字段建设关系
用户(粉丝)表:follows关注/追随
import { Column, Entity, PrimaryGeneratedColumn, ManyToMany, JoinTable,} from 'typeorm';import { AvatarEntity } from './avatar.entity';@Entity({ name: 'users' })export class UsersEntity { @PrimaryGeneratedColumn() id: number; @Column() username: string; @Column() password: string; @ManyToMany(() => StarEntity, (star) => star.followers) @JoinTable() follows: StarEntity[]; 留神这里是数组类型}
明星表:followers跟随者
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from 'typeorm';import { UsersEntity } from './user.entity';@Entity({ name: 'star' })export class StarEntity { @PrimaryGeneratedColumn() id: number; @Column({ type: 'varchar' }) name: string; @ManyToMany(() => UsersEntity, (user) => user.follows) followers: UsersEntity;}
留神:
程序运行后,将会默认在数据库中生成三张表,users,star,users_follows_star,users_follows_star是两头表,用于记录users和star之间的多对多关系,它是主动生成的。
为了测试不便,你能够在users表和star表创立一些数据:这些属于单表操作
多对多增删改查
只有波及两种表操作的,就须要开启事务:同时失败或同时胜利,防止数据不对立
留神:所有数据应该是由前端传递过去的,这里为了不便,间接硬编码了(写死)
user.controller.ts
import { Controller, Get, Post, Body, Patch, Query, Param, Delete, HttpCode, HttpStatus, ParseIntPipe,} from '@nestjs/common';import { Transaction, TransactionManager, EntityManager } from 'typeorm'; 开启事务第一步:引入import { UserService } from './user.service-oto';@Controller('user')export class UserController { constructor(private readonly userService: UserService) { } @Get() async findAll() { const [data, count] = await this.userService.findAll(); return { count, data }; } @Get(':id') async findOne(@Param('id', new ParseIntPipe()) id: number) { return await this.userService.findOne(id); } @Post(':id') @HttpCode(HttpStatus.OK) @Transaction() 开启事务第二步:装璜接口 async create( @Param('id', new ParseIntPipe()) id: number, @TransactionManager() maneger: EntityManager, 开启事务第三步:获取事务管理器 ) { return await this.userService.create(id, maneger); 开启事务第四步:传递给service,应用数据库时调用 } @Patch(':id') @Transaction() async update( @Param('id', new ParseIntPipe()) id: number, @TransactionManager() maneger: EntityManager, ) { return await this.userService.update(id, maneger); } @Delete(':id') @Transaction() async remove( @Param('id', new ParseIntPipe()) id: number, @TransactionManager() maneger: EntityManager, ) { return await this.userService.remove(id, maneger); }}
user.service.ts
import { Injectable } from '@nestjs/common';import { Repository, EntityManager } from 'typeorm';import { InjectRepository } from '@nestjs/typeorm';import { UsersEntity } from './entities/user.entity';import { StarEntity } from './entities/star.entity';import { ToolsService } from '../../utils/tools.service';@Injectable()export class UserService { constructor( @InjectRepository(UsersEntity) private readonly usersRepository: Repository<UsersEntity>, @InjectRepository(StarEntity) private readonly starRepository: Repository<StarEntity>, ) { } 一对多增删改查 async findAll() { // return await this.usersRepository.findAndCount({ relations: ['follows'] }); const list = await this.usersRepository .createQueryBuilder('UsersEntity') .leftJoinAndSelect('UsersEntity.follows', 'StarEntity.followers') .getManyAndCount(); return list; } 依据主表id查找多对多 async findOne(id: number) { 查问一个用户关注了哪些明星 // const result = await this.usersRepository.findOne(id, { // relations: ['follows'], // }); // if (!result) ToolsService.fail('用户id不存在'); // return result; 查问一个明星有多少粉丝 const result = await this.starRepository.findOne(id, { relations: ['followers'], }); if (!result) ToolsService.fail('明星id不存在'); return result; } 依据主表id创立多对多 粉丝关注明星 async create(id: number, manager: EntityManager) { 要关注的明星id数组 const willFollow = [3, 4]; const user = await this.usersRepository.findOne({ id }); if (!user) ToolsService.fail('用户id不存在'); if (willFollow.length !== 0) { const followList = []; for (let i = 0; i < willFollow.length; i++) { const star = await manager.findOne(StarEntity, { id: willFollow[i], }); if (!star) ToolsService.fail('主播id不存在'); followList.push(star); } const userEntity = new UsersEntity(); 重点: 不指定id是创立新的用户,还须要填写username和password等必填的字段 指定id就是更新某些字段:只关注明星,不创立新的用户,同样可用于批改 userEntity.id = id; userEntity.follows = followList; 建设关联,数据表会自动更新 await manager.save(userEntity); } return '新增胜利'; } 依据主表id更改多对多 假如:某用户关注了id为[3, 4]的明星, 当初批改为只关注[2] 逻辑和创立一样 async update(id: number, manager: EntityManager) { const willFollow = [2]; const user = await this.usersRepository.findOne({ id }); if (!user) ToolsService.fail('用户id不存在'); if (willFollow.length !== 0) { const followList = []; for (let i = 0; i < willFollow.length; i++) { const listOne = await manager.findOne(StarEntity, { id: willFollow[i], }); if (!listOne) ToolsService.fail('主播id不存在'); followList.push(listOne); } const userEntity = new UsersEntity(); userEntity.id = id; userEntity.follows = followList; await manager.save(userEntity); } return '更新胜利'; } 依据主表id删除多对多 多种删除 async remove(id: number, manager: EntityManager): Promise<any> { const user = await this.usersRepository.findOne(id, { relations: ['follows'], }); if (!user) ToolsService.fail('用户id不存在'); 依据id删除一个:勾销关注某个明星,明星id应由前端传递过去,这里写死 须要获取以后用户的的follows,应用关系查问 const willDeleteId = 2; if (user.follows.length !== 0) { 过滤掉要删除的数据,再从新赋值 const followList = user.follows.filter((star) => star.id != willDeleteId); const userEntity = new UsersEntity(); userEntity.id = id; userEntity.follows = followList; await manager.save(userEntity); } 全副删除关联数据,不删用户 // const userEntity = new UsersEntity(); // userEntity.id = id; // userEntity.follows = []; // await manager.save(userEntity); 如果连用户一起删,会将关联数据一起删除 // await manager.delete(UsersEntity, id); return '删除胜利'; }}
nest连贯Redis
Redis 字符串数据类型的相干命令用于治理 redis 字符串值
- 查看所有的key:
keys *
- 一般设置:
set key value
- 设置并加过期工夫:
set key value EX 30
示意30秒后过期 - 获取数据:
get key
- 删除指定数据:
del key
- 删除全副数据:
flushall
- 查看类型:
type key
- 设置过期工夫:
expire key 20
示意指定的key5
秒后过期
Redis列表是简略的字符串列表,依照插入程序排序。你能够增加一个元素到列表的头部(右边)或者尾部(左边)
- 列表右侧增加值:
rpush key value
- 列表左侧增加值:
lpush key value
- 右侧删除值:
rpop key
- 左侧删除值:
lpop key
- 获取数据:
lrange key
- 删除指定数据:
del key
- 删除全副数据:
flushall
- 查看类型:
type key
Redis 的 Set 是 String 类型的无序汇合。汇合成员是惟一的,这就意味着汇合中不能呈现反复的数据。它和列表的最次要区别就是没法减少反复值
- 给汇合增数据:
sadd key value
- 删除汇合中的一个值:
srem key value
- 获取数据:
smembers key
- 删除指定数据:
del key
- 删除全副数据:
flushall
Redis hash 是一个string类型的field和value的映射表,hash特地适宜用于存储对象。
- 设置值hmset :
hmset zhangsan name "张三" age 20 sex “男”
- 设置值hset :
hset zhangsan name "张三"
- 获取数据:
hgetall key
- 删除指定数据:
del key
- 删除全副数据:
flushall
Redis 公布订阅(pub/sub)是一种音讯通信模式:发送者(pub)发送音讯,订阅者(sub)接管音讯
// 公布client.publish('publish', 'message from publish.js');// 订阅client.subscribe('publish');client.on('message', function(channel, msg){console.log('client.on message, channel:', channel, ' message:', msg);});
Nestjs中应用redis
Nestjs Redis 官网文档:https://github.com/kyknow/nes...
npm install nestjs-redis --save
如果是nest8须要留神该问题:https://github.com/skunight/n...
// app.modules.tsimport { RedisModule } from 'nestjs-redis';import { RedisTestModule } from '../examples/redis-test/redis-test.module';@Module({ imports: [ // 加载配置文件目录 ConfigModule.load(resolve(__dirname, 'config', '**/!(*.d).{ts,js}')), // redis连贯 RedisModule.forRootAsync({ useFactory: (configService: ConfigService) => configService.get('redis'), inject: [ConfigService], }), RedisTestModule, ], controllers: [], providers: [ ],})export class AppModule implements NestModule {}
// src/config/redis.ts 配置export default { host: '127.0.0.1', port: 6379, db: 0, password: '', keyPrefix: '', onClientReady: (client) => { client.on('error', (err) => { console.log('-----redis error-----', err); }); },};
创立一个cache.service.ts 服务 封装操作redis的办法
// src/common/cache.service.ts import { Injectable } from '@nestjs/common';import { RedisService } from 'nestjs-redis';@Injectable()export class CacheService { public client; constructor(private redisService: RedisService) { this.getClient(); } async getClient() { this.client = await this.redisService.getClient(); } //设置值的办法 async set(key: string, value: any, seconds?: number) { value = JSON.stringify(value); if (!this.client) { await this.getClient(); } if (!seconds) { await this.client.set(key, value); } else { await this.client.set(key, value, 'EX', seconds); } } //获取值的办法 async get(key: string) { if (!this.client) { await this.getClient(); } const data = await this.client.get(key); if (!data) return; return JSON.parse(data); } // 依据key删除redis缓存数据 async del(key: string): Promise<any> { if (!this.client) { await this.getClient(); } await this.client.del(key); } // 清空redis的缓存 async flushall(): Promise<any> { if (!this.client) { await this.getClient(); } await this.client.flushall(); }}
应用redis服务
redis-test.controller
import { Body, Controller, Get, Post, Query } from '@nestjs/common';import { CacheService } from 'src/common/cache/redis.service';@Controller('redis-test')export class RedisTestController { // 注入redis服务 constructor(private readonly cacheService: CacheService) {} @Get('get') async get(@Query() query) { return await this.cacheService.get(query.key); } @Post('set') async set(@Body() body) { const { key, ...params } = body as any; return await this.cacheService.set(key, params); } @Get('del') async del(@Query() query) { return await this.cacheService.del(query.key); } @Get('delAll') async delAll() { return await this.cacheService.flushall(); }}
redis-test.module.ts
import { Module } from '@nestjs/common';import { RedisTestService } from './redis-test.service';import { RedisTestController } from './redis-test.controller';import { CacheService } from 'src/common/cache/redis.service';@Module({ controllers: [RedisTestController], providers: [RedisTestService, CacheService], // 注入redis服务})export class RedisTestModule {}
redis-test.service.ts
import { Injectable } from '@nestjs/common';@Injectable()export class RedisTestService {}
集成redis实现单点登录
在要应用的controller或service中应用redis
- 这里以实现
token
存储在redis
为例子,实现单点登陆 - 须要在
passport
的login
中,存储token
,如果不会passport
验证
单点登陆原理
- 一个账户在第一个中央登陆,登陆时,JWT生成token,保留token到redis,同时返回token给前端保留到本地
- 同一账户在第二个中央登陆,登陆时,JWT生成新的token,保留新的token到redis。(token曾经扭转)
此时,第一个中央登陆的账户在申请时,应用的本地token就会和redis外面的新token不统一(留神:都是无效的token)
import { Injectable } from '@nestjs/common';import { JwtService } from '@nestjs/jwt';import { Repository } from 'typeorm';import { InjectRepository } from '@nestjs/typeorm';import { compareSync, hashSync } from 'bcryptjs';import { UsersEntity } from '../user/entities/user.entity';import { ToolsService } from '../../utils/tools.service';import { CreateUserDto } from '../user/dto/create-user.dto';import { CacheService } from '../../common/db/redis-ceche.service';@Injectable()export class AuthService { constructor( @InjectRepository(UsersEntity) private readonly usersRepository: Repository<UsersEntity>, private readonly jwtService: JwtService, private readonly redisService: CacheService, ) { } async create(user: CreateUserDto) { const { username, password } = user; const transformPass = hashSync(password); user.password = transformPass; const result = await this.usersRepository.findOne({ username }); if (result) ToolsService.fail('用户名已存在'); return await this.usersRepository.insert(user); } async validateUser(userinfo): Promise<any> { const { username, password } = userinfo; const user = await this.usersRepository.findOne({ where: { username }, select: ['username', 'password', 'id'], }); if (!user) ToolsService.fail('用户名或明码不正确'); //应用bcryptjs验证明码 if (!compareSync(password, user.password)) { ToolsService.fail('用户名或明码不正确'); } return user; } async login(user: any): Promise<any> { const { id, username } = user; const payload = { id, username }; const access_token = this.jwtService.sign(payload); await this.redisService.set(`user-token-${id}`, access_token, 60 * 60 * 24); 在这里应用redis return access_token; }}
验证token
import { Strategy, ExtractJwt, StrategyOptions } from 'passport-jwt';import { Injectable } from '@nestjs/common';import { PassportStrategy } from '@nestjs/passport';import { jwtConstants } from './constants';import { CacheService } from '../../common/db/redis-ceche.service';import { Request } from 'express';import { ToolsService } from '../../utils/tools.service';@Injectable()export class JwtStrategy extends PassportStrategy(Strategy) { constructor(private redisService: CacheService) { super({ jwtFromRequest: ExtractJwt.fromHeader('token'), //应用ExtractJwt.fromHeader从header获取token ignoreExpiration: false, //如果为true,则不验证令牌的过期工夫。 secretOrKey: jwtConstants.secret, //应用密钥解析,能够应用process.env.xxx passReqToCallback: true, } as StrategyOptions); } //token验证, payload是super中曾经解析好的token信息 async validate(req: Request, payload: any) { console.log('payload', payload); const { id } = payload; const token = ExtractJwt.fromHeader('token')(req); const cacheToken = await this.redisService.get(`user-token-${id}`); 获取redis的key //单点登陆验证 if (token !== JSON.parse(cacheToken)) { ToolsService.fail('您账户曾经在另一处登陆,请从新登陆', 401); } return { username: payload.username }; }}
QA
Q:nestJS注入其余依赖时为什么还须要导入其module
A模块的Service须要调用B模块的service中一个办法,则须要在A的Service导入B的service
场景如下:
// A.Serviceimport { BService } from '../B/B.service';@Injectable()export class A { constructor( private readonly _BService: BService, ) {}}
我的了解
- 在此处@Injectable装璜器曾经将B的Service类实例化了,
- 曾经能够应用B的类办法了。
- 但为什么还须要在A的module.ts中导入B模块呢?像是这样:
// A.module.tsimport { BModule } from '../B/B.module';@Module({ imports: [BModule], controllers: [AController], providers: [AService], exports: [AService],})export class AModule {}
A
为啥"为什么还须要在A的module.ts中导入B模块呢"?
因为 BService
的作用域只在 BModule
里,所以你要在 AController
里间接用,就会报错拿不到实例。
再来说,"有什么方法能够让 BService
随处间接用么?",参考如下伎俩:
B 的module 申明时,加上@Global
,如下:
import { Module, Global } from '@nestjs/common';import { BService } from './B.service';@Global()@Module({ providers: [BService], exports: [BService],})export class BModule {}
这样,你就不必在 AModule
的申明里引入 BModule
了。
对于『你的了解』局部,貌似你把@Inject
和 @Injectable
搞混了,倡议再读一读这个局部的文档,多做些练习/尝试,本人感触下每个api的特点。
最初,官网文档里其实有介绍 ,看依赖注入
:https://docs.nestjs.com/modul...