NestJS學習總結篇

程式設計師小月發表於2022-05-27

原文連結 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 平臺:expressfastify。 您可以選擇最適合您需求的產品
  • 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中服務相當於MVCModel

建立服務

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-validatorclass-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();

中介軟體是請求的第一道關卡

  1. 執行任何程式碼。
  2. 對請求和響應物件進行更改。
  3. 結束請求-響應週期。
  4. 呼叫堆疊中的下一個中介軟體函式。
  5. 如果當前的中介軟體函式沒有結束請求-響應週期, 它必須呼叫 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": "賬號不能為空"
    }
}

這個時候就可以使用攔截器來做一下處理了。
攔截器作用:

  1. 在函式執行之前/之後繫結額外的邏輯
  2. 轉換從函式返回的結果
  3. 轉換從函式丟擲的異常
  4. 擴充套件基本函式行為
  5. 根據所選條件完全重寫函式 (例如, 快取目的)

攔截器的執行順序分為兩個部分:

  • 第一個部分在管道和自定義邏輯(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進行配合驗證相關的輸入操作內容

認識官方的三個內建管道

  1. ValidationPipe:基於class-validatorclass-transformer這兩個npm包編寫的一個常規的驗證管道,可以從class-validator匯入配置規則,然後直接使用驗證(當前不需要了解ValidationPipe的原理,只需要知道從class-validator引規則,設定到對應欄位,然後使用ValidationPipe即可)
  2. ParseIntPipe:轉換傳入的引數為數字

如:傳遞過來的是/test?id=‘123’"這裡會將字串‘123’轉換成數字123

  1. 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;
}

controllerservice一起使用

// 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的設計

使用者表和角色表的關係設計:

如果你希望一個使用者可以有多個角色,如:一個人即是銷售總監,也是人事管理,就設計多對多關係
如果你希望一個使用者只能有一個角色,就設計一對多,多對一關係

角色表和許可權表的關係設計:

一個角色可以擁有多個許可權,一個許可權被多個角色使用,設計多對多關係

多對多關係設計

使用者表與角色表是多對多關係,角色表與選單表是多對多關係

更加複雜的設計

實現流程

  1. 資料表設計
  2. 實現角色的增刪改查
  3. 實現使用者的增刪改查,增加和修改使用者的時候需要選擇角色
  4. 實現許可權的增刪改查
  5. 實現角色與授權的關聯
  6. 判斷當前登入的使用者是否有訪問選單的許可權
  7. 根據當前登入賬戶的角色資訊動態顯示左側選單(前端)

程式碼實現

這裡將實現一個使用者,部門,角色,許可權的例子:
使用者通過成為部門的一員,則擁有部門普通角色的許可權,還可以單獨給使用者設定角色,通過角色,獲取許可權。
許可權模組包括,模組,選單,操作,通過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/typeormInjectRepository來注入實體

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 開啟事務
  1. EntityManagerRepository都封裝了運算元據的方法,注意:兩者的使用方式是不一樣的,(實在不明白搞這麼多方法做什麼,學得頭大)
    getManagerEntityManager的型別,getRepositoryRepository的型別
  2. 都可以使用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事務的使用方式

  1. 使用裝飾器,在controller中編寫,傳遞給service使用
  2. 使用getManagergetConnection,在service中編寫與使用
  3. 使用connectiongetConnection,開啟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的別名,可隨便寫,和AvatarEntityuserinfo欄位建立關係
  • 第三個引數: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的別名,可隨便寫,和PhotoEntityuserinfo欄位建立關係
第三個引數: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的別名,可隨便寫,和PhotoEntityfollowers欄位建立關係

使用者(粉絲)表: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為例子,實現單點登陸
  • 需要在passportlogin中,儲存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...