71
38

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

はじめに

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
  2. Next.jsをVercelにデプロイ
  3. Hasuraをローカルで立ち上げ
  4. HasuraをCloud Runにデプロイ
  5. Apolloを使ってNext.jsからHasuraにリクエスト
  6. 認証の実装
  7. メモの投稿, 編集, 削除機能の実装
  8. 他人のメモの閲覧機能の実装

Next.jsを使ってHello World

学ぶこと

  • Next.jsのセットアップ
昔作ったテンプレもあります(不要な人はスルーでOK)

昔に自分用に作ったテンプレートもあるので、ここから初めてもらってもOKです!!!

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

不要な人はスルーでOKです

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

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

ターミナル.
yarn create next-app --typescript memo-app-sample

VSCodeで見るとこんな感じになっています!!
参考(Next.js公式ドキュメント)
スクリーンショット 2022-03-16 15.50.38.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

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

Githubに上げることができました!
スクリーンショット 2022-03-16 16.13.36.png

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

Next.jsをVercelにデプロイ

学ぶこと

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

Vercelのアカウント作成はお願いします。
Vercel開いて、右上の方からアカウント作成できます。
Githubとの連携ですぐにできるはずです。

アカウントを作成したら、左のような画面になると思います。

1 2 3
「New Project」からプロジェクトを作成 Import Git Repositoryから、先程GithubにあげたNext.jsのアプリを選択 Deployボタンを押す
スクリーンショット 2022-03-16 16.28.13.png スクリーンショット 2022-03-16 16.29.47.png スクリーンショット 2022-03-16 16.34.21.png

もしかしたら最初は、Githubとの連携設定などが必要になるかもです。
この記事にそこらへんのことが書いてありました。

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

デプロイ完了画面 Go to Dashboard
スクリーンショット 2022-03-16 16.44.46.png スクリーンショット 2022-03-16 16.40.26.png

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

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

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

Hasuraをローカルで立ち上げ

学ぶこと

  • 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 %
コマンドについて詳しく

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

コマンド 解説
〇〇 % 今、〇〇ディレクトリにいる
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ディレクトリにいる

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

Hasuraを立ち上げる

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

スクリーンショット 2022-03-25 11.20.30.png

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

Hasuraの変更をファイルで管理できるようにする

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

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

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

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

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

スクリーンショット 2022-03-25 15.45.21.png

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

.ターミナル
hasura init

hasura init を実行したことで、hasuraフォルダが作成されました。
この中にHasuraで操作した変更内容が保存されていきます。
スクリーンショット 2022-03-25 16.01.25.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

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

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

Create Tableをクリック
スクリーンショット 2022-03-25 16.40.59.png

Add Tableをクリック
スクリーンショット 2022-03-25 16.43.15.png

memoテーブルが追加されました。
スクリーンショット 2022-03-25 16.47.04.png

metadataファイルやmigrationsファイルにも追加されました。
スクリーンショット 2022-03-25 16.47.51.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

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

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

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

最初に作った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

Githubにあげる

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

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

スクリーンショット 2022-03-26 21.03.41.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

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

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

HasuraをCloud Runにデプロイ

学ぶこと

  • 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のリソースの管理ページに移動して、 「プロジェクトを作成」をクリックします。
スクリーンショット 2022-03-27 10.33.26.png

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

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

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

Cloud SQLを使ってDBを作成

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

スクリーンショット 2022-03-27 10.45.14.png

SQLのページに移動しました。
スクリーンショット 2022-03-27 10.46.50.png

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

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

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

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

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

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

Cloud SQL for PostgreSQLの料金に設定値と料金について書いてあります。
記事の中にもあるGoogle Cloud 料金計算ツールを使ってシュミレーションすることもできます。
今回の設定では、以下のように変更しました。

  • マシンタイプは共有コアの1 vCPU、1.7GBにしました。(0.614にすると、最大接続数が25になります。Hasura繋いだらエラーが発生したので1個あげときました。)
  • ストレージはSSD、容量は10GBにしました。
  • バックアップは自動化するのチェックを外しました。

次に、接続を開いて、プライベートIPにチェックを付けます。
これにチェックを入れて、サーバーレスVPCアクセスを使い、Cloud RunとCloud SQLを接続します。
※プライベートIPを使わずに行うことも可能なようです。詳細

スクリーンショット 2022-03-27 11.40.06.png

接続を設定をクリックしてService Networking APIの有効化をし、IP範囲を割り振り、接続を作成します。
スクリーンショット 2022-03-27 11.46.24.png

設定が完了したらインスタンスを作成をクリックします!
ここで入力したパスワードは後でも使います。メモしておきましょう!
スクリーンショット 2022-03-27 11.17.34.png

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

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

左のメニューからデータベースを選択し、データベースの作成をクリックします。
docker-compose(.env)で設定していたlocalのデータベースと同じ名前にします。この記事ではmemoapp。
スクリーンショット 2022-03-27 21.33.00.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

これで、サーバーレス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にデプロイという流れになります。
この記事を参考にしました

GithubとCloud Buildの接続準備
左のメニューからCloud Buildを開き、リポジトリを接続をクリックします。
スクリーンショット 2022-03-27 19.46.57.png
ソースを選択でGithubを選択し、リポジトリを選択で自分のアカウントと今回作ったリポジトリを選択し、リポジトリとの接続をします。(トリガーの作成は後でします。)
※ これを先にやることでCloud Runの設定時にリポジトリの選択ができたので、一旦このように進めます。ここではGithubとCloud Buildが接続出来るようにしただけです。

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

スクリーンショット 2022-03-27 16.27.43.png

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

Hasura側のリポジトリ(この記事なら、memo-app-sample-hasura)を選択して次へ進みます。
※ 別の時に発生した事象ですが、リポジトリ名が大文字の時にエラーが発生しました。小文字にしたら治りました。
スクリーンショット 2022-03-27 20.41.16.png

ブランチは^main$、Build TypeはDockerfile、ソースの場所は/Dockerfileを選択して保存をクリックします。
保存したら、Cloud Runの設定に戻るので、設定を続けます。
今回は以下のように設定しました。

  • リージョン:東京
  • リクエストの処理中にのみCPUを割り当てる
  • 自動スケーリング:最小0、最大1
  • すべてのトラフィックを許可する
  • 未認証の呼び出しを許可

コンテナ、変数とシークレット、セキュリティは特に変更しませんでした。(後から変更もできます)
接続タブからVPCコネクタを設定します。先程設定したpostgres-connectを使ってCloud SQLと接続します。
スクリーンショット 2022-03-27 20.53.31.png

設定ができたら、作成をクリックします。
エラーになりましたが、気にせず進めます。(多分、環境変数の設定とかビルドの設定とか色々してないからエラーになってる。)
スクリーンショット 2022-03-27 20.59.23.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

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

代入変数の少し上にスクロールして、構成のロケーションに移動します。
GCP上で直接書く(インライン)か、ymlファイルとしてGithubで管理する(リポジトリ)か決める事ができます。
スクリーンショット 2022-03-27 21.49.19.png

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

VSCodeを開いて、cloudbuild.ymlファイルを作成し、先程コピーしたYAML構文をcloudbuild.ymlにペーストします。
スクリーンショット 2022-03-27 21.55.52.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
      .
      .
      ....省略
コード追記分だけ確認

以下の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"

これで、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 スクリーンショット 2022-03-27 22.36.53.png

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

次にCloud Runを開き、自分が作ったサービスを選択して、画像右のURL:をクリックします。
スクリーンショット 2022-03-27 22.40.26.png

すると、Hasuraのconsoleを開く事ができました。
これで、無事、デプロイ出来ている事が確認できました。
スクリーンショット 2022-03-27 22.43.13.png

※ Cloud Runを開いて、カスタムドメインを管理からドメインの変更ができます。
スクリーンショット 2022-03-27 22.44.26.png

admin secretを設定

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

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

local
まずは、localのHasuraにadmin secretの設定をする所までします。
変更するファイルは、3つです。

  • Dockerfile
  • docker-compose.yml
  • .env
省略...

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_SECRETENV 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

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

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ディレクトリにまとめておこうと思います。参考

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

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

学ぶこと

  • 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公式ドキュメントNext.jsの公式examplesを参考に実装します。

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

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

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

.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ファイルで作れないかもです。)

yarn graphql-codegen init使った例
.ターミナル(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?:
  • ? 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

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

プラグインをインストールします。

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

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

.ターミナル(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

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

Browse Rowsを見てみると、無事にデータが入っていました。
スクリーンショット 2022-04-01 9.53.54.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

yarn codegenを実行してみます!

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

成功したらこんな感じ
スクリーンショット 2022-04-01 10.08.51.png

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

src/libs/apollo/graphql.tsxの下の方にある、useGetMemoTestQueryがgqlから作られたhooksです。
スクリーンショット 2022-04-01 10.14.06.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

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

Hasuraからデータを取得したい時の開発の流れはこんな感じにします!!

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

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

認証の実装

学ぶこと

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

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

以下の流れで作成していきます。

  1. ユーザーテーブルの作成
  2. Auth0アプリの作成
  3. Next.jsにログイン機能の追加
  4. HasuraとAuth0の接続設定
  5. JWTを使ってHasuraにアクセス
  6. Auth0とDBの同期
  7. 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

スクリーンショット 2022-04-01 13.38.58.png
user テーブルの作成が完了しました。

Auth0アプリの作成

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

Tenant Domainを作成します。

スクリーンショット 2022-04-01 13.42.51.png

ダッシュボードが開けたら成功です。
スクリーンショット 2022-04-01 13.44.20.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

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

スクリーンショット 2022-04-01 14.26.06.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_DOMAINNEXT_PUBLIC_AUTH0_CLIENT_IDNEXT_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

ログインボタンを押した後にこんな画面が表示され、そのまま進めてログインすることができたら成功です。
スクリーンショット 2022-04-01 14.27.41.png

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

HasuraとAuth0の接続設定

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

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

Auth0でAPIの作成

スクリーンショット 2022-04-01 14.56.54.png

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

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

スクリーンショット 2022-04-01 15.11.06.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

スクリーンショット 2022-04-01 14.54.19.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

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に渡す変数
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 スクリーンショット 2022-04-02 11.27.50.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

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

写真で選択している graphqlというのがuseGetMemoTestQueryのリクエストです。
スクリーンショット 2022-04-02 11.41.32.png

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

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

Previewを見ると今回はエラーが出ているようです。
スクリーンショット 2022-04-02 11.46.00.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

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
userにmemoのselectの全権限を追加しました。

権限が追加できたので、もう1度、リクエストしてみます。
すると、無事にデータが取得できていました👏
スクリーンショット 2022-04-02 11.56.44.png

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

Auth0とDBの同期

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

改修後の流れ

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

現在の流れ

  1. ログインボタンを押す
  2. ログイン完了

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

Hausra公式ドキュメントに従って実装していきます!(ほぼそのままです)

先程と同様にAuth0のrulesに処理を追加します!
Createボタンから追加していきます!
スクリーンショット 2022-04-02 14.07.10.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

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

localでの開発時には、HASURA_URLには、ngrokを使って値を設定します。
理由は、RulesがAuth0のサーバー上で実行されるので、手元のlocalhostへはアクセスできないためです!参考

本番環境では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

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

(いい方法なのかわからないのですが) クライアントから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"を追加します。

...省略
# 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

これで、未ログイン時にも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

userIdの追加が完了したら、Foreign Keysに写真のように追加します。
スクリーンショット 2022-04-05 12.44.31.png

Foreign Keysの追加が完了したら、Modifyの右にあるRelationshipsを開きます。
Suggested Object Relationshipsの下にあるAddをクリックし、Name: userでSaveします。
スクリーンショット 2022-04-05 12.48.03.png

これで、memoとuserが紐付く形になりました。
このように、memoに紐付くユーザーの取得も可能になりました。
スクリーンショット 2022-04-05 12.50.59.png

user側のRelationshipsも同様に変更しておきます。
スクリーンショット 2022-04-05 13.01.45.png

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

userのselectを編集していきます。
写真のように設定します。
スクリーンショット 2022-04-05 13.03.40.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 スクリーンショット 2022-04-05 13.17.07.png スクリーンショット 2022-04-05 13.19.52.png

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

最終的にはこうなりました。

memo user
スクリーンショット 2022-04-05 13.21.08.png スクリーンショット 2022-04-05 13.21.24.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

あとは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

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のデプロイが完了してからデプロイしてください。

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

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

学ぶこと

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

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

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

次に、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の使い方

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

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

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

初めに遷移先のページを空で作成します。
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

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

メモ削除機能

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

まずは、Hasura consoleを使ってmemoを削除するクエリを作ります!
スクリーンショット 2022-04-05 23.14.55.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のキャッシュとリストの更新

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

メモ投稿機能

メモの投稿機能を作成していきます。
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

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

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を使うとわかりやすいです。

スクリーンショット 2022-04-06 14.45.30.png
スクリーンショット 2022-04-06 14.45.54.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の中にあるメモ一覧のキャッシュを書き換える方法は先程紹介した記事で紹介しています。)
memo:14やmemo:16自体はupdate時に、idとmemoを返すと自動で更新してくれます。
これで、メモの編集機能の実装ができました。

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

学ぶこと

  • 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
zenn - 【Next.js】CSR,SSG,SSR,ISRがあやふやな人へざっくり解説する

まずは、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

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

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

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

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

すみません、node v14だとデプロイ失敗します

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

スクリーンショット 2022-04-07 11.58.02.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についてはここを参照

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

yarn add -D @types/node-fetch

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

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

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

おわりに

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

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

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

終わったら

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

参考記事

Next.js公式ドキュメント - Create Next App
zenn - Vercelのプレビューデプロイで特定のブランチ以外を無視する
Hasura公式ドキュメント - Quickstart with Docker
Hasura公式ドキュメント - Auto-apply migrations/metadata when the server starts
Hasura公式ドキュメント - Hasura CLI
GCP公式ドキュメント - Cloud Run から接続する | Cloud SQL for PostgreSQL
Qiita - CloudRunならたった5分でwebアプリをリリースできる!(自動デプロイも)
Next.js公式ドキュメント - src Directory
Apollo公式ドキュメント - Get started with Apollo Client
Next.js examples - with-apollo
GraohQL Code Generator公式ドキュメント - Getting Started
Hasura公式ドキュメント - Authentication using JWT
jwt.io
Auth0 Community - How to test a RULE when developing on localhost?
ApolloのFragmentの使い方
Qiita - Apolloのキャッシュとリストの更新
Chrome 拡張機能 - Apollo Client Devtools
Hasura公式ドキュメント - セキュリティ
Next.js公式ドキュメント - Custom Document

71
38
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
71
38

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?