Chapter 05

Todo作成機能を追加する

shitakemura
shitakemura
2022.02.13に更新

はじめに

本チャプターでは Todo 作成機能を追加します。
インフラ&バックエンドはGraphQL のスキーマ定義を作成 -> TypeScript の型定義を自動生成 -> Lamda関数の作成 -> AWS CDKの更新、フロントエンドはGraphQLクエリ、ミューテーション定義を作成 -> TypeScript の型定義を自動生成 -> 実装の流れで行います。

インフラ&バックエンド

スキーマ定義を更新する

Todo 作成するためのミューテーションaddTodoを追加します。

./infra-backend/graphql/schema.graphql
+ input AddTodoInput {
+   title: String!
+ }

+ type Mutation {
+   addTodo(addTodoInput: AddTodoInput!): Todo
+ }

型定義ファイルを自動生成する

./infra-backend
$ npm run codegen

Todo を作成する Lambda 関数を実装する

必要なパッケージをインストールします。

./frontend
$ npm i uuid
$ npm i -D @types/uuid

Lambda 関数addTodoを作成します。

./infra-backend/lambda/addTodo.ts
import { AppSyncResolverHandler } from "aws-lambda";
import {
  MutationAddTodoArgs,
  Todo,
} from "../graphql/generated/generated-types";
import { DynamoDB } from "aws-sdk";
import { v4 } from "uuid";

const docClient = new DynamoDB.DocumentClient();

export const handler: AppSyncResolverHandler<
  MutationAddTodoArgs,
  Todo | null
> = async (event) => {
  const addTodoInput = event.arguments.addTodoInput;
  const uuid = v4();

  try {
    if (!process.env.TODO_TABLE) {
      console.log("TODO_TABLE was not specified");
      return null;
    }

    const newTodo: Todo = {
      id: uuid,
      ...addTodoInput,
      completed: false,
      createdAt: Date.now(),
    };

    await docClient
      .put({
        TableName: process.env.TODO_TABLE,
        Item: newTodo,
      })
      .promise();
    return newTodo;
  } catch (err) {
    console.error(`DynamoDB error: ${err}`);
    return null;
  }
};

AWS CDK を更新して、作成した Lambda 関数をデプロイする

CDK を更新します

./infra-backend/lib/infra-backend-stack.ts
export class InfraBackendStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Lambda function
+    const commonLambdaNodeJsProps: Omit<
+      lambdaNodeJs.NodejsFunctionProps,
+      "entry"
+    > = {
+      handler: "handler",
+      environment: {
+        TODO_TABLE: todoTable.tableName,
+      },
+    };

    const getTodosLambda = new lambdaNodeJs.NodejsFunction(
      this,
      "getTodosHandler",
      {
        entry: path.join(__dirname, "../lambda/getTodos.ts"),
-       handler: "handler",
-       environment: {
-         TODO_TABLE: todoTable.tableName,
-       },
+       ...commonLambdaNodeJsProps,
      }
    );

    todoTable.grantReadData(getTodosLambda);

+    const addTodoLambda = new lambdaNodeJs.NodejsFunction(
+      this,
+      "addTodoHandler",
+      {
+        entry: path.join(__dirname, "../lambda/addTodo.ts"),
+        ...commonLambdaNodeJsProps,
+      }
+    );

+    todoTable.grantReadWriteData(addTodoLambda);

    // DataSource
+    const addTodoDataSource = todoApi.addLambdaDataSource(
+      "addTodoDataSource",
+      addTodoLambda
+    );

    // Resolever
+    addTodoDataSource.createResolver({
+      typeName: "Mutation",
+      fieldName: "addTodo",
+    });
  }
}

cdk diffcdk deployを実行してデプロイを行います。

./infra-backend
$ cdk diff
$ cdk deploy

フロントエンド

GraphQL ミューテーションを定義する

Todo 作成時に呼びだすミューテーションAddTodoの定義ファイルを作成します。

./frontend/graphql/AddTodo.graphql
mutation AddTodo($addTodoInput: AddTodoInput!) {
  addTodo(addTodoInput: $addTodoInput) {
    id
    title
    completed
    createdAt
  }
}

型定義ファイルを生成する

./frontend
$ npm run codegen

Todo 入力コンポーネントを作成する

Todo 入力、登録を行うためのコンポーネントを作成します。

./frontend/components/Todo/TodoInput.tsx
import { Button, HStack, Input } from "@chakra-ui/react";
import { useState } from "react";

export const TodoInput = () => {
  const [titleInput, setTitleInput] = useState("");

  return (
    <HStack spacing={6}>
      <Input
        borderColor='blue.500'
        borderWidth={2}
        height={12}
        width={400}
        value={titleInput}
        onChange={(e) => setTitleInput(e.target.value)}
      />
      <Button paddingX={8} colorScheme='blue' disabled={!titleInput}>
        Add Todo
      </Button>
    </HStack>
  );
};
./frontend/components/Todo/TodoScreen.tsx
+ import { TodoInput } from "./TodoInput";

export const TodoScreen = () => {
  const { data } = useGetTodosQuery();

  return (
    <VStack w='full' spacing={10} paddingX={48} paddingY={16}>
+       <TodoInput />
      <TodoList todos={data?.getTodos ?? []} />
    </VStack>
  );
};

npm run devを実行して、画面を確認します。

Todo 作成機能を実装する

GraphQL Code Generatorで作成したuseAddTodoMutation()を使用して、Todo 作成機能を実装します。

./frontend/components/Todo/TodoInput.tsx
+ import { useCallback, useState } from "react";
+ import {
+   namedOperations,
+   useAddTodoMutation,
+ } from "../../graphql/generated/generated-types";

export const TodoInput = () => {
  const [titleInput, setTitleInput] = useState("");
+ const clearTitleInput = useCallback(() => setTitleInput(""), []);

+ const [addTodo, { loading }] = useAddTodoMutation();

+ const handleAddTodo = useCallback(() => {
+   addTodo({
+     variables: { addTodoInput: { title: titleInput } },
+     refetchQueries: [namedOperations.Query.GetTodos],
+     onCompleted: clearTitleInput,
+   });
+ }, [titleInput, addTodo, clearTitleInput]);

  return (
    <HStack spacing={6}>
      ...
      <Button
        paddingX={8}
        colorScheme='blue'
        disabled={!titleInput}
+       isLoading={loading}
+       onClick={handleAddTodo}>
        Add Todo
      </Button>
    </HStack>
  );
};

npm run devを実行して動作を確認します。

以上で Todo 作成機能の追加は完了です。