Nestjs最佳實踐教程:4排序,分頁與過濾

pincman1988發表於2022-07-10

另,本人在找工作中,希望能有遠端工作匹配(無法去外地),有需要的老闆可以看一下我的個人介紹:pincman.com/about

學習目標

  • 過載TreeRepository自帶方法來對樹形結構的資料進行扁平化處理
  • 對Typeorm查詢出的資料列表進行分頁處理
  • 透過請求中的query查詢對資料進行篩選處理,比如排序,過濾等
  • 實現釋出文章和取消釋出的功能
  • Typeorm 模型事件和Subscriber(訂閱者)的使用
  • 使用sanitize-html對文章內容進行防注入攻擊處理

預裝依賴

~ pnpm add nestjs-typeorm-paginate sanitize-html deepmerge && pnpm add @types/sanitize-html -D

檔案結構

建立檔案

cd src/modules/content && \
mkdir subscribers && \
touch dtos/query-category.dto.ts \
dtos/query-post.dto.ts \
subscribers/post.subscriber.ts \
subscribers/index.ts \
services/sanitize.service.ts \
&& cd ../../../

與上一節一樣,這一節的新增和修改集中於ContentModule

src/modules/content
├── constants.ts
├── content.module.ts
├── controllers
│   ├── category.controller.ts
│   ├── comment.controller.ts
│   ├── index.ts
│   └── post.controller.ts
├── dtos
│   ├── create-category.dto.ts
│   ├── create-comment.dto.ts
│   ├── create-post.dto.ts
│   ├── index.ts
│   ├── query-category.dto.ts
│   ├── query-post.dto.ts
│   ├── update-category.dto.ts
│   └── update-post.dto.ts
├── entities
│   ├── category.entity.ts
│   ├── comment.entity.ts
│   ├── index.ts
│   └── post.entity.ts
├── repositories
│   ├── category.repository.ts
│   ├── comment.repository.ts
│   ├── index.ts
│   └── post.repository.ts
├── services
│   ├── category.service.ts
│   ├── comment.service.ts
│   ├── index.ts
│   ├── post.service.ts
│   └── sanitize.service.ts
└── subscribers
    ├── index.ts
    └── post.subscriber.ts

應用編碼

這節多了一個新的概念,即subscriber,具體請查閱typeorm文件,當然你也可以在模型中使用事件處理函式,效果沒差別

模型

CategoryEntity

程式碼:src/modules/content/entities/category.entity.ts

  • 新增order欄位用於排序
  • 新增level屬性(虛擬欄位)用於在打平樹形資料的時候新增當前項的等級

PostEntity

程式碼: src/modules/content/entities/post.entity.ts

type欄位的型別用enum列舉來設定,首先需要定義一個PostBodyTypeenum型別,可以新增一個constants.ts檔案來統一定義這些enum和常量

  • 新增publishedAt欄位用於控制釋出時間和釋出狀態
  • 新增type欄位用於設定釋出型別
  • 新增customOrder欄位用於自定義排序

儲存類

CategoryRepository

程式碼: src/modules/content/repositories/category.repository.ts

因為CategoryRepository繼承自TreeRepository,所以我們在typeorm原始碼中找到這個類,並對部分方法進行覆蓋,如此我們就可以對樹形分類進行排序,覆蓋的方法如下

當然後面會講到更加深入的再次封裝,此處暫時先這麼用

  • findRoots 為根分類列表查詢新增排序
  • createDescendantsQueryBuilder 為子孫分類查詢器新增排序
  • createAncestorsQueryBuilder 為祖先分類查詢器新增排序

DTO驗證

新增QueryCategoryDtoQueryPostDto用於查詢分類和文章時進行分頁以及過濾資料和設定排序型別等

在新增DTO之前,現在新增幾個資料轉義函式,以便把請求中的字串改成需要的資料型別

// src/core/helpers.ts

// 用於請求驗證中的number資料轉義
export function tNumber(value?: string | number): string |number | undefined
// 用於請求驗證中的boolean資料轉義
export function tBoolean(value?: string | boolean): string |boolean | undefined
// 用於請求驗證中轉義null
export function tNull(value?: string | null): string | null | undefined

修改create-category.dto.tscreate-comment.dto.tsparent欄位的@Transform裝飾器

export class CreateCategoryDto {
...
    @Transform(({ value }) => tNull(value))
    parent?: string;
}

新增一個通用的DTO介面型別

// src/core/types.ts

// 分頁驗證DTO介面
export interface PaginateDto {
    page: number;
    limit: number;
}

QueryCategoryDto

程式碼: src/modules/content/dtos/query-category.dto.ts

  • page屬性設定當前分頁
  • limit屬性設定每頁資料量

QueryPostDto

除了與QueryCateogryDto一樣的分頁屬性外,其它屬性如下

  • orderBy用於設定排序型別
  • isPublished根據釋出狀態過濾文章
  • category過濾出一下分類及其子孫分類下的文章

orderBy欄位是一個enum型別的欄位,它的可取值如下

  • CREATED: 根據建立時間降序
  • UPDATED: 根據更新時間降序
  • PUBLISHED: 根據釋出時間降序
  • COMMENTCOUNT: 根據評論數量降序
  • CUSTOM: 根據自定義的order欄位升序

服務類

SanitizeService

程式碼: src/modules/content/services/sanitize.service.ts

此服務類用於clean html

sanitize方法用於對HTML資料進行防注入處理

CategoryService

程式碼:src/modules/content/services/category.service.ts

新增一個輔助函式,用於對打平後的樹形資料進行分頁

// src/core/helpers.ts
export function manualPaginate<T extends ObjectLiteral>(
    { page, limit }: PaginateDto,
    data: T[],
): Pagination<T>

新增paginate(query: QueryCategoryDto)方法用於處理分頁

async paginate(query: QueryCategoryDto) {
    // 獲取樹形資料
    const tree = await this.findTrees();
    // 打平樹形資料
    const list = await this.categoryRepository.toFlatTrees(tree);
    // 呼叫手動分頁函式進行分頁
    return manualPaginate(query, list);
}

PostService

程式碼:src/modules/content/services/post.service.ts

  • getListQuery: 用於構建過濾與排序以及透過分類查詢文章資料等功能的query構建器
  • paginate: 呼叫getListQuery生成query,並作為nestjs-typeorm-paginatepaginate的引數對資料進行分頁
async paginate(params: FindParams, options: IPaginationOptions) {
    const query = await this.getListQuery(params);
    return paginate<PostEntity>(query, options);
}

訂閱者

PostSubscriber

程式碼: src/modules/content/subscribers/post.subscriber.ts

  • beforeInsert(插入資料前事件): 如果在新增文章的同時釋出文章,則設定當前時間為釋出時間
  • beforeUpdate(更新資料前事件): 更改釋出狀態會同時更新發布時間的值,如果文章更新為未釋出狀態,則把釋出時間設定為null
  • afterLoad(載入資料後事件): 對HTML型別的文章內容進行去標籤處理防止注入攻擊

一個需要注意的點是需要在subcriber類的建構函式中注入Connection才能獲取連結

   constructor(
        connection: Connection,
        protected sanitizeService: SanitizeService,
    ) {
        connection.subscribers.push(this);
    }

註冊訂閱者

把訂閱者註冊成服務後,由於在建構函式中注入了connection這個連線物件,所以typeorm會自動把它載入到這個預設連線的subscribers配置中

// src/modules/content/subscribers/post.subscriber.ts
import * as SubscriberMaps from './subscribers';
const subscribers = Object.values(SubscriberMaps);
@Module({
    ....
    providers: [...subscribers, ...dtos, ...services],
})

控制器

CategoryController

程式碼: src/modules/content/controllers/category.controller.ts

  • list: 透過分頁來查詢扁平化的分類列表
  • index: 把url設定成@Get('tree')
    @Get()
    // 分頁查詢
    async list(
        @Query(
            new ValidationPipe({
                transform: true,
                forbidUnknownValues: true,
                validationError: { target: false },
            }),
        )
        query: QueryCategoryDto,
    ) {
        return this.categoryService.paginate(query);
    }

    // 查詢樹形分類
    @Get('tree')
    async index() {
        return this.categoryService.findTrees();
    }

PostController

程式碼: src/modules/content/controllers/post.controller.ts

修改index方法用於分頁查詢

// 透過分頁查詢資料
async index(
        @Query(
            new ValidationPipe({
                transform: true,
                forbidUnknownValues: true,
                validationError: { target: false },
            }),
        )
        { page, limit, ...params }: QueryPostDto,
    ) {
        return this.postService.paginate(params, { page, limit });
    }
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章