リゾルバーでAppSyncの中を開発する

# リゾルバーでAppSyncの中を開発する
## はじめに
AmplifyでAppSyncをセットアップする際、認証タイプでCognitoを選択し、サンプルスキーマとしてFGAC(=Fine Grained Access Control = きめ細かなアクセス制御)の雛形を選択すると、Cognitoユーザー名が勝手にDynamoDBのowner列に設定されますよね。(で、認証済みユーザーでフィルタして取ってこれるようになる。)

これ、不思議だったんです。
AppSyncのMutation(Create)で、引数に指定してないのにownerの他にもcreatedAtやupdatedAtなどが勝手に入ってくる。
![Screenshot 2020-03-06 at 22.28.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/84460d6a-5830-4b3c-d065-38dcf8c61756.png)
contentしか指定してないのに、
![Screenshot 2020-03-06 at 22.29.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/b6f62e60-97ee-ba4e-3c3a-3c7fb9d61ce1.png)
DynamoDBには色々入ってる。

なにAppSyncそんなことできるんけ。どうやるん？んん？？ってな具合で調べてみたところ、リゾルバー(Resolver)なるもので記述するということで。

以前、[S3ファイルやDynamoDBレコードの自動削除](https://qiita.com/w2or3w/items/045a1facd15291d7a51a)という記事を書きました。
ここではDynamoDBのレコードを30分後に削除するため、AppSyncを呼び出すクライアント側で30分後のエポック秒を求めて渡してたのですが、正直面倒だな、微妙だな、他にいい手段ないものか、とモヤモヤを感じてました。

あぁ、コレだと。AppSyncのリゾルバーに書いとけばええやんけと。
案の定いい塩梅に実現できましたので、忘れないうちに記事化。

## AppSyncのリゾルバー(Resolver)とは
AppSyncのリゾルバーは、GraphQLのスキーマで受け付けたリクエストに対し実際にデータ操作を行う部分で、Apache Velocity Template Language(VTL)というプログラミング言語で記述することができます。
AppSyncのサーバーサイドの機能(関数)ということになりますね。

例えばDynamoDBをデータソースとするシンプルなAppSyncをセットアップしたとして、そうするとリゾルバーも既に作成済みの状態で存在しています。
AppSyncのAPIを呼ぶとDynamoDBに対してデータ操作(作成/更新/削除/取得)ができますが、間にいるのがリゾルバー。そしてそのリゾルバーは編集して独自実装を加えられると。

AppSyncとはリゾルバーのAPIであり、リゾルバーとはAppSyncのDAOである。(あってる？)
リゾルバーを制するものがAppSyncを制すると言っても過言ではないでしょう。(あってる？？)


さて名言がでたところで、ここからは以前書いた [AppSyncをフロントエンドとバックエンドで利用する](https://qiita.com/w2or3w/items/70f6b6d8d13b60afaee5) という記事で作成したAppSyncのリゾルバーを編集してゆく形で進めようと思います。


## Mutation Create用のリゾルバーを編集する
各種データ操作ごとにリゾルバーは存在しますが、ここではMutationのCreateに対してリゾルバーを編集してゆきます。

AWSコンソール > AppSync > 目的のAPI > スキーマ
Schemaの右側にあるResolversのリストから、編集したいリゾルバーを探します。
今回はcreateSampleAppsyncTableフィールドのリゾルバーを編集します。フィールドの右側にリゾルバーを編集するためのリンクがあるのですが、ずいぶんと見つけづらいところにありますよね。
![Screenshot 2020-03-07 at 09.45.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/ecef63de-5739-27ad-9e1f-c66f7e65cc5e.png)
ResolversのMutationのところで、タッチパッドに指を2本あてがい、スッと右にスライドするとそのリンクは姿を表します。(フィールド名称が短かったり、ディスプレイの解像度が大きければスライドしなくても見えるのですが、、。)
![ink (4).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/58b38ea2-cf92-da27-7311-285e23fd412e.png)
これがかの有名な「ダークサイドへのスライドドア」ですね。
ようこそ、AppSyncの裏の世界へ。Hello, AppSync's real world !

### 既存の実装を確認する
おめでとうございます。リゾルバーの編集画面にたどり着くことができました。
![Screenshot 2020-03-07 at 10.01.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/b1b279cd-1901-92f8-d34b-177b2a98d85d.png)

「リクエストマッピングテンプレートを設定します」のところにある既存の実装を見てみましょう。

```json

{
  "version": "2017-02-28",
  "operation": "PutItem",
  "key": {
    "group": $util.dynamodb.toDynamoDBJson($ctx.args.input.group),
    "path": $util.dynamodb.toDynamoDBJson($ctx.args.input.path),
  },
  "attributeValues": $util.dynamodb.toMapValuesJson($ctx.args.input),
  "condition": {
    "expression": "attribute_not_exists(#group) AND attribute_not_exists(#path)",
    "expressionNames": {
      "#group": "group",
      "#path": "path",
    },
  },
}
```
実装というかJSONですね。
これはリゾルバーのリクエストテンプレートと呼ばれるものだそうです。
`AWS AppSync はこのテンプレートを使用して、DynamoDB との通信とデータの取得 (または必要に応じてその他の処理) を行うための指示を生成します。`
と、 [リゾルバーのマッピングテンプレートの概要](https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/resolver-mapping-template-reference-overview.html) に書いてありました。

groupとpathをkey、引数(args.input)にある全ての項目をattributeValuesとしてPutItemをoperationする、というようなことが書いてありますね。
conditionのところにはgroupとpathは必須だという条件が書かれています。

このJSONは特に編集する必要はないと思います。
引数で渡していない情報を設定する場合、リゾルバの上部にて引数(args.input)に任意のkey-valueをputします。そうすると、このリクエストテンプレートのattributeValuesに指定され、DynamoDBにも値が設定されるわけです。
引数で渡された値を書き換えたり、バリデーションする用途としてもリゾルバーは使えますね。

### リゾルバーに実装を加える
ということで、いよいよリゾルバーを更新してゆきましょう。
Apache Velocity Template Language(VTL)というプログラミング言語で記述します。言語仕様についてはここでは触れません。考えられるユースケースごとにサンプルコードを書いていきます。

#### updatedAtフィールドに現在時刻を設定する
```js
$util.qr($context.args.input.put("updatedAt", $util.time.nowISO8601()))
```
引数パラメータにupdatedAtを指定してもしなくても、現在時刻で上書きします。
もし引数パラメータに指定されたらそれを優先したい場合以下のように書きます。

```js
$util.qr($context.args.input.put("updatedAt", $util.defaultIfNull($ctx.args.input.updatedAt, $util.time.nowISO8601())))
```

#### deleteTime(TTL)フィールドに30分後のエポック秒を設定する
```js
#set( $deleteTime = $util.time.nowEpochSeconds() + 60 * 30)
$util.qr($context.args.input.put("deleteTime", $util.defaultIfNull($ctx.args.input.deleteTime, $deleteTime)))
```
これが「はじめに」に書いていた、やりたかったやつですね。
クライアント側で計算して渡すという手間が省けていい感じです。

#### ownerフィールドにCognitoユーザーIDを設定する
```js
## [Start] Determine request authentication mode **
#if( $util.isNullOrEmpty($authMode) && !$util.isNull($ctx.identity) && !$util.isNull($ctx.identity.sub) && !$util.isNull($ctx.identity.issuer) && !$util.isNull($ctx.identity.username) && !$util.isNull($ctx.identity.claims) && !$util.isNull($ctx.identity.sourceIp) && !$util.isNull($ctx.identity.defaultAuthStrategy) )
  #set( $authMode = "userPools" )
#end
## [End] Determine request authentication mode **
## [Start] Check authMode and execute owner/group checks **
#if( $authMode == "userPools" )
  ## No Static Group Authorization Rules **

  ## No Dynamic Group Authorization Rules **

  ## [Start] Owner Authorization Checks **
  #set( $isOwnerAuthorized = false )
  ## Authorization rule: { allow: owner, ownerField: "owner", identityClaim: "cognito:username" } **
  #set( $allowedOwners0 = $util.defaultIfNull($ctx.args.input.owner, null) )
  #set( $identityValue = $util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____")) )
  #if( $util.isList($allowedOwners0) )
    #foreach( $allowedOwner in $allowedOwners0 )
      #if( $allowedOwner == $identityValue )
        #set( $isOwnerAuthorized = true )
      #end
    #end
  #end
  #if( $util.isString($allowedOwners0) )
    #if( $allowedOwners0 == $identityValue )
      #set( $isOwnerAuthorized = true )
    #end
  #end
  #if( $util.isNull($allowedOwners0) && (! $ctx.args.input.containsKey("owner")) )
    $util.qr($ctx.args.input.put("owner", $identityValue))
    #set( $isOwnerAuthorized = true )
  #end
  ## [End] Owner Authorization Checks **

  ## [Start] Throw if unauthorized **
  #if( !($isStaticGroupAuthorized == true || $isDynamicGroupAuthorized == true || $isOwnerAuthorized == true) )
    $util.unauthorized()
  #end
  ## [End] Throw if unauthorized **
#end
## [End] Check authMode and execute owner/group checks **
```
※Amplifyが生成したコードのコピペです。((^_^;))
Cognito認証していた場合はユーザーIDが設定されますし、そうでない場合(IAM認証など)値は設定されません。

#### 入力値のバリデーション(文字列長)
```js
#if ($ctx.args.input.group.length() > 5)
  $util.appendError("group.length over 5 charactors.", "group", null, $ctx.args.input.group)
#end
```

#### 入力値のバリデーション(正規表現)
```js
#if (!$util.matches("^[a-zA-Z]+[a-zA-Z]", $ctx.args.input.group))
  $util.appendError("regex error (alphabet)", "group", null, $ctx.args.input.group)
#end
#if (!$util.matches("^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$", $ctx.args.input.mail))
  $util.appendError("regex error (mail)", "mail", null, $ctx.args.input.mail)
#end
```
必須かどうかはGraphQLのスキーマに「!」で指定できますが、文字列長や正規表現(Regular expression)によるバリデーションもこのようにして実現できますね。

## あとがき
AmplifyでサクッとAppSyncを構築してフロントからチョロチョロ使ってたとき完全に理解したと思ってましたが、やっとチョットワカルようになってきた気がします。や、嘘です。なんもわからん。まだまだ先は長そうです。

データソースには、DynamoDBの他にも Elasticsearch, RDS, Lambda, HTTPエンドポイント などを選択することができます。
データソースを複数登録しておいて、リクエストの種類によって参照先を変えたりすることができるそうです。
様々なデータソースを組み合わせて利用することで、その可能性は無限に広がりますね。

データソース、DynamoDBだけじゃ退屈、、。って言われてしまう日も近そうです。


## 参考資料

* [AppSync 開発者ガイド(日本語)](https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/appsync-dg%20.pdf)
* [リゾルバーのマッピングテンプレートの概要](https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/resolver-mapping-template-reference-overview.html)
* [Resolver Mapping Template Context Reference](https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html)
* [Resolver Mapping Template Utility Reference](https://docs.aws.amazon.com/appsync/latest/devguide/resolver-util-reference.html)