mutliple-validation-errors-in-aws-appsync

なにこれ

AppSyncのリゾルバーでDynamoDBのデータを更新する場合、更新処理の前に入力チェックは必ず実施すると思います。 この時、入力チェックエラーを見つけた時点で1つのエラーメッセージを返すより、 すべてのチェック実施後にまとめてエラーメッセージを返してあげたほうが、AppSyncの呼び出し側としては助かります。 ただ、このやり方は調べてもあまり出てきませんでした。そのため本記事で紹介します。

結論

たとえば、人(ID、名前、組織名)を更新する場合

人(ID、名前、組織名)を更新するmutation
mutation {
  updatePerson (input: {
    id
    name
    organizationName
  }){
    person {
      id
      name
      organizationName
    }
  }
}

以下のようにリクエストマッピングテンプレートのVTLを記述します。

複数エラーメッセージを返すVTL
#if($util.isNullOrBlank($ctx.args.input.name))
  $util.appendError("名前は30文字以下で入力してください。", "name", null, $ctx.args.input.name)
#end 
#if($ctx.args.input.name.length() > 30))
  $util.appendError("名前は30文字以下で入力してください。", "name", null, $ctx.args.input.name)
#end

#if($util.isNullOrBlank($ctx.args.input.organizationName))
  $util.appendError("組織名は30文字以下で入力してください。", "organizationName", null, $ctx.args.input.organizationName)
#end 
#if($ctx.args.input.organizationName.length() > 30))
  $util.appendError("組織名は30文字以下で入力してください。", "organizationName", null, $ctx.args.input.organizationName)
#end


#if($ctx.outErrors.size() > 0)
  #return($ctx.outErrors)
#end

{
  "operation" : "PutItem",
  "key" : {
    "id" : { "S" : "${ctx.args.input.id}" }
  },
  "condition" : {
    "expression" : "attribute_exists(id)"
  },
  "attributeValues" : $util.dynamodb.toMapValuesJson($ctx.args.input)
}

実際どのようなレスポンスが返ってくるか

以下のmutationを発行してみると...

複数の入力チェックエラーになるようなmutation
mutation {
  updatePerson(input: {
    id: "1234"
    name: "あきらかに文字数オーバーの名前あきらかに文字数オーバーの名前あきらかに文字数オーバーの名前あきらかに文字数オーバーの名前"
    organizationName: "あきらかに文字数オーバーの組織名あきらかに文字数オーバーの組織名あきらかに文字数オーバーの組織名あきらかに文字数オーバーの組織名"
  }) {
    person {
      id
      name
      organizationName
    }
  }  
}

複数エラーメッセージを含むレスポンスが返ってきます!

複数エラーメッセージを含むレスポンス
{
  "data": {
    "updatePerson": {
      "person": null
    }
  },
  "errors": [
    {
      "path": [
        "updatePerson"
      ],
      "data": null,
      "errorType": "name",
      "errorInfo": "あきらかに文字数オーバーの名前あきらかに文字数オーバーの名前あきらかに文字数オーバーの名前あきらかに文字数オーバーの名前",
      "locations": [
        {
          "line": 2,
          "column": 3,
          "sourceName": null
        }
      ],
      "message": "名前は30文字以下で入力してください。"
    },
    {
      "path": [
        "updatePerson"
      ],
      "data": null,
      "errorType": "organizationName",
      "errorInfo": "あきらかに文字数オーバーの組織名あきらかに文字数オーバーの組織名あきらかに文字数オーバーの組織名あきらかに文字数オーバーの組織名",
      "locations": [
        {
          "line": 2,
          "column": 3,
          "sourceName": null
        }
      ],
      "message": "組織名は30文字以下で入力してください。"
    }
  ]
}

結論に至るまでの試行錯誤

AppSyncの公式ドキュメントを見ると、$util.appendErrorが使えそうだと思ったのですが、いざ書こうとすると、以下2点につまづきました。

  1. 「1つ以上入力チェックエラーがある」ことをどうやって判定するのか
  2. 複数エラー情報をどうやってレスポンスとして返すのか

1. 「1つ以上入力チェックエラーがある」ことをどうやって判定するのか

最初に考えたのは、#set($valid=true)のようにフラグで判断する方法です。

validフラグで判定する方法
## 事前にフラグを定義しておく
#set($valid = true)

#if($util.isNullOrBlank($ctx.args.input.name))
  ## チェックNGの場合はフラグを更新
  #set($valid=false)
  $util.appendError("名前は30文字以下で入力してください。", "name", null, $ctx.args.input.name)
#end 
#if($ctx.args.input.name.length() > 30))
  #set($valid=false)
  $util.appendError("名前は30文字以下で入力してください。", "name", null, $ctx.args.input.name)
#end

#if($util.isNullOrBlank($ctx.args.input.organizationName))
  #set($valid=false)
  $util.appendError("組織名は30文字以下で入力してください。", "organizationName", null, $ctx.args.input.organizationName)
#end 
#if($ctx.args.input.organizationName.length() > 30))
  #set($valid=false)
  $util.appendError("組織名は30文字以下で入力してください。", "organizationName", null, $ctx.args.input.organizationName)
#end


#if($valid == false)
  ## 異常時の処理
#end

ただこれだと記述が冗長です。
appendErrorしたエラーはどこに格納されているのか?
$util.error($util.toJson($context))をして$contextの中身を確認してみると、 どうやら$context.outErrorsに格納されているということがわかりました。
それを踏まえて処理を以下のようにリファクタリングしました。

validフラグを使わずに判定する方法
#if($util.isNullOrBlank($ctx.args.input.name))
  $util.appendError("名前は30文字以下で入力してください。", "name", null, $ctx.args.input.name)
#end 
#if($ctx.args.input.name.length() > 30))
  $util.appendError("名前は30文字以下で入力してください。", "name", null, $ctx.args.input.name)
#end

#if($util.isNullOrBlank($ctx.args.input.organizationName))
  $util.appendError("組織名は30文字以下で入力してください。", "organizationName", null, $ctx.args.input.organizationName)
#end 
#if($ctx.args.input.organizationName.length() > 30))
  $util.appendError("組織名は30文字以下で入力してください。", "organizationName", null, $ctx.args.input.organizationName)
#end


#if($ctx.outErrors.size() > 0)
  ## 異常時の処理
#end

2. 複数エラー情報をどうやってレスポンスとして返すのか

$util.appendErrorはエラーを追加してくれるだけです。$util.errorは1つのエラー情報しか返してくれません。
試行錯誤した結果、#returnで返せそうだということがわかりました。
1の調査結果により$util.appendErrorで詰めたエラー情報は$ctx.outErrorsに格納されていることがわかっていたので、 それら踏まえて以下のようにしました。

エラー時に複数エラーメッセージを返すにはreturnを使う
  #return($ctx.outErrors)

まとめ

VLTは結構めんどくさいですが、ちょっとずつ慣れてきました。 ググってもAppSyncのVTL系の情報は、まだあまり見かけません。そのため今後もノウハウがたまったら記事を書きます🍅