TypeGraphql+Class-validatorで作成されるエラー(Argument Validation Error)をフォーマットして使いやすくする方法

#はじめに

個人的にNodeJsでGraphQLを実装する時にはTypeGraphQLを使用していますが、`@InputType`に`class-validator`を使用したバリデーションは便利な反面返ってくるエラーの形式がそのままだとフロントエンドでは使いにくいため、エラーをフォーマットして使いやすくする方法を共有します。

#TypeGraphql+class-validatorのバリデーション
TypeGraphqlでInputを実装する際はデコレータ`@InputType`を付与した上でクラスを定義し、クラス内に入力項目として使いたい値を定義していきます。その際に`class-validator`と併用することで入力項目のバリデーションができるようになり、非常に便利です。

````typescript:UserInput.ts

//例：新規ユーザーを作成する際の入力項目とバリデーションを定義
@InputType()
export class CreateUserInput {
	@Field()
	@IsNotEmpty({ message: 'ユーザー名は必須項目です！' })
	@MaxLength(15, { message: '15文字以内に収めて下さい！' })
	username: string;

	@Field()
	@IsNotEmpty({ message: 'Eメールは必須項目です！' })
	@IsEmail({}, { message: '無効なEメールの形式です！' })
	email: string;

	@Field()
	@IsNotEmpty({ message: 'パスワードは必須項目です！' })
	@MaxLength(15, { message: '15文字以内に収めて下さい！' })
	@MinLength(2, { message: '2文字以上に設定して下さい！' })
	password: string;

	@Field()
	@IsNotEmpty({ message: '確認用パスワードは必須項目です！' })
	@Match('password', { message: 'パスワードが一致しません！' })
	confirmPassword: string;
}

````

例では新規ユーザーを作成する時の入力項目とバリデーション項目を定義しています。
こうすることで、入力項目に対してどのようなバリデーションが適用されるのかが一目瞭然になり、コードの見通しが非常に良くなります。
では実際にどのようにエラーが返ってくるか確認しましょう。

````json
{
  "errors": [
    {
      "message": "Argument Validation Error",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "createUser"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "exception": {
          "validationErrors": [
            {
              "target": {
                "username": "",
                "email": "",
                "password": "1234",
                "confirmPassword": "1234"
              },
              "value": "",
              "property": "username",
              "children": [],
              "constraints": {
                "isNotEmpty": "ユーザー名は必須項目です！"
              }
            },
            {
              "target": {
                "username": "",
                "email": "",
                "password": "1234",
                "confirmPassword": "1234"
              },
              "value": "",
              "property": "email",
              "children": [],
              "constraints": {
                "isEmail": "無効なEメールの形式です！",
                "isNotEmpty": "Eメールは必須項目です！"
              }
            }
          ],
・・・・//以下省略
}
````

このようにとても長いエラーオブジェクトが返されてきました・・・・
このエラーオブジェクトはそのままでは使えなさそうです。
理由としては
１・エラー形式が「INTERNAL_SERVER_ERROR」となっていること（GraphQLErrorの形式ではない）
２・`varidationErrors.constraints`オブジェクトのkeyがclass-validatorのバリデータの名称になっているため、複数ヶ所同じ名前のエラーが発生したらどの入力項目で発生したエラーなのか判定するのはこのままでは難しい（例では`isNotEmpty`が複数で発生しています）
なので、エラーをフロントエンドで扱いやすくなるようにフォーマットする必要がありそうです。
↑のエラーの理想としてはこんな形式ではないでしょうか↓。

````javascript
//理想のエラーオブジェクト(例：入力項目「username」「email」が空白だった場合)
"errors": [
    "validationErrors": [
         {"username": "ユーザー名は必須項目です！"},
         {"email": "Eメールは必須項目です！"}
    ]
]
````
このようにすると、どの項目でどのようなエラーが発生したのか一目瞭然となり、フロントエンドでも扱いやすいオブジェクトとなるのではないでしょうか。
では`ApolloServer`の機能を使ってこのエラーをフォーマットしてみましょう。

#ApolloServerの`formatError`関数を使用してエラーをフォーマットする

ApolloServerには`formatError`関数が用意されており、この関数を使用することでエラーをフォーマットすることができます。

````typescript:server.ts
const apolloServer = new ApolloServer({
		schema: await buildSchema({
			resolvers: [UserResolver],
		}),
		formatError: (err: GraphQLError) => {
			return err;
		},
	});

````

`formatError`で受け取ったエラー(↑の例でいうと「err」)にはリゾルバーがスローしたエラーである`originalError`が含まれています。
この`originalError`のインスタンスの種類によってエラー処理を変えることや、エラーを使いやすい形にフォーマットすることが可能になります。

````typescript:server.ts
const apolloServer = new ApolloServer({
        schema: await buildSchema({
            resolvers: [UserResolver],
        }),
        formatError: (err: GraphQLError) => {
			if (err.originalError instanceof ArgumentValidationError) {
				const errorMessage = err.extensions?.exception.validationErrors;
                //validationErrors.propertyのvalueをkeyに、validationErrors.constraintsの　　　　　
                //valueをvalueに持つ新たな配列を作成
				const object = errorMessage.map(
					(e: { property: string; constraints: string }) => ({
						[e.property]: Object.values(e.constraints),
					})
				);
                //変数「object」は配列のため、reduceで値を取り出しオブジェクトにする
				const formattedErrors = object.reduce(
					(result: string[], current: string[]) => {
						let key: any = Object.keys(current);
						result[key] = current[key];
						return result;
					}
				);
				throw new UserInputError('Errors', { formattedErrors });
		       }
             return err;
        },
    });

````
先程のエラーをフォーマットするコードはこのようになります。
まず、`class-validator`でスローされたエラーは`ArgumentValidationError`インスタンスが含まれているため、その場合の処理を`if`文で分岐させます。
その後`validationErrors.property`のvalueをkeyに、`validationErrors.constraints`のvalueをvalueに持つ新たな配列を作成し、さらに`reduce`により配列→オブジェクトへと変換しています。
最後に作成したオブジェクトを`UserInputError`でスローすれば完成です。
フォーマットされたエラーがどのようになったか確認してみましょう。

````json
{
  "errors": [
    {
      "message": "Errors",
      "extensions": {
        "formattedErrors": {
          "username": [
            "ユーザー名は必須項目です！"
          ],
          "email": [
            "無効なEメールの形式です！",
            "Eメールは必須項目です！"
          ]
        },
        "code": "BAD_USER_INPUT",
  //以下省略
}

````

大分扱いやすいデータになったのではないかと思います。
フロントエンドでこのエラーを使用する時は以下のようにすれば取得できます。

````typescript
//例:React+ApolloClientでフォーマットしたエラーを取得する場合

const [error, setError] = useState<Record<string, string>>({});

const [createUser, { loading }] = useMutation(CREATE_USER,{
		onError: (err) =>
			setError(err.graphQLErrors[0].extensions?.formattedErrors),
		onCompleted: () => router.push('/login'),
	});

````

#まとめ

`ApolloServer`の`formatError`を使用すれば扱いやすいデータへとエラーをフォーマットできることがわかったと思います。
今後も新しいことが分かり次第随時記事を更新していこうと思います。