Amplify Gen2の構築方法(その1)

前回、Amplify Gen1 で、DBへの登録と読み込みを試した。同じようにAmplify Gen2 で構築するとどうなるのか試す。前回と同じ順番で認証方式を試していく。

Amplify Gen2のトップページ

以下のドキュメントをベースに進める。

■Amplify Docs (Gen 2) – AWS Amplify Gen 2 Documentation
https://docs.amplify.aws/gen2/

Amplify Gen2 セットアップ

下記のGen2ドキュメントの通り、Next.js(App Router)の Client Components の初期セットアップを行う。

■Next.js App Router (Client Components) – AWS Amplify Gen 2 Documentation
https://docs.amplify.aws/gen2/start/quickstart/nextjs-app-router-client-components/

npm create next-app@14 -- next-amplify-gen2 --typescript --eslint --app --no-src-dir --no-tailwind --import-alias '@/*'
cd next-amplify-gen2
npm create amplify@beta

以下はGen1と同様である。

npm run dev

Gen1との違いは、ローカル環境でSandboxを起動する点。別のターミナルを立ち上げ、上記と並行して実行状態とする。

npx amplify sandbox

Sandboxを起動するとCloudFormationが実行され、AWS AppSync、DynamoDB、AWS Lambda、Amazon Cognito などが自動生成される。Sandbox起動時の流れは以下になる。

Sandbox起動時の流れ

以下のようなCloudFormationが生成される。

自動生成されたCloudFormation
自動生成されたAWS AppSync
自動生成されたAWS Lambda
自動生成されたDynamoDB
自動生成されたAmazon Cognito

バックエンド構築

Gen2ドキュメントの通り、 amplify/data/resource.ts を修正してデプロイする。最低限書き込み・読み込みができるようにし、詳細の検証は次回に回す。認証方式はuserPool、ログイン必須で自分の書き込んだTodoのみ読み込むことができる。doneとpriorityのカラムも追加している。

const schema = a.schema({
  Todo: a
    .model({
      content: a.string(),
      done: a.boolean(),
      priority: a.enum(['low', 'medium', 'high'])
    })
    .authorization([a.allow.owner()]),
});

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: 'userPool'
  },
});

上記テキストを保存すると自動的にSandboxがデプロイされる。

UIの構築

ドキュメント通り、以下のコマンドを実行する。

npm install @aws-amplify/ui-react

初期処理を実行するファイルを新規作成する。

// components/ConfigureAmplify.tsx
"use client";

import config from "@/amplifyconfiguration.json";
import { Amplify } from "aws-amplify";

Amplify.configure(config, { ssr: true });

export default function ConfigureAmplifyClientSide() {
  return null;
}

メイン処理である /app/page.tsx の実行前に必ず呼ばれる /app/layout.tsx を修正する。先ほど作成したコンポーネントを呼び出し、初期処理を実行する。あと細かいが、画面が真っ黒で見た目があまりよくないため、globals.css は呼び出さないようにしている。

// app/layout.tsx
import ConfigureAmplifyClientSide from "@/components/ConfigureAmplify";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
//import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <ConfigureAmplifyClientSide />
        {children}
      </body>
    </html>
  );
}

順番が前後するが先にTodoListコンポーネント(CSR)を新規作成しておく。細かいが一部スタイルを適用し、0件時の処理を加えている。useEffect() にてlistTodos()を呼び出してもよいが、Create時に追加したTodoが表示されないため、subscribeという機能を使って、DBに値が入ると自動表示されるようにしている。例えば別タブで同じ画面を開きTodoをCreateした場合、元の画面でもその別タブで追加したTodoが(リロードしなくても)自動で表示される。

これはGraphQLを使っていると享受できる大きなメリットである。この仕組みを使うことで、例えば簡単なチャットページであればすぐに作れてしまう(ある特定の条件でテーブルを監視するだけでよい)。

// components/TodoList.tsx
"use client";

import { useState, useEffect } from "react";
import { generateClient } from "aws-amplify/data";
import type { Schema } from "@/amplify/data/resource";

// generate your data client using the Schema from your backend
const client = generateClient<Schema>();

export default function TodoList() {
  const [todos, setTodos] = useState<Schema["Todo"][]>([]);

  async function listTodos() {
    // fetch all todos
    const { data } = await client.models.Todo.list();
    setTodos(data);
  }

  useEffect(() => {
    //listTodos();
    // Create a couple of to-dos, then refresh the page to see them. 
    // You can also subscribe to new to-dos in your useEffect to have them live reload on the page.
      const sub = client.models.Todo.observeQuery().subscribe(({ items }) =>
        setTodos([...items])
      )
      return () => sub.unsubscribe()
  }, []);

  return (
    <div>
      <h1>Todos</h1>
      <button onClick={async () => {
        // create a new Todo with the following attributes
        const { errors, data: newTodo } = await client.models.Todo.create({
          // prompt the user to enter the title
          content: window.prompt("title"),
          done: false,
          priority: 'medium',
        })
      }}>Create </button>
      <ul style={{ paddingLeft: '0px' }}>
        {todos.length > 0 ?
            todos.map((todo) => (
              <li key={todo.id} style={{ listStyle: 'none' }}>{todo.content}</li>
            ))
            : ''
        }
      </ul>
    </div>
  );
}

呼び出し側の/page.tsxは以下の通り。見た目を整えるためメイン領域はセンタリングしておく。今回、userPoolを採用するため、withAuthenticatorを使ってログイン必須としている(ログインしていないと一切アクセスできないため)。

// app/page.tsx
"use client";

import { withAuthenticator } from "@aws-amplify/ui-react";
import "@aws-amplify/ui-react/styles.css";
import TodoList from "@/components/TodoList";

function App() {
  return (
    <div
        style={{
            maxWidth: '500px',
            margin: '0 auto',
            textAlign: 'center',
            marginTop: '100px'
        }}
    >
      <h1>Hello, Amplify 👋</h1>
      <TodoList />
    </div>
  );
}

export default withAuthenticator(App);

このような画面が表示されるはずである。

次回はIAM認証方式での対応を行う。