AWS AppSync がアツい！ RDS Data API を使って GraphQL API を爆速で作ってみた

:::note warn
本記事の続編を出しました。ぜひこちらの記事からお読みください。
:::
https://qiita.com/inada_riku/items/f4cd77c254fb6fd6f81e


こんにちは。[いなりく](https://twitter.com/inada_riku)です✋
本記事は、[AWS Amplify と AWS × フロントエンド #AWSAmplifyJP Advent Calendar 2023](https://qiita.com/advent-calendar/2023/amplify) の 22 日目の記事です。

:::note warn
本記事は 2023 年 12 月 23 日時点の情報に基づいて執筆しています。
:::

2023 年 11 月 27 日に、AWS AppSync は RDS Data API を使用して Amazon Aurora クラスター内のデータベースに対して[イントロスペクション](https://graphql.org/learn/introspection/)を行うことで、検出したテーブルに適合した GraphQL API のインターフェイスを簡単に作成することが可能になりました。


>[RDS Data API を使用して設定された Amazon Aurora クラスターに対する AWS AppSync のサポートが改善](https://aws.amazon.com/jp/about-aws/whats-new/2023/11/aws-appsync-aurora-clusters-rds-data-api/)


すごい便利な機能ですが、RDS Data API が **2023 年 11 月 27 日時点では** Aurora Serverless v1 のみの対応だったため利用できるケースが限定的でした。

しかし、ちょうど昨日 (2023 年 12 月 21 日)、[Amazon Aurora PostgreSQL の Serverless v2 と Provisoned で RDS Data API が対応した](https://aws.amazon.com/jp/about-aws/whats-new/2023/12/amazon-aurora-postgresql-rds-data-api/)ため、AWS AppSync の新しい機能の利用ユースケースが広がりました。


>補足 : [現在 (2023 年 12 月 22 日)、Aurora PostgreSQLでは、Data API は Aurora Serverless v2、Aurora Serverless v1、およびプロビジョニングされたデータベースでサポートされています。Aurora MySQL では、Data API は Aurora Serverless v1 データベースでのみサポートされています](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/data-api.html#data-api.enabling)。


「[Build a GraphQL API for your Amazon Aurora MySQL database using AWS AppSync and the RDS Data API](https://aws.amazon.com/jp/blogs/mobile/build-a-graphql-api-for-your-amazon-aurora-mysql-database-using-aws-appsync-and-the-rds-data-api/)」では Aurora Serverless v1 (MySQL) に対して AWS AppSync と RDS Data API を使って GraphQL API を構築しているので、今回は、**Amazon Aurora Serverless v2 (PostgreSQL) で AWS AppSync の新しい機能が使えるのかどうか検証してみます**！

:::note warn
内容に不備、あるいは追加情報などありましたら、気軽にコメント頂けますと幸いです！
:::

## Step1. Amazon Aurora Serverless v2 (PostgreSQL) の作成

まず、Aurora Serverless v2 (PostgreSQL) の DB クラスタを作成します。手順については「[Aurora PostgreSQL DB クラスターの作成と接続](https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/CHAP_GettingStartedAurora.CreatingConnecting.AuroraPostgreSQL.html)」を参考にしてください。

DB クラスターの作成が完了したら、テーブルの作成を行います。以下のようなテーブル定義にします。

```sql
CREATE TABLE conversations (
  id SERIAL PRIMARY KEY,
  name VARCHAR(255) NOT NULL,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE messages (
  id UUID PRIMARY KEY,
  conversation_id INT NOT NULL,
  sub UUID NOT NULL,
  body TEXT NOT NULL,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (conversation_id) REFERENCES conversations(id)
);

CREATE TABLE conversation_participants (
  conversation_id INT NOT NULL,
  sub UUID NOT NULL,
  last_read_at TIMESTAMP WITH TIME ZONE,
  PRIMARY KEY (conversation_id, sub),
  FOREIGN KEY (conversation_id) REFERENCES conversations(id)
);
```

## Step2. Secret Manager に DB 認証情報の保存

AWS AppSync は Amazon Aurora クラスターに接続する際に Secret Manager に保存されたシークレットを使用します。シークレットのタイプを「Amazon RDS データベースの認証情報」を選択肢、認証情報をデータベースを選択します。それ以外はデフォルトのまま、作成に進みます。

![スクリーンショット 2023-12-22 16.07.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3625536/15ad47c0-00cc-f5fe-62f8-900c1028cdc4.png)

## Step3. GraphQL API の作成
次に、GraphQL API を作成しましょう。GraphQL API データソースで「Amazon Aurora クラスターから始める - New」を選択し、次に進みます。

![スクリーンショット 2023-12-22 13.22.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3625536/ed6becfe-5a77-8b90-277d-2828aff2fb93.png)

次に、API 名にわかり易い名前を入力します。今回はデフォルトの「My AppSync API」としました。

![スクリーンショット 2023-12-22 13.23.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3625536/fb231f5f-56d5-1f1a-1fce-d392c630ca20.png)

次にデータベースを選択します。

![スクリーンショット 2023-12-22 16.13.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3625536/c105701d-1b6f-7771-1acf-a8f8fa93673f.png)

「データベースを選択」をクリックすると接続したい Amazon Aurora クラスターが選択出来ます。AWS Secret  Manager シークレットは Step2 で作成したものを選択します。入力が終えたら「インポート」をクリックします。

![database.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3625536/b5b191d1-bbcf-2bf9-ffc0-a98241014984.png)

「インポート」をクリックするとイントロスペクションが開始され、下の画像のようにデータベースのテーブルがインポートされました。タイプ名はカスタマイズすることができるので、以下のように変更します。(画像は変更前です。)

* `Conversation_participants` : `Participant`
* `Conversations` : `Conversation`
* `Messages` : `Message`

問題なければ、「次へ」をクリックします。

![スクリーンショット 2023-12-22 13.31.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3625536/7b54176f-d936-c1f1-babb-7c64a78ff382.png)

スキーマの設定では、クエリのみを作成するか、クエリ、ミューテーション、サブスクリプションを作成するかを選択することが出来ます。今回は、「すべてのモデルに対してクエリ、ミューテーション、サブスクリプションを作成」を選択します。これで GraphQL API の作成は完了です！

![スクリーンショット 2023-12-22 13.31.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3625536/9be8fb68-3655-102d-1941-dd5928b7a5d1.png)

## Step4. 動作確認

では、実際にどんなスキーマやリゾルバーが作成されたか見てみましょう。まずはスキーマを見てみましょう。

### 確認 1 : GraphQL スキーマ

以下が自動で生成されたスキーマ (一部抜粋) です。正直初めてやったときに結構感動しました。ただ、一点気になったこととしては `created_at` が `String` になっていることです。Amazon Aurora PostgreSQL の場合、RDS Data API は常に UTC タイムゾーンの Aurora PostgreSQL データ型 `TIMESTAMPTZ` を返すのですが、[AWS AppSync のスカラー型では `TIMESTAMPTZ` はサポートしておらず、かつ AWS AppSync はカスタムスカラーをサポートしていません。](https://docs.aws.amazon.com/appsync/latest/devguide/scalars.html)そのため、リゾルバー側で対応する必要がありそうです。

(今回は力尽きたのでリゾルバーはいじりません..またの機会をお楽しみに...)

```graphql
type Conversation {
    name: String!
    created_at: String
    id: Int!
}

type ConversationConnection {
    items: [Conversation]
    nextToken: String
}

input CreateConversationInput {
    name: String!
    created_at: String
    id: Int!
}

input CreateMessageInput {
    created_at: String
    conversation_id: Int!
    id: ID!
    body: String!
    sub: ID!
}

input CreateParticipantInput {
    conversation_id: Int!
    last_read_at: String
    sub: ID!
}

input DeleteConversationInput {
    id: Int!
}

input DeleteMessageInput {
    id: ID!
}

input DeleteParticipantInput {
    conversation_id: Int!
    sub: ID!
}

type Message {
    created_at: String
    conversation_id: Int!
    id: ID!
    body: String!
    sub: ID!
}

type MessageConnection {
    items: [Message]
    nextToken: String
}

type Participant {
    conversation_id: Int!
    last_read_at: String
    sub: ID!
}

type ParticipantConnection {
    items: [Participant]
    nextToken: String
}

(〜〜〜〜〜〜〜〜〜省略〜〜〜〜〜〜〜〜〜)

type Mutation {
    createMessage(input: CreateMessageInput!): Message
    updateMessage(input: UpdateMessageInput!, condition: TableMessageConditionInput): Message
    deleteMessage(input: DeleteMessageInput!, condition: TableMessageConditionInput): Message
    createConversation(input: CreateConversationInput!): Conversation
    updateConversation(input: UpdateConversationInput!, condition: TableConversationConditionInput): Conversation
    deleteConversation(input: DeleteConversationInput!, condition: TableConversationConditionInput): Conversation
    createParticipant(input: CreateParticipantInput!): Participant
    updateParticipant(input: UpdateParticipantInput!, condition: TableParticipantConditionInput): Participant
    deleteParticipant(input: DeleteParticipantInput!, condition: TableParticipantConditionInput): Participant
}

type Query {
    getMessage(id: ID!): Message
    listMessages(filter: TableMessageFilterInput, limit: Int, nextToken: String): MessageConnection
    getConversation(id: Int!): Conversation
    listConversations(filter: TableConversationFilterInput, limit: Int, nextToken: String): ConversationConnection
    getParticipant(conversation_id: Int!, sub: ID!): Participant
    listParticipants(filter: TableParticipantFilterInput, limit: Int, nextToken: String): ParticipantConnection
}

type Subscription {
    onCreateMessage(
        created_at: String,
        conversation_id: Int,
        id: ID,
        body: String,
        sub: ID
    ): Message
        @aws_subscribe(mutations: ["createMessage"])
    onUpdateMessage(
        created_at: String,
        conversation_id: Int,
        id: ID,
        body: String,
        sub: ID
    ): Message
        @aws_subscribe(mutations: ["updateMessage"])
    onDeleteMessage(
        created_at: String,
        conversation_id: Int,
        id: ID,
        body: String,
        sub: ID
    ): Message
        @aws_subscribe(mutations: ["deleteMessage"])
    onCreateConversation(name: String, created_at: String, id: Int): Conversation
        @aws_subscribe(mutations: ["createConversation"])
    onUpdateConversation(name: String, created_at: String, id: Int): Conversation
        @aws_subscribe(mutations: ["updateConversation"])
    onDeleteConversation(name: String, created_at: String, id: Int): Conversation
        @aws_subscribe(mutations: ["deleteConversation"])
    onCreateParticipant(conversation_id: Int, last_read_at: String, sub: ID): Participant
        @aws_subscribe(mutations: ["createParticipant"])
    onUpdateParticipant(conversation_id: Int, last_read_at: String, sub: ID): Participant
        @aws_subscribe(mutations: ["updateParticipant"])
    onDeleteParticipant(conversation_id: Int, last_read_at: String, sub: ID): Participant
        @aws_subscribe(mutations: ["deleteParticipant"])
}
```
### 確認 2 : リゾルバー
次に、リゾルバーを見てみます。以下は `createMessage` のリゾルバーコードです。
JavaScript で書かれているので読みやすいですね。

```javascript
import { util } from '@aws-appsync/utils';
import { insert, createPgStatement, toJsonObject } from '@aws-appsync/utils/rds';

/**
 * Puts an item into the messages table using the supplied input.
 * @param {import('@aws-appsync/utils').Context} ctx the context
 * @returns {*} the request
 */
export function request(ctx) {
    const { input } = ctx.args;
    const insertStatement = insert({
        table: 'messages',
        values: input,
        returning: '*',
    });
    return createPgStatement(insertStatement)
}

/**
 * Returns the result or throws an error if the operation failed.
 * @param {import('@aws-appsync/utils').Context} ctx the context
 * @returns {*} the result
 */
export function response(ctx) {
    const { error, result } = ctx;
    if (error) {
        return util.appendError(
            error.message,
            error.type,
            result
        )
    }
    return toJsonObject(result)[0][0]
}
```

### 確認 3 : GraphQL スキーマの作成タイミング
最後に AWS AppSync の GraphQL スキーマ作成のタイミングを確認してみました。
`conversations` テーブルに `status` というカラムを追加してみました。

```sql
ALTER TABLE conversations ADD COLUMN status VARCHAR(100);
```
これで、 AWS AppSync コンソール側でスキーマの反映ができれば...と思いましたが、現時点 (2023 年 12 月 22 日) 時点ではスキーマの反映は GraphQL API を新規作成するタイミングだけでした。既存の GraphQL API にデータソースを新規追加する際にもスキーマは反映されません。今後の Update に期待ですね。

## まとめ

今回は、AWS AppSync の RDS Data API を使用して Amazon Aurora Serverless v2 (PostgreSQL) クラスター内のデータベースに対して[イントロスペクション](https://graphql.org/learn/introspection/)を行うことで、検出したテーブルに適合した GraphQL API のインターフェイスを作成する機能を試してみました。スカラーの問題があったため、少し手を加える必要がありますが、爆速で構築できたことに驚きました。

[AWS AppSync は 2023 年に多くのアップデートがあり](https://speakerdeck.com/inariku/14)、これからが楽しみです。

明日から旅行に行くのでここでおしまいとします。続報をお楽しみに