本記事の続編を出しました。ぜひこちらの記事からお読みください。
こんにちは。いなりくです✋
本記事は、AWS Amplify と AWS × フロントエンド #AWSAmplifyJP Advent Calendar 2023 の 22 日目の記事です。
本記事は 2023 年 12 月 23 日時点の情報に基づいて執筆しています。
2023 年 11 月 27 日に、AWS AppSync は RDS Data API を使用して Amazon Aurora クラスター内のデータベースに対してイントロスペクションを行うことで、検出したテーブルに適合した GraphQL API のインターフェイスを簡単に作成することが可能になりました。
RDS Data API を使用して設定された Amazon Aurora クラスターに対する AWS AppSync のサポートが改善
すごい便利な機能ですが、RDS Data API が 2023 年 11 月 27 日時点では Aurora Serverless v1 のみの対応だったため利用できるケースが限定的でした。
しかし、ちょうど昨日 (2023 年 12 月 21 日)、Amazon Aurora PostgreSQL の Serverless v2 と Provisoned で RDS Data API が対応したため、AWS AppSync の新しい機能の利用ユースケースが広がりました。
「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 の新しい機能が使えるのかどうか検証してみます!
内容に不備、あるいは追加情報などありましたら、気軽にコメント頂けますと幸いです!
Step1. Amazon Aurora Serverless v2 (PostgreSQL) の作成
まず、Aurora Serverless v2 (PostgreSQL) の DB クラスタを作成します。手順については「Aurora PostgreSQL DB クラスターの作成と接続」を参考にしてください。
DB クラスターの作成が完了したら、テーブルの作成を行います。以下のようなテーブル定義にします。
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 データベースの認証情報」を選択肢、認証情報をデータベースを選択します。それ以外はデフォルトのまま、作成に進みます。
Step3. GraphQL API の作成
次に、GraphQL API を作成しましょう。GraphQL API データソースで「Amazon Aurora クラスターから始める - New」を選択し、次に進みます。
次に、API 名にわかり易い名前を入力します。今回はデフォルトの「My AppSync API」としました。
次にデータベースを選択します。
「データベースを選択」をクリックすると接続したい Amazon Aurora クラスターが選択出来ます。AWS Secret Manager シークレットは Step2 で作成したものを選択します。入力が終えたら「インポート」をクリックします。
「インポート」をクリックするとイントロスペクションが開始され、下の画像のようにデータベースのテーブルがインポートされました。タイプ名はカスタマイズすることができるので、以下のように変更します。(画像は変更前です。)
-
Conversation_participants
:Participant
-
Conversations
:Conversation
-
Messages
:Message
問題なければ、「次へ」をクリックします。
スキーマの設定では、クエリのみを作成するか、クエリ、ミューテーション、サブスクリプションを作成するかを選択することが出来ます。今回は、「すべてのモデルに対してクエリ、ミューテーション、サブスクリプションを作成」を選択します。これで GraphQL API の作成は完了です!
Step4. 動作確認
では、実際にどんなスキーマやリゾルバーが作成されたか見てみましょう。まずはスキーマを見てみましょう。
確認 1 : GraphQL スキーマ
以下が自動で生成されたスキーマ (一部抜粋) です。正直初めてやったときに結構感動しました。ただ、一点気になったこととしては created_at
が String
になっていることです。Amazon Aurora PostgreSQL の場合、RDS Data API は常に UTC タイムゾーンの Aurora PostgreSQL データ型 TIMESTAMPTZ
を返すのですが、AWS AppSync のスカラー型では TIMESTAMPTZ
はサポートしておらず、かつ AWS AppSync はカスタムスカラーをサポートしていません。そのため、リゾルバー側で対応する必要がありそうです。
(今回は力尽きたのでリゾルバーはいじりません..またの機会をお楽しみに...)
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 で書かれているので読みやすいですね。
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
というカラムを追加してみました。
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) クラスター内のデータベースに対してイントロスペクションを行うことで、検出したテーブルに適合した GraphQL API のインターフェイスを作成する機能を試してみました。スカラーの問題があったため、少し手を加える必要がありますが、爆速で構築できたことに驚きました。
AWS AppSync は 2023 年に多くのアップデートがあり、これからが楽しみです。
明日から旅行に行くのでここでおしまいとします。続報をお楽しみに