AppSync+Serverless Frameworkでoperationを追加する一案

## はじめに

Serverless Framework+AppSync各種プラグインによる開発を進めているのですが、一つのoperationフィールドを追加するだけで割と多くの操作が必要になってくることに気付きました。
個人的に苦手なマッピングテンプレートに工数を割くのが嫌なので、マッピングテンプレートは可能な限り避けて、JavaScript/TypeScript側で操作できる方法をご紹介します。

## 必要なもの

Serverless Frameworkテンプレートとして以下のものを使わせていただきました。

[[daisuke-awaji/serverless-appsync-offline-typescript-template](https://github.com/daisuke-awaji/serverless-appsync-offline-typescript-template)]
解説: [Effective AppSync 〜 Serverless Framework を使用した AppSync の実践的な開発方法とテスト戦略 〜](https://qiita.com/G-awa/items/095faa9a94da09bc3ed5)



また事前に以下もインストールが必要です。

- JDK(DynamoDB-localに必要)
- Serverless Framework



## GraphQL operation

Operationというのは、API Gatewayでいうリソースに相当する(と思う)ものです。
たとえばTodoリストを登録したり表示したりするAPIを作ろうとするとき、Todoのリスト一覧を返してくれる「listTodo」のようなQueryフィールドが必要になります。このフィールドの追加、および実際に処理を実行するLambda関数との紐付けるは、すべてServerless Frameworkの設定ファイル`serverless.yml`に記述することになります

一例として以下のようになります。

```yaml
custom:
  appSync:
    # ローカルではこの認証方式でないと動かない……
    authenticationType: AMAZON_COGNITO_USER_POOLS
    userPoolConfig:
      awsRegion: ap-northeast-1
      userPoolId: ap-northeast-1_xxxxx
      defaultAction: ALLOW
    mappingTemplates:
      - field: {フィールド名}
        datasource: {dataSources.nameで定義された名前}
        request: default.request.vtl
        response: default.response.vtl
    dataSources:
      - type: AWS_Lambda
        name: {dataSources名}
        description: Lambda
        config:
          functionName: {Lambda関数名}
          iamRoleStatements:
            - Effect: "Allow"
              Action:
                - "lambda:invokeFunction"
              Resource:
                - "*"
functions:
  {Lambda関数名}:
    handler: src/handler.main
```

以上のうち、フィールドを増やすには`mappingTemplates`という箇所と、Lambda関数のファイルである`src/handler.main`を変更します。

### Queryフィールド追加の一例

例えばTodoのIDを指定して特定Todoだけを抽出するQueryフィールド`getTodoById(id: String)`を追加したい、という場合を考えてみます。
まずスキーマを定義します。定義は`schema.graphql`のQueryタイプに定義を追加します。

```
type Todo {
  id: ID!
  name: String!
  completed: Boolean!
}

type Query {
  getTodoById(id: String!): Todo
}
```



次に、serverless.ymlのmappingTemplatesに追加をします。dataSources名はds_mainとしています。

```yaml
    mappingTemplates:
      - field: getTodoById
        datasource: ds_main
        request: default.request.vtl
        response: default.response.vtl
    dataSources:
      - type: AWS_LAMBDA
        name: ds_main
        description: Lambda DataSource for application
        config:
          functionName: dsMain
          iamRoleStatements:
            - Effect: "Allow"
              Action:
                - "lambda:invokeFunction"
              Resource:
                - "*"
                
functions:
  dsMain:
    handler: src/handler.handler
```



ハンドラー自体は、`src/handler.ts`として保存しています。

あとはLambda関数のコードで、`event.info.fieldName`でQueryフィールド名`getTodoById`を取得できるので、switch構文などで呼び出すTypeScriptコードを呼び出す、ということができます。

src/handler.ts

```javascript
import getTodoById from './getTodoById';

exports.handler = async(event, _context, _callback)=> {
    switch (event.info.fieldName) {
        case "getTodoById":
            return await getTodoById(event.arguments.id);
        default:
            return null;
    }
}
```

上記コードで呼び出されるgetTodoById.tsの中身は例えば以下のような感じです。

```typescript
const AWS = require('aws-sdk');
AWS.config.update({region:'ap-northeast-1'});
const docClient = new AWS.DynamoDB.DocumentClient();


async function getTodoById(id: String) {
    const params = {
        TableName: process.env.TODO_TABLE,
        Key: { id }
    }
    try {
        const { Item } = await docClient.get(params).promise()
        return Item
    } catch (err) {
        console.log('DynamoDB error: ', err)
    }
}

export default getTodoById
```


### マッピングテンプレート

AppSyncではリゾルバをマッピングテンプレートとして書きます
マッピングテンプレートはVTLで書きますが、個人的にはこの言語をいじるのは避けたいので適当なファイル名`default.request.vtl`などを作ってこの2つだけを使用することにします。

mapping-templates/default.request.vtl

```vtl
{
    "version" : "2017-02-28",
    "operation": "Invoke",
    "payload": $utils.toJson($context)
}
```



またレスポンス用のマッピングテンプレートは以下の通りです。

mapping-templates/default.response.vtl

```vtl
$util.toJson($context.result)
```



これで一つのフィールドを追加することができます。
マッピングテンプレートは使い回すので、基本的には

- schema.graphqlにフィールドを追加
- serverless.ymlにフィールドを追加
- Lambda用ファイルを追加・修正

の3つを行う必要があります。

## 終わりに
マッピングテンプレートを書くべきかどうかは開発規模によると思うので、適宜よい方法を見つけることをおすすめします。