@QueryをPipeを使って検証する場合。

ここの章ではQueryはPipeを使った検証のサンプルを提示する。

Queryデータを取得するには@Queryデコレーターを使用する。使用方法は大雑把に分けると、キー指定したもの、キー指定なしのものに分ける事ができる。

// キー指定あり
async method1(@Query('key1') key1: string) : Promise<any> {
  console.log({key1});
}
// キー指定なし
async method2(@Query() key1: any) : Promise<any> {
  console.log({JSON.stringify(key1)});
}
1
2
3
4
5
6
7
8
9

キー指定ありの場合は、@Query('キー指定' , パイプ)を指定することにより、初期値や検証などを行うことができる。

キー指定なしの場合は、any型となり、何でも自由に入ってしまい、型の安全性はないや検証はない。安全性を担保するために@Query() 変数名 : クラス名を宣言し、class-validatorを利用するためにクラスを別ファイルに宣言する。次の章でclass-validatorの使用例を記載する。
※ファイル名の拡張子を.dto.tsにしないと動作しないため(設定で変更が可能)

この章の末尾に記載しているが、Pipeを使った検証、class-validatorを利用した検証ともに、完璧ではないことを宣言しておく。

Pipeのみで値を検証する

NestJSではいくつか役立つPipeがある。下記はNestJS標準のPipe一覧。

https://docs.nestjs.com/pipes

  • ValidationPipe
  • ParseIntPipe
    • 数値を検証、取得する
  • ParseFloatPipe
    • 浮動小数点を検証、取得する
  • ParseBoolPipe
    • true/falseを検証、取得する
  • ParseArrayPipe
    • 配列を検証、取得する
  • ParseUUIDPipe
    • UUIDを検証、取得する
  • ParseEnumPipe
    • Enum(決められた文字列)かどうか検証、取得する
  • DefaultValuePipe
    • 値が指定されていない場合の初期値を設定する
  • ParseFilePipe
    • ファイルデータがどうか検証、取得する(@Queryでは仕様しない)

これだけでQuery値を検証するには役不足である。そのため、カスタムPipeを作成する。

SwaggerとQueryの書き方を下記に示す。

OpenAPI
code
省略時
省略不可能
string @ApiQuery({ name: 'key', required: true })
@Query('key' , ParseStringPipe) val: string
400 ERROR
string[] @ApiQuery({ name: 'key', required: true })
@Query('key', ParseStringPipe, ParseArrayPipe) val: string[]
400 ERROR
enum @ApiQuery({ name: 'key', enum: Object.values(EnumClass), required: true })
@Query('key' , new ParseEnumPipe(EnumClass)) val: EnumClass
400 ERROR
enum[] @ApiQuery({ name: 'key', type: [String], enum: EnumClass, isArray: true, required: true })
@Query('key' , new ParseArrayEnumPipe(EnumClass, { optional: false})) part: EnumClass[]
400 ERROR
number @ApiQuery({ name: 'key', required: true })
@Query('key', new ParseNumberPipe({ optional: false })) val: number
400 ERROR
number[] @ApiQuery({ name: 'key', type: Number, isArray: true, required: false })
@Query('key', new ParseArrayNumberPipe({ empty: false })) val: number[]
400 ERROR
省略可能
boolean @ApiQuery({ name: 'key',required: false })
@Query('key', ParseBoolPipe) val: boolean
false
string @ApiQuery({ name: 'key', required: false })
@Query('key') val: string | undefined
undefined
string[] @ApiQuery({ name: 'key', isArray: true, required: false })
@Query('key', new DefaultValuePipe([])) val: string[]
[]
enum @ApiQuery({ name: 'key', enum: Object.values(EnumClass), required: false })
@Query('key', new DefaultValuePipe(EnumClass.AAA), new ParseEnumPipe(EnumClass)) val: EnumClass
EnumClass.AAA
enum[] @ApiQuery({ name: 'key', type: [String], enum: EnumClass, isArray: true, required: false })
@Query('key', new ParseArrayEnumPipe(EnumClass, { optional: true })) val: EnumClass[]
[]
enum[] @ApiQuery({ name: 'key', type: [String], enum: EnumClass, isArray: true, required: false })
@Query('key', new DefaultValuePipe([EnumClass.AAA]), new ParseArrayEnumPipe(EnumClass, { optional: false })) val: EnumClass[]
[EnumClass.AAA]
number @ApiQuery({ name: 'key', required: false })
@Query('key', new ParseNumberPipe({ optional: true })) val: number | undefined
undefined
number[] @ApiQuery({ name: 'key', type: Number, isArray: true, required: false })
@Query('key', new ParseArrayNumberPipe({ empty: true })) val: number[]
[]
数値の範囲指定
number @ApiQuery({ name: 'key', required: false, schema: { minimum: 1, maximum: 99, exclusiveMaximum: true, exclusiveMinimum: true, default: 10 }, })
@Query('key', new ParseBetweenNumberPipe(10, 1, 99)) val: number
指定値

サンプルコード

ぐちゃぐちゃだが勘弁して欲しい。非常に面倒だったのだ。
https://github.com/mosapride/learn-nestjs/blob/main/src/endpoint/sample-request-query-pipe.controller.ts

Queryの役割と種類について

上の表で、大体のQueryに対応できるはず。上の表にした理由だが役割から考えた。

Queryの役割 1. 条件の追加

あるエンドポイントから条件に該当するデータを取得する場合。SQLならばWHERE句に該当する。

/users?nameSearch=田中の場合は、名前に田中が含まれるユーザー一覧を返す。条件の文字列は制限がない

platformsには、「pc,smartphone,tablet」が3種があるとする場合
/users?platforms=pcは、PCプラットフォームユーザー一覧を返す。上記と違う箇所は条件に該当する文字列の制限がある

/users?minimumAge=10ならば、年齢が10歳以上のユーザー一覧を返す。

/users?maximumAge=20ならば、年齢が20歳以下のユーザー一覧を返す。

Queryの役割 2. 取得データカラムの増加

あるエンドポイントから取得するデータの種類を追加する場合。SQLならばJOIN句に該当する。

/users/part=jobの場合は、ユーザー情報に加えて職業情報を追加して返す。

/users/part=job,jobHistoryこの場合も配列を考慮しておく必要がある。

データの種類を増やすため、必ず文字列には規則がある。

Queryの種類 1. 省略の可否

Queryは省略が可能な場合、不可能な場合がある。

省略が可能な場合は、Where句やJOIN句に該当する処理が不要な場合。または、初期値が設定されている場合。

省略が不可能な場合で、Queryパラメーターを渡さなかった場合は、400 ERRORを返す必要がある。

Queryの種類 2. 指定する文字列が決められている場合

検索する文字列に種別があり、指定する文字列が決められている場合がある。

Enum型を宣言し、変数値と値を同じ指定にすれば良い。

enum EnumClass {
  AAA = 'AAA',
  BBB = 'BBB',
  CCC = 'CCC',
}
1
2
3
4
5

Enum型を毛嫌いする人もいるが、使い方次第である。公開モジュールのように第三者?が使用するようなライブラリに関してはEnumを控えるべきだが、プロジェクト内で文字列の列挙型として扱う列挙型ならば問題ない。これに関してはO'Reillyの書籍に詳しく説明されている。詳細を記載すると著作権的に問題があるため省略するが良書なので購入を検討するとよい。

プログラミングTypeScript

プログラミングTypeScript ―スケールするJavaScriptアプリケーション開発 - Amazon

3章 型について(P44)に記載されている。

Queryの種類 3. 単体か配列か

Query値が単体か配列かの切り分けも必要になる。

Queryの種類 4. 範囲が決まっている場合

わかりやすい例としてはリクエストサイズを指定する場合である。resultMaxは1~100まで、省略時は10とする。などの仕様はよく見る。

再度注目して欲しいところは範囲指定の場合は、省略時のデフォルト値が必ず存在するところだ。

範囲指定があり、省略時のデフォルト値が不要(undefinedやnull)な仕様があれば教えて欲しい。

Queryの検証はPipeだけでは終了しない

残念なことに、Queryの検証は各値のチェックだけでは済まされない場合がある

例として、Youtubeの動画検索パラメータを見てみると良い。

https://developers.google.com/youtube/v3/docs/videos/list?hl=ja

Queryのフィルタとして、chart、id、myRatingのいづれか1つのみ指定する必要がある。パラメータがある条件により必須・省略可能が変わる仕様がある場合は、Pipeやclass-validationのみでは検証が不可能であるため、別途チェックするコーディングが必要となる。

この問題はOpenAPIのissueに上がっており、2015年からOpenの状態である。

@see https://github.com/OAI/OpenAPI-Specification/issues/256