CDKでGraphQLに挑戦する(lambda編)

# はじめに

今日は[GWアドベントカレンダー](https://gw-advent.9wick.com/calendars/2020/89)の７日目です。今回は番外編ということで**AppSync**から**Lambda**を呼び出す実装について書いていきます。

# Lambdaを使いたい理由

本当は面倒な実装をやりたくないのですが、どうしても自分でLambdaを書かないといけない場合も存在するようです。このシリーズでは`Item`という型のスキーマを[最初](https://gw-advent.9wick.com/calendars/2020/89/posts/2020-04-29/view)に定義しましたが、例えばそこに`category`という属性を足したいとします。最初に書いたスキーマを以下のように書き換えます。

```graphql
enum Category {
    HOGEHOGE
    HUGAHUGA
}

type Item {
    id: String!
    data: String!
    category: Category!
}

type Query {
    allItem: [Item]
    allItemOnCategory(category: Category): [Item]
}

input ItemInput {
    data: String!
    category: Category!
}

type Mutation {
    addItem(item: ItemInput!): Item
}
```

`Item`に`category`というenumで定義した値が追加されました。また、`allItemOnCategory`を実行するとカテゴリ別でデータを取得させるようにスキーマを定義しました。

ここで`id`は必ず一意な値として必要なのでプライマリキーとして、`category`をソートキーとして定義します。ここまでは特に困ることはないのですが、この`allItemOnCategory`のリゾルバーを実装するにはプライマリキーではなくソートキーから値を引っ張ってくる必要があるのですが、[調べたところ](https://github.com/aws/aws-cdk/pull/5940)**AppSyncのDynamoDBへのマッピングテンプレートは現在プライマリキーのみしかサポートされていないようです**（わたしの勘違いかもしれないのでその場合はご指摘ください。。。）。
なので少し複雑なデータ操作をする場合は自分でLambdaを書くしかなさそうです。

# テーブルの定義の拡張

`category`の値をプライマリキーのように扱いたいので今回は**グローバルセカンダリインデックス**をしようします。こんな感じでcdkを書き換えました。

```typescript
const itemTable = new Table(this, 'itemTable', {
  partitionKey: {
    name: 'id',
    type: AttributeType.STRING,
  },
  sortKey: {
    name: 'category'
    type: AttributeType.STRING,
  }
})

itemTable.addGlobalSecondaryIndex({
  indexName: 'categoryKey',
  partitionKey: {
    name: 'category',
    type: AttributeType.STRING,
  }
})
```

# Lambdaを呼び出す

`GraphQLApi`に`addLambdaDataSource`というメソッドがサポートされているのでそれを用います。実装はこんな感じです。

```typescript
const allItemOnCategory = new Function(this, 'allItemOnCategory', {
  code: Code.asset('lambda/allItemOnCategory'),
  handler: 'index.handler',
  runtime: Runtime.NODEJS_12_X,
  environment: {
    TABLE_NAME: itemTable.tableName,
  },
})
itemTable.grantReadData(allItemOnCategory)

api.addLambdaDataSource('ItemLambda', 'Item lambda source', allItemOnCategory).createResolver({
  typeName: 'Query',
  fieldName: 'allItemOnCategory',
  requestMappingTemplate: MappingTemplate.lambdaRequest(),
  responseMappingTemplate: MappingTemplate.lambdaResult(),
})
```
`MappingTemplate.lambdaRequest()`にはペイロードが指定できるみたいですが、クエリ変数とは別なので別段使う理由がなければ何も指定しなくて問題なさそうです。

# Lambdaの実装

AppSyncから`arguments: {...}`という形でクエリ変数を受け取れるので、それを用いてデータの取得を実装します。

```typescript
import { DynamoDB } from 'aws-sdk'

const dynamodb = new DynamoDB()

interface AllItemOnCategoryEvent {
  arguments: {
    category: string
  }
}

export const handler = async (event: AllItemOnCategoryEvent) => {

  const result = await dynamodb.query({
    TableName: process.env.TABLE_NAME!,
    ExpressionAttributeValues: {
      ':category': {
        S: event.arguments.category,
      },
    },
    KeyConditionExpression: 'category = :category',
    IndexName: 'categoryKey',
  }).promise()

  return result.Items?.map(item=>({
    id: item.id.S,
    data: item.data.S,
    category: item.data.S,
  }))
}
```

必要分のデータ（つまりは`[Item]`にマッチするデータ）さえ返せばクライアントとのリクエストやレスポンスのマッピングとかは勝手にAppSyncがやってくれるので何も考えずにデータのみを返します。

実装はこれだけです。API Gateway＋Lambdaで書くよりも実装がシンプルになるのがいいですよね。

# さいごに

全体のコードは[こちら](https://github.com/ufoo68/cdk-appsync-test)で公開しています。とりあえずベーシックでないDynamoDBの操作とかは、今の所自分でLambdaを組む必要があるみたいです（しかしもしかしたら勘違いしていることもあるかもしれないので知見のある方のご指摘を頂きたいです。。。）。

さて、７日間に渡ってAppSyncを追いかけてきました。そしてなんとかAppSyncというテーマで連日投稿してゴールをすることができました（そのせいで内容が薄くなった記事がほとんどでしたがw）。AppSyncの素晴らしさを少し学べたので今後もいかしていきたいなと思います。

明日は@nnhiguchiさんの投稿です。お楽しみに！