快速上手Nestjs+Prisma

参考资料:

学完这篇 Nest.js 实战,还没入门的来锤我!(长文预警)

Nest.js 实战系列二-手把手带你-实现注册、扫码登录、jwt认证等

Mysql+Prisma+Nest 实现JWT登录注册

官方文档

快速入门 ORM 框架 Prisma

本文文章仓库链接:nest: 快速上手nestjs+prisma

创建

项目开发离不开工程化的部分,比如创建项目、编译构建、开发时 watch 文件变动自动构建等。

Nest 项目自然也是这样,所以它在 @nestjs/cli 这个包里提供了 nest 命令。

可以直接 npx 执行,npm 会把它下载下来然后执行:

npx @nestjs/cli new 项目名

也可以安装到全局,然后执行,更推荐这种

npm install -g @nestjs/cli

nest new 项目名

不过后者要时不时升级下版本,不然可能用它创建的项目版本不是最新的:

npm update -g @nestjs/cli

  • nest new 快速创建项目
  • nest generate 快速生成各种代码
  • nest build 使用 tsc 或者 webpack 构建代码
  • nest start 启动开发服务,支持 watch 和调试
  • nest info 打印 node、npm、nest 包的依赖版本

项目结构

app.controller.ts

单个路由的基本控制器(Controller)

app.controller.spec.ts

针对控制器的单元测试

app.module.ts

应用程序的根模块(Module).

app.service.ts

具有单一方法的基本服务(Service)

main.ts

应用程序的入口文件将使用核心函数 NestFactory 来创建一个 Nest 应用程序实例.

npm run start:dev

这个命令将监视你的文件,自动重新编译并重新加载服务器。


ClI提供了一个可靠的开发工作流程。因此,生成的 Nest 项目预先安装了代码检查工具(eslint)和代码格式化工具(prettier).

# Lint and autofix with eslint
$ npm run lint

# Format with prettier
$ npm run format

第一个接口

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

// 应用程序的入口文件将使用核心函数 NestFactory 来创建一个 Nest 应用程序实例.
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);// 启动了我们的 HTTP 监听器,使应用程序能够处理传入的 HTTP 请求。
}
bootstrap();

这里看到的Hello World就是接口地址http://localhost:9080返回的内容, 不信我们也可以使用常用 Postman看看:

说明Nest.js创建项目默认就给写了一个接口例子,那就通过这个接口例子来看,我们应该怎么实现一个接口。

前边看到mian.ts中也没有别的文件引入, 只有AppModule, 打开src/app.module.ts:

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

// 应用程序的根模块.
@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

.mudule文件需要使用一个@Module() 装饰器的类,装饰器可以理解成一个封装好的函数,其实是一个语法糖(对装饰器不了解的,可以看走近MidwayJS:初识TS装饰器与IoC机制)。@Module() 装饰器接收四个属性:providers、controllers、imports、exports。

  • providers:Nest.js注入器实例化的提供者(服务提供者),处理具体的业务逻辑,各个模块之间可以共享(注入器的概念后面依赖注入部分会讲解);
  • controllers:处理http请求,包括路由控制,向客户端返回响应,将具体业务逻辑委托给providers处理;
  • imports:导入模块的列表,如果需要使用其他模块的服务,需要通过这里导入;
  • exports:导出服务的列表,供其他模块导入使用。如果希望当前模块下的服务可以被其他模块共享,需要在这里配置导出;

在app.module.ts中,看到它引入了app.controller.ts和app.service.ts,分别看一下这两个文件:

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

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

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

使用@Controller装饰器来定义控制器, @Get是请求方法的装饰器,对getHello方法进行修饰, 表示这个方法会被GET请求调用。

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

@Injectable()
export class AppService { 
  getHello(): string {
    return 'Hello World!';
  }
}

从上面,我们可以看出使用@Injectable修饰后的 AppService, 在AppModule中注册之后,在app.controller.ts中使用,我们就不需要使用new AppService()去实例化,直接引入过来就可以用。

路由装饰器

Nest.js中没有单独配置路由的地方,而是使用装饰器。Nest.js中定义了若干的装饰器用于处理路由。

@Controller

如每一个要成为控制器的类,都需要借助@Controller装饰器的装饰,该装饰器可以传入一个路径参数,作为访问这个控制器的主路径:

// 主路径为 app
@Controller("app")
export class AppController {
  constructor(private readonly appService: AppService) {}

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

HTTP方法处理装饰器

@Get、@Post、@Put等众多用于HTTP方法处理装饰器,经过它们装饰的方法,可以对相应的HTTP请求进行响应。同时它们可以接受一个字符串或一个字符串数组作为参数,这里的字符串可以是固定的路径,也可以是通配符。

继续修改app.controller.ts,看下面的例子:

// 主路径为 app
@Controller("app")
export class AppController {
  constructor(private readonly appService: AppService) {}
  
  // 1. 固定路径:
  // 可以匹配到 get请求,http://localhost:9080/app/list
  @Get("list")
  getHello(): string {...}
  
  // 可以匹配到 post请求,http://localhost:9080/app/list
  @Post("list")
  create():string{...}
  
  // 2.通配符路径(?+* 三种通配符 )
  // 可以匹配到 get请求, http://localhost:9080/app/user_xxx
  @Get("user_*")
  getUser(){return "getUser"}
  
  // 3.带参数路径
  // 可以匹配到put请求,http://localhost:9080/app/list/xxxx
  @Put("list/:id")
  update(){ return "update"}
}
  

这里要提一个关于路由匹配时的注意点, 当我们有一个put请求,路径为/app/list/user,此时,我们在app.controller.ts控制器文件中增加一个方法:

@Put("list/user")
 updateUser(){
      return {userId:1}
  }

你觉得这个路由会被匹配到吗?我们测试一下:

发现/app/list/user匹配到的并不是updateUser方法, 而是update方法。这就是我要说的注意点。

如果因为在匹配过程中, 发现@Put("list/:id")已经满足了,就不会继续往下匹配了,所以 @Put("list/user")装饰的方法应该写在它之前。

全局路由前缀

除了上面这些装饰器可以设置路由外, 我们还可以设置全局路由前缀, 比如给所以路由都加上/api前缀。此时需要修改main.ts

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

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.setGlobalPrefix('api'); // 设置全局路由前缀
  await app.listen(3000);
}
bootstrap();

此时之前的路由,都要变更为:http://localhost/api/xxx

到此我们认识了ControllerServiceModule、路由以及一些常用的装饰器

编写代码

写代码之前首先介绍几个nest-cli提供的几个有用的命令:

//语法

nest g [文件类型] [文件名] [文件目录]

nest g resource user

自动生成这个接口所有部分

nest g resource posts // 创建所有资源

nest g mo posts --no-spec     //创建模块

nest g s posts --no-spec  //创建服务类

nest g co posts --no-spec  //创建控制器

--no-spec 就是不要测试文件

注意创建顺序: 先创建Module, 再创建Controller和Service, 这样创建出来的文件在Module中自动注册,反之,后创建Module, Controller和Service,会被注册到外层的app.module.ts

创建模块

nest g mo posts

创建了一个 posts模块,文件目录不写,默认创建和文件名一样的posts目录,在posts目录下创建一个posts.module.ts。

执行完命令后,我们还可以发现同时在根模块app.module.ts中引入PostsModule这个模块,也在@Model装饰器的inports中引入了PostsModule

创建控制器

nest g co posts

此时创建了一个posts控制器,命名为posts.controller.ts以及一个该控制器的单元测试文件.

执行完命令, 文件posts.module.ts中会自动引入PostsController,并且在@Module装饰器的controllers中注入。

创建服务类

nest g service posts

创建app.service.ts文件,并且在app.module.ts文件下,@Module装饰器的providers中注入注入。

看一下现在的目录结构:

(如果执行命令的时候加了--no-spec那么不会生成测试文件)

连接Mysql

数据库

Naviact Premium

Prisma连接数据库

前置知识

首先,简单说一下什么是ORM?

我们如果直接使用Node.js操作mysql提供的接口, 那么编写的代码就比较底层, 例如一个插入数据代码:

// 向数据库中插入数据 
connection.query(`INSERT INTO posts (title, content) VALUES ('${title}', '${content}')`,
 (err, data) => {
   if (err) { 
     console.error(err) 
   } else {
     console.log(data) 
   }
 })

考虑到数据库表是一个二维表,包含多行多列,例如一个posts的表:

mysql> select * from posts;
+----+--------+------------+
| id | title       | content      |
+----+-------------+--------------+
|  1 | Nest.js入门 | 文章内容描述 |
+----+--------+------------+

每一行可以用一个JavaScript对象来表示, 比如第一行:

{
  id: 1,
  title:"Nest.js入门",
  content:"文章内容描述"
}

这就是传说中的ORM技术(Object-Relational Mapping),把关系数据库的变结构映射到对象上。

所以就出现了SequelizetypeORMPrisma这些ORM框架来做这个转换, 我们这里选择Prisma来操作数据库。

TypeORM:传统的 ORM 框架

TypeORM 是一种传统的 ORM(对象关系映射)框架,它将数据库表映射为实体类(entity),将表之间的关联映射为实体类属性的关联。
完成实体类和表的映射后,通过调用 userRepository 和 postRepository 的 API(如 find、delete、save 等),
TypeORM 会自动生成对应的 SQL 语句并执行。这就是对象关系映射的概念,即将对象和关系型数据库之间进行映射。

Prisma:颠覆传统的 ORM

Prisma 与 TypeORM 不同,它没有实体类的概念。
相反,Prisma 创造了一种领域特定语言(DSL),类似这样:


将数据库表映射为 DSL 中的 model,然后编译这个 DSL 将生成 Prisma Client 的代码:
之后,可以调用 Prisma Client 的 API(如 find、delete、create 等)来进行 CRUD(创建、读取、更新、删除)操作。虽然 Prisma 使用了 DSL 的语法,但整个流程与 TypeORM 类似。

Prisma使用方式

pnpm install prisma // 安装

npx prisma init // 初始化

// 例如连接mysql:修改.env文件 [DATABASE_URL="mysql://账号:密码@主机:端口/库名"]
DATABASE_URL="mysql://root:123456@localhost:3306/test
// 修改prisma/schema.prisma进行数据库连接
datasource db {
    provider = "mysql"
    url = env("DATABASE_URL")
}
定义 Model

在 schema 文件中定义数据模型(model),例如:

// 生成器
generator client {
  provider = "prisma-client-js"
}

// 数据源
datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

// 定义模型
model Post {
  id       Int     @id @default(autoincrement()) //表示id字段是整数且自增,主键
  title    String  //表示title字段是字符串类型
  publish  Boolean @default(false) //表示发布字段是布尔值默认false
  author   User   @relation(fields: [authorId], references: [id]) //表示author字段关联User用户表,关联关系:authorId 关联User表的id,外键
  authorId Int //表示authorId字段是关联id,和User表的id进行关联,定义User表和Post表是一对多的关系
}

model User {
  id    Int    @id @default(autoincrement()) //主键
  name  String
  email String @unique // 表示email字段是字符串且是唯一的
  posts Post[] //表示User表和Post表是一对多的关系
}
生成 Prisma Client 代码

pnpm add @prisma/client@5.16.1 // 安装@prisma/client包( prisma 客户端)

npx prisma migrate dev // 会根据 schema 文件生成 sql 并执行

npx prisma migrate dev --name test // 也可以通过这个指定名字。

npx prisma db push // 这个命令用于快速同步 Prisma 模型定义和数据库结构,而不需要创建明确的迁移文件。

安装 VSC 插件

在编辑模式文件前,在 VS Code 中安装 Prisma 插件,它针对 .prisma 文件提供了代码高亮、格式化、自动补全、跳转定义和检查的功能。没有这个插件的加持,模式文件就是一个纯文本。

CRUD

创建模型

// 定义数据模型(model)
model User {
  id         Int      @id @default(autoincrement()) //主键
  username   String   @db.VarChar(100)
  nickname   String   @db.VarChar(100)
  password   String   @db.VarChar(100)
  avatar     String
  email      String
  role       String   @db.VarChar(50)
  createTime DateTime @default(now())
  updateTime DateTime @default(now())
  posts      Post[] //表示User表和Post表是一对多的关系
}

model Post {
  id        Int     @id @default(autoincrement()) //表示id字段是整数且自增,主键
  title     String
  content   String?
  published Boolean @default(false) //表示发布字段是布尔值默认false
  author    User?   @relation(fields: [authorId], references: [id]) // 表示author字段关联User用户表
  authorId  Int? // 关联关系:authorId 关联User表的id,外键表示authorId字段是关联id,和User表的id进行关联,定义User表和Post表是一对多的关系
}

执行指令:npx prisma migrate dev // 会根据 schema 文件生成 sql 并执行

  • 如果你在应用程序的数据模型中进行了更改(例如添加新的表、更改字段类型等),你可以运行 npx prisma migrate dev 来创建一个新的数据库迁移文件,该文件包含了这些变更。

npx prisma migrate dev --name test // 也可以通过这个指定名字。

注入 Prisma 客户端

确保在 NestJS 的模块中正确注入 Prisma 客户端。例如,在 NestJS 的根模块或者相关的特定模块中进行注入:

import { Module } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { PostsController } from './posts.controller';
import { PostsService } from './posts.service';

@Module({
  imports: [],
  controllers: [PostsController],
  providers: [PostsService,PrismaClient],
})
export class PostsModule {}

Service

import { PrismaClient } from '@prisma/client';
import { Injectable } from '@nestjs/common';

@Injectable()
// 们创建了一个PostsService,它使用了 Prisma 客户端来执行与帖子相关的 CRUD 操作。
export class PostsService {
  constructor(private prisma: PrismaClient) {}

  async findAllPosts() {
    return this.prisma.post.findMany();
  }

  async findPostsById(id: number) {
    return this.prisma.post.findUnique({
      where: { id },
    });
  }

  async createPosts(data: any) {
    return this.prisma.post.create({
      data,
    });
  }

  async updatePosts(id: number, data: any) {
    return this.prisma.post.update({
      where: { id },
      data,
    });
  }

  async deletePosts(id: number) {
    return this.prisma.post.delete({
      where: { id },
    });
  }
}

Controller

import { Controller, Get, Post, Body, Param, Put, Delete } from '@nestjs/common';
import { PostsService } from './posts.service';
import { Post as MyPost } from '@prisma/client';

@Controller('posts')
export class PostsController {
  constructor(private readonly postsService: PostsService) {}

  @Get()
  async findAll(): Promise<MyPost[]> {
    return this.postsService.findAllPosts();
  }

  @Get(':id')
  async findOne(@Param('id') id: string): Promise<MyPost | null> {
    return this.postsService.findPostsById(+id);
  }

  @Post()
  async create(@Body() postData: { title: string; content?: string; published?: boolean; authorId: number }): Promise<MyPost> {
    return this.postsService.createPosts(postData);
  }

  @Put(':id')
  async update(@Param('id') id: string, @Body() updateData: { title?: string; content?: string; published?: boolean }): Promise<MyPost | null> {
    return this.postsService.updatePosts(+id, updateData);
  }

  @Delete(':id')
  async remove(@Param('id') id: string): Promise<MyPost | null> {
    return this.postsService.deletePosts(+id);
  }
}

(因为存在外键原因,这里手动增加user表的一条数据,定义id为1即可)

{
  "title": "Sample Post",
  "content": "This is the content of the post.",
  "published": true,
  "authorId": 1
}

接口统一规范:拦截请求

拦截错误请求

首先使用命令创建一个过滤器

nest g filter core/filter/http-exception

import {ArgumentsHost,Catch, ExceptionFilter, HttpException} from '@nestjs/common';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp(); // 获取请求上下文
    const response = ctx.getResponse(); // 获取请求上下文中的 response对象
    const status = exception.getStatus(); // 获取异常状态码

    // 设置错误信息
    const message = exception.message
      ? exception.message
      : `${status >= 500 ? 'Service Error' : 'Client Error'}`;
    const errorResponse = {
      data: {},
      message: message,
      code: -1,
    };

    // 设置返回的状态码, 请求头,发送错误信息
    response.status(status);
    response.header('Content-Type', 'application/json; charset=utf-8');
    response.send(errorResponse);
  }
}

拦截成功的返回数据

首先使用命令创建一个拦截器

nest g interceptor core/interceptor/transform

import {CallHandler, ExecutionContext, Injectable,NestInterceptor} from '@nestjs/common';
import { map, Observable } from 'rxjs';

@Injectable()
  export class TransformInterceptor implements NestInterceptor {
    intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
      return next.handle().pipe(
        map((data) => {
          return {
            data,
            code: 0,
            msg: '请求成功',
          };
        }),
      );
    }
  }

在main.ts注册

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { TransformInterceptor } from './core/interceptor/transform/transform.interceptor';
import { HttpExceptionFilter } from './core/filter/http-exception/http-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.setGlobalPrefix('api'); // 设置全局路由前缀
  // 注册全局错误的过滤器
  app.useGlobalFilters(new HttpExceptionFilter());
  // 全局注册拦截器
  app.useGlobalInterceptors(new TransformInterceptor());
  await app.listen(3000);
}
bootstrap();

Swagger

pnpm install @nestjs/swagger swagger-ui-express -S

...
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';

// 应用程序的入口文件将使用核心函数 NestFactory 来创建一个 Nest 应用程序实例.
async function bootstrap() {
 ...
  // 设置swagger文档
  const config = new DocumentBuilder()
    .setTitle('管理后台')
    .setDescription('管理后台接口文档')
    .setVersion('1.0')
    .addBearerAuth()
    .build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('docs', app, document);
  
  await app.listen(3000);
}
bootstrap();

配置完成,我们就可以访问:http://localhost:3000/docs,此时就能看到Swagger生成的文档:

接口标签

我们可以根据Controller来分类, 只要添加@ApiTags就可以

...
import { ApiTags } from '@nestjs/swagger';

@ApiTags("文章")
@Controller('post')
export class PostsController {...}

接口说明

进一步优化文档, 给每一个接口添加说明文字, 让使用的人直观的看到每个接口的含义,不要让使用的人去猜。同样在Controller中, 在每一个路由的前面使用@ApiOperation装饰器:


...
import { ApiTags,ApiOperation } from '@nestjs/swagger';
export class PostsController {
  ...
  @ApiOperation({ summary: '创建文章' })
  @Post()
  async create(@Body() post) {....}
  ....
}

接口传参

在posts目录下创建一个dto文件夹,再创建一个create-post.dot.ts文件:

import { ApiProperty } from '@nestjs/swagger';

export class PostDto {
  @ApiProperty({ description: '文章id' })
  readonly id?: number;
  @ApiProperty({ description: '文章标题' })
  readonly title: string;
  @ApiProperty({ description: '内容' })
  readonly content: string;
  @ApiProperty({ description: 'published' })
  readonly published?: boolean;
  @ApiProperty({ description: '作者' })
  readonly author?: string;
  @ApiProperty({ description: 'authorId' })
  readonly authorId?: number;
}

然后在Controller中对创建文章是传入的参数进行类型说明:

数据验证(异常处理)

Nest.js中数据验证,发现Nest.js中的管道就是专门用来做数据转换的

定义:管道是具有 @Injectable() 装饰器的类。管道应实现 PipeTransform 接口。

管道有两个类型:

  • 转换:管道将输入数据转换为所需的数据输出
  • 验证:对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常;

管道在异常区域内运行。这意味着当抛出异常时,它们由核心异常处理程序和应用于当前上下文的 异常过滤器 处理。当在 Pipe 中发生异常,controller 不会继续执行任何方法。

通俗来讲就是,对请求接口的入参进行验证和转换的前置操作,验证好了我才会将内容给到路由对应的方法中去,失败了就进入异常过滤器中。

pnpm install class-validator class-transformer -S

然后在create-post.dto.ts文件中添加验证, 完善错误信息提示:

import { IsNotEmpty, IsNumber, IsString } from 'class-validator';

export class CreatePostDto {
  @ApiProperty({ description: '文章标题' })
  @IsNotEmpty({ message: '文章标题必填' }) 
  readonly title: string;

  ...

  @IsNumber() // 使用 IsNumber 确保输入的类型是数字。
  @ApiProperty({ description: '文章类型' })
  readonly type: number;
}

入门阶段,我们使用的数据比较简单,上面只编写了一些常用的验证,class-validator还提供了很多的验证方法, 大家感兴趣可以自己看官方文档.

中间件logger

nest g middleware logger common

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: () => void) {
    const { method, path } = req;
    console.log(`${method},${path}`);
    next(); // 走下一步
  }
}
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PostsModule } from './posts/posts.module';
import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common';
import { LoggerMiddleware } from './common/logger/logger.middleware';

// 应用程序的根模块.根模块提供了用来启动应用的引导机制,可以包含很多功能模块。
@Module({
  //一个@Module() 装饰器的类,装饰器可以理解成一个封装好的函数,其实是一个语法糖
  imports: [PostsModule], // 导入模块的列表,如果需要使用其他模块的服务,需要通过这里导入;
  controllers: [AppController], // 处理http请求,包括路由控制,向客户端返回响应,将具体业务逻辑委托给providers处理;
  providers: [AppService], // Nest.js注入器实例化的提供者(服务提供者),处理具体的业务逻辑,各个模块之间可以共享(注入器的概念后面依赖注入部分会讲解);
  // exports:导出服务的列表,供其他模块导入使用。如果希望当前模块下的服务可以被其他模块共享,需要在这里配置导出;
})
export class AppModule {
  // 为 posts 路由添加中间件
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .exclude({ path: 'posts', method: RequestMethod.POST }) // post方法不会走这个中间件,但是其它方法会走
      .forRoutes('posts');// 监听posts路线
  }
}

角色控制守卫(guard)

我在后面还定义了一个使用守卫限制token:在后面基础服务用户登录模块login.guard.ts

守卫有单一的责任。它们根据运行时存在的某些条件(如权限、角色、ACL 等)确定给定请求是否将由路由处理程序处理。这通常称为授权。授权(及其通常与之合作的身份验证)通常由传统 Express 应用中的 中间件 处理。中间件是身份验证的不错选择,因为诸如令牌验证和将属性附加到 request 对象之类的事情与特定路由上下文(及其元数据)没有紧密联系。

nest g guard guards

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core'; // 反射期
import { Observable } from 'rxjs';

@Injectable()
export class RoleGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}
  canActivate(context: ExecutionContext): boolean {// canActivate用于判断是否允许访问请求。
    // 装饰器roles.decorator.ts 中定义了一个名为 roles 的元数据,它返回一个包含允许访问的角色数组。
    // 通过 Reflector 获取装饰器中定义的 roles 元数据。 
    // 通过使用 装饰器@Roles('admin')来检查用户的角色是否匹配所需的角色(例如 'admin')
    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!roles) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const { user } = request.query;
    return !!roles.find(role=>role === user);
  }
}

装饰器

nest g decorator decorators

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

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { RoleGuardService } from './role-guard.service';
import { Roles } from '../decorators/roles.decorator';
import { RoleGuard } from '../guards/roles.guard';

@ApiTags('role-guard')
@Controller('role-guard')
@UseGuards(RoleGuard)
export class RoleGuardController {
  constructor(private readonly roleGuardService: RoleGuardService) {}

  @Get()
  @Roles('admin') //只有admin才可以查询
  fetch(@Query() { id }): any {
    return '角色正确';
    // return this.roleGuardService.findPostsById(id);
  }
}

邮件服务(没用成功,后面在操作)

pnpm install --save @nestjs-modules/mailer nodemailer pug

pnpm install nestjs-config --save

一些小tips

删除node_modules后重新下载,导入包失败

  1. 使用 pnpm 自带的清除命令:
pnpm store prune

这个命令会清除 pnpm 的缓存和未使用的依赖项。

  1. 重新安装依赖:首先确认你使用了正确的命令重新安装依赖。使用 pnpm 或其他包管理工具重新安装项目依赖:
pnpm install

这将重新安装 package.json 中指定的所有依赖项

在运行有prisma的项目的时候,要先运行 prisma generate

在终端中运行以下命令,确保生成 Prisma 客户端的代码:

npx prisma generate

基础服务

// 定义数据模型(model)
model User {
  id         Int      @id @default(autoincrement()) //主键
  username   String   @db.VarChar(100)
  nickname   String   @db.VarChar(100)
  password   String   @db.VarChar(100)
  avatar     String
  email      String
  role       String   @db.VarChar(50)
  createTime DateTime @default(now())
  updateTime DateTime @default(now())
  posts      Post[] //表示User表和Post表是一对多的关系
}

model Post {
  id        Int     @id @default(autoincrement()) //表示id字段是整数且自增,主键
  title     String
  content   String?
  published Boolean @default(false) //表示发布字段是布尔值默认false
  author    User?   @relation(fields: [authorId], references: [id]) // 表示author字段关联User用户表
  authorId  Int? // 关联关系:authorId 关联User表的id,外键表示authorId字段是关联id,和User表的id进行关联,定义User表和Post表是一对多的关系
}

启动:npx prisma generate

nest g resource user

自动生成这个接口所有部分。

(这个我自己后面代码在增加了login-user.dto.ts文件,不是自动生成的)

  • DTO(数据传输对象)用于定义数据传输对象,主要用于验证和描述数据的传输格式。(常是普通的 TypeScript 类或接口,用来定义请求的数据结构或者响应的数据格式。)
  • Entities(实体)是领域模型中的对象或类,用于与数据库表之间的映射和操作,主要涉及数据持久化和业务逻辑的实现。(是领域模型中的对象或者类,通常代表数据库中的一张表或者集合)

用户注册

密码我们不能直接存明文在数据库中,所以采用bcryptjs实现加密, 然后再存入数据库。

pnpm install bcryptjs

import * as bcrypt from 'bcryptjs';
import { Injectable } from '@nestjs/common';

@Injectable()
export class EncryptionService {
  private fixedSalt = 'lingyang'; // 固定加盐值,示例中使用了一个字符串,实际中可以更复杂和安全

  async hashPassword(password: string): Promise<string> {
    const saltRounds = 10; // 加盐轮数,可以根据需要调整
    const hashedPassword = await bcrypt.hash(password + this.fixedSalt, saltRounds);
    return hashedPassword;
  }

  async comparePasswords(plainPassword: string, hashedPassword: string): Promise<boolean> {
    const isMatch = await bcrypt.compare(plainPassword + this.fixedSalt, hashedPassword);
    return isMatch;
  }
}
...

@Module({
  controllers: [UserController],
  providers: [UserService, PrismaClient, EncryptionService],
})
export class UserModule {}
import { PrismaClient } from '@prisma/client';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { EncryptionService } from './encryption.service';
@Injectable()
export class UserService {
  constructor(
    private prisma: PrismaClient,
    private readonly encryptionService: EncryptionService,
  ) {}

  async register(createUser: CreateUserDto) {
    const { username, password } = createUser;

    // 检查用户名是否已经存在
    const existUser = await this.prisma.user.findFirst({
      where: { username },
    });
    if (existUser) {
      throw new HttpException('用户名已存在', HttpStatus.BAD_REQUEST);
    }

    // 对密码进行加密
    const hashedPassword = await this.encryptionService.hashPassword(password);

    // 创建新用户
    const newUser = await this.prisma.user.create({
      data: { ...createUser, password: hashedPassword },
    });
    
    // 不返回密码字段给用户
    const { password: _, ...newUserWithoutPassword } = newUser;
    return newUserWithoutPassword;
  }
}

{ ...createUser, password: hashedPassword }{ password: hashedPassword, ...createUser }区别

  1. { ...createUser, password: hashedPassword }
    • 意味着如果createUser对象中已经有password属性,那么这里的password: hashedPassword将会覆盖掉原有的密码属性值。
  1. { password: hashedPassword, ...createUser }
    • 同样地,如果createUser对象中已经有password属性,那么这里的password: hashedPassword会覆盖掉原有的密码属性值。

tip: 后面的会覆盖前面的

用户登录

...
@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService) {}
  ...
  @ApiOperation({ summary: '登录' })
  @Post('login')
  login(@Body() loginDto: LoginUserDto) {
    return this.userService.login(loginDto);
  }
}
...
@Injectable()
export class UserService {
  constructor(
    private prisma: PrismaClient,
    private readonly encryptionService: EncryptionService,
  ) {}

  async register(createUser: CreateUserDto) {...}

  async login(loginUser: LoginUserDto) {
    const user = await this.prisma.user.findFirst({
      where: {
        username: loginUser.username,
      },
    });
    if (user) {
      const isPasswordCorrect = await this.encryptionService.comparePasswords(
        loginUser.password,
        user.password,
      );
      if (isPasswordCorrect) {
        // 不返回密码字段给用户
        const { password: _, ...userWithoutPassword } = user;
        return userWithoutPassword;
      } else {
        throw new HttpException('密码错误', 200);
      }
    } else {
      throw new HttpException('用户不存在', 200);
    }
  }
}

JWT配置
  • 登录或者注册成功,我们都应该返回token
  • 前端存一份,以后请求别的接口的时候,带上,
    • 验证通过就可以请求接口数据
    • 验证失败就抛出错误,不返回数据

pnpm install @nestjs/jwt

引入jwt

AppModule 里引入 JwtModule

...
import { JwtModule } from '@nestjs/jwt';

@Module({
  imports: [
    ...
    JwtModule.register({  // 注册jwt模块
      global: true,  // 全局使用
      secret: 'dehua',  // 秘钥
      signOptions: { // 签名配置
        expiresIn: '7d', // 过期时间
      },
    })],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule { }

global:true 声明为全局模块,这样就不用每个模块都引入它了,指定加密密钥,token 过期时间

...
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class UserService {
  constructor(
    private prisma: PrismaClient,
    private readonly encryptionService: EncryptionService,
    private readonly jwtService: JwtService,
  ) {}

  async register(createUser: CreateUserDto) {
    ...
    const token = await this.jwtService.signAsync({
      id: newUser.id,
      username: newUser.username
    })
    return token;
  }

  async login(loginUser: LoginUserDto) {
    ...
      if (isPasswordCorrect) {
        // 返回token给前端,让前端存起来
        const token = await this.jwtService.signAsync({
          id: user.id,
          username: user.username
        })
        return token;
      } else {
        throw new HttpException('密码错误', 200);
      }
    ...
  }
}

接下来前端存储一份,请求头上携带就可以了

接口要token验证通过之后,才可以返回数据

使用守卫限制token

现在都可以查,不管有没有token,不管验证通过没有

我们现在创建一个Guard(守卫)来限制,满足才可以访问接口

nest g guard login --no-spec --flat

  • --no-spec 参数告诉 NestJS 不生成单元测试规范文件。
  • --flat 参数告诉 NestJS 将生成的文件放在当前文件夹中,而不是创建一个新的文件夹。

然后把他放到这里面

import { JwtService } from '@nestjs/jwt';
import {
  CanActivate,
  ExecutionContext,
  Inject,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { Request } from 'express';
import { Observable } from 'rxjs';
interface JwtUserData {
  id: any;
  username: string;
}
declare module 'express' {
  interface Request {
    user: JwtUserData;
  }
}
@Injectable()
export class LoginGuard implements CanActivate {
  @Inject(JwtService)
  private jwtService: JwtService;
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request: Request = context.switchToHttp().getRequest();
    const token = request.header('token') || '';
    try {
      const info = this.jwtService.verify(token);
      (request as any).user = info;
      return true;
    } catch (e) {
      throw new UnauthorizedException('登录 token 失效,请重新登录');
    }
  }
}
  @Post()
  @UseGuards(LoginGuard) // 使用守卫
  @ApiOperation({ summary: '创建文章' })
  async create(@Body() postData: PostDto): Promise<PostDto> {
    return this.postsService.createPosts(postData);
  }

获取当前token用户信息

同时更改之前帖子创建接口创建人信息由token用户信息得到而不是手动传

import { Request } from 'express';
...

  @Post()
  @UseGuards(LoginGuard) // 使用守卫
  @ApiOperation({ summary: '创建文章' })
  async create(
    @Body() postData: PostDto,
    @Req() req: Request,
  ): Promise<PostDto> {
    console.log(req.user);
    const _postData={...postData,authorId:req.user.id}
    return this.postsService.createPosts(_postData);
  }

(现在这样就和当前用户绑定啦)

  • 10
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
首先,你需要安装Prisma和NestJS的依赖: ``` npm install @prisma/client prisma @nestjs/prisma ``` 接下来,你需要在你的NestJS应用程序中创建一个Prisma服务。你可以使用`PrismaService`类来实现这一点。在`PrismaService`中,你需要创建一个Prisma客户端实例: ```typescript import { Injectable } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; @Injectable() export class PrismaService { private prisma: PrismaClient; constructor() { this.prisma = new PrismaClient(); } getClient() { return this.prisma; } async onModuleDestroy() { await this.prisma.disconnect(); } } ``` 现在,你可以在你的控制器或服务中注入`PrismaService`,并使用`getClient`方法访问Prisma客户端。 例如,以下是一个使用Prisma的控制器: ```typescript import { Controller, Get } from '@nestjs/common'; import { PrismaService } from './prisma.service'; @Controller('users') export class UsersController { constructor(private prisma: PrismaService) {} @Get() async getUsers() { const users = await this.prisma.getClient().user.findMany(); return users; } } ``` 这里我们使用Prisma客户端的`user.findMany`方法来获取所有用户。 最后,你需要在你的应用程序模块中注册`PrismaService`: ```typescript import { Module } from '@nestjs/common'; import { PrismaService } from './prisma.service'; import { UsersController } from './users.controller'; @Module({ imports: [], controllers: [UsersController], providers: [PrismaService], }) export class AppModule {} ``` 现在,你已经成功地将Prisma集成到了NestJS中。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值