Next.js, Hasura, Cloud Run, Cloud SQLでWebアプリを作る！0 から デプロイまで！

## はじめに

Next.jsとHasuraを使ってWebアプリを作り、デプロイして運用していくまでの方法を紹介していきます！

3ヶ月前からHasuraやGCP,Auth0を触り始めまして勉強中なので、間違っている点やわかりづらい点があった場合は指摘いただけると嬉しいです！！
(特にAuth0回りのアドバイス頂けるととても嬉しいです。)

デザインや細かい機能の実装などは行わず、Next.js, Hasura, Vercel, Cloud Runを使った開発の全体像を掴むこと、そしてこれらの技術を使った開発を始めることができるようになることを目的として書いてみます！

## ソースコード

https://github.com/tokio-k/memo-app-sample
https://github.com/tokio-k/memo-app-sample-hasura

## 使用技術

* TypeScript
* Next.js
* Vercel
* Apollo
* Hasura
* Docker
* Cloud Run
* Cloud SQL
* Cloud Build
* サーバーレスVPCアクセス
* Auth0

## 学べること

* Next.jsの基本的な使い方
* Hasuraの基本的な使い方
* Cloud Run, Cloud SQLを使ってHasuraを動かす方法
* Hasuraを本番環境とローカルで分けて作業する方法
* Apolloを使ってのSSGの方法

## しないこと

* デザイン
* 細かい機能の実装
* Reactの細かい説明

## 作るもの

* メモ投稿サービス
  * メモの投稿/編集/削除ができる機能
  * 他人のメモの閲覧ができる機能
  * ログイン/ログアウトができる機能

## 流れ

1. [Next.jsを使ってHello World](#nextjsを使ってhello-world)
1. [Next.jsをVercelにデプロイ](#nextjsをvercelにデプロイ)
1. [Hasuraをローカルで立ち上げ](#hasuraをローカルで立ち上げ)
1. [HasuraをCloud Runにデプロイ](#hasuraをcloud-runにデプロイ)
1. [Apolloを使ってNext.jsからHasuraにリクエスト](#apolloを使ってnextjsからhasuraにリクエスト)
1. [認証の実装](#認証の実装)
1. [メモの投稿, 編集, 削除機能の実装](#メモの投稿-編集-削除機能の実装)
1. [他人のメモの閲覧機能の実装](#他人のメモの閲覧機能の実装)

## Next.jsを使ってHello World

:::note info
学ぶこと

* Next.jsのセットアップ

:::

<details><summary>昔作ったテンプレもあります(不要な人はスルーでOK)</summary>

昔に自分用に作った[テンプレート](https://github.com/tokio-k/Template-Next.js-Tailwind-apollo)もあるので、ここから初めてもらってもOKです！！!

Next.js, apollo, Tailwind CSS, GraphQL Code Generator, ESLint, Prittterなどの設定がしてあります！少し古いかもですが。。

不要な人はスルーでOKです
</details>

#### Next.jsのプロジェクトを作成

まずは、Next.jsのプロジェクトを作成します。
ターミナルで以下のコマンドを実行することで、Next.jsのアプリケーションが構築されます。
memo-app-sampleは自分が作るアプリの名前です。


```ターミナル.
yarn create next-app --typescript memo-app-sample
```
VSCodeで見るとこんな感じになっています！！
[参考(Next.js公式ドキュメント)](https://nextjs.org/docs/api-reference/create-next-app)
![スクリーンショット 2022-03-16 15.50.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/f0a958fd-680b-c77f-41f1-b2fb7bf1ea78.png)

#### Next.jsを使ってHello World

Next.jsの構築ができたら、作ったアプリのrootディレクトリ(memo-app-sample)に移動します。

```ターミナル.
cd memo-app-sample
```

開発モードでNext.jsを立ち上げます。
rootディレクトリで以下を実行。
```ターミナル (memo-app-sample %).
yarn dev
```
http://localhost:3000 にアクセスしてこんな画面が表示されたら成功！

![スクリーンショット 2022-03-16 15.58.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/ae7f55fc-2fa2-0517-0087-6b15ac5c57c0.png)

Next.jsのアプリケーションが作れました！
pages/index.tsxの内容が表示されています

#### Githubにあげる

Next.jsのアプリが作成できたので、Githubにあげておきます。
Githubのアカウント作成などはお願いします。
https://github.com/new を開きRepository nameを入力してリポジトリの作成をします。

リポジトリ作成後の画面真ん中らへんに表示されているコマンドをrootディレクトリで実行。
```ターミナル (memo-app-sample %).
git remote add origin git@github.com:tokio-k/memo-app-sample.git
git push -u origin main
```

![スクリーンショット 2022-03-16 16.11.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/0adfaa9a-6049-b933-d75c-7730de46e7ca.png)

Githubに上げることができました!
![スクリーンショット 2022-03-16 16.13.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/adf0c7af-a58d-58cd-c0d1-cf502b47651f.png)

※ 今回は全てmainブランチで作業を進めていきます。
※ ブランチを分けて開発をする人は、mainブランチの変更を検知してデプロイするのでデプロイ時はmainへのマージまで忘れないでください！

## Next.jsをVercelにデプロイ

:::note info
学ぶこと

* Next.jsをVercelにデプロイする方法

:::

Vercelのアカウント作成はお願いします。
[Vercel](https://vercel.com)開いて、右上の方からアカウント作成できます。
Githubとの連携ですぐにできるはずです。

アカウントを作成したら、左のような画面になると思います。
|1|2|3|
|:-:|:-:|:-:|
|「New Project」からプロジェクトを作成|Import Git Repositoryから、先程GithubにあげたNext.jsのアプリを選択|Deployボタンを押す|
|![スクリーンショット 2022-03-16 16.28.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/591187e4-84c3-a257-61ee-8c3c0db38377.png)|![スクリーンショット 2022-03-16 16.29.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/8e369e8f-7c7d-2c3b-7112-a43cb298426a.png)|![スクリーンショット 2022-03-16 16.34.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/412f060c-92b0-2f8d-04e7-dde0d0e2f8d3.png)|

もしかしたら最初は、Githubとの連携設定などが必要になるかもです。
[この記事](https://reffect.co.jp/react/next-js-github-vercel)にそこらへんのことが書いてありました。

これでエラーがなければ、デプロイが完了です！！

|デプロイ完了画面|Go to Dashboard|
|:-:|:-:|
|![スクリーンショット 2022-03-16 16.44.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/101efdd6-8144-6b42-e33e-7925baff52b7.png)|![スクリーンショット 2022-03-16 16.40.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/7bab085a-156c-5495-15ca-4b094ec498bf.png)|

この記事のサンプルアプリの場合、https://memo-app-sample.vercel.app でアクセスできます。
Hobbyプランなら無料で使えます！

Githubのmainブランチにマージされると自動で、本番環境に反映されます！

今回は行いませんが、main以外のブランチにマージされると自動でプレビュー環境に反映されます！
developブランチへのマージのみプレビュー環境に反映などもできます！
[参考(zenn - Vercelのプレビューデプロイで特定のブランチ以外を無視する)](https://zenn.dev/catnose99/articles/b37104fc7ef214)

## Hasuraをローカルで立ち上げ

:::note info
学ぶこと

* Dockerを使ってHasuraを立ち上げる方法 (+ postgres)
* Dockerfileに変数を渡す方法
* Hasuraの変更をファイルとしてGitで管理する方法
* Hasuraを通してDBを変更する方法
* Hasuraの変更を自動で適応する方法 (マイグレーションの実行)

:::

#### Hasura用のリポジトリ作成
Hasura側に関係するファイルかNext.js側に関係するファイルかわかりやすいように、ここではプロジェクトを分けようと思います。

今回は、memo-app-sampleと同じ階層にmemo-app-sample-hasuraを作成します。
Qiitaというディレクトリの中に、memo-app-sampleとmemo-app-sample-hasuraを作っています。

```.ターミナル
memo-app-sample % cd ..
Qiita % mkdir memo-app-sample-hasura
Qiita % ls
memo-app-sample        memo-app-sample-hasura
Qiita % cd memo-app-sample-hasura
memo-app-sample-hasura %
```

<details><summary>コマンドについて詳しく</summary>

% の右側が実際に打ち込み実行されるコマンドです。

|コマンド|解説|
|-|-|
|〇〇 %|今、〇〇ディレクトリにいる|
|cd ..|親ディレクトリに移動|
|cd 〇〇|〇〇ディレクトリに移動|
|ls|今いるディレクトリにあるファイル/フォルダを一覧表示|
|mkdir 〇〇|〇〇ディレクトリを新規作成|

|コマンド|解説|
|-|-|
|memo-app-sample % cd ..|memo-app-sampleディレクトリから親のディレクトリ(Qiitaディレクトリ)に移動する|
|Qiita % mkdir memo-app-sample-hasura|Qiitaディレクトリの下にmemo-app-sample-hasuraディレクトリを作成する|
|Qiita % ls|Qiitaディレクトリに素材するフォルダを一覧で表示する (memo-app-sampleとmemo-app-sample-hasuraが表示されている)|
|Qiita % cd memo-app-sample-hasura|memo-app-sample-hasuraディレクトリに移動する|
|memo-app-sample-hasura %|今、memo-app-sample-hasuraディレクトリにいる|
</details>

#### Dockerfile / docker-compose.ymlの作成
ここからは、memo-app-sample-hasuraをVSCodeで開いて作業していきます。

まずは、Dockerfileとdocker-compose.ymlを作成します
Dockerfileの内容がCloud Run上で動きます。
docker-compose.ymlはlocal環境でのみ利用します。
![スクリーンショット 2022-03-25 10.19.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/961d8028-9b1a-f957-aba9-18236cd18b00.png)

#### Hasuraを立ち上げる

Hasuraを立ち上げるために、Dockerfile、docker-compose.ymlを編集して行きます。
localでのみ使う設定値を、.envファイルに書いて行きます。
localでしか使わないので、直接docker-compose.ymlに書いても問題はないと思いますが、複数箇所で使うのと設定値なのでとりあえず.envに書いておきます。

![スクリーンショット 2022-03-25 11.20.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/5330522d-c26c-abdf-c58f-23a56e9a193a.png)

**Dockerfile**
```Dockerfile
FROM hasura/graphql-engine:v2.3.1

# Dockerfileに渡す変数
ARG DB_USER
ARG DB_PASSWORD
ARG DB_HOST
ARG DB_PORT
ARG DB_DATABASE

# Hasura環境変数値設定
ENV HASURA_GRAPHQL_DATABASE_URL="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_DATABASE}"
ENV HASURA_GRAPHQL_ENABLE_CONSOLE="true"

# 通信用ポート開放
EXPOSE 8080
```

**docker-compose.yml**
```docker-compose.yml
version: '3.6'
services:
  postgres:
    image: docker.io/postgres:13.5-alpine3.15
    command: postgres -c jit=off
    container_name: memo_app_postgres13.5
    environment:
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=${POSTGRES_DB}
    restart: always
    volumes:
      - db_data:/var/lib/postgresql/data
  graphql-engine:
    container_name: memo_app_hasura
    build:
      context: .
      dockerfile: ./Dockerfile
      args:
        - DB_USER=${POSTGRES_USER}
        - DB_PASSWORD=${POSTGRES_PASSWORD}
        - DB_HOST=${POSTGRES_HOST}
        - DB_PORT=${POSTGRES_PORT}
        - DB_DATABASE=${POSTGRES_DB}
    restart: always
    depends_on:
      - postgres
    ports:
      - "8080:8080"
volumes:
  db_data:
```

${}の部分には、.envの値が入ります。
PostgresとDockerfileのHasuraを立ち上げます。

**.env**
```.env
# postgresql
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
POSTGRES_DB=memoapp
```

上記のようにファイルを編集し、ターミナルで以下を実行することで、Hasuraが立ち上がります。
```ターミナル (memo-app-sample-hasura %).
docker-compose up
```

http://localhost:8080/console にアクセスして以下のような画面が開けたら成功です。
![スクリーンショット 2022-03-25 15.23.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/8b7ef286-bf8f-26c7-16a8-80f4c7c7ea32.png)

#### Hasuraの変更をファイルで管理できるようにする
このままでもHasuraを使うことは出来ますが、Hasuraの変更をmigrationファイルとしてGithubで管理して、開発環境と本番環境を分けて作業できるようにして行きます。

1. hasura cliをインストールする
1. migrationファイル管理の初期準備をする
1. hasura cliを使ってhasura consoleを立ち上げる
1. migrationファイルを自動で適応するように修正する

の順番で進めて行きます。


まずはhasura cliをインストールします。

```.ターミナル(memo-app-sample-hasura %)
yarn add -D hasura-cli
```

![スクリーンショット 2022-03-25 15.45.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/88ced59c-c324-eea3-70d2-7656dc26ba67.png)

次に、migrationファイル管理の初期準備をします。
hasura initを実行することで、Hasuraの変更をmigrationファイルとして管理するためのhasuraフォルダが作成されます。

```.ターミナル
hasura init
```
hasura init を実行したことで、hasuraフォルダが作成されました。
この中にHasuraで操作した変更内容が保存されていきます。
![スクリーンショット 2022-03-25 16.01.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/cf5b60ba-cb48-6e12-7fa4-35fbd336bc12.png)

次に、hasura cliから hasura consoleを立ち上げます。

```.ターミナル
memo-app-sample-hasura % cd hasura
hasura % hasura console --endpoint http://localhost:8080
```

http://localhost:9695 でアクセスして以下の画面が表示されたら成功です。

![スクリーンショット 2022-03-25 16.36.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/98e5ded2-1b3a-20d7-8ce2-c39224a02d40.png)

hasura cliから立ち上げたconsoleから変更をすると、変更がmigrationファイルに保存されるようになります。

memoテーブルを作ってみましょう。

Create Tableをクリック
![スクリーンショット 2022-03-25 16.40.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/63f9fe61-16a6-96f6-6792-6e1718767a97.png)

Add Tableをクリック
![スクリーンショット 2022-03-25 16.43.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/fd0accca-e805-0ff2-63f7-d7a38c12b5c4.png)

memoテーブルが追加されました。
![スクリーンショット 2022-03-25 16.47.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/9f5d6bb6-f713-03b1-568e-b709c8118003.png)

metadataファイルやmigrationsファイルにも追加されました。
![スクリーンショット 2022-03-25 16.47.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/cadca5da-6432-accb-962d-14eea4447761.png)

hasura console上(GUI上)で行った変更がファイルとして出力されています。
このmigration/metadataファイルを適応することで、他の人も同様の変更を適応することができます。

開発環境でHasuraに変更を加え、migration/metadataファイルを作成し、本番環境にmigration/metadataファイルを適応することで本番環境を好きな時に同じ状態にすることができます。
これで、Hasuraの変更をファイルで管理できるようになりました。

しかし、ファイルとして保存するだけでは、変更は適応されないです。
1度、Hasuraを止めて動作確認をしてみます。

ボリュームも削除します。
```.ターミナル (memo-app-sample-hasura %)
docker-compose down -v  
```

もう1度起動してみます。
```.ターミナル (memo-app-sample-hasura % )
docker-compose up  
```
http://localhost:8080/console にアクセスしてみると、先程追加したmemoテーブルが存在しないことが確認できます。
![スクリーンショット 2022-03-26 19.21.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/ab2cd1ac-99d0-2166-5849-569f2524805f.png)

この状態から、hausra cliのコマンド`hasura migrate apply`などを使うことで、/hasuraに吐き出したファイルの変更(memoテーブルの追加)を適応させることもできますが、Dockerfileを変更することでサーバー起動時に自動で変更を適応することが可能です。

#### Hasuraのサーバー起動時に自動で変更を適応出来るようにする

このmigration/metadataファイルの適応を自動で行えるようにします。

最初に作ったDockerfileを修正します。

```Dockerfile
FROM hasura/graphql-engine:v2.3.1.cli-migrations-v3

# Dockerfileに渡す変数
ARG DB_USER
ARG DB_PASSWORD
ARG DB_HOST
ARG DB_PORT
ARG DB_DATABASE

# # migration, metadataをコピー
COPY ./hasura/migrations /hasura_migrations
COPY ./hasura/metadata /hasura_metadata

# Hasura環境変数値設定
ENV HASURA_GRAPHQL_DATABASE_URL="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_DATABASE}"
ENV HASURA_GRAPHQL_ENABLE_CONSOLE="true"
ENV HASURA_GRAPHQL_MIGRATIONS_DIR=/hasura_migrations
ENV HASURA_GRAPHQL_METADATA_DIR=/hasura_metadata

# 通信用ポート開放
EXPOSE 8080
```
cli-migrations-v3を付けるとサーバー起動時にmigration/metadataを自動で適応できるようになります。

COPYの部分でmigrations/metadataファイルをDockerイメージにコピーして、HASURA_GRAPHQL_MIGRATIONS_DIR、HASURA_GRAPHQL_METADATA_DIRで、コピーしたmigrations/metadataファイルを指定しています。

これで自動で適応されるようになります。

動作確認をしてみます。
Dockerfileを書き換えたのでビルドからします。
```.ターミナル (memo-app-sample-hasura %)
docker-compose up --build
```

http://localhost:8080/console にアクセスしてみると、先程追加したmemoテーブルが存在することが確認できました。
![スクリーンショット 2022-03-26 19.27.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/6e8c90c3-ad86-2460-4cdd-80b32518ef5f.png)

#### Githubにあげる

Hasuraでの開発の準備ができたので一旦Githubにpushします。
後で、Cloud Runにデプロイするためにも必要です。

https://github.com/new を開きRepository nameを入力してリポジトリの作成をします。

![スクリーンショット 2022-03-26 21.03.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/e26852d5-6a1b-160a-6548-ac03b4b78e0a.png)

まず上から2つを実行します。(readmeファイルの作成とgit initを実行)
```.ターミナル (memo-app-sample-hasura %)
echo "# memo-app-sample-hasura" >> README.md
git init
```

.gitignoreファイルを作成します。
.envファイルとnode_modulesファイルをgitで管理しないようにします。
![スクリーンショット 2022-03-26 21.11.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/7df39eb7-e307-ed74-3194-c18dee3163b8.png)

addして、commitして、pushします。

```.ターミナル
memo-app-sample-hasura % git add .
memo-app-sample-hasura % git commit -m "first commit"
memo-app-sample-hasura % git branch -M main
memo-app-sample-hasura % git remote add origin git@github.com:tokio-k/memo-app-sample-hasura.git
memo-app-sample-hasura % git push -u origin main
```

Githubにpush出来ました。
![スクリーンショット 2022-03-26 21.17.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/96992990-50a3-3507-7b09-ffd9b420a57c.png)

※ 今回は全てmainブランチで作業を進めていきます。
※ ブランチを分けて開発をする人は、mainブランチの変更を検知してデプロイするのでデプロイ時はmainへのマージまで忘れないでください！

## HasuraをCloud Runにデプロイ

:::note info
学ぶこと

* HasuraをCloud Runにデプロイする方法
* Cloud RunとCloud SQLを接続する方法
* Cloud Run上で動くHasuraに環境変数を設定する方法 (Cloud Buildの環境変数)
* Hasuraのadmin secretについて

:::

Hasuraをデプロイして行きます。
デプロイにはGCPの以下のサービスを利用します。
* Cloud Run・・Dockerコンテナをサーバレス環境で実行できるサービス。Hasuraをデプロイする。
* Cloud SQL・・データベース
* Cloud Build・・GitHubへpushをトリガーにビルドして、デプロイを自動化する
* サーバーレスVPCアクセス・・Cloud RunとCloud SQLを接続するために使用

以下の流れで進めて行きます。
* GCPのプロジェクトを作成
* Cloud SQLを使ってDBを作成
* サーバーレスVPCアクセスの設定
* Cloud Runにデプロイ
* 動作確認

#### GCPのプロジェクトを作成
まずは、GCPのプロジェクトを作成します。

GCPの[リソースの管理](https://console.cloud.google.com/cloud-resource-manager?hl=ja&_ga=2.46349921.753510163.1648344703-839872380.1648344703)ページに移動して、　「プロジェクトを作成」をクリックします。
![スクリーンショット 2022-03-27 10.33.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/cffffb1a-3893-a8b8-d4e4-b61d6aef0553.png)

プロジェクト名を入力して作成をクリックします。
今回は、「memo-app-sample-hasura」にしました。

この作成したプロジェクト内でこの記事の作業を進めて行きます。このプロジェクトの中にDBやHasuraを作っていくイメージです。

※ 自分は他にプロジェクトを作成して少し触ったことがあるので、初めてGCPを利用する場合とは少し違うところが出てくるかもしれません。例えば、支払い設定をしないとCloud SQLなど一部機能が利用できない事があるかもしれません。支払い設定が必要等表示されましたら各自従って進めてください。([プロジェクトの課金の有効化、無効化、変更](https://cloud.google.com/billing/docs/how-to/modify-project))

#### Cloud SQLを使ってDBを作成

左のナビゲーションメニューからSQLを選択します。(左上の3本横線をクリックで表示される)

![スクリーンショット 2022-03-27 10.45.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/edd374c3-2f3b-8793-37f1-a5081f17620f.png)

SQLのページに移動しました。
![スクリーンショット 2022-03-27 10.46.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/d80fcf74-daac-caaf-efed-26dfa71a55ec.png)

Google Cloud Platformという文字の右側を確認すると先程作成したプロジェクトが選択されているのが確認できました。
もし他のプロジェクトが指定されている場合は、▽をクリックして使いたいプロジェクトを選択してください。

「インスタンスを作成」をクリックして進めて行きます。


インスタンスを作成をクリックしたら、PostgresSQLを選択します。(HasuraのMySQL対応がbetaになっていましたし、この記事ではMySQLじゃなくてPostgresSQLで進めます。)

「インスタンスを作成するには、まず Compute Engine API を有効にする必要があります。詳細」って出てきたら、APIを有効にするをクリックします。

インスタンス作成のページに移動したら、各項目を入力して進めて行きます。

設定はサービスに合わせて調整していく必要がありますが、今回は低めのものを適当に選んでます。性能を上げれば料金が高くなります。
構成オプションを表示から詳細を設定できます。後から変更もできます。(出来ないものもあります。例えば、リージョンは変更できないと書いてあります。)

[Cloud SQL for PostgreSQLの料金](https://cloud.google.com/sql/docs/postgres/pricing)に設定値と料金について書いてあります。
記事の中にもある[Google Cloud 料金計算ツール](https://cloud.google.com/products/calculator)を使ってシュミレーションすることもできます。
今回の設定では、以下のように変更しました。
* マシンタイプは共有コアの1 vCPU、1.7GBにしました。(0.614にすると、最大接続数が25になります。Hasura繋いだらエラーが発生したので1個あげときました。)
* ストレージはSSD、容量は10GBにしました。
* バックアップは自動化するのチェックを外しました。


次に、接続を開いて、プライベートIPにチェックを付けます。
これにチェックを入れて、サーバーレスVPCアクセスを使い、Cloud RunとCloud SQLを接続します。
※プライベートIPを使わずに行うことも可能なようです。[詳細](https://cloud.google.com/sql/docs/postgres/connect-run?hl=ja)

![スクリーンショット 2022-03-27 11.40.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/11e95a22-3e2f-053a-7f0c-dbdb6ed02769.png)

接続を設定をクリックしてService Networking APIの有効化をし、IP範囲を割り振り、接続を作成します。
![スクリーンショット 2022-03-27 11.46.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/78acfa60-dc79-ecce-051b-e9521ef3fe0b.png)

設定が完了したらインスタンスを作成をクリックします！
ここで入力したパスワードは後でも使います。メモしておきましょう！
![スクリーンショット 2022-03-27 11.17.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/d2f6c1dc-2cc2-c3fe-5849-3282b6786fc4.png)

これで、インスタンス作成が始まりました！！
※ インスタンスを立ち上げると時間でお金がかかるので、勉強目的ならば使わない時は停止をしておくといいと思います。(記事書き終わった時には300円程かかってました。2週間位かけてゆっくり書いてます。止めていた時と止めていなかった時がありました。)

インスタンスの作成が完了したら、データベースを作成します。
※ インスタンスの作成には少し時間がかかるので、先に進んで、後からデータベースの作成をすることをオススメします。Cloud Buildのトリガーの設定をするところまでは進める事ができます。

左のメニューからデータベースを選択し、データベースの作成をクリックします。
docker-compose(.env)で設定していたlocalのデータベースと同じ名前にします。この記事ではmemoapp。
![スクリーンショット 2022-03-27 21.33.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/b3954b80-2f8f-ded6-9b73-057d11f5928f.png)

#### サーバーレスVPCアクセスの設定

次にVPCネットワークの設定をします。
Cloud RunとCloud SQLを接続するために利用します。
Cloud Runはサーバーレスなサービスで、動的IPアドレスです。
サーバーレス VPC アクセスを構成すると、サーバーレス環境で、内部 DNS と内部 IP アドレスを使用して VPC ネットワークにリクエストを送信できるみたいです。

左のメニューからVPCネットワークのサーバーレスVPCアクセスを開きます。
APIを有効にしてくださいと表示されたので、有効にしました。

サーバーレスVPCアクセスのページが開けたら、コネクタを作成します。
ネットワークは先程と同じdefaultを選択します。
設定したら作成をクリックします。
![スクリーンショット 2022-03-27 16.22.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/a8d3d134-c88d-b0f9-be68-968efaad135d.png)

これで、サーバーレスVPCアクセスの設定は完了です。
#### Cloud Runにデプロイ

Cloud Buildを使ってCloud Runにデプロイします。
Cloud BuildはCloud Runにあげるために必須ではないですが、Cloud Buildを使うことでデプロイの自動化ができます。
GitHubのmainへpushした時に、Cloud BuildでビルドしContainer Registryにpush、Container RegistryからCloud Runにデプロイという流れになります。
この[記事](https://qiita.com/riku-shiru/items/d3f7dda5a5e87c4b26e9#%E5%85%A8%E4%BD%93%E3%81%AE%E6%A7%8B%E6%88%90)を参考にしました

**GithubとCloud Buildの接続準備**
左のメニューからCloud Buildを開き、リポジトリを接続をクリックします。
![スクリーンショット 2022-03-27 19.46.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/87342e2e-72e0-776f-8b82-c948ba65adb2.png)
ソースを選択でGithubを選択し、リポジトリを選択で自分のアカウントと今回作ったリポジトリを選択し、リポジトリとの接続をします。(トリガーの作成は後でします。)
※ これを先にやることでCloud Runの設定時にリポジトリの選択ができたので、一旦このように進めます。ここではGithubとCloud Buildが接続出来るようにしただけです。

**Cloud Runの設定**
次にCloud Runの設定をして行きます。
左のメニューからCloud Runを開き、サービスの作成をクリックします。

![スクリーンショット 2022-03-27 16.27.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/19870d57-4f88-ca19-1ac8-93cb0d3590da.png)

「ソースリポジトリから新しいリビジョンを継続的にデプロイする」を選択し、SET UP WITH CLOUD BUILDをクリックします。
Cloud Build APIを有効にしてくださいと表示されたので有効にします。

Hasura側のリポジトリ(この記事なら、memo-app-sample-hasura)を選択して次へ進みます。
※ 別の時に発生した事象ですが、リポジトリ名が大文字の時にエラーが発生しました。小文字にしたら治りました。
![スクリーンショット 2022-03-27 20.41.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/bb9940b2-f8a5-61a2-43e1-3936f032a9f2.png)

ブランチは`^main$`、Build Typeは`Dockerfile`、ソースの場所は`/Dockerfile`を選択して保存をクリックします。
保存したら、Cloud Runの設定に戻るので、設定を続けます。
今回は以下のように設定しました。
* リージョン：東京
* リクエストの処理中にのみCPUを割り当てる
* 自動スケーリング：最小0、最大1
* すべてのトラフィックを許可する
* 未認証の呼び出しを許可

コンテナ、変数とシークレット、セキュリティは特に変更しませんでした。(後から変更もできます)
接続タブからVPCコネクタを設定します。先程設定したpostgres-connectを使ってCloud SQLと接続します。
![スクリーンショット 2022-03-27 20.53.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/49a570a8-fbf9-4269-9983-0b583035150a.png)

設定ができたら、作成をクリックします。
エラーになりましたが、気にせず進めます。(多分、環境変数の設定とかビルドの設定とか色々してないからエラーになってる。)
![スクリーンショット 2022-03-27 20.59.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/57dcc376-116e-b0a4-e839-0cb0981d299a.png)

**Cloud Buildのトリガーの設定 - 代入変数の設定**
次に、左のメニューからCloud Buildを再度開き、トリガーを開きます。
Cloud Runの作成時に一緒に作成したトリガーが表示されてると思うので、クリックして開きます。

トリガーの編集ページが開けたら、代入変数に環境変数を追加して行きます。 

今回追加するのは以下の値です。
* _DB_USER・・・postgres
* _DB_PASSWORD・・・Cloud SQLの設定時に自分で設定したパスワード
* _DB_HOST・・・Cloud SQLの概要のページに書いてあるプライベートIPアドレス ( 例：10.5x.xx.x
* _DB_PORT・・・5432
* _DB_DATABASE・・・memoapp

![スクリーンショット 2022-03-27 21.38.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/24022220-c064-8883-1ea7-2582353d97cd.png)

**Cloud Buildのトリガーの設定 - ビルド設定ファイルの設定**
トリガーの編集を開いたまま、次にビルド設定のファイルを編集して行きます。

代入変数の少し上にスクロールして、構成のロケーションに移動します。
GCP上で直接書く(インライン)か、ymlファイルとしてGithubで管理する(リポジトリ)か決める事ができます。
![スクリーンショット 2022-03-27 21.49.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/0790f8df-0600-4a15-9cfb-ade32b8604b0.png)

今回は、リポジトリで進めます。
インラインの下にあるエディタを開くをクリックし、右側に表示されたYAML構文をコピーします。

VSCodeを開いて、cloudbuild.ymlファイルを作成し、先程コピーしたYAML構文をcloudbuild.ymlにペーストします。
![スクリーンショット 2022-03-27 21.55.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/088d8984-16ae-fb69-e605-8dc93d05a79a.png)

ペーストが完了したらGCPに戻り、右側に開かれてる「インライン構成を編集」を閉じます。(ビルド構成タイプを変更したら、インライン構成が消えるので、ちゃんとコピーしましょう。)
そして、ロケーションをリポジトリに変更します。
Cloud Build 構成ファイルの場所は /cloudbuild.yml を指定します。
※ yamlでもymlでもどちらでもいいと思いますが、ymlをよく見る気がするのでyamlからymlに変更しました。

出来たら、トリガーの編集ページは保存します。保存をクリックします。

**cloudbuild.ymlの修正**

先程コピーしただけのcloudbuild.ymlファイルを修正して行きます。
先程代入変数に指定したDBの情報を、Dockerfileをビルドする時の引数に渡していきます。

docker-composeでDockerfileのビルド時の引数に5つを渡しているのと同じ形式で5つの値を渡していきます。

1つ目のstep(id: Build)がDockerfileをビルドしている部分なので、ここを修正して行きます。

cloudbuild.yml
```.cloudbuild.yml
steps:
  - name: gcr.io/cloud-builders/docker
    args:
      - build
      - "--build-arg"
      - "DB_USER=$_DB_USER"
      - "--build-arg"
      - "DB_PASSWORD=$_DB_PASSWORD"
      - "--build-arg"
      - "DB_HOST=$_DB_HOST"
      - "--build-arg"
      - "DB_PORT=$_DB_PORT"
      - "--build-arg"
      - "DB_DATABASE=$_DB_DATABASE"
      - '--no-cache'
      - '-t'
      - '$_GCR_HOSTNAME/$PROJECT_ID/$REPO_NAME/$_SERVICE_NAME:$COMMIT_SHA'
      - .
      - '-f'
      - Dockerfile
    id: Build
  - name: gcr.io/cloud-builders/docker
    args:
      - push
      - '$_GCR_HOSTNAME/$PROJECT_ID/$REPO_NAME/$_SERVICE_NAME:$COMMIT_SHA'
    id: Push
  - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk:slim'
    args:
      - run
      .
      .
      ....省略
```

<details><summary>コード追記分だけ確認</summary>

以下の10行を追加しました。
1つ目のstepsのargsのbuildの下の行に追加しました。
```cloudbuild.yml
- "--build-arg"
- "DB_USER=$_DB_USER"
- "--build-arg"
- "DB_PASSWORD=$_DB_PASSWORD"
- "--build-arg"
- "DB_HOST=$_DB_HOST"
- "--build-arg"
- "DB_PORT=$_DB_PORT"
- "--build-arg"
- "DB_DATABASE=$_DB_DATABASE"
```
</details>

これで、cloudbuild.ymlの編集は完了です。
#### 動作確認

コードの編集が終わったら、addしてcommitしてpushします。

```
git add .
git commit -m "create cloudbuild.yml"
git push origin main
```

mainブランチにpushした事をトリガーにビルドが実行されてCloud Runにデプロイするように設定したので、
pushするとビルドが始まります。

**ビルドを確認**
|Cloud Buildを開くと以下が表示される|←の1つ目の行のビルド(6559...)を選択すると以下が表示される|
|-|-|
|![スクリーンショット 2022-03-27 22.36.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/68250344-3791-fee3-4abd-6e7179e69c6c.png)|![スクリーンショット 2022-03-27 22.36.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/b334a25e-9f16-3a55-bb63-9e710929fee7.png)|

ビルドが成功してる事が確認できました。

次にCloud Runを開き、自分が作ったサービスを選択して、画像右のURL:をクリックします。
![スクリーンショット 2022-03-27 22.40.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/200440f1-ff0d-91ac-5a38-a74f7e516245.png)

すると、Hasuraのconsoleを開く事ができました。
これで、無事、デプロイ出来ている事が確認できました。
![スクリーンショット 2022-03-27 22.43.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/bcdb9d1e-b1c8-ae78-a7eb-6ee9b7046c6c.png)

※ Cloud Runを開いて、カスタムドメインを管理からドメインの変更ができます。
![スクリーンショット 2022-03-27 22.44.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/12ce33dc-3ecc-208e-523b-4a746722e8ed.png)

#### admin secretを設定
今のままだとURLを知ってる全員が好きにデータを触れる状態なので、admin secretだけ設定します。

Hasuraに環境変数HASURA_GRAPHQL_ADMIN_SECRETを設定することで、admin secretの設定ができます。

**local**
まずは、localのHasuraにadmin secretの設定をする所までします。
変更するファイルは、3つです。
* Dockerfile
* docker-compose.yml
* .env

```Dockerfile
省略...

ARG DB_DATABASE
# ↓ 追記
ARG HASURA_ADMIN_SECRET

省略...

ENV HASURA_GRAPHQL_ENABLE_CONSOLE="true"
# ↓ 追記
ENV HASURA_GRAPHQL_ADMIN_SECRET="${HASURA_ADMIN_SECRET}"
ENV HASURA_GRAPHQL_MIGRATIONS_DIR=/hasura_migrations

省略...
```

`ARG HASURA_ADMIN_SECRET`と`ENV HASURA_GRAPHQL_ADMIN_SECRET`を追加しました。
固定値にするなら、直接`ENV HASURA_GRAPHQL_ADMIN_SECRET=xxxx`と指定することも可能ですが、prodの時には変更すると思うのでDockerfileの外から値を渡すようにしています。

docker-compose.yml
```docker-compose.yml
省略...

      dockerfile: ./Dockerfile
      args:
        - DB_USER=${POSTGRES_USER}
        - DB_PASSWORD=${POSTGRES_PASSWORD}
        - DB_HOST=${POSTGRES_HOST}
        - DB_PORT=${POSTGRES_PORT}
        - DB_DATABASE=${POSTGRES_DB}
        # ↓ 追記
        - HASURA_ADMIN_SECRET=${HASURA_ADMIN_SECRET}
    restart: always

省略...
```
`HASURA_ADMIN_SECRET`を追加しました。
${}はenvファイルの値です。最後にenvファイルに追記します。

.env
```.env
# hasura
HASURA_ADMIN_SECRET=xxxxxx
```
最後の行に追加します。
xxxxxxは自分の好きな値を設定します。

これで、dockerを再起動させて開いてみます。
```.ターミナル
memo-app-sample-hasura % docker-compose up --build
```

http://localhost:8080/console にアクセスしてみると、写真のようにアクセスするためにはadmin secretの入力が求められるようになりました。
![スクリーンショット 2022-03-27 23.06.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/1e82b614-932d-6274-f85e-cb03f701e0b8.png)

localのHasuraにadmin secretの設定は完了しました。
**prod**
次は、本番環境のHasuraにadmin secretの設定をしていきます。
変更する箇所は2つです。
* Cloud Buildのトリガーの代入変数
* cloudbuild.ymlファイル

**Cloud Buildのトリガーの代入変数**
GCPを開き、Cloud Buildを開き、トリガーを開き、作成したトリガーを選択してトリガーの編集を開きます。
代入変数を追加から、_HASURA_ADMIN_SECRETを追加します。ここも自分の好きな値をValueに設定します。

![スクリーンショット 2022-03-27 23.28.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/81e7ed3c-7ffe-b5ba-8171-1b9cf4005a93.png)

**cloudbuild.ymlファイル**

```cloudbuild.yml
省略...

      - "DB_DATABASE=$_DB_DATABASE"
      # ↓ 追記　ここから
      - "--build-arg"
      - "HASURA_ADMIN_SECRET=$_HASURA_ADMIN_SECRET"
      # ↑ ここまで
      - '--no-cache'

省略...
```
他の環境変数を渡してるところと同じところに、この2行を追加します。
変更が完了したら、Githubにpushします。(pushの仕方は省略。上記に記載。)

ビルドが完了したら、Cloud RunにデプロイしたHasuraを開いて、localと同様にadmin secretを求める画面が表示されたら成功です。

これでadmin secretの設定が完了しました。
環境変数の追加はこのadmin secretを追加した流れと同じなので、他に環境変数を追加するときは参照してください。

## おまけ　Next.js src ディレクトリ

pagesなどが平置きになっているので、これからNext.js側での実装に入りコードが増えてくる前に、srcディレクトリにまとめておこうと思います。[参考](https://nextjs.org/docs/advanced-features/src-directory)

<p>
  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/97185736-cd82-6b43-e929-acb0ca81ac2a.png" />
  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/ad960b7e-6680-2e0a-8a41-2c74a2018b8b.png"/>
</p>

pagesとstylesをsrcディレクトリの中に配置しました。

## Apolloを使ってNext.jsからHasuraにリクエスト

:::note info
学ぶこと

* Apolloの導入方法
* Next.jsでApolloを便利に使うための最低限の実装(公式を参照)
* GraphQL Code Generatorの概要・導入方法
* Hasura consoleを使ってデータを追加する方法
* Hasura, Apollo, GraphQL Code Generatorを使った時の開発の流れ
* Next.jsからHasuraにリクエストを送る方法

:::

次は、Next.jsからHasuraにリクエストを送ってデータの取得ができるところまでを実装します。
初めに作ったNext.jsの方を進めていきます。

以下の流れで進めていきます。
* Apolloの導入、設定
* GraphQL Code Generatorの導入、設定
* Next.jsからHasuraにリクエストを送ってデータの取得

#### Apolloの導入、設定

[Apollo公式ドキュメント](https://www.apollographql.com/docs/react/get-started/)と[Next.jsの公式examples](https://github.com/vercel/next.js/tree/canary/examples/with-apollo)を参考に実装します。

**ライブラリのインストール**

Next.jsの方のルートディレクトリ(memo-app-sample)で以下を実行します。
```.ターミナル(memo-app-sample %)
yarn add @apollo/client graphql
```

**Apollo Clientの作成**

src/libs/apollo/apolloClient.tsファイルを作成します。
![スクリーンショット 2022-03-31 14.57.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/3cf06ae4-4a92-e145-8347-933064e0511a.png)

Vercel公式のexamplesを参考に作成していきます。

src/libs/apollo/apolloClient.ts
```src/libs/apollo/apolloClient.ts
import { createHttpLink, HttpLink, NormalizedCacheObject } from "@apollo/client";
import { ApolloClient, InMemoryCache } from "@apollo/client";
import merge from "deepmerge";
import isEqual from "lodash/isEqual";
import type { AppProps } from "next/app";
import { useMemo } from "react";

export const APOLLO_STATE_PROP_NAME = "__APOLLO_STATE__";

let apolloClient: ApolloClient<NormalizedCacheObject>;

const cache: InMemoryCache = new InMemoryCache({
  typePolicies: {},
});

const createApolloClient = () => {
  return new ApolloClient({
    ssrMode: typeof window === "undefined",
    link: createHttpLink({
        uri: process.env.NEXT_PUBLIC_HASURA_GRAPHQL_ENDPOINT,
        credentials: 'same-origin', // Additional fetch() options like `credentials` or `headers`
        headers: { 
          "x-hasura-admin-secret": "xxxxx",
        },// 動作確認のために使用。あとで消します。x-hasura-admin-secretはHasuraの全てのことができる権限なので、絶対に載せてはいけません。localの動作確認で使うだけなので、環境変数も使ってません。
      }),
    cache,
  });
};

export const initializeApollo = (initialState: AppProps["pageProps"] = null) => {
  const _apolloClient = apolloClient ?? createApolloClient();

  // ページにApollo Clientを使用したNext.jsのデータ取得メソッドがある場合、初期状態はここでハイドレーションされます。
  if (initialState) {
    // クライアント側のデータ取得中に読み込まれた既存のキャッシュを取得します。
    const existingCache = _apolloClient.extract();

    // 既存のキャッシュをgetStaticProps/getServerSidePropsから渡されたデータにマージします。
    const data = merge(initialState, existingCache, {
      // オブジェクトの平等性を利用して配列を結合する。
      arrayMerge: (destinationArray, sourceArray) => {
        return [
          ...sourceArray,
          ...destinationArray.filter((d) => {
            return sourceArray.every((s) => {
              return !isEqual(d, s);
            });
          }),
        ];
      },
    });

    // マージされたデータでキャッシュを復元する。
    _apolloClient.cache.restore(data);
  }

  // SSGとSSRでは、常に新しいApollo Clientを作成します。
  if (typeof window === "undefined") {
    return _apolloClient;
  }

  // クライアントで一度アポロクライアントを作成。
  if (!apolloClient) {
    apolloClient = _apolloClient;
  }

  return _apolloClient;
};

export const addApolloState = (client: ApolloClient<NormalizedCacheObject>, pageProps: AppProps["pageProps"]) => {
  if (pageProps?.props) {
    pageProps.props[APOLLO_STATE_PROP_NAME] = client.cache.extract();
  }
  return pageProps;
};

export const useApollo = (pageProps: AppProps["pageProps"]) => {
  const state = pageProps[APOLLO_STATE_PROP_NAME];
  const store = useMemo(() => {
    return initializeApollo(state);
  }, [state]);
  return store;
};
```

Vercel公式のexamplesをほぼ丸コピで作成しました。
apolloの公式ドキュメントとかと比べるとすごく長く書いてありますが、こう書く事でSSG時のキャッシュを扱うことができます。GetStaticPropsの中でaddApolloStateを呼び出して、クライアント側でSSG時のキャッシュを使えるようにします。
コメントを見るとどんな処理が行われてるか大体わかると思います！

必要になったライブラリをインストールします。
```.ターミナル(memo-app-sample %)
yarn add lodash.isequal deepmerge
yarn add -D @types/lodash.isequal
```

.env.localファイルを作成します。
![スクリーンショット 2022-03-31 18.15.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/a885830e-14c6-8d4b-5ac2-fe204e241b17.png)

.env.local
```
NEXT_PUBLIC_HASURA_GRAPHQL_ENDPOINT="http://localhost:8080/v1/graphql"
```
エンドポイントはlocal環境と本番環境で変わるので、環境変数にしました。

src/pages/_app.tsxを編集します。
_app.tsxのMyAppは全部のページの親になります。
ApolloProviderで囲うことで、どこのコンポーネントからでもApolloを使ってアクセスできます。

```src/pages/_app.tsx
import type { AppProps } from 'next/app'
import { ApolloProvider } from '@apollo/client';
import { useApollo } from '../libs/apollo/apolloClient';

function MyApp({ Component, pageProps }: AppProps) {
  const apolloClient = useApollo(pageProps);
  return <ApolloProvider client={apolloClient}><Component {...pageProps} /></ApolloProvider>;
}

export default MyApp

```

#### GraphQL Code Generatorの導入、設定

GraphQL Code Generatorを使うと、GraphQLのクエリやスキーマ等を元にTypeScriptの型やhooksなどを自動生成できます。

yarn graphql-codegen initを使って設定ファイル等を作成していくこともできますが、今回は直接自分で作成していきます！(init使うとjsファイルで作れないかもです。)

<details><summary>yarn graphql-codegen init使った例</summary>


```.ターミナル(memo-app-sample %)
yarn add -D @graphql-codegen/cli
yarn graphql-codegen init
```
設定ファイルの作成や実行するためのスクリプト登録などをします。

* ? What type of application are you building?
  * Application built with React
* ? Where is your schema?:
  * http://localhost:8080/v1/graphql
* ? Where are your operations and fragments?:
  * src/pages/**/*.{ts,tsx}
* ? Pick plugins:
  * TypeScript (required by other typescript plugins)
  * TypeScript Operations (operations and fragments)
  * TypeScript React Apollo (typed components and HOCs)
* ? Where to write the output:
  * src/libs/apollo/graphql.tsx
* ? Do you want to generate an introspection file?
  * No
* ? How to name the config file?
  * src/libs/apollo/codegen.yml
* ? What script in package.json should run the codegen?
  * codegen

![スクリーンショット 2022-03-31 21.50.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/d98c9cfa-3116-af92-1274-48f4d92448be.png)

src/libs/apollo/codegen.ymlファイルが作成されたり、
package.jsonのscriptにcodegenが追加されたりしました。

プラグインをインストールします。
```.ターミナル(memo-app-sample %)
yarn
```
</details>

**ライブラリのインストール**
```.ターミナル(memo-app-sample %)
yarn add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo
```

* @graphql-codegen/typescript
  * GraphQLスキーマに基づき、TypeScriptの基本型を生成するプラグイン
* @graphql-codegen/typescript-operations
  * GraphQLSchemaやGraphQL operations、fragmentsをもとに、TypeScriptの型を生成するプラグイン
* @graphql-codegen/typescript-react-apollo
  * TypeScriptの型付けをしたコンポーネントやHooksなどを生成するプラグイン

**codegen設定ファイルの作成**

src/libs/apolloに、codegen.jsファイルを作成します。

```src/libs/apollo/codegen.js
module.exports = {
    schema: [
      {
        "http://localhost:8080/v1/graphql": {
          headers: {
            "x-hasura-admin-secret":
              process.env.HASURA_GRAPHQL_ADMIN_SECRET,
          },
        },
      },
    ],
    documents: ["src/pages/**/*.{ts,tsx}"],
    overwrite: true,
    generates: {
      "src/libs/apollo/graphql.tsx": {
        plugins: [
          "typescript",
          "typescript-operations",
          "typescript-react-apollo",
        ],
        config: {
          skipTypename: false,
          withHooks: true,
          withHOC: false,
          withComponent: false,
          preResolveTypes: false,
        },
      },
    },
};
```

.env.localにHASURA_GRAPHQL_ADMIN_SECRETを追加します。

```.env.local
HASURA_GRAPHQL_ADMIN_SECRET=xxxxxxx
```

package.jsonにscriptを追加します。

![スクリーンショット 2022-04-01 9.39.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/4b5450f7-c986-187a-afec-f08de3ab3090.png)

```package.json
"codegen": "DOTENV_CONFIG_PATH=./.env.local graphql-codegen -r dotenv/config --config src/libs/apollo/codegen.js"
```

これで設定は完了です。
`yarn codegen`を実行することでHasuraを元に型などを作成してくれます。その準備ができました。※ Hasuraの起動を忘れずに！！

#### Next.jsからHasuraにリクエストを送ってデータの取得

Hasuraを閉じていたらまずHasuraを立ち上げてください。
cliからもHasura consoleを立ち上げておきます。

```.ターミナル
memo-app-sample-hasura %docker-compose up --build
```
```.ターミナル
memo-app-sample-hasura % cd hasura
hasura % hasura console --endpoint http://localhost:8080 --admin-secret xxxxxxx
```
※ admin secretの設定をしたので、cliでconsoleを立ち上げる時には、admin-secretの指定が必要です！！
※ 今回の動作確認だけならcliから立ち上げる必要はないですが、cliではない方でDB変更してしまったなどの間違いを減らすためにcliから作業します！！

Hasuraがcliから立ち上がったら、http://localhost:9695/console にアクセスします！

動作確認用のデータが欲しいので、DATA → memo → Insert Row → memoに値を入力 → Saveをクリックします。

![スクリーンショット 2022-04-01 9.52.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/9dd338ee-eee9-85cd-cb7c-b5214be4d7c6.png)

Browse Rowsを見てみると、無事にデータが入っていました。
![スクリーンショット 2022-04-01 9.53.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/8bab3558-ef80-6a58-cf88-b21e20a8c28c.png)

動作確認用のデータの追加は以上です！

次に、yarn codegenを使って型やhooksを作ります。

operation(query, mutation)が1つもないとエラーになってしまうみたいなので、src/pages/index.tsxにqueryを1つ追加しておきます。(一旦気にせずコピーしてください)

src/pages/index.tsxを修正します。
不要なものは全部削除しました。(src/pages/indes.tsxにこのままコピペで大丈夫です)

```src/pages/index.tsx
import { gql } from '@apollo/client'
import type { NextPage } from 'next'

const Home: NextPage = () => {
  return (
    <div>
      <p>ここはTOPページ</p>
    </div>
  )
}

export default Home

gql`
  query getMemoTest {
    memo {
      id
      memo
    }
  }
`
```

gqlの中がGraphQLの構文で、yarn codegenを実行するとこのgqlを元にhooksとそれに関連する型を作成してくれます。

Hasura Consoleで、APIタブを開きExplorerを開くとクエリの作成/動作確認が楽にできます。MyQueryは名前で今回はgetMemoTestに変更しました。
![スクリーンショット 2022-04-01 9.57.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/4d7175fa-556a-1947-1897-4511544f46b5.png)

yarn codegenを実行してみます！
```.ターミナル(memo-app-sample % )
yarn codegen
```
成功したらこんな感じ
![スクリーンショット 2022-04-01 10.08.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/4f214fe6-c0fa-3c54-4fb1-3adde2a0ecae.png)

成功したら、src/libs/apollo/graphql.tsxが作成されます！

src/libs/apollo/graphql.tsxの下の方にある、`useGetMemoTestQuery`がgqlから作られたhooksです。
![スクリーンショット 2022-04-01 10.14.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/cc4779e9-5b2e-fb82-f69f-1b3c3fd9a148.png)
* type GetMemoTestQuery
→ useGetMemoTestQueryで帰ってくるデータの型
* type GetMemoTestQueryVariables
→ useGetMemoTestQueryのvariables(引数的なやつ)の型

yarn codegenの動作確認が終わりました！！
yarn codegenを使ってhooksの作成ができました。

実際にNext.jsからHasuraにリクエストを送ってみます！

yarn codegenで作成したuseGetMemoTestQueryをsrc/pages/index.tsxで実行してみます。

```src/pages/index.tsx
import { gql } from '@apollo/client'
import type { NextPage } from 'next'
import { useGetMemoTestQuery } from '../libs/apollo/graphql'

const Home: NextPage = () => {
  const { data } = useGetMemoTestQuery();
  console.log(data)

  return (
    <div>
      <p>ここはTOPページ</p>
    </div>
  )
}

export default Home

gql`
  query getMemoTest {
    memo {
      id
      memo
    }
  }
`

```

http://localhost:3000 にアクセスしてみると、取得したデータがconsoleに出力されるのが確認できます！

![スクリーンショット 2022-04-01 11.52.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/83c54f7a-27fa-0b98-bfd1-bf446e7fa0a2.png)

Next.jsからHasuraへのリクエストは成功しました👏

Hasuraからデータを取得したい時の開発の流れはこんな感じにします！！
1. Hasura consoleを使ってクエリを作る
1. gqlタグの中に書く
1. yarn codegenを実行する
1. コンポーネントで作成されたhooksを利用する

Next.jsからHasuraへリクエストしてデータの取得はできたので、次は認証周りの実装をしていきます。

## 認証の実装

:::note info
学ぶこと

* Auth0の設定方法
* Next.jsでAuth0を使う方法
* Layoutコンポーネント
* Auth0とHasuraを一緒に使う時の設定
* Apolloを使ったリクエストのヘッダーにJWTを載せる方法
* HasuraのPermissionsについて
* HasuraのRelationshipsについて
* カスタム hooksの作り方,使い方
* 環境変数の設定、環境の分け方について

:::

認証にはAuth0を使います。
ログイン/新規登録機能を作っていきます。

以下の流れで作成していきます。
1. ユーザーテーブルの作成
1. Auth0アプリの作成
1. Next.jsにログイン機能の追加
1. HasuraとAuth0の接続設定
1. JWTを使ってHasuraにアクセス
1. Auth0とDBの同期
1. Hasuraの権限設定

#### ユーザーテーブルの作成

http://localhost:9695/console/data/default/schema/public を開き、Hasura consoleからテーブルを作成します。

memoテーブルを作成した時と同様にテーブルを作成します。

* id
  * integer(auto-increment)
  * 主キー
* uid
  * Text ※ ここの型は何が正しいのかわかってません🙇‍♂️
  * Googleの識別id
* name
  * Text
  * 名前を入れる

![スクリーンショット 2022-04-01 13.34.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/9859f599-45cb-0619-9753-576dfbcd50cb.png)

![スクリーンショット 2022-04-01 13.38.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/ce48ee32-9891-0d88-84d7-6374b6113a9a.png)
user テーブルの作成が完了しました。

#### Auth0アプリの作成
Auth0アプリを作成していきます。
https://auth0.com/jp を開いて、アカウント作成/ログインをします。

Tenant Domainを作成します。

![スクリーンショット 2022-04-01 13.42.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/21de1e63-93ba-d787-bb27-2d51dccb8359.png)

ダッシュボードが開けたら成功です。
![スクリーンショット 2022-04-01 13.44.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/3fe88255-2b0e-677b-12f8-095a45d6666c.png)

左のメニューから、Applicationsを選択し、+ Create Applicationをクリックします。

名前を入力し、Single Page Web Applicationsを選択して作成します。

作成できたら、Settingsを開き、以下3つの項目を設定します。
入力したら、Save Changesをクリックします。
* Allowed Callback URLs
* Allowed Logout URLs
* Allowed Web Origins

3項目全て、http://localhost:3000 と入力してください。

![スクリーンショット 2022-04-01 13.48.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/d5ff294d-88b7-02b3-652a-4e0feb5b1ee0.png)

今回は、Google ログインだけを考えることにするので、Connectionsタブを開き、パスワード認証のチェックを外しておきます。

![スクリーンショット 2022-04-01 14.26.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/1b88b3ab-7c75-fd5c-f17f-1cfe127dd687.png)


#### Next.jsにログイン機能の追加

ライブラリ(React用のAuth0のSDK)をインストールします。

```.ターミナル(memo-app-sample %)
yarn add @auth0/auth0-react
```

src/pages/_app.tsxを編集します。
```src/pages/_app.tsx
import type { AppProps } from 'next/app'
import { ApolloProvider } from '@apollo/client';
import { useApollo } from '../libs/apollo/apolloClient';
import { Auth0Provider } from '@auth0/auth0-react';

function MyApp({ Component, pageProps }: AppProps) {
  const apolloClient = useApollo(pageProps);
  return (
    <Auth0Provider
      domain={process.env.NEXT_PUBLIC_AUTH0_DOMAIN}
      clientId={process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID}
      redirectUri={`${process.env.NEXT_PUBLIC_APP_URL}/`}
    >
      <ApolloProvider client={apolloClient}>
        <Component {...pageProps} />
      </ApolloProvider>
    </Auth0Provider>
  );
}

export default MyApp
```
Auth0Providerで囲うことで、コンポーネント内でAuth0のログインなどの機能を使うことができます。

.env.localに`NEXT_PUBLIC_AUTH0_DOMAIN`、`NEXT_PUBLIC_AUTH0_CLIENT_ID`、`NEXT_PUBLIC_AUTH0_CLIENT_ID`を追加します。

```.env.local

NEXT_PUBLIC_AUTH0_DOMAIN=xxxxxx.jp.auth0.com
NEXT_PUBLIC_AUTH0_CLIENT_ID=xxxxxXxXXXxxXx
NEXT_PUBLIC_APP_URL=http://localhost:3000
```

ログインボタンはヘッダーなどに置かれることが多いので、全ページで共有のコンポーネントを作成して実装していきます。
src/component/Layout/index.ts　ファイルを作成します。
このファイルにログインボタンなどを追加します。

```src/component/Layout/index.ts
import { useAuth0 } from "@auth0/auth0-react";

type Props = {
  children: React.ReactNode
}

export const Layout = ({children}: Props) => {
  const {loginWithRedirect, logout, isAuthenticated} = useAuth0()

  const handleLogout = () => {
    logout()
  }
  return (
    <div>
      <div>
        {isAuthenticated ?
          <button onClick={handleLogout}>ログアウト</button>
          : <button onClick={loginWithRedirect}>ログイン</button>
        }
      </div>
      <hr/>
      <div>
        {children}
      </div>
    </div>
  );
};

```

src/pages/index.tsxでLayoutコンポーネントを呼び出します。
```src/pages/index.tsx
import type { NextPage } from 'next'
import { Layout } from '../component/Layout';

const Home: NextPage = () => {
  return (
    <Layout>
      <p>ここはTOPページ</p>
    </Layout>
  )
}

export default Home
```
不要なコードは削除しました。

ログインボタンが表示されたら、ログインをしてみます。
![スクリーンショット 2022-04-06 12.24.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/91b27401-a1e4-d498-ff51-66cca7c0afe0.png)

ログインボタンを押した後にこんな画面が表示され、そのまま進めてログインすることができたら成功です。
![スクリーンショット 2022-04-01 14.27.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/1d1ee751-f3d4-1512-8933-d4822c9c80b3.png)

これでログイン機能は作成できました。
※ DBにユーザーのデータの登録はできていない状態なのであとで修正します。

#### HasuraとAuth0の接続設定

ログインしているユーザーのみがデータにアクセスできるなど、認可の機能がHasuraにはあります。以下の手順で設定をしていきます。

* Auth0でAPIの作成
* Auth0のRulesを使って、カスタムクレームを追加
* Hasura JWT Secretの設定

**Auth0でAPIの作成**

![スクリーンショット 2022-04-01 14.56.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/a6d4a4c4-d46e-3c1a-644d-f295fa96a5c9.png)

APIsを開いて、Create APIをクリックして作成します。

NameやIdentifierを入力して作成します。RS256のままで大丈夫です！

![スクリーンショット 2022-04-01 15.11.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/99a2f9dc-5b0e-8f8a-9bc3-0262abffdd78.png)

src/pages/_app.tsxにaudienceを追加します。
先程作成したAPIを指定しています。写真を例にすると`http://localhost:8080/v1/graphql`です。

```_app.tsx
<Auth0Provider
      domain={process.env.NEXT_PUBLIC_AUTH0_DOMAIN}
      clientId={process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID}
      redirectUri={`${process.env.NEXT_PUBLIC_APP_URL}/`}
      audience={process.env.NEXT_PUBLIC_HASURA_GRAPHQL_ENDPOINT} // 追加
    >
```

Identifierの値をNEXT_PUBLIC_HASURA_GRAPHQL_ENDPOINTと同じ値にしているので、NEXT_PUBLIC_HASURA_GRAPHQL_ENDPOINTを使います。

**Auth0のRulesを使って、カスタムクレームを追加**
Auth0のRulesという機能を使って、Hasuraが認可の制限をするための情報を追加します。

Rulesを開いて、+ Createボタンを押します。
Empty Ruleから作りました。
![スクリーンショット 2022-04-01 14.51.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/7393941c-e6ae-6ba0-41f2-c3bc05c6276c.png)

![スクリーンショット 2022-04-01 14.54.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/6374811f-5686-3b10-ad77-dd805949cc5d.png)

`hasura-jwt-claims`という名前をつけて、Scriptを以下のように書き換え、Save changesをクリックします。

```.js
function (user, context, callback) {
  const namespace = "https://hasura.io/jwt/claims";
  context.accessToken[namespace] =
    {
      'x-hasura-default-role': 'user',
      'x-hasura-allowed-roles': ['user'],
      'x-hasura-user-id': user.user_id
    };
  callback(null, user, context);
}
```

カスタムクレーム追加の設定は完了です。

**Hasura JWT Secretの設定**

https://hasura.io/jwt-config を開きます。
`Select Provider`でAuth0を選択し、`Enter Auth0 Domain Name`にAuth0のdomainを指定して、GENARATE CONFIGをクリックします。

作成されたJWT Configをコピーします。
Hasuraリポジトリの方の、.envファイルにコピーします。

名前はHASURA_JWT_SECREにしました。
![スクリーンショット 2022-04-01 15.26.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/c1b6c849-1b17-da71-a83f-b448a044cac2.png)

HASURA_JWT_SECREをHasuraに設定するために、docker-composeとDockerfileに追加します！

docker-compose.yml
```docker-compose.yml
args:
        - DB_USER=${POSTGRES_USER}
        - DB_PASSWORD=${POSTGRES_PASSWORD}
        - DB_HOST=${POSTGRES_HOST}
        - DB_PORT=${POSTGRES_PORT}
        - DB_DATABASE=${POSTGRES_DB}
        - HASURA_ADMIN_SECRET=${HASURA_ADMIN_SECRET}
        // ↓ 追加
        - HASURA_JWT_SECRET=${HASURA_JWT_SECRET} 
```
Dockerfile
```Dockerfile
..省略
# Dockerfileに渡す変数
ARG DB_USER
..
ARG HASURA_ADMIN_SECRET
// ↓ 追加
ARG HASURA_JWT_SECRET

..省略

ENV HASURA_GRAPHQL_ADMIN_SECRET="${HASURA_ADMIN_SECRET}"
// ↓ 追加
ENV HASURA_GRAPHQL_JWT_SECRET="${HASURA_JWT_SECRET}"
..省略
```
※ Dockerfileを変更したので、`docker-compose up --build`で再ビルド

これで設定は終わりです。

#### JWTを使ってHasuraにアクセス
※ ここの良い実装方法がわかってないのでコメント頂けると嬉しいです。
※ 試してみてうまく動いた方法で進めます。

src/libs/apollo/apolloClient.tsを編集します。

```src/libs/apollo/apolloClient.ts
import { createHttpLink, NormalizedCacheObject } from "@apollo/client";
import { ApolloClient, InMemoryCache } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
import { useAuth0 } from "@auth0/auth0-react";
import merge from "deepmerge";
import isEqual from "lodash/isEqual";
import type { AppProps } from "next/app";
import { useMemo } from "react";

export const APOLLO_STATE_PROP_NAME = "__APOLLO_STATE__";
let apolloClient: ApolloClient<NormalizedCacheObject>;
let getAccessTokenSilently: () => Promise<string | null>;

const cache: InMemoryCache = new InMemoryCache({
  typePolicies: {},
});

const httpLink = createHttpLink({
  uri: process.env.NEXT_PUBLIC_HASURA_GRAPHQL_ENDPOINT,
  credentials: "same-origin", // Additional fetch() options like `credentials` or `headers`
});

const authLink = setContext(async (_, { headers }) => {
  const getAccessToken = async () => {
    try {
      return await getAccessTokenSilently();
    } catch (error) {
      return null;
    }
  };
  const accessToken = await getAccessToken();
  console.log(accessToken); //動作確認用にconsoleに出力してます。あとで消してください。
  return accessToken
    ? { headers: { ...headers, authorization: `Bearer ${accessToken}` } }
    : { headers };
});

const createApolloClient = () => {
  return new ApolloClient({
    ssrMode: typeof window === "undefined",
    link: typeof window === "undefined" ? httpLink : authLink.concat(httpLink),
    cache,
  });
};


// ...省略 initializeApollo と addApolloStateの変更なし

export const useApollo = (pageProps: AppProps["pageProps"]) => {
  const auth0 = useAuth0();
  getAccessTokenSilently = auth0.getAccessTokenSilently;
  const state = pageProps[APOLLO_STATE_PROP_NAME];
  const store = useMemo(() => {
    return initializeApollo(state);
  }, [state]);
  return store;
};

```

getAccessTokenSilentlyが、Auth0のaccessTokenを取得する関数です。
ログインしており、accessTokenが取得できた場合は、headerにaccessTokenを載せるようにします。
accessTokenの中に、先程rulesで指定したx-hasura-default-roleやx-hasura-user-idなどがあります。


次に、_app.tsxを修正します。
useApolloの中でuseAuth0を使っているので、Auth0Providerの中で、useApolloを呼ぶように修正します。
```_app.tsx
import type { AppProps } from 'next/app'
import { ApolloProvider } from '@apollo/client';
import { useApollo } from '../libs/apollo/apolloClient';
import { Auth0Provider } from '@auth0/auth0-react';
import { VFC } from 'react';

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <Auth0Provider
      domain={process.env.NEXT_PUBLIC_AUTH0_DOMAIN}
      clientId={process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID}
      redirectUri={`${process.env.NEXT_PUBLIC_APP_URL}/`}
      audience={process.env.NEXT_PUBLIC_AUTH0_AUDIENCE}
    >
      <ApolloContext pageProps={pageProps}>
        <Component {...pageProps} />
      </ApolloContext>
    </Auth0Provider>
  );
}

type ApolloContextProos = {
  pageProps: AppProps["pageProps"];
  children: React.ReactNode;
};

const ApolloContext: VFC<ApolloContextProos> = (props) => {
  const apolloClient = useApollo(props.pageProps);
  return <ApolloProvider client={apolloClient}>{props.children}</ApolloProvider>;
};

export default MyApp
```

これでHasuraアクセス時にJWTを載せるためのコードの編集は終わりです。
動作確認をしてみます！


src/pages/index.tsxにもう1度、getMemoTestを追加して、`yarn codegen`を実行します。

```src/pages/index.tsx
import { gql } from '@apollo/client';
import type { NextPage } from 'next'
import { Layout } from '../component/Layout';
import { useGetMemoTestQuery } from '../libs/apollo/graphql';

const Home: NextPage = () => {
  const { data } = useGetMemoTestQuery()
  return (
    <Layout>
      <p>ここはTOPページ</p>
    </Layout>
  )
}

export default Home

gql`
  query getMemoTest {
    memo {
      id
      memo
    }
  }
`
```

これで動作確認をしてみます。
 `yarn dev`で立ち上げて、http://localhost:3000/ にアクセスします。

|未ログイン|ログイン済み|
|-|-|
|![スクリーンショット 2022-04-02 11.28.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/76469f0e-8b4a-83b9-629f-9de7e402973e.png)|![スクリーンショット 2022-04-02 11.27.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/4ed4dc3e-1599-8d58-1259-7a02c8effbb8.png)|
|null|jwtがある|

JWTがコンソールに出力されているのが確認できました！
https://jwt.io/ を使ってこのJWTを確認してみます！

コンソールに出力されたJWTをコピーしてEncodedにコピーすると
先程rulesで指定したx-hasura-default-roleやx-hasura-user-idなどが存在することがわかりました。
![スクリーンショット 2022-04-02 11.38.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/e2d48a27-d706-a152-c9f2-ad1f81e9f83d.png)

次に、開発者ツールのNetworkタブをみて、ヘッダーにJWT(accessToken)を載せてリクエストしていることを確認してみます。

写真で選択している `graphql`というのがuseGetMemoTestQueryのリクエストです。
![スクリーンショット 2022-04-02 11.41.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/c04faac3-55a1-0db2-5309-642ba9f48e34.png)

Headersを見ると(写真右下) Bearerの後に、JWTがあることがわかります。
JWT(accessToken)をヘッダーに載せてリクエストすることが成功しました。

Payloadを見るとクエリの内容を見ることができます。
Previewを見るとクエリの結果を見ることができます。

Previewを見ると今回はエラーが出ているようです。
![スクリーンショット 2022-04-02 11.46.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/0a67b41c-b56f-286a-69db-cf0f3419194b.png)

これは、現在Hasuraでは、user roleには全ての権限がない為です。
Hasuraでは権限を持っているfiledのみを見ることができ、userはfield memoをselectする権限を持っていないため、not foundと表示されています。

※ 今までmemoを取得できたのは、admin secretというadmin roleで実行することができる値を渡していたからです。admin roleは全ての権限を持っています。

次にuser roleがmemoを取得できるように、権限を追加します。

![スクリーンショット 2022-04-02 11.50.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/2032b66b-5d28-1d60-d8d2-033a58b778fc.png)

http://localhost:9695/console/data/default/schema/public/tables/memo/permissions を開きます。

ここで、memoの権限を設定することができます。
Enter new Roleにuserと入力し、Selectを編集します。
Selectにカーソルを合わせて鉛筆マークから編集できます。

![スクリーンショット 2022-04-02 11.52.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/7974631c-352c-9278-b9a2-7ccb0bb3fa4d.png)
userにmemoのselectの全権限を追加しました。

権限が追加できたので、もう1度、リクエストしてみます。
すると、無事にデータが取得できていました👏
![スクリーンショット 2022-04-02 11.56.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/763c659d-f021-77f5-f556-5d7b269b5510.png)

これで、HasuraとApolloとAuth0の連携はうまくいきました！！
memoのselectに権限を与えたように、userが扱えるfiledを1つずつ設定することでセキュアなサーバー(Hasura)になっています。
自分のデータのみにアクセスできるなどの細かい設定も行うことができます。
これで、認可の仕組みの実装が終わりました！！
また、未ログインユーザーも取得できるようにするなど権限の設定もできます。これはあとで行います。

#### Auth0とDBの同期
ログインした時にDB上にユーザーを作成するようにして、ユーザーを同期できるようにします！

**改修後の流れ**
1. ログインボタンを押す(クライアント)
1. ユーザーが存在するか確認する(Auth0のrulesからHasuraにアクセス)
    1. ユーザーが存在したらクライアントに戻す
    1. ユーザーが存在しなかったらユーザーを作成して、クライアントに戻す
1. ログイン完了

**現在の流れ**
1. ログインボタンを押す
2. ログイン完了

Auth0のrulesを追加していきます。
ログインしているユーザーはDB上にも存在するようにしていきます。

[Hausra公式ドキュメント](https://hasura.io/learn/ja/graphql/hasura/authentication/4-user-sync-rule/)に従って実装していきます！(ほぼそのままです)

先程と同様にAuth0のrulesに処理を追加します！
Createボタンから追加していきます！
![スクリーンショット 2022-04-02 14.07.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/2ac40970-5313-8289-dd40-fda97faf8043.png)

`select-and-insert-user`という名前でrulesを作りました！

```.js
function (user, context, callback) {
  const axios = require('axios');
  
  const uid = user.user_id;
  const name = user.name;
  
  const url = `${configuration.HASURA_URL}/v1/graphql`;
  const header = {
    'content-type' : 'application/json',
    'x-hasura-admin-secret': configuration.HASURA_GRAPHQL_ADMIN_SECRET
  };
  
  const graphqlQuery = {
    query: `mutation ($name: String!, $uid: String!) {
  insert_user_one(object: {name: $name, uid: $uid}, on_conflict: {constraint: user_uid_key, update_columns: uid}) {
    id
  }
}`,
    variables: {name: name, uid: uid}
  };
  
  axios.post(url,{...graphqlQuery},{headers: header})
    .then(res => {
      const userId = res.data.data.insert_user_one.id;
      context.idToken[configuration.NAMESPACE+'/userId'] = userId;
      return callback(null, user, context);
  }).catch(err => {
      return callback(new Error(`Hasuraへのリクエストでエラーが発生しました。`));
  });
}
```

今回、「既に同じuidが存在する場合、そのユーザーのuidを更新する」というようにしています (= 何も変わらない。)
戻り値(id)を返すためには何か更新しないといけないみたいです！
本来なら、最終ログイン時間などを更新するといいと思いますが、テーブル増やさずシンプルにしたいので一旦uidを更新にしました。

configuration.〇〇は環境変数みたいなもので、Rulesの下の方から追加できます！
本番環境とlocal開発時に同じコードを使いたいので、環境変数を使っています。

![スクリーンショット 2022-04-02 15.08.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/633d5cc7-2e39-d8e7-0371-531ccda62b31.png)

* NAMESPACE
  * 値：http://localhost:3000
  * `http://` or `https://`から始まればなんでもいいはずですが、環境変数を減らして分かりやすくするためにクライアントと同じにしておきます。
* HASURA_URL
  * あとで解説。
  * Hasuraを示すが、localhostでは使えない。
* HASURA_GRAPHQL_ADMIN_SECRET
  * 自分が設定したadmin secret

localでの開発時には、HASURA_URLには、ngrokを使って値を設定します。
理由は、RulesがAuth0のサーバー上で実行されるので、手元のlocalhostへはアクセスできないためです！[参考](https://community.auth0.com/t/how-to-test-a-rule-when-developing-on-localhost/6351)

本番環境ではngrokは不要です

local開発時に毎回やる必要があり面倒なのですが、他に良い方法が見つかっていないので、もし良い方法があればコメントいただけると嬉しいです。🙇‍♂️
Hasuraのdev環境を作って、開発もHasuraはlocalではなくdev環境でやるのでもいいと思います！(今回はlocalで！)

ngrokのインストールや使い方は以下の記事などを参考にしてください。
https://qiita.com/mininobu/items/b45dbc70faedf30f484e

インストールができたらngrokを使い、localのHasuraを公開し、公開したURLをHASURA_URLに設定します。

```.ターミナル
ngrok http 8080
```

```
.
.
Forwarding                    https://XXXXXXXX.ngrok.io -> localhost:8080 
```
 https://XXXXXXXX.ngrok.io の部分をHASURA_URLに入力します。
※ ngrokは無課金だと一定時間で終わってしまうので、時間を空けて開発するときは、再度`ngrok http 8080`を実行し、HASURA_URLに設定しなおします。

3つの変数の設定とrules(select-and-insert-user)の作成が完了したら、準備は完了です！

ログインしみてみます！！

ログイン時にユーザーが登録されました！
ログアウトし、ログインし直してもユーザーは追加されていません。
新規登録時のみユーザーの作成ができていました。

![スクリーンショット 2022-04-02 14.55.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/0755f7b4-8eeb-6c66-1b5e-fbde4d826cd7.png)

これでログイン機能は完成です。

(いい方法なのかわからないのですが) クライアントからuserのidを取得できるようにします。
rulesの`context.idToken[configuration.NAMESPACE+'/userId'] = userId;`でuserのidを設定しているので、クライアントから取得できます。

クライアントから取得するときは以下のようにします。
```.ts
const { user } = useAuth0()
console.log(user?.[`http://localhost:3000/userId`]) //1
```

環境ごとに値が変わるので、環境変数を使います。(既存のNEXT_PUBLIC_APP_URLを使用)
```.ts
console.log(user?.[`${process.env.NEXT_PUBLIC_APP_URL}/userId`]) //1
```

userId取得時に毎回書くのが大変なので、hooksを作成します。
src/hooks/useAuth0User.tsファイルを作成します。
```src/hooks/useAuth0User.ts
import { useAuth0 } from "@auth0/auth0-react";

export const useAuth0User = () => {
  const auth0 = useAuth0();
  return {
    ...auth0,
    userId: auth0.user?.[`${process.env.NEXT_PUBLIC_APP_URL}/userId`],
  };
};
```

これで以下のようにuserのidを使えるようになりました。
useAuth0の代わりにuseAuth0Userを使います。
```.ts
const { userId } = useAuth0User()
console.log(userId)
```

ちなみにuseAuth0UserでuseAuth0の他の値も扱えます。
```.ts
const { loginWithRedirect, logout, isAuthenticated, user, userId } = useAuth0User()
```
#### Hasuraの権限設定

先程、memoのselectに設定したように、他にもHasuraの権限(Permissions)の設定をしていきます！

まずは、未認証のユーザーに権限の設定をできるようにします。

Dockerfileを編集して、`ENV HASURA_GRAPHQL_UNAUTHORIZED_ROLE="anonymous"`を追加します。
```Dockerfile
...省略
# Hasura環境変数値設定
ENV HASURA_GRAPHQL_DATABASE_URL="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_DATABASE}"
ENV HASURA_GRAPHQL_ENABLE_CONSOLE="true"
ENV HASURA_GRAPHQL_UNAUTHORIZED_ROLE="anonymous"
ENV HASURA_GRAPHQL_ADMIN_SECRET="${HASURA_ADMIN_SECRET}"
...省略
```

HASURA_GRAPHQL_UNAUTHORIZED_ROLEは、未認証のユーザーのRoleを設定するための変数です。
ここでは「anonymous」が未認証のユーザーになります。
anonymousにmemoのselect権限を追加します。

![スクリーンショット 2022-04-05 12.23.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/f797d498-f9b8-e72e-6c9b-6250d0f999aa.png)


これで、未ログイン時にもmemoを取得できるようになりました。

次にuser roleに、userとmemoの権限を設定していきます。
以下の権限を追加します。
* userに自分のデータのselect権限
* memoに自分に紐づくデータのinsert,update,delete権限

権限の設定の前に、自分が作成したmemoのみ更新できるというように設定するためにも、memoはuserが作るという形にしていきます。
まずはmemoがuserに紐付く形になるようHasuraで編集していきます。

memoを開いて、Modifyを開きます。
`Add a new column`からuserIdを追加します。
※ 既にmemoテーブルが存在しているので、一旦nullableにしました。
※ userId追加後にnullableではなくしました。(userId追加 → memoテーブルのデータのuserIdを1にする → userIdのnullableをfalseにする)

![スクリーンショット 2022-04-05 12.36.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/4d498bb3-5e84-80ec-be1e-d9b968bfc88b.png)

userIdの追加が完了したら、Foreign Keysに写真のように追加します。
![スクリーンショット 2022-04-05 12.44.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/f714fe76-2c4d-c54a-6835-4fdcf203c2e5.png)

Foreign Keysの追加が完了したら、Modifyの右にあるRelationshipsを開きます。
Suggested Object Relationshipsの下にある`Add`をクリックし、Name: userでSaveします。
![スクリーンショット 2022-04-05 12.48.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/90b7be87-5be7-db69-d043-f659abc88cc6.png)

これで、memoとuserが紐付く形になりました。
このように、memoに紐付くユーザーの取得も可能になりました。
![スクリーンショット 2022-04-05 12.50.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/eea5a1f8-cc33-8cf2-c969-04b9117ef74c.png)

user側のRelationshipsも同様に変更しておきます。
![スクリーンショット 2022-04-05 13.01.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/7ffd00e7-1f16-5bce-fd5f-b0df62efbfb7.png)

memoとuserの関係の作成が完了したので、権限の設定をしていきます。

userのselectを編集していきます。
写真のように設定します。
![スクリーンショット 2022-04-05 13.03.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/065e7bd0-18a3-c04f-0e20-399718808b2c.png)

先程と違うのは、全userの権限を与えているのではなく、uidがX-Hasura-User-Idと一致するuserのみの権限を与えていることです。
uidはgoogleログイン時に発行されていたユニークなidです。
X-Hasura-User-Idもgoogleログイン時に発行されていたユニークなidです。

どちらもAuth0のrulesの中で追加しています。
uidは新規登録時にテーブルに保存されているデータで、X-Hasura-User-Idは新規登録/ログイン時にJWTの中に追加されています。
こうすることで、自分のデータのみをselectすることができます。

次はmemoに、「自分に紐づくデータのinsert,update,delete権限」を追加します。
userのselectと同様に、uidとX-Hasura-User-Idが一致するデータのみに権限を与えます。

|select|update|delete|
|-|-|-|
|![スクリーンショット 2022-04-05 13.16.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/ea69f724-dfe2-463e-35c2-d42c1fef320f.png)|![スクリーンショット 2022-04-05 13.17.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/a01f0cb5-c6bd-3d10-02da-259975dc3bfa.png)|![スクリーンショット 2022-04-05 13.19.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/cc36349f-d940-d5b6-aaab-8fcd62322d69.png)

これで、自分のuserIdを持つmemoのみを追加,更新,削除できるようになりました。

最終的にはこうなりました。
|memo|user|
|-|-|
|![スクリーンショット 2022-04-05 13.21.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/618bf840-fa49-4ff3-48ae-d8ab6b06c78d.png)|![スクリーンショット 2022-04-05 13.21.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/891ea097-243a-1527-d504-dbcdffcad8a8.png)|

(解説ないですが、memoのselectのuserIdにも権限追加したので、緑のチェックマークになってます。これはどっちでもいいです。)

これでHasuraの権限の設定は完了です。

#### 本番環境にも設定
ここまでできたら、本番環境にも環境変数の設定やAuth0のアプリケーション作成などをします。

以下の3つの作業をしていきます。
* Auth0の設定
* Hasura(Cloud Build)の環境変数の設定
* Next.js(Vercel)の環境変数の設定

**Auth0の設定**

Auth0を開きます。
左上のドロップダウンのメニューを開き、Create tenantをクリックします。
![スクリーンショット 2022-04-05 13.46.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/5979b506-f65c-2900-15a3-d0cf23994d34.png)

あとはlocal用のAuth0と同様に、Tenant Domainに好きな名前をつけて進めていきます。
Environment TagはProductionにしておきました。

新しいテナントが作れたら、local用のAuth0と同様に以下3つを進めます。
* Applicationsの作成
* APIsの作成
* Rulesの作成

local用のAuth0を作成した箇所を参考に進めてください。
分からない部分がありましたらコメント頂ければ回答させて頂きます🙇‍♂️
この記事では以下のように設定してます。
* Applicationsの作成
    * Allowed Callback URLs, Allowed Logout URLs, Allowed Web Originsには、Vercelにデプロイしたときに確認したDomainsの値を入力します。
  この記事のサンプルでは、`https://memo-app-sample.vercel.app`です。
* APIsの作成
    * Identifierには、Cloud RunのURL + `/v1/graphql`を入力しました。(`https://memo-app-samp....run.app/v1/graphql`こんな感じ)
* Rulesの作成
    * コード部分は同じなのでコピーで良いです。
    * 変数 NAMESPACE → VercelのDomainsの値(`https://memo-app-sample.vercel.app`)
    * 変数 HASURA_URL → Cloud Runに書いてあったRUL (`https://memo-app-samp....run.app`)
    * 変数 HASURA_GRAPHQL_ADMIN_SECRET → 自分が設定したadmin secret (Cloud Buildのトリガーで確認可能)

これでAuth0の設定は終わりました。

**Hasura(Cloud Build)の環境変数の設定**

local用のHASURA_JWT_SECRETを追加したので、本番環境にも設定します。

以下のような流れで設定します。
1. cloudbuild.ymlの修正
2. HASURA_JWT_SECRETの作成
3. Cloud Buildのトリガーに代入変数を追加


1 cloudbuild.ymlの修正
cloudbuild.ymlの修正をして、Cloud BuildからHasuraへ値を渡せるようにします。

```cloudbuild.yml
..省略

- "--build-arg"
- "HASURA_ADMIN_SECRET=$_HASURA_ADMIN_SECRET"
// 以下2行を追加
- "--build-arg"
- "HASURA_JWT_SECRET=$_HASURA_JWT_SECRET"
- '--no-cache'
- '-t'

..省略
```

2 HASURA_JWT_SECRETの作成
local用のHASURA_JWT_SECRETを作成した時と同様に、https://hasura.io/jwt-config から作成します。 

3 Cloud Buildのトリガーに代入変数を追加
Cloud Buildのトリガーを開き、代入変数を追加します。
変数；_HASURA_JWT_SECRET
値：2で作成したJWT Config

これでHasura(Cloud Build)の環境変数の設定は終わりました。
Hasura側の設定が終わったので一旦デプロイします！GitHubにpushします！

**Next.js(Vercel)の環境変数の設定**

Vercelで自分のプロジェクトを開いて、Settingsタブを開きます。
SettingsのEnvironment Variablesから環境変数の設定ができます。
![スクリーンショット 2022-04-05 14.38.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/f5f06486-8ff4-7a13-b1ff-06639d87ba7b.png)

NAMEとVALUEを入力してENVIRONMENTを選択しAddをクリックすることで環境変数の設定ができます。
今回の記事では、local環境と本番環境しか考えていないのでProductionにチェックを付けます。
本来は、本番環境にデプロイする前に動作確認などをするための環境が必要で、それらの環境に環境変数を設定するときに他のENVIRONMENTにチェックを付けます。

.env.localに設定していた環境変数(local用)の本番用の環境変数をここに追加します。
* NEXT_PUBLIC_HASURA_GRAPHQL_ENDPOINT → Cloud RunのURL + `/v1/graphql` ( `https://memo-app-samp....run.app/v1/graphql` こんな感じ)
* NEXT_PUBLIC_AUTH0_DOMAIN → Auth0のDomain (xxxx.auth0.com)
* NEXT_PUBLIC_AUTH0_CLIENT_ID → Auth0のClient ID 
* NEXT_PUBLIC_APP_URL → VercelのDomainsの値(`https://memo-app-sample.vercel.app`)

※ HASURA_GRAPHQL_ADMIN_SECRETはcodegenの実行にのみ使っていたので、本番環境には不要です。

これでNext.js(Vercel)の環境変数の設定は終わりました。
Next.js(Vercel)側の設定も終わったので一旦デプロイします！Githubにpushします！
こちらはHasuraのデプロイが完了してからデプロイしてください。

これで設定周りなどは全て完了しました。
ここからは実際に機能の実装を進めていきます。

## メモの投稿, 編集, 削除機能の実装

:::note info
学ぶこと

* Next.jsのルーティングについて
* Hasura, Apolloを使った開発の流れ
* Next.jsからApollo, Hasuraを使ってデータの取得, 追加, 更新, 削除をする方法
* Apolloのキャッシュについて
:::


今回は以下のようにします！

||TOPページ|メモ投稿ページ|メモ編集ページ|
|-|-|-|-|
|path|/|/memo/new|/memo/[id]/edit|
|機能|自分のメモ一覧の閲覧<br>メモ投稿/編集ページへの遷移<br>メモ削除|メモ投稿|メモ編集|
|ページに対応するファイル|/pages/index.tsx|/pages/memo/new.tsx|/pages/memo/[id]/edit.tsx|

Next.jsでは、pages下のファイル名/ディレクトリ名がpathになります。
indexという名前は、ディレクトリのルートを表します。
ページ名に角括弧を使うことで、動的なルーティングの作成ができます。

例) 
`pages/index.tsx` → `/`
`pages/memo/index.tsx` → `/memo`
`pages/memo.tsx` → `/memo`
`pages/memo/new.tsx` → `/memo/new`
`pages/memo/[id].tsx` → `/memo/1`, `/memo/2`, .....


**自分のメモ一覧の閲覧機能**

TOPページを作成します。
/ のページに作成するので、src/pages/index.tsxを編集していきます。

おさらいになりますが、Hasuraへのリクエスト部分の実装する流れはこんな感じになります。

* Hasura consoleを使ってクエリを作る
* gqlタグの中に書く
* yarn codegenを実行する
* コンポーネント内で作成されたhooksを利用する

まずは、Hasura Consoleを使ってuserIdが自分のmemoを取得するクエリを作成します。
![スクリーンショット 2022-04-05 20.22.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/433a8d2d-aa9a-6a32-dd32-9bb170b5105d.png)

次に、gqlタグの中にコピーします。

```src/pages/index.tsx
省略
..
}

export default Home

gql`
  query GetMyMemoList($userId: Int!) {
    memo(where: {userId: {_eq: $userId}}) {
      id
      memo
    }
  }
`
```

gqlにコピーしたら、`yarn codegen`を実行します。

```.ターミナル(memo-app-sample % )
yarn codegen
```

`yarn codegen`でhooks(useGetMyMemoListQuery)が作成できたら、コンポーネント内で作成されたhooksを利用します。

```src/pages/index.tsx
import { gql } from '@apollo/client';
import type { NextPage } from 'next'
import { Layout } from '../component/Layout';
import { useAuth0User } from '../hooks/useAuth0User';
import { useGetMyMemoListQuery } from '../libs/apollo/graphql';

const Home: NextPage = () => {
  const { userId } = useAuth0User()
  const { data } = useGetMyMemoListQuery({variables: { userId }})

  return (
    <Layout>
      <div></div>
    </Layout>
  )
}

export default Home

gql`
  query GetMyMemoList($userId: Int!) {
    memo(where: {userId: {_eq: $userId}}) {
      id
      memo
    }
  }
`
```

`{variables: { userId }}`は`{variables: { userId: userId }}`と同じです。
Auth0から取得したuserIdを`query GetMyMemoList($userId: Int!)`で定義している`$userId`に渡しています。
動的な値をqueryの外部から指定する場合に、variablesを使います。

ここでは、`{variables: { userId: 1 }}`を渡しているので、`memo(where: {userId: {_eq: 1}})`になります。
userIdが1のmemoの一覧を取得しています。

次にデータの取得ができたので表示してみます。

```src/pages/index.tsx
import { gql } from '@apollo/client';
import type { NextPage } from 'next'
import { Layout } from '../component/Layout';
import { useAuth0User } from '../hooks/useAuth0User';
import { MemoInfoFragment, useGetMyMemoListQuery } from '../libs/apollo/graphql';

const MemoItem = ({id, memo}: MemoInfoFragment) => {
  return (
    <div>
      <p>{memo}</p>
    </div>
  )
}

const Home: NextPage = () => {
  const { userId } = useAuth0User()
  const { data } = useGetMyMemoListQuery({variables: {userId}})

  return (
    <Layout>
      <div>
        {data?.memo.map((item)=>(
          <MemoItem {...item} key={item.id} />
        ))}
      </div>
    </Layout>
  )
}

export default Home

gql`
  query GetMyMemoList($userId: Int!) {
    memo(where: {userId: {_eq: $userId}}) {
      ...MemoInfo
    }
  }
`

gql`
  fragment MemoInfo on memo {
    id
    memo
  }
`
```
MemoItemコンポーネントは、1つのmemoを表示するコンポーネントです。
data.memoはmemo(idとmemo)の配列なので、mapを使って個数分のMemoItemコンポーネントを呼び出しています。
MemoItemコンポーネントには、idとmemoを渡しています。

MemoItemコンポーネントの型はFragmentを使って作っています。(yarn codegen必要)

この2つは同じです！
```
memo(where: {userId: {_eq: $userId}}) {
  ...MemoInfo
}

memo(where: {userId: {_eq: $userId}}) {
  id
  memo
}
```
Fragmentを使うことで、MemoInfoFragmentという型が作成されます。

Fragmentについての記事も昔に書いているのでよかったら読んでみてください！
[ApolloのFragmentの使い方](https://qiita.com/tokio_k_/items/f9c2258ea6d24247e661)

これでデータの表示ができるようになりました！！


**メモ投稿/編集ページへの遷移機能**

次にメモ投稿/編集ページへの遷移を実装します。

初めに遷移先のページを空で作成します。
src/pages/memo/new.tsxとsrc/pages/memo/[id]/edit.tsxを作成します。

メモ投稿ページ
```src/pages/memo/new.tsx
import { NextPage } from "next"
import { Layout } from "../../component/Layout"

const AddMemoPage: NextPage = () => {
    return (
      <Layout>
        メモ投稿ページ
      </Layout>
    )
}

export default AddMemoPage
```

メモ編集ページ
```src/pages/memo/[id]/edit.tsx
import { NextPage } from "next"
import { Layout } from "../../../component/Layout"

const EditMemoPage: NextPage = () => {
    return (
      <Layout>
        メモ編集ページ
      </Layout>
    )
}

export default EditMemoPage
```

遷移先のページの作成が完了したので、遷移するボタンを追加します。

```src/pages/index.tsx
import { gql } from '@apollo/client';
import type { NextPage } from 'next'
import { useRouter } from 'next/router';
import { Layout } from '../component/Layout';
import { useAuth0User } from '../hooks/useAuth0User';
import { MemoInfoFragment, useGetMyMemoListQuery } from '../libs/apollo/graphql';

const MemoItem = ({id, memo}: MemoInfoFragment) => {
  const router = useRouter()
  const handleEditMemo = () => {
    router.push(`/memo/${id}/edit`)
  }

  return (
    <div>
      <p>{memo}</p>
      <div>
        <button onClick={handleEditMemo}>編集</button>
      </div>
      <hr />
    </div>
  )
}

const Home: NextPage = () => {
  const router = useRouter()
  const { userId } = useAuth0User()
  const { data } = useGetMyMemoListQuery({variables: {userId}})

  const handleCreateMemo = () => {
    router.push("/memo/new")
  }

  return (
    <Layout>
      <div>
        <button onClick={handleCreateMemo}>メモを投稿</button>
      </div>
      <div>
        {data?.memo.map((item)=>(
          <MemoItem {...item} key={item.id} />
        ))}
      </div>
    </Layout>
  )
}

..省略
```

メモ投稿ページへ遷移するボタンと、メモ編集ページへ遷移するボタンを作成しました。(今回はデザインはしません)
![スクリーンショット 2022-04-06 12.29.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/aa485428-ae8d-6c4a-4284-cee77193c2cf.png)

クリックすると、先程作成したページにも遷移しました！

**メモ削除機能**

次にメモの削除機能を追加します。

まずは、Hasura consoleを使ってmemoを削除するクエリを作ります！
![スクリーンショット 2022-04-05 23.14.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/75b008f8-79dd-fa08-04cf-1827b86c9d74.png)

src/pages/index.tsxの一番下でgqlの中に貼り付けて、yarn codegenをします。

```src/pages/index.tsx
...省略

gql`
  mutation DeleteMemo($memoId: Int!) {
    delete_memo_by_pk(id: $memoId) {
      id
    }
  }
`
```

yarn codegenで作成されたuseDeleteMemoMutationを使って削除機能を実装します。

```src/pages/index.tsx
...省略
const MemoItem = ({id, memo}: MemoInfoFragment) => {
  const router = useRouter()

  const [deleteMemo] = useDeleteMemoMutation({variables: {memoId: id}})

  const handleEditMemo = () => {
    router.push(`/memo/${id}/edit`)
  }
  const handleDeleteMemo = () => {
    deleteMemo()
  }

  return (
    <div>
      <p>{memo}</p>
      <div>
        <button onClick={handleEditMemo}>編集</button>
        <button onClick={handleDeleteMemo}>削除</button>
      </div>
      <hr />
    </div>
  )
}
...省略
```
これで削除機能は実装されましたが、今のままだとリロードしないと表示が変わらないので削除したらすぐに更新できるようにします。
`refetchQueries`を使うことで、削除をすぐに反映させることができます。再度Hasuraにリクエストしています。

```src/pages/index.tsx
const [deleteMemo] = useDeleteMemoMutation({variables: {memoId: id}, refetchQueries: ["GetMyMemoList"] })
```
キャッシュを書き換えて表示を更新する方法も過去に記事にしているので、よかったら読んでみてください！
[Qiita - Apolloのキャッシュとリストの更新](https://qiita.com/tokio_k_/items/945670f580c5cf507d97)

これでメモの削除機能の実装ができました。

**メモ投稿機能**

メモの投稿機能を作成していきます。
src/pages/memo/new.tsxを編集して、メモ投稿機能を作っていきます。


insertであっても、先程と同じ流れになります。
* Hasura consoleを使ってクエリを作る
* gqlタグの中に書く
* yarn codegenを実行する
* コンポーネント内で作成されたhooksを利用する

まずは、Hasura consoleを使ってクエリを作ります。(慣れてきたら使わなくてもOK)

実行ボタン「▶️」を押すとクエリが実行されるのでデータが追加されます。
selectの場合は何回実行しても変わらないですが、insert, update, deleteの場合はDBが変更されます！

![スクリーンショット 2022-04-06 12.35.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/9ec4a8bc-7883-f7fe-7ffc-83b34e7cd991.png)

src/pages/memo/new.tsxの一番下でgqlの中に貼り付けて、yarn codegenをします。

```src/pages/memo/new.tsx
...省略
export default AddMemoPage

gql`
  mutation CreateMemo($memo: String!, $userId: Int!) {
    insert_memo_one(object: {memo: $memo, userId: $userId}) {
      id
    }
  }
`
```

yarn codegenで作成されたuseCreateMemoMutationを使ってメモ投稿機能を実装します。


```src/pages/memo/new.tsx
import { gql } from "@apollo/client"
import { NextPage } from "next"
import { useRouter } from "next/router"
import { useState } from "react"
import { Layout } from "../../component/Layout"
import { useAuth0User } from "../../hooks/useAuth0User"
import { useCreateMemoMutation } from "../../libs/apollo/graphql"

const AddMemoPage: NextPage = () => {
  const {userId} = useAuth0User()
  const [memo, setMemo] = useState<string>("")
  const router = useRouter()
  const [createMemo] = useCreateMemoMutation({refetchQueries: ["GetMyMemoList"]})

  const handleInputMemo = (event: React.ChangeEvent<HTMLTextAreaElement> ) => {
    setMemo(event.target.value)
  }
  const handleBackPage = () => {
    router.push("/")
  }
  const handleCreateMemo = () => {
    createMemo({variables: {memo, userId }})
    router.push("/")
  }

  return (
    <Layout>
      <textarea onChange={handleInputMemo} value={memo} />
      <div>
        <button onClick={handleBackPage} >戻る</button>
        <button onClick={handleCreateMemo}>メモを投稿</button>
      </div>
    </Layout>
  )
}

export default AddMemoPage

gql`
  mutation CreateMemo($memo: String!, $userId: Int!) {
    insert_memo_one(object: {memo: $memo, userId: $userId}) {
      id
    }
  }
`
```
handleInputMemoでtextareaに入力した値を使って`memo`を更新しています。

`createMemo({variables: {memo, userId }})`は`createMemo({variables: {memo: memo, userId: userId }})`と同じです。
textareaに入力したmemoの値とuseAuth0Userから取得したuserIdをvariablesに渡しています。

削除時と同様に、`refetchQueries: ["GetMyMemoList"]`も設定しています。


これでメモの投稿機能の実装ができました。
**メモ編集機能**

メモ編集機能を作成していきます。
src/pages/memo/[id]/edit.tsxを編集して、メモ編集機能を作っていきます。

updatedもinsertやselectと同じ流れで進めます。
メモ更新のMutationとメモ取得のQueryを作成しました。
![スクリーンショット 2022-04-06 14.12.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/8dfe2322-d3ae-02e6-de7e-0671c275f2ae.png)


gqlに貼り付け、yarn codegenをして、hooksを作成します。
`...MemoInfo`の部分は、先程作成したFragmentです。filed(idとmemo)が展開されます。

```src/pages/memo/[id]/edit.tsx
...省略

gql`
  query GetMemo($id: Int!) {
    memo_by_pk(id: $id) {
      ...MemoInfo
    }
  }
`
gql`
  mutation EditMemo($memoId: Int!, $memo: String!) {
    update_memo_by_pk(pk_columns: {id: $memoId}, _set: {memo: $memo}) {
      ...MemoInfo
    }
  }
`
```

yarn codegenで作成した`useEditMemoMutation`,`useGetMemoQuery`を使って実装していきます。

```src/pages/memo/[id]/edit.tsx
import { gql } from "@apollo/client"
import { NextPage } from "next"
import { useRouter } from "next/router"
import { useState } from "react"
import { Layout } from "../../../component/Layout"
import { useEditMemoMutation, useGetMemoQuery } from "../../../libs/apollo/graphql"

const EditMemoPage: NextPage = () => {
  const [memo, setMemo] = useState<string>("")
  const router = useRouter()
  const { id } = router.query
  const [editMemo] = useEditMemoMutation();

  useGetMemoQuery({variables: { memoId: Number(id) }, skip: !id, onCompleted: ({memo_by_pk}) => {
    if(!memo_by_pk)return;
    setMemo(memo_by_pk.memo)
  } })

  const handleInputMemo = (event: React.ChangeEvent<HTMLTextAreaElement> ) => {
    setMemo(event.target.value)
  }
  const handleBackPage = () => {
    router.push("/")
  }
  const handleEditMemo = () => {
    editMemo({variables: {memoId: Number(id), memo}})
    router.push("/")
  }

  return (
    <Layout>
      <textarea onChange={handleInputMemo} value={memo} />
      <div>
        <button onClick={handleBackPage} >戻る</button>
        <button onClick={handleEditMemo}>メモを編集</button>
      </div>
    </Layout>
  )
}

export default EditMemoPage

gql`
  query GetMemo($memoId: Int!) {
    memo_by_pk(id: $memoId) {
      ...MemoInfo
    }
  }
`
gql`
  mutation EditMemo($memoId: Int!, $memo: String!) {
    update_memo_by_pk(pk_columns: {id: $memoId}, _set: {memo: $memo}) {
      ...MemoInfo
    }
  }
`

```

`onCompleted`を使うことで、Queryのレスポンス取得完了後に処理を実行することができます。
今までは、`const { data } = useGetMemoQuery()`のような形で取得結果の値を使っていましたが、
今回は、onCompletedの中で取得結果の値を使っています。`{memo_by_pk}`が取得結果の値です。
それをmemoの初期値にしています。

あとは殆どメモ投稿と同じです。
もう1つ違う点は、`refetchQueries`を使っていない点です。
updateでは、idと変更を反映させたい値を返すことで自動でキャッシュを書き換えることができます。
id, memoがないと自動で更新はされません！

キャッシュの挙動については、[Chrome 拡張機能 - Apollo Client Devtools](https://chrome.google.com/webstore/detail/apollo-client-devtools/jdkknkkbebbapilgoeccciglkfbmbnfm?hl=ja&gl=NL)を使うとわかりやすいです。

![スクリーンショット 2022-04-06 14.45.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/2c7e0b61-d679-5cf7-e945-f4154235ef0c.png)
![スクリーンショット 2022-04-06 14.45.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/671c228f-98c3-d847-16f1-cf9a5bfb72c1.png)

ROOT_QUERYの中にあるメモ一覧では、`[{__ref:"memo:16"}, {__ref:"memo:14"}]`のように、__typename(memo)とid(16,14)のセットでキャッシュしています。
このROOT_QUERYの中にあるメモ一覧のキャッシュを書き換えなくても、memo:14やmemo:16自体が書き換わるので書き換わって表示されるのです。
メモ一覧では、idと__typenameを使って参照しているだけなので、更新時は変更を検知する必要がないということです。
追加や削除の場合は、このROOT_QUERYの中にあるメモ一覧のキャッシュに変更を加える必要があるので、`refetchQueries`を使っていたわけです。
(例えば、{__ref:"memo:17"}を追加したり、{__ref:"memo:14"}を削除したりする必要がある)
(リクエストなしで、ROOT_QUERYの中にあるメモ一覧のキャッシュを書き換える方法は先程紹介した[記事](https://qiita.com/tokio_k_/items/945670f580c5cf507d97)で紹介しています。)
memo:14やmemo:16自体はupdate時に、idとmemoを返すと自動で更新してくれます。
これで、メモの編集機能の実装ができました。

## 他人のメモの閲覧機能の実装

:::note info
学ぶこと

* Apolloを使ってのSSGの方法

:::

自分のメモ以外のメモも一覧で表示できるようにします。
メモの一覧が表示されて、メモをクリックすると、クリックしたメモのページ(メモ詳細ページ)に移動するというシンプルなものです。

今回は、memoテーブルのデータが少なくメモ一覧とメモ詳細にデータの違いがないため、メモ詳細ページは殆ど意味がないですがSSGを学ぶために実装していきます。

||メモ一覧ページ|メモ詳細ページ(SSG)|
|-|-|-|
|path|/memo|/memo/[id]|
|機能|メモ一覧の閲覧|メモ情報の閲覧|
|ページに対応するファイル|/pages/memo.tsx|/pages/memo/[id].tsx|

メモ一覧にはタイトルが並んでいて、メモ詳細には細かい内容まで表示されるというのがよくあるパターンだと思います。
この記事が終わったら各自挑戦してみてください。

この2つのページはどちらもログイン不要なページです。


**メモ一覧ページの作成**

src/pages/memo/index.tsxにメモ一覧ページを作成していきます。
今までと同様に、クエリを作りyarn codegenを実行し、作成されたhooksを使って実装していきます。

```src/pages/memo/index.tsx
import { gql } from '@apollo/client';
import type { NextPage } from 'next'
import Link from 'next/link';
import { Layout } from '../../component/Layout';
import { MemoListResultFragment, useGetMemoListQuery } from '../../libs/apollo/graphql';

const MemoListItem = ({id, memo}: MemoListResultFragment) => {
  return (
    <div>
      <Link href={`/memo/${id}`}>
        {memo}
      </Link>
    </div>
  )
}

const MemoListPage: NextPage = () => {
  const { data } = useGetMemoListQuery()

  return (
    <Layout>
      <h2>メモ一覧</h2>
      <div>
        {data?.memo.map((item)=>(
          <MemoListItem {...item} key={item.id} />
        ))}
      </div>
    </Layout>
  )
}

export default MemoListPage

gql`
  query GetMemoList {
    memo {
      ...MemoListResult
    }
  }
`

gql`
  fragment MemoListResult on memo {
    id
    memo
  }
`
```

ここは今までのページと特に違う点はありません。
src/pages/index.tsxのFragmentとこのファイルのFragmetが同じ内容なのにこちらでも作っている理由は、
`/`ページと`/memo/`ページで表示する情報が変わった時に対応しやすいためです。
例えば、このページにだけmemoを投稿したユーザーの名前を載せたいという時にとても簡単に変更できます。(良い方法なのかは分かってません。)

メモを一覧で表示して、メモをクリックしたら画面遷移できるようになりました。

**メモ詳細ページの作成**

src/pages/memo/[id]/index.tsxにメモの詳細ページを作成していきます。
このページをSSGにします。

SSGは、今までのようにクライアント側からHasuraにリクエストするのではなく、ビルド時にサーバー側からHasuraにリクエストしてそのデータを使ってページを構築します。
似たもので、ISRというものもあって、ISRはSSGのように事前にページを構築しますが、一定間隔でサーバーサイドレンダリングを行ってデータを新しくするため、ビルド後に変更したデータも反映させることができます。しかし、変更したら必ず反映されているというわけではないので注意が必要です。

SSGやISRは、以下の記事などを参考にしてみてください。
[Next.js公式ドキュメント - Pages](https://nextjs.org/docs/basic-features/pages)
[zenn - 【Next.js】CSR,SSG,SSR,ISRがあやふやな人へざっくり解説する](https://zenn.dev/akino/articles/78479998efef55)

まずは、Hasura consoleを使ってクエリを作成し、yarn codegenを実行します。

```src/pages/memo/[id]/index.tsx
gql`
  query GetMemoIdList {
    memo {
      id
    }
  }
  query GetMemoListItem($memoId: Int!) {
    memo_by_pk(id: $memoId) {
      id
      memo
    }
  }
`
```

yarn codegenを実行したら、実装していきます。

```src/pages/memo/[id]/index.tsx
import { gql } from "@apollo/client";
import { GetStaticPaths, GetStaticProps, NextPage } from "next";
import { useRouter } from "next/router";
import { Layout } from "../../../component/Layout";
import { addApolloState, initializeApollo } from "../../../libs/apollo/apolloClient";
import { GetMemoIdListDocument, GetMemoIdListQuery, GetMemoListItemDocument, GetMemoListItemQuery, useGetMemoListItemQuery } from "../../../libs/apollo/graphql";

export const getStaticPaths: GetStaticPaths<{id: string}> = async () => {
  const apolloClient = initializeApollo();
  const { data } = await apolloClient.query<GetMemoIdListQuery>({ query: GetMemoIdListDocument });
  const paths = data.memo.map(({id}) => {
      return { params: {id: String(id) } }
  })
  return {paths, fallback: true}
}

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const apolloClient = initializeApollo();
  await apolloClient.query<GetMemoListItemQuery>({ query: GetMemoListItemDocument, variables: { memoId: Number(params?.id) } });
  return addApolloState(apolloClient, { props: {} });
};

const MemoListItemPage: NextPage = () => {
  const rouetr = useRouter()
  const { id } = rouetr.query
  const { data } = useGetMemoListItemQuery({ variables: { memoId: Number(id) } })

  return (
    <Layout>
      <p>{data?.memo_by_pk?.memo}</p>
    </Layout>
  )
}

gql`
  query GetMemoIdList {
    memo {
      id
    }
  }
  query GetMemoListItem($memoId: Int!) {
    memo_by_pk(id: $memoId) {
      id
      memo
    }
  }
`

export default MemoListItemPage

```

Next.jsでは、getStaticPropsを使うことで、そのページをSSGにすることができます。
`/memo/1`, `/memo/5`,....のような、idによって変わるダイナミックルーティングの時は、getStaticPathsも使います。
getStaticPathsの役割はSSGで作成するページの一覧の作成です。
getStaticPathsでidの一覧をreturnして、その返したidを使ってgetStaticPropsを実行します。

getStaticPropsに記述されたコードはサーバー側でビルド時に実行されます。
getStaticPropsの戻り値のpropsにデータを渡すことで、MemoListItemPageのpropsにデータを渡すことができます。(ここでは渡してません。)
Apolloの場合は、getStaticPropsからMemoListItemPageに直接データを渡すのではなくキャッシュに書き込んでリクエストをなくすことが可能です。
※ getStaticPropsから`return {props: {data: data}}`みたいな感じでreturnして、`const MemoListItemPage = ({data}) => `で受け取ることも可能です。

Apolloを使った時のSSGの流れは以下です。
```.ts
// 1. getStaticPathsが実行される
export const getStaticPaths: GetStaticPaths<{id: string}> = async () => {
  const apolloClient = initializeApollo();
  const { data } = await apolloClient.query<GetMemoIdListQuery>({ query: GetMemoIdListDocument });
  const paths = data.memo.map(({id}) => {
      return { params: {id: String(id) } }
  })
  return {paths, fallback: true}
}

// 2. getStaticPathsからメモのidの一覧が返される
//こんな感じのデータ
[
  { params: { id: '14' } },
  { params: { id: '16' } },
  { params: { id: '20' } }
]

// 3. getStaticPropsが実行される (getStaticPathsからreturnされたidを使って、今回なら3ページ分、14,16,20)
export const getStaticProps: GetStaticProps = async ({ params }) => {
  const apolloClient = initializeApollo();
  await apolloClient.query<GetMemoListItemQuery>({ query: GetMemoListItemDocument, variables: { memoId: Number(params?.id) } }); // 4. getStaticPropsの中でHasuraにリクエストをしたのでapolloClientにキャッシュが作られる。
  // console.log(apolloClient.cache.extract()) ※ キャッシュを出力して確認することも可能
  return addApolloState(apolloClient, { props: {} }); // 5. キャッシュの入ったapolloClientを引数に渡して、addApolloStateを実行する
};

// src/libs/apollo/apolloClient.ts
// 6. addApolloStateが実行される
export const addApolloState = (
  client: ApolloClient<NormalizedCacheObject>,
  pageProps: AppProps["pageProps"]
) => {
  if (pageProps?.props) {
    pageProps.props[APOLLO_STATE_PROP_NAME] = client.cache.extract(); // 7. pagePropsにキャッシュを格納する
  }
  return pageProps;
};

// src/pages/_app.tsx useApolloの呼び出し箇所
// src/libs/apollo/apolloClient.ts useApolloの定義箇所
// 8. useApolloを使って、Apollo Clientを作成(initializeApollo)するときに、7.で格納したデータをキャッシュに復元する
// initializeApolloの中で処理されてる

//src/pages/memo/[id]/index.tsx
const { data } = useGetMemoListItemQuery({ variables: { memoId: Number(id) } }) // 9. useGetMemoListItemQuery実行時には、キャッシュが存在するので、Hasuraにリクエストせずにすぐに結果が帰る
// 10. キャッシュから帰ってきた結果を使ってレンダリング
``` 

getStaticPropsでエラーが出た場合は、src/pages/memo/[id]/index.tsxの`useGetMemoListItemQuery`でHasuraにリクエストをします。
これでSSGができました。

開発者ツールのNetworkタブで確認してみても、リクエストが送られていないことが確認できます。
![スクリーンショット 2022-04-06 21.09.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/b7762666-7624-1a7b-f721-fb0d15ae9098.png)


これで、メモ一覧ページ、メモ詳細ページは実装完了ですが、おまけでいくつか修正をしておきます。

**Layoutにメニュー追加**

メモ一覧ページに遷移する箇所がないので、遷移できるようにLayoutにメニューの追加をしておきます。

```src/component/Layout/index.tsx
...省略
<div>
  {isAuthenticated ?
    <button onClick={handleLogout}>ログアウト</button>
    : <button onClick={loginWithRedirect}>ログイン</button>
  }
  <Link href="/">TOP</Link>
  <Link href="/memo">みんなのメモ一覧</Link>
</div>
...省略
```

**未ログイン時はメモ一覧ページにリダイレクト**

TOPページ,メモ投稿ページ,メモ編集ページはログインしている時のみに使える機能なので、未ログインの時はメモ一覧にリダイレクトするようにします。
src/hooks/useRequireLogin.tsを作成します。

```src/hooks/useRequireLogin.ts
import { useRouter } from "next/router";
import { useEffect } from "react";
import { useAuth0User } from "./useAuth0User";

export const useRequireLogin
 = () => {
  const { isAuthenticated, isLoading } = useAuth0User();
  const router = useRouter();

  useEffect(() => {
    if (!isAuthenticated && !isLoading) {
      router.push("/memo");
    }
  }, [isAuthenticated, isLoading, router]);
};
```
ローディングが終わった状態で、isAuthenticated(ログイン状態を表す)がfalseの時は、/memoページへ遷移するようにします。
このhooksをログインが必須なページで呼び出します！

src/pages/index.tsx, src/pages/memo/[id]/edit.tsx, src/pages/memo/new.tsxの3ページに`useRequireLogin()`を追加しました。

![スクリーンショット 2022-04-07 12.32.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/9d5ae4d1-fcb2-8898-6142-09a946125268.png)


これでログインが必要なページを開いたときは、強制的にメモ一覧ページに遷移するようになりました。

ここまでできたらデプロイします。

<details><summary>すみません、node v14だとデプロイ失敗します</summary><div>

現在Vercelの最新のnodeのバージョンは14なのですが、localのnodeのバージョンを16で進めていました。そのままではnode14では使えない処理を使っていたのでデプロイ時にエラーが発生します。

![スクリーンショット 2022-04-07 11.58.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506867/4875cb2a-249b-0316-32f8-43cb38e5c2b8.png)


もし、この記事を読んでくださっている時にVercelでnodeのバージョン16が使えましたら、ここは不要になります。

M1でnode14使うのが面倒だったのでnode16を使っちゃった記憶があります。。バージョンは本番と開発環境で合わせるべきですね。。

**起きてること・原因の考察**

* @auth0/auth0-react内で abortcontrollerを使ってる。
* node 15以上では、abortcontrollerが標準で実装されている。
* Vercelは最新がnode 14だから使えない。
* abortcontrollerをinstallする必要がある。

**abortcontrollerを使うために必要なコード**

```.tsx
import { AbortController } from "abort-controller";
import fetch, { Headers, Request, Response } from "node-fetch";

Object.assign(globalThis, {
  fetch,
  Headers,
  Request,
  Response,
  AbortController,
});
```

**改善**

src/pages/_document.tsxを作成します。

```src/pages/_document.tsx
import type { DocumentContext } from "next/document";
import Document, { Head, Html, Main, NextScript } from "next/document";
import { AbortController } from "abort-controller";
import fetch, { Headers, Request, Response } from "node-fetch";

Object.assign(globalThis, {
  fetch,
  Headers,
  Request,
  Response,
  AbortController,
});

class MyDocument extends Document {
  static async getInitialProps(ctx: DocumentContext) {
    const initialProps = await Document.getInitialProps(ctx);
    return { ...initialProps };
  }

  render() {
    return (
      <Html lang="ja">
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;
```
[_document.tsxについてはここを参照](https://nextjs.org/docs/advanced-features/custom-document)

`node-fetch`の型がないとエラーが出ていたので、型定義をインストールしておきます。

```
yarn add -D @types/node-fetch
```

これでデプロイしてみたらエラーがなくなっていました。

**参考**
https://github.com/apollographql/apollo-client/issues/6765
https://github.com/reduxjs/redux-toolkit/issues/1240

</div></details>

これで、この記事で紹介する全ての実装が終わりました👏👏

## おわりに

最後までお読みくださった方、ありがとうございます🙇‍♂️
非常に長い記事ですが、1通りNext.js, Hasura, Cloud Run, Cloud SQLを使った開発のイメージが掴めたのではないかと思います！

頑張って書いたので、LGTM頂けたらとっても嬉しいです！！
おかしな点があった場合は、コメントして頂けるととても嬉しいです。
よく分からない点もコメントしていただけると嬉しいです。


次何やろうって思ってる方がいましたら、もし良かったら「終わったら」に挑戦してみてください！！
自分で色々好きなように触ってみるのが、一番成長すると思います。
自分もチュートリアルとか終わった時に、何やろうってなることが多かったので、やることを幾つか書き出してみました！自分で手を動かすことで、ここに書いてあることの理解も深まると思います！！

## 終わったら
* dev環境(本番環境へのデプロイ前の動作確認用環境)の設定
* 機能の追加
  * メモにtitleを追加(メモ一覧ではtitleのみが表示されて、メモ詳細ではtitleとmemoが表示される)
  * メモ編集時の自動保存機能
  * メモの検索機能
  * メモの更新時間、作成時間の表示
  * メモ一覧にユーザー情報の表示
  * などなど。。。
* メモ詳細ページをSSGからISRに変更
* ESLint, prettierの導入
* セキュリティ周りの設定 
    * この[記事](https://hasura.io/learn/ja/graphql/hasura-advanced/security/)を参考
    * CORSポリシーの設定
    * レート制限
    * などなど。。。
* デザイン

## 参考記事

[Next.js公式ドキュメント - Create Next App](https://nextjs.org/docs/api-reference/create-next-app)
[zenn - Vercelのプレビューデプロイで特定のブランチ以外を無視する](https://zenn.dev/catnose99/articles/b37104fc7ef214)
[Hasura公式ドキュメント - Quickstart with Docker](https://hasura.io/docs/latest/graphql/core/getting-started/docker-simple.html)
[Hasura公式ドキュメント - Auto-apply migrations/metadata when the server starts](https://hasura.io/docs/latest/graphql/core/migrations/advanced/auto-apply-migrations.html)
[Hasura公式ドキュメント - Hasura CLI](https://hasura.io/docs/latest/graphql/core/hasura-cli/index.html)
[GCP公式ドキュメント - Cloud Run から接続する | Cloud SQL for PostgreSQL](https://cloud.google.com/sql/docs/postgres/connect-run?hl=ja)
[Qiita - CloudRunならたった5分でwebアプリをリリースできる！(自動デプロイも)](https://qiita.com/riku-shiru/items/d3f7dda5a5e87c4b26e9)
[Next.js公式ドキュメント - src Directory](https://nextjs.org/docs/advanced-features/src-directory)
[Apollo公式ドキュメント - Get started with Apollo Client](https://www.apollographql.com/docs/react/get-started/)
[Next.js examples - with-apollo](https://github.com/vercel/next.js/tree/canary/examples/with-apollo)
[GraohQL Code Generator公式ドキュメント - Getting Started](https://www.graphql-code-generator.com/docs/getting-started)
[Hasura公式ドキュメント - Authentication using JWT](https://hasura.io/docs/latest/graphql/core/auth/authentication/jwt/)
[jwt.io](https://jwt.io/)
[Auth0 Community - How to test a RULE when developing on localhost?](https://community.auth0.com/t/how-to-test-a-rule-when-developing-on-localhost/6351)
[ApolloのFragmentの使い方](https://qiita.com/tokio_k_/items/f9c2258ea6d24247e661)
[Qiita - Apolloのキャッシュとリストの更新](https://qiita.com/tokio_k_/items/945670f580c5cf507d97)
[Chrome 拡張機能 - Apollo Client Devtools](https://chrome.google.com/webstore/detail/apollo-client-devtools/jdkknkkbebbapilgoeccciglkfbmbnfm?hl=ja&gl=NL)
[Hasura公式ドキュメント - セキュリティ](https://hasura.io/learn/ja/graphql/hasura-advanced/security/)
[Next.js公式ドキュメント - Custom Document](https://nextjs.org/docs/advanced-features/custom-document)