共计 115021 个字符,预计需要花费 288 分钟才能阅读完成。
原文链接 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.json
2 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.ts
import {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
由本模块提供并应在其余模块中可用的提供者的子集
// 创立模块 posts
nest g module posts
Nestjs 中的共享模块
每个模块都是一个共享模块。一旦创立就能被任意模块重复使用。假如咱们将在几个模块之间共享 PostsService 实例。咱们须要把 PostsService 放到 exports 数组中:
// posts.modules.ts
import {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.ts
async 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.ts
import {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.ts
import {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 调用设置的 cookie
console.log(req.signedCookies);
Session 的应用
session
是另一种记录客户状态的机制,不同的是 Cookie 保留在客户端浏览器中,而session
保留在服务器上- 当浏览器拜访服务器并发送第一次申请时,服务器端会创立一个 session 对象,生成一个相似于 key,value 的键值对,而后将 key(cookie)返回到浏览器 (客户) 端,浏览器下次再拜访时,携带 key(cookie),找到对应的 session(value)。客户的信息都保留在 session 中
装置 express-session
npm i express-session --save
npm i -D @types/express-session --save
// main.ts
import {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.ts
import {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.ts
import {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.ts
async 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.ts
import {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.ts
import {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.ts
import {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.ts
import {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 — 400
UnauthorizedException — 401
ForbiddenException — 403
NotFoundException — 404
NotAcceptableException — 406
RequestTimeoutException — 408
ConflictException — 409
GoneException — 410
PayloadTooLargeException — 413
UnsupportedMediaTypeException — 415
UnprocessableEntityException — 422
InternalServerErrorException — 500
NotImplementedException — 501
BadGatewayException — 502
ServiceUnavailableException — 503
GatewayTimeoutException — 504
本例中应用的是自定义的异样类,代码如下:
// common/filters/http-exception.filter.ts
import {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.ts
import {
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.ts
import {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.ts
import * 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 -> database
import {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.ts
import '../env.parse'; // 导入环境变量
.env.local
PORT=9000
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
MYSQL_USER=root
MYSQL_PASSWORD=123
MYSQL_DATABASE=test
.env.prod
PORT=9000
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
MYSQL_USER=root
MYSQL_PASSWORD=1234
MYSQL_DATABASE=test
读取环境变量 process.env.MYSQL_HOST
模式
文件上传与下载
yarn add @nestjs/platform-express compressing
compressing 文件下载依赖,提供流的形式
配置文件的目录地址,以及文件的名字格局
// 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.ts
import {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.ts
import {
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.ts
import {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.ts
import {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.ts
import {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.ts
import {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 -S
import {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.ts
import {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 passport
yarn add -D @types/passport @types/passport-jwt @types/passport-local
jwt 策略 jwt.strategy.ts
// src/modules/auth/jwt.strategy.ts
import {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.ts
import {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.ts
export const jwtConstants = {secret: 'secretKey',};
应用守卫 auth.controller.ts
// src/modules/auth/auth.controller.ts
import {Controller, Get, Post, Request, UseGuards} from '@nestjs/common';
import {AuthGuard} from '@nestjs/passport';
import {AuthService} from './auth.service';
@Controller('auth')
export class AuthController {constructor(private readonly authService: AuthService) {}
// 登录测试 无需 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.ts
import {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.ts
import {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.ts
import {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 bcryptjs
yarn add -D @types/bcryptjs
同一明码,每次生成不一样的值
import {compareSync, hashSync} from 'bcryptjs';
const passwrod = '123456';
const transformPass = hashSync(passwrod); $2a$10$HgTA1GX8uxbocSQlbQ42/.Y2XnIL7FyfKzn6IC69IXveD6F9LiULS
const transformPass2 = hashSync(passwrod); $2a$10$mynd130vI1vkz4OQ3C.6FeYXGEq24KLUt1CsKN2WZqVsv0tPrtOcW
const 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.ts
import {Module} from '@nestjs/common';
import {TasksService} from './tasks.service';
@Module({providers: [TasksService],
})
export class TasksModule {}
在这里编写你的定时工作
// src/tasks/task.service.ts
import {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.ts
import {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.ts
import {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.ts
import {
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/mongodb
npm install --save @nestjs/mongoose mongoose
npm install --save-dev @types/mongoose
在 app.module.ts 中配置数据库连贯
// app.module.ts
import {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.ts
export default {uri: 'mongodb://localhost:27017/nest', // 指定 nest 数据库};
配置 Schema
// article.schema
import * as mongoose from 'mongoose';
export const ArticleSchema = new mongoose.Schema({
title: String,
content:String,
author: String,
status: Number,
});
在控制器对应的 Module 中配置 Model
// mongodb.module.ts
import {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.ts
import {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.ts
const {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.ts
import {
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.ts
import {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.ts
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) {
// 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.ts
import {
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.ts
import {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.ts
import {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.ts
import {
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.ts
import {
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.ts
import {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 查找,找不到返回 undefined
findOne({where: { username} }); 条件查找,找不到返回 undefined
依据 ID 查找一个或多个
findByIds([1,2,3]); 查找 n 个,全副查找不到返回空数组,找到就返回找到的
其余
hasId(new UsersEntity()) 检测实体是否有合成 ID,返回布尔值
getId(new UsersEntity()) 获取实体的合成 ID,获取不到返回 undefined
create({username: 'admin12345', password: '123456',}) 创立一个实体,须要调用 save 保留
count({status: 1}) 计数,返回数量,无返回 0
increment({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 10
return 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 user
LEFT JOIN photos photo ON photo.user = user.id
WHERE 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 user
INNER JOIN photos photo ON photo.user = user.id AND photo.isRemoved = FALSE
WHERE 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.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, 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.ts
import {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.Service
import {BService} from '../B/B.service';
@Injectable()
export class A {
constructor(private readonly _BService: BService,) {}}
我的了解
- 在此处 @Injectable 装璜器曾经将 B 的 Service 类实例化了,
- 曾经能够应用 B 的类办法了。
- 但为什么还须要在 A 的 module.ts 中导入 B 模块呢?像是这样:
// A.module.ts
import {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…