實(shí)現(xiàn)Nest中參數(shù)的聯(lián)合類型校驗(yàn)

前言

在nest的dto層對(duì)參數(shù)進(jìn)行校驗(yàn)時(shí),某個(gè)參數(shù)可能有多種類型隶症,遇到這種情況你會(huì)怎么處理?本文將跟大家分享這個(gè)問題的解決方案措嵌,歡迎各位感興趣的開發(fā)者閱讀本文企巢。

場(chǎng)景概述

我們?cè)谶M(jìn)行接口開發(fā)時(shí)顿颅,客戶端需要傳入一個(gè)名為text的字段,它可能是string類型或Array<Object>類型(在TS中我們把這種關(guān)系稱之為 聯(lián)合類型 )啸澡,class-validator庫中提供了相關(guān)的校驗(yàn)注解吏口,那把他們寫在一起能否完成相關(guān)的校驗(yàn)?zāi)乇缦滤荆?/p>

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: "修復(fù)了一些bug", description: "內(nèi)容" })
  @IsString()
  content!: string;
  @ApiProperty({ example: "2022-04-20 07:52", description: "創(chuàng)建時(shí)間" })
  @IsString()
  createTime?: string;
  @ApiProperty({ example: true, description: "是否為新功能標(biāo)識(shí)" })
  @IsBoolean()
  mark?: boolean;
}

啟動(dòng)項(xiàng)目脏里,用postman測(cè)試后發(fā)現(xiàn)并不好使,傳了array類型的數(shù)據(jù)又要求是string類型呛讲,傳了string類型的數(shù)據(jù)又要求是array類型。

[圖片上傳失敗...(image-4edd50-1650497116759)]

注意:嵌套類型的對(duì)象驗(yàn)證需要使用@ValidateNested和@Type注解关面, @Type接受一個(gè)回調(diào)函數(shù)捂齐,函數(shù)內(nèi)部需要返回一個(gè)用class聲明的dto類压真。

解決方案

經(jīng)過一番求助,翻了一圈class-validator的文檔吼肥,發(fā)現(xiàn)沒有現(xiàn)成的解決方案。那么,就只能自己拿到參數(shù)搞自定義校驗(yàn)了握童。

class-transformer這個(gè)庫中姆怪,提供了Transform方法,它接受一個(gè)回調(diào)函數(shù)作為參數(shù)澡绩,回調(diào)函數(shù)中提供了一個(gè)TransformFnParams類型的參數(shù)稽揭,其中的value字段就是客戶端傳過來的參數(shù),我們只需要對(duì)其進(jìn)行校驗(yàn)即可肥卡。

[圖片上傳失敗...(image-cdc3c2-1650497116759)]

接下來溪掀,我們來看下實(shí)現(xiàn)代碼,如下所示:

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

上述代碼中步鉴,我們有一個(gè)名為checkTitleKey的校驗(yàn)函數(shù)揪胃,因?yàn)樾枰约盒r?yàn),所以就需要自己把TS的類型校驗(yàn)復(fù)刻一遍出來氛琢,實(shí)現(xiàn)代碼如下所示:

  • 如果校驗(yàn)通過直接返回value參數(shù)即可
  • 如果校驗(yàn)不通過直接使用nest內(nèi)置異常進(jìn)行拋出即可
export function checkTitleKey(
  value: string | number | Array<TextObjType> | undefined | null
): any {
  if (typeof value === "string") {
    // 不做更改喊递,直接返回
    return value;
  } else if (value instanceof Array) {
    // 不能為空數(shù)組
    if (value.length <= 0) {
      throw new BadRequestException(
        "property text cannot be an empty array",
        "Bad Request"
      );
    }
    for (let i = 0; i < value.length; i++) {
      // 校驗(yàn)數(shù)組中的對(duì)象字段
      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"
        );
      }
      // 對(duì)每個(gè)key進(jìn)行校驗(yàn)
      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的聲明也需要進(jìn)行相對(duì)應(yīng)的修改,如下所示:

  • 全部變?yōu)榭蛇x參數(shù)艺沼,參數(shù)的必傳與否已經(jīng)在校驗(yàn)函數(shù)中處理了
  • 類型全部變?yōu)閍ny
export type TextObjType = {
  content?: any;
  createTime?: any;
  mark?: any;
};

有一部分開發(fā)者可能比較迷惑册舞,不是說ts用any是可恥行為嗎,這我就要糾正下你了障般,既然它存在自然有使用場(chǎng)景调鲸。在我這個(gè)場(chǎng)景中,對(duì)象里所有key的類型校驗(yàn)都手動(dòng)處理了挽荡,如果在此處定義了它的類型藐石,在校驗(yàn)函數(shù)中就會(huì)報(bào)黃色警告,因此針對(duì)于需要手動(dòng)校驗(yàn)類型的場(chǎng)景而言定拟,使用any是最合適的于微。

結(jié)果校驗(yàn)

最后,我們針對(duì)于代碼里定義的異常規(guī)則來驗(yàn)證下其是否能正常工作青自,如下所示:

# text字段為string類型
{
    "id":"122211",
    "title":"新的標(biāo)題",
    "text":"新替換的文本內(nèi)容",
    "name":"新的名字",
    "config":"var config = {\"name\":\"aa\",\"age\":\"21\",\"title\":\"標(biāo)題測(cè)試\"}"
}
>>> 接口調(diào)用成功

# text字段為Array類型所有key都存在
{
    "id":"122211",
    "title":"新的標(biāo)題",
    "text":[{"content":"新文本","createTime":"2022-04-20","mark":false}],
    "name":"新的名字",
    "config":"var config = {\"name\":\"aa\",\"age\":\"21\",\"title\":\"標(biāo)題測(cè)試\"}"
}

>>> 接口調(diào)用成功

# text字段缺少content
{
    "id":"122211",
    "title":"新的標(biāo)題",
    "text":[{"createTime":"2022-04-20","mark":false}],
    "name":"新的名字",
    "config":"var config = {\"name\":\"aa\",\"age\":\"21\",\"title\":\"標(biāo)題測(cè)試\"}"
}
>>> 接口報(bào)錯(cuò)400:property text objects in the array must contain 'content'

# text字段為number類型
{
    "id":"122211",
    "title":"新的標(biāo)題",
    "text":19,
    "name":"新的名字",
    "config":"var config = {\"name\":\"aa\",\"age\":\"21\",\"title\":\"標(biāo)題測(cè)試\"}"
}
>>> 接口報(bào)錯(cuò)400:text must be an array or string

# text字段缺少createTime與mark
{
    "id":"122211",
    "title":"新的標(biāo)題",
    "text":[{"content":"新文本"}],
    "name":"新的名字",
    "config":"var config = {\"name\":\"aa\",\"age\":\"21\",\"title\":\"標(biāo)題測(cè)試\"}"
}
>>> 接口調(diào)用成功

如下圖所示株依,我們列舉一個(gè)text字段為數(shù)字時(shí)的報(bào)錯(cuò)截圖,運(yùn)行結(jié)果符合預(yù)期延窜,文章開頭的問題成功解決??

[圖片上傳失敗...(image-595d1-1650497116759)]

示例代碼

文中所舉代碼的完整版請(qǐng)移步:

寫在最后

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

我是神奇的程序員逆瑞,一位前端開發(fā)工程師荠藤。

如果你對(duì)我感興趣伙单,請(qǐng)移步我的個(gè)人網(wǎng)站,進(jìn)一步了解哈肖。

  • 文中如有錯(cuò)誤吻育,歡迎在評(píng)論區(qū)指正,如果這篇文章幫到了你淤井,歡迎點(diǎn)贊和關(guān)注??
  • 本文首發(fā)于神奇的程序員公眾號(hào)布疼,未經(jīng)許可禁止轉(zhuǎn)載??
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市币狠,隨后出現(xiàn)的幾起案子缎除,更是在濱河造成了極大的恐慌,老刑警劉巖总寻,帶你破解...
    沈念sama閱讀 217,907評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異梢为,居然都是意外死亡渐行,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門铸董,熙熙樓的掌柜王于貴愁眉苦臉地迎上來祟印,“玉大人,你說我怎么就攤上這事粟害≡桃洌” “怎么了?”我有些...
    開封第一講書人閱讀 164,298評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵悲幅,是天一觀的道長(zhǎng)套鹅。 經(jīng)常有香客問我,道長(zhǎng)汰具,這世上最難降的妖魔是什么卓鹿? 我笑而不...
    開封第一講書人閱讀 58,586評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮留荔,結(jié)果婚禮上吟孙,老公的妹妹穿的比我還像新娘。我一直安慰自己聚蝶,他們只是感情好杰妓,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,633評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著碘勉,像睡著了一般巷挥。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上恰聘,一...
    開封第一講書人閱讀 51,488評(píng)論 1 302
  • 那天句各,我揣著相機(jī)與錄音吸占,去河邊找鬼。 笑死凿宾,一個(gè)胖子當(dāng)著我的面吹牛矾屯,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播初厚,決...
    沈念sama閱讀 40,275評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼件蚕,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼痰腮!你這毒婦竟也來了溜族?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,176評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤寇僧,失蹤者是張志新(化名)和其女友劉穎亚情,沒想到半個(gè)月后妄痪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,619評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡楞件,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,819評(píng)論 3 336
  • 正文 我和宋清朗相戀三年衫生,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片土浸。...
    茶點(diǎn)故事閱讀 39,932評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡罪针,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出黄伊,到底是詐尸還是另有隱情泪酱,我是刑警寧澤,帶...
    沈念sama閱讀 35,655評(píng)論 5 346
  • 正文 年R本政府宣布还最,位于F島的核電站墓阀,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏拓轻。R本人自食惡果不足惜岂津,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,265評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望悦即。 院中可真熱鬧吮成,春花似錦、人聲如沸辜梳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽作瞄。三九已至茶宵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間宗挥,已是汗流浹背乌庶。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評(píng)論 1 269
  • 我被黑心中介騙來泰國打工种蝶, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人瞒大。 一個(gè)月前我還...
    沈念sama閱讀 48,095評(píng)論 3 370
  • 正文 我出身青樓螃征,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親透敌。 傳聞我的和親對(duì)象是個(gè)殘疾皇子盯滚,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,884評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容