AWSCDK に AppSync の High-level constructs が来たぞ！

## はじめに

タイトルに書いたとおり AWSCDK 1.19.0 で AppSync の High-level constructs がきて嬉しくなっちゃったので書きます！

## AWSCDKとは

CloudFormaiton(以下CFｎ)のテンプレートをTypeScriptやPythonといったプログラミング言語で書くことができるフレームワークです。
詳しくは[公式サイト](https://aws.amazon.com/jp/cdk/)を見てください。

## AWS AppSyncとは

AWSでGraphQLを使うならAppSync！って感じのサービスです。
GraphQLのDataSourceにDynamoDBやLambdaを指定することができ、GraphQLのAPIから操作することができます。
詳しくは[公式サイト](https://aws.amazon.com/jp/appsync/)を見てください。

## 書いていくぞ！

今までAWSCDKでAppSyncを書こうと思うと `Cfnxxxx` といったLow-level constructsで書かなければいけませんでした。
そのため必要なプロパティをすべて指定しないといけなかったりと手間がかかりました…
今回のアップデートでHigh-level constructsが来たので、どういう風に書き方が変わったのか書いていきます。

GraphQLのスキーマとなるgraphqlファイル

```graphql:schema.graphql
type Post {
  id: String,
  title: String,
  create_time: String
}
input InputPost {
  id: String
  title: String,
  create_time: String
}
type Query {
  all: [Post]
  query(id: String!, start: String!, end: String!): [Post]
}
type Mutation {
  save(input: InputPost!): Post
  delete(id: ID!): Post
}
type Schema {
  query: Query
  mutation: Mutation
}
```

Stackとなるソースコード

```ts:stack.ts
import cdk = require("@aws-cdk/core")
import { join } from "path"

import { UserPool, SignInType } from "@aws-cdk/aws-cognito"
import {
  GraphQLApi,
  FieldLogLevel,
  UserPoolDefaultAction,
  MappingTemplate
} from "@aws-cdk/aws-appsync"
import {
  TableProps,
  AttributeType,
  BillingMode,
  Table
} from "@aws-cdk/aws-dynamodb"

export class AppSyncStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    const userPool = new UserPool(this, "UserPool", {
      userPoolName: "DemoAPIUserPool",
      signInType: SignInType.USERNAME
    })

    const table = new Table(this, "CDKPostTable", {
      tableName: "CDKPostTable",
      partitionKey: {
        name: "id",
        type: AttributeType.STRING
      },
      sortKey: {
        name: "create_time",
        type: AttributeType.STRING
      },
      billingMode: BillingMode.PROVISIONED,
      readCapacity: 1,
      writeCapacity: 1
    })

    const api = new GraphQLApi(this, "PostAPI", {
      name: "PostAPI",
      logConfig: {
        fieldLogLevel: FieldLogLevel.ALL
      },
      userPoolConfig: {
        userPool,
        defaultAction: UserPoolDefaultAction.ALLOW
      },
      schemaDefinitionFile: join(__dirname, "schema.graphql")
    })

    const datasource = api.addDynamoDbDataSource("PostAPIDataSource", "", table)

    datasource.createResolver({
      typeName: "Query",
      fieldName: "all",
      requestMappingTemplate: MappingTemplate.dynamoDbScanTable(),
      responseMappingTemplate: MappingTemplate.dynamoDbResultList()
    })
    datasource.createResolver({
      typeName: "Mutation",
      fieldName: "save",
      requestMappingTemplate: MappingTemplate.dynamoDbPutItem(
        PrimaryKey.partition("id").is("input.id"),
        Values.projecting("input")
      ),
      responseMappingTemplate: MappingTemplate.dynamoDbResultItem()
    })
    datasource.createResolver({
      typeName: "Mutation",
      fieldName: "delete",
      requestMappingTemplate: MappingTemplate.dynamoDbDeleteItem("id", "id"),
      responseMappingTemplate: MappingTemplate.dynamoDbResultItem()
    })

    const queryMappingTemplate = `
      {
        "version": "2017-02-28",
        "operation": "Query",
        "query": {
          "expression": "#id = :id AND #createTime BETWEEN :start AND :end",
          "expressionNames": {
            "#id": "id"
            "#createTime": "create_time"
          },
          "expressionValues": {
            ":id": $util.dynamodb.toDynamoDBJson($ctx.args.id),
            ":start": $util.dynamodb.toDynamoDBJson($ctx.args.start),
            ":end": $util.dynamodb.toDynamoDBJson($ctx.args.end)
          }
        }
      }
    `
    datasource.createResolver({
      typeName: "Query",
      fieldName: "query",
      requestMappingTemplate: MappingTemplate.fromString(queryMappingTemplate),
      responseMappingTemplate: MappingTemplate.dynamoDbResultList()
    })
  }
}
```

## 説明していく

スタックとなるTSファイルの中身を説明していきます。
今回の例ではGraphQLでDynamoDBにデータを読み書きするAppSyncを定義しています。
またその認証にCognitoを使っています。

### AppSync以外のリソース定義

CognitoとDynamoDBを定義します。

```ts
// CognitoのUserPool定義
const userPool = new UserPool(this, "UserPool", {
  userPoolName: "DemoAPIUserPool",
  signInType: SignInType.USERNAME
})

// DynamoDBのTable定義
const table = new Table(this, "CDKPostTable", {
  tableName: "CDKPostTable",
  partitionKey: {
    name: "id",
    type: AttributeType.STRING
  },
  sortKey: {
    name: "create_time",
    type: AttributeType.STRING
  },
  billingMode: BillingMode.PROVISIONED,
  readCapacity: 1,
  writeCapacity: 1
})
```

### AppSync本体の定義

AppSync本体の定義をします。

| propaty | description |
|:--|:--|
|name|API名|
|logConfig|残すログのレベル|
|userPoolConfig|CognitoUserPoolの情報|
|schemaDefinitionFile|GraphQLのSchemaファイルのパス|

```ts
const api = new GraphQLApi(this, "PostAPI", {
  name: "PostAPI",
  logConfig: {
    fieldLogLevel: FieldLogLevel.ALL
  },
  userPoolConfig: {
    userPool,
    defaultAction: UserPoolDefaultAction.ALLOW
  },
  schemaDefinitionFile: join(__dirname, "schema.graphql")
})
```

### DataSource定義

AppSyncのDataSourceを定義します。
先程定義した `GraphQLAApi` に `addDynamoDbDataSource()` というメソッドが生えているので、それを使ってDataSourceを定義します。
第1引数にDataSource名、第2引数に説明、第3引数にDynamoDBのTableを渡します。

```ts
const datasource = api.addDynamoDbDataSource("PostAPIDataSource", "", table)
```

### Resolver定義

GraphQLの各Query/MutationにResolverを定義します。
先程定義したDataSourceに `createResolver()` というメソッドが生えているので、それを使ってResolverを定義します。

| propaty | description |
|:--|:--|
|typeName|QueryなのかMutationなのか|
|fieldName|GraphQLのフィールド名|
|requestMappingTemplate|リクエスト時のマッピングテンプレート|
|responseMappingTemplate|レスポンス時のマッピングテンプレート|

```ts
datasource.createResolver({
  typeName: "Query",
  fieldName: "all",
  requestMappingTemplate: MappingTemplate.dynamoDbScanTable(),
  responseMappingTemplate: MappingTemplate.dynamoDbResultList()
})
```

また `MappingTemplate` にDynamoDBを操作するときのテンプレートを生成してくれるメソッドがあります。
現時点では Scan / GetItem / PutItem / DeleteItem が用意されています。

```ts
MappingTemplate.dynamoDbScanTable()
MappingTemplate.dynamoDbGetItem()
MappingTemplate.dynamoDbPutItem()
MappingTemplate.dynamoDbDeleteItem()
```

ここで気づく人は気づくと思うんですが、 Query がないんですよね…
そういう場合はMappingTemplateを自分で書く必要があります。
自分でMappingTemplateを書いた場合は `MappingTemplate.fromString()` を使って定義します。
あくまでも先程紹介したのは便利関数なので、もちろんこうやって自分で書いて定義することもできます。

```ts
const queryMappingTemplate = `
  {
    "version": "2017-02-28",
    "operation": "Query",
    "query": {
      "expression": "#id = :id AND #createTime BETWEEN :start AND :end",
      "expressionNames": {
        "#id": "id"
        "#createTime": "create_time"
      },
      "expressionValues": {
        ":id": $util.dynamodb.toDynamoDBJson($ctx.args.id),
        ":start": $util.dynamodb.toDynamoDBJson($ctx.args.start),
        ":end": $util.dynamodb.toDynamoDBJson($ctx.args.end)
      }
    }
  }
`
datasource.createResolver({
  typeName: "Query",
  fieldName: "query",
  requestMappingTemplate: MappingTemplate.fromString(queryMappingTemplate),
  responseMappingTemplate: MappingTemplate.dynamoDbResultList()
})
```

またレスポンスのMappingTemplateについても用意されています。
配列で返す場合は `dynamoDbResultList()` 、単体で返す場合は `dynamoDbResultItem()` を使います。

```ts
MappingTemplate.dynamoDbResultList()
MappingTemplate.dynamoDbResultItem()
```

## まとめ

AppSync の High-level constructs が来て何が嬉しいのか、個人的に感じているのは、Resolverの定義のところです。
`MappingTemplate.dynamoDbPutItem()` といった便利関数が用意されたおかげで、すべてのResolverを手書きする必要がなくなったので、コード量が少なくなりました。
また `Cfnxxxx` で定義していたときはリソースのデプロイされる順番を `addDependsOn()` でコード側で担保しなければいけませんでしたが、それも内部でやってくれるようになったので、そこもかなり助かる部分です。(忘れてしまうこと多いので…)

AWSCDK、みなさんもぜひ試してみてください！
ではまた！！！