昨年発表されたAmplify Gen2(プレビュー)ですが、日々機能が少しづつ追加されており、完成度が高まってきました。 今回はチュートリアルに従ってNextJSを構築し、動作を確認してみます。
Amplify Gen2と、従来のAmplify
従来のAmplifyは「ツールファースト」と呼ばれています。豊富なメニューが用意されており、メニューを選択するだけで、必要な機能を構築することができました。 一方、メニューで選択できない部分の修正の難易度が高かったり、メニューや内容を把握する、「Amplifyならではの知識」が必要となる、というポイントがありました。
新しいAmplify Gen2は「コードファースト」です。TypeScript等の言語で、CDKをベースとした構築を行うスタイルに変わりました。これにより、CDKの知識は必要なものの、自由度が増し、開発者がIaCの細部まで手を入れることが出来る、というスタイルに変化しています。
構築
チュートリアルの構築はよく出来ており、手順に従えば簡単にTodoアプリを構築できます。
なお、Amplify gen2はプレビュー版の為、従来のAmplifyほどに機能が充実しておらず、順次増えていく想定です。(Storage機能もほんの最近追加されました) 今回はチュートリアルに従い、Next.js App Router (Server Components)の構築を行いつつ、ポイントを解説してみました。
プロジェクトの作成
npm create amplify@beta
構築を行うと、auth(Cognito)、data(AppSync)がデフォルトで提供されていることが分かります。
ローカルサーバの起動
npx amplify sandbox --name hogehoge
フロントエンドの起動に加え、sandboxという、開発者個々に割当が可能な、AWS上のクラウド環境を起動します。これにより、同じAWSアカウント上で複数の開発者が開発しても、Cognitoやlambdaのコンフリクトを防ぐことができます。
実行する場合、---nameオプションで識別名を付与することをお勧めします(lambdaの関数名等に反映され、関数の特定が容易になります)。
バックエンドを構築する
AmplifyはGraphQLを推しています。以前と比べ、直感的にスキーマを定義できるようになっています。 a.model()を使うことで、GET、PUT、UPDATE、DELETE、LIST、サブスクリプション機能を追加できます。 authorization()やidentifier()、secondaryIndexes()をチェインすることで、R/W権限や、デフォルトのデータソースであるDynamoDBのPrimary Key,Sort Key、Secondery Indexの指定ができます。
なお、model()で得られるLIST機能は、内部でDynamoDBへのスキャンを行ってしまうため、indexを効かせたQueryを実行したい場合、カスタムクエリでリゾルバを作成する必要がありそうです(後述します)。
data/resource.ts
const schema = a.schema({
Todo: a
.model({
content: a.string(),
done: a.boolean(),
priority: a.enum(['low', 'medium', 'high'])
})
.authorization([a.allow.owner(), a.allow.public().to(['read'])]),
});
認証の追加
Cognitoの設定を記述します。 Amplify Gen2は、ログイン方法にusernameを選ぶことが出来ず、emailか、phoneNumberからの選択となるようです(CDKのL1コンストラクトから書き換えを試してみましたが、不許可のエラーとなってしまいました)。
amplify/auth/resource.ts
import { defineAuth } from '@aws-amplify/backend';
export const auth = defineAuth({
loginWith: {
email: {
verificationEmailSubject: 'Welcome! Verify your email!'
},
}
});
UIの構築
Amplify クライアント側の構成 <ConfigureAmplifyClientSide /> コンポーネントを挟むことで、クライアント側でamplifyのAPIの実行を可能にしています.
ログインコンポーネント
クライアントサイドで認証をチェックし、認証時はホーム画面、 未認証時はログイン画面を表示するようにしています。
サーバー側リダイレクト用のミドルウェアの追加
NextJSのmiddrewareの機能を使用して、ページ遷移時にサーバ側で認証をチェックすることにより、認証のないユーザーをloginページに遷移させています。
To Do アイテムのリストを表示する
dataで作成したTodoモデルを元に、listを呼び出し、DynamoDBに格納されたデータを AppSync経由で取り出しています。
const { data: todos } = await cookiesClient.models.Todo.list();
新しい To Do アイテムを作成する
formに格納されたデータがSubmitに対して、Server Actionを直接割り当て、ToDoを作成します。 revalidatePathにより、コンポーネントの再検証が行われ、最新のデータが表示される仕組みです。
async function addTodo(data: FormData) {
"use server";
const title = data.get("title") as string;
await cookiesClient.models.Todo.create({
content: title,
done: false,
priority: "medium",
});
revalidatePath("/");
}
カスタムクエリ・ミューテーションを設定する
チュートリアルから外れて、カスタムクエリを実行できるように改修を行ってみます。
前述の通り、model()によるLIST機能はScanによるものなので、Indexを効かせてQueryを発行したり、Mutationに合わせて通知を発行したりしたい場合、appSyncリゾルバやLambdaによるリゾルバを設定する必要があります。 なお、パイプラインリゾルバも設定可能です。
今回は、スキーマ定義を変更して、複合キーを貼り、createDateをbegins_withで絞り込んで取得してみます。
Todo型を変更し、tagとcreateDateの複合キーに変更します。 次に、カスタムクエリ用の型と、リゾルバに対するリクエスト・レスポンスを定義します。 TodosをArrayで返却するのがポイントです。
amplify/data/resource.ts
const schema = a.schema({
Todo: a
.model({
tag: a.string().required(),
content: a.string(),
done: a.boolean(),
createDate: a.string().required(),
priority: a.enum(["low", "medium", "high"]),
})
.identifier(["tag", "createDate"])
.authorization([a.allow.owner()]),
Todos: a.customType({
tag: a.string().required(),
content: a.string(),
done: a.boolean(),
createDate: a.string().required(),
priority: a.enum(["low", "medium", "high"]),
}),
RangeTodos: a
.query()
.arguments({
tag: a.string().required(),
year: a.string().required(),
})
.returns(a.ref("Todos").array())
.authorization([a.allow.private()])
.handler([
a.handler.custom({
dataSource: a.ref("Todo"),
entry: "./appsyncResolver.js",
}),
]),
});
リゾルバは、関数ではなく、AppSyncリゾルバを使用しました。
amplify/data/appsyncResolver.js
export function request(ctx) {
if (!ctx.identity) {
runtime.earlyReturn([]);
}
return {
operation: "Query",
query: {
expression: "tag = :tag and begins_with(createDate, :date)",
expressionValues: {
":tag": { S: ctx.arguments.tag },
":date": { S: ctx.arguments.date },
},
},
};
}
export function response(ctx) {
return ctx.result.items;
}
page.tsxを、クエリパラメータを使用し、rangeTodoListからデータを取得するように修正します。 日付の入力部分もServer Actionで定義します。
app/pages.tsx
import { revalidatePath } from "next/cache";
import { AuthGetCurrentUserServer, cookiesClient } from "@/utils/amplify-utils";
import Logout from "@/components/Logout";
import { redirect } from "next/navigation";
async function App({
searchParams,
}: {
searchParams?: { [key: string]: string };
}) {
const user = await AuthGetCurrentUserServer();
const dateParam = !searchParams?.date ? "20" : (searchParams?.date as string);
const { data: rangeTodoList } =
(await cookiesClient.queries.RangeTodos({
tag: "test",
date: dateParam,
})) ?? [];
async function addTodo(data: FormData) {
"use server";
await cookiesClient.models.Todo.create({
tag: "test",
content: data.get("title") as string,
done: false,
priority: "medium",
createDate: data.get("createDate") as string,
});
revalidatePath("/");
}
async function rangeTodo(data: FormData) {
"use server";
const dateParam = data.get("date");
redirect("/" + dateParam ? `?date=${data.get("date")}` : "");
}
return (
<>
<h1>Hello, Amplify 👋</h1>
{user && <Logout />}
<form action={rangeTodo}>
<input
type="text"
name="date"
/>
<button type="submit">Range Todo</button>
</form>
<form action={addTodo}>
<input
type="date"
name="createDate"
/>
<input
type="text"
name="title"
/>
<button type="submit">Add Todo</button>
</form>
<ul>
{rangeTodoList!.map((rangeTodo) => (
<li key={rangeTodo!.tag + rangeTodo!.createDate}>
{rangeTodo!.createDate} : {rangeTodo!.content}
</li>
))}
</ul>
</>
);
}
export default App;
通常の表示
「2023」で絞り込んで表示
begins_withによる前方一致の絞り込みが機能していることが分かります。 また、CDKでApp Syncのリゾルバを定義する場合に比べ、比較的に簡単に構築を行うことができました。
その他、構築で気になった点
今回のチュートリアルでは触れていない点となりますが、Amplify Gen2のdataでも、元のAppSyncの機能である、リアルタイムな購読を行うことができます。ただし、購読はClient Componentで実行する必要がありました。
まとめ
Amplify Gen2の構築のチュートリアルに従い、Next.js App RouterへのAmplifyの組み込みのポイントを確認してみました。
次回は、Amplify Gen2のホスティングを使用し、デプロイの解説を行います。