實現Nest中引數的聯合型別校驗

神奇的程式設計師發表於2022-04-26

前言

在nest的dto層對引數進行校驗時,某個引數可能有多種型別,遇到這種情況你會怎麼處理?本文將跟大家分享這個問題的解決方案,歡迎各位感興趣的開發者閱讀本文。

場景概述

我們在進行介面開發時,客戶端需要傳入一個名為text的欄位,它可能是string型別或Array<Object>型別(在TS中我們把這種關係稱之為 聯合型別 ),class-validator庫中提供了相關的校驗註解,那把他們寫在一起能否完成相關的校驗呢,如下所示:

export class AppDto {
  @ApiProperty({ example: "2022年4月20日修改", description: "備註" })
  @IsString()
  @IsArray()
  @ValidateNested({ each: true })
  @Type(() => TextObjDto)
  public text!: string | Array<TextObjType>; 
}

TextObjDto的程式碼如下所示:

export class TextObjDto {
  @ApiProperty({ example: "修復了一些bug", description: "內容" })
  @IsString()
  content!: string;
  @ApiProperty({ example: "2022-04-20 07:52", description: "建立時間" })
  @IsString()
  createTime?: string;
  @ApiProperty({ example: true, description: "是否為新功能標識" })
  @IsBoolean()
  mark?: boolean;
}

啟動專案,用postman測試後發現並不好使,傳了array型別的資料又要求是string型別,傳了string型別的資料又要求是array型別。

image-20220420115628178

注意:巢狀型別的物件驗證需要使用@ValidateNested和@Type註解, @Type接受一個回撥函式,函式內部需要返回一個用class宣告的dto類。

解決方案

經過一番求助,翻了一圈class-validator的文件,發現沒有現成的解決方案。那麼,就只能自己拿到引數搞自定義校驗了。

class-transformer這個庫中,提供了Transform方法,它接受一個回撥函式作為引數,回撥函式中提供了一個TransformFnParams型別的引數,其中的value欄位就是客戶端傳過來的引數,我們只需要對其進行校驗即可。

image-20220420170201041

接下來,我們來看下實現程式碼,如下所示:

export class AppDto {
  @ApiProperty({ example: "2022年4月20日修改", description: "備註" })
  @IsOptional()
  @Transform(({ value }) => checkTitleKey(value))
  public text!: string | Array<TextObjType>;
}

上述程式碼中,我們有一個名為checkTitleKey的校驗函式,因為需要自己校驗,所以就需要自己把TS的型別校驗復刻一遍出來,實現程式碼如下所示:

  • 如果校驗通過直接返回value引數即可
  • 如果校驗不通過直接使用nest內建異常進行丟擲即可
export function checkTitleKey(
  value: string | number | Array<TextObjType> | undefined | null
): any {
  if (typeof value === "string") {
    // 不做更改,直接返回
    return value;
  } else if (value instanceof Array) {
    // 不能為空陣列
    if (value.length <= 0) {
      throw new BadRequestException(
        "property text cannot be an empty array",
        "Bad Request"
      );
    }
    for (let i = 0; i < value.length; i++) {
      // 校驗陣列中的物件欄位
      const objKeys = Object.keys(value[i]);
      if (objKeys.length <= 0) {
        throw new BadRequestException(
          "property text contains empty objects",
          "Bad Request"
        );
      }
      // 必須包含content欄位
      if (!objKeys.includes("content")) {
        throw new BadRequestException(
          "property text objects in the array must contain 'content'",
          "Bad Request"
        );
      }
      // 對每個key進行校驗
      for (let j = 0; j < objKeys.length; j++) {
        switch (objKeys[j]) {
          case "content":
            // content欄位必須為string型別
            if (typeof value[i].content !== "string") {
              throw new BadRequestException(
                "property text 'content' of the objects in the array must be of type string",
                "Bad Request"
              );
            }
            break;
          case "duration":
            if (typeof value[i].createTime !== "string") {
              throw new BadRequestException(
                "property text 'createTime' of the objects in the array must be of type number",
                "Bad Request"
              );
            }
            break;
          case "delay":
            if (typeof value[i].mark !== "boolean") {
              throw new BadRequestException(
                "property text 'mark' of the objects in the array must be of type number",
                "Bad Request"
              );
            }
            break;
          default:
            break;
        }
      }
    }
    return value;
  } else {
    throw new BadRequestException(
      "text must be an array or string",
      "Bad Request"
    );
  }
}

TextObjType的宣告也需要進行相對應的修改,如下所示:

  • 全部變為可選引數,引數的必傳與否已經在校驗函式中處理了
  • 型別全部變為any
export type TextObjType = {
  content?: any;
  createTime?: any;
  mark?: any;
};
有一部分開發者可能比較迷惑,不是說ts用any是可恥行為嗎,這我就要糾正下你了,既然它存在自然有使用場景。在我這個場景中,物件裡所有key的型別校驗都手動處理了,如果在此處定義了它的型別,在校驗函式中就會報黃色警告,因此針對於需要手動校驗型別的場景而言,使用any是最合適的。

結果校驗

最後,我們針對於程式碼裡定義的異常規則來驗證下其是否能正常工作,如下所示:

# text欄位為string型別
{
    "id":"122211",
    "title":"新的標題",
    "text":"新替換的文字內容",
    "name":"新的名字",
    "config":"var config = {\"name\":\"aa\",\"age\":\"21\",\"title\":\"標題測試\"}"
}
>>> 介面呼叫成功

# text欄位為Array型別所有key都存在
{
    "id":"122211",
    "title":"新的標題",
    "text":[{"content":"新文字","createTime":"2022-04-20","mark":false}],
    "name":"新的名字",
    "config":"var config = {\"name\":\"aa\",\"age\":\"21\",\"title\":\"標題測試\"}"
}

>>> 介面呼叫成功

# text欄位缺少content
{
    "id":"122211",
    "title":"新的標題",
    "text":[{"createTime":"2022-04-20","mark":false}],
    "name":"新的名字",
    "config":"var config = {\"name\":\"aa\",\"age\":\"21\",\"title\":\"標題測試\"}"
}
>>> 介面報錯400:property text objects in the array must contain 'content'

# text欄位為number型別
{
    "id":"122211",
    "title":"新的標題",
    "text":19,
    "name":"新的名字",
    "config":"var config = {\"name\":\"aa\",\"age\":\"21\",\"title\":\"標題測試\"}"
}
>>> 介面報錯400:text must be an array or string

# text欄位缺少createTime與mark
{
    "id":"122211",
    "title":"新的標題",
    "text":[{"content":"新文字"}],
    "name":"新的名字",
    "config":"var config = {\"name\":\"aa\",\"age\":\"21\",\"title\":\"標題測試\"}"
}
>>> 介面呼叫成功

如下圖所示,我們列舉一個text欄位為數字時的報錯截圖,執行結果符合預期,文章開頭的問題成功解決?

image-20220420174933835

示例程式碼

文中所舉程式碼的完整版請移步:

寫在最後

至此,文章就分享完畢了。

我是神奇的程式設計師,一位前端開發工程師。

如果你對我感興趣,請移步我的個人網站,進一步瞭解。

  • 文中如有錯誤,歡迎在評論區指正,如果這篇文章幫到了你,歡迎點贊和關注?
  • 本文首發於神奇的程式設計師公眾號,未經許可禁止轉載?

相關文章