はじめに
こんにちは!かほです♪🐥
現在、本業では技術広報業務やエンジニア業務などに従事しています。
今回の記事では、Next.js×Supabaseを用いたアプリ開発における投稿機能の実装とそれに伴うRLS(Row Level Security)の使用方法について説明します。
今回は下記の2点を実装しました。
- 投稿機能・・・タイトルと内容を投稿する
- 一覧機能・・・投稿した内容を表示する
Next.js×Supabase×vercelの連携方法から知りたい方は、下記の記事を参考にしてください🙌
本記事の投稿機能では、下記の認証機能が実装済みです。
認証機能をまだ実装していない方は、下記の記事を参考にしてください🙌
この記事の読者対象
- Next.js×SupabaseでCRUD処理(投稿機能、一覧機能)を含むアプリを作ってみたい方
- SupabaseのRLS設定について知りたい方
- Supabaseを今後使ってみたい方
- Next.jsを使って高速でアプリを作りたい方
開発環境
"dependencies": {
"@supabase/supabase-js": "^2.2.1",
"eslint": "8.28.0",
"eslint-config-next": "13.0.6",
"next": "13.0.6",
"react": "18.2.0",
"react-dom": "18.2.0",
},
"devDependencies": {
"@types/node": "^18.11.13",
"@types/react": "18.0.26"
}
前提
今回は、postsテーブルを使って投稿機能を実装します。
テーブルはSupabase上のsqlエディターから作成します。
todosテーブルを作った事例があるため、よければ下記の記事「Supabaseのプロジェクトを作成しよう!」の項目を参考にしてください🙌
また、今回のテーブル構造は下記になります。
カラム | 型 | デフォルト値 |
id | int4 | |
title | text | |
created_at | timestamptz | now() |
user_id | uuid | |
content | text |
そもそもRLSとはなんぞや?
最初に、すべての機能に対する認可をRLSで設定します。
そもそもRLSを知らない人が多いと思うので簡単にご紹介。
RLSはRow Level Securityの略称で、いわゆる行単位セキュリティーと呼ばれるものです。
ポリシーを設定することにより、テーブルに紐づけて、アクセス制御の記述を簡潔に書くことができます。
詳しくは下記の記事を参考にしてください🙌
RLSを設定しよう
RLSの有効化を確認しよう
最初に、posts
テーブルに対してRLSを設定できる状態か確認します。
- 左側のバーの人マークである
Authentication
を押します。 -
Configuration
のPolicies
を選択します。 - テーブル名「posts」の右横に、下記のようなマークが出ていることを確認します。
Policyを作成しよう
次にPolicyを作成します。
- RLS有効化を確認した画面の右端にある
New Policy
というボタンを押します。 - ボタンを押すと下記の画面のように選択肢が2つ出てきます。
テンプレートを使用してPolicyを作成したい方はGet started quickly
を選択し、完全に0からPolicyを作成したい方はFor full customization
を選択します。
. - 今回はテンプレートから作成したいので、
Get started quickly
を選択しました。下記のような画面ができます。上から取得
、挿入
、更新
、削除
の機能に対するSQLテンプレートがあります。
.
投稿機能に対する認可を設定しよう
最初に投稿機能に対する認可の設定を行います。
- 投稿機能に対する認可を設定したいので、4つのSQLテンプレートの中から
Enable insert access for authenticated users only
(上から2つ目)を選択し、Use this template
ボタンを押します。すると下記の画面が出てきます。
. -
Policy name
を設定します。今回はログイン済みのアカウントのみが投稿できるようにしたいので、認証済みのアカウントのみ投稿できる
という名前を設定します。 -
Allowed operation
を設定します。選択肢が4つありますが、データを挿入したいのでINSERT
を選択します。 -
Target roles
を設定します。今回はログイン済みのアカウントを対象とした認可のため、選択肢からauthenticated
を選択します。 -
WITH CHECK expression
を設定します。ここに書くもののイメージとしてはSQLでいうWHERE句だと考えていいでしょう。今回は、ログイン済みのアカウントであれば、常に投稿できる状態にしたいのでtrue
と書きます。 - 右下の
Review
のボタンを押します。設定したSQL情報が下記の画面のように出てくるため、情報に間違えがなければSave policy
を押してPolicyを保存します。
一覧機能に対する認可を設定しよう
次に一覧機能に対する認可の設定を行います。
- 一覧機能に対する認可を設定したいので、4つのSQLテンプレートの中から
Enable read access to everyone
(上から1つ目)を選択し、Use this template
ボタンを押します。すると下記の画面が出てきます。
. -
Policy name
を設定します。今回は全アカウントが閲覧できるようにしたいので、全員データの閲覧を可能にする
という名前を設定します。 -
Allowed operation
を設定します。全てのデータを選択して表示させたいのでSELECT
を選択します。 -
Target roles
を設定します。今回は全てのアカウントを対象とした認可のため、設定は不要です。 -
USING expression
を設定します。今回は、全てのアカウントが常にデータを閲覧できる状態にしたいのでtrue
と書きます。 - 右下の
Review
のボタンを押します。設定したSQL情報が下記の画面のように出てくるため、情報に間違えがなければSave policy
を押してPolicyを保存します。
Policyの作成を確認しよう
投稿機能と一覧機能に対するPolicyの設定が終わりました。
Policyの設定が完了している場合は、下記の画像のように行として表示されるためご確認ください。また、Policyを編集・削除する場合は右端の縦3点のボタンを押すと可能です。
投稿機能を実装しよう
最初に、投稿機能についての説明を行います。
下記がtop.tsx
の投稿機能における全体像です。
import Head from "next/head";
import styles from "../styles/Home.module.css";
import { supabase } from "../utils/supabase";
import { useRouter } from 'next/router';
import { useState,useEffect } from 'react';
export default function Top(){
const [newTitle, setNewTitle] = useState("");
const [newContent, setNewContent] = useState("");
const addPost = async (e) => {
e.preventDefault();
try {
const { error } = await supabase.from("posts").insert([
{
title: newTitle,
content: newContent,
},
]);
if (error) throw error;
await indexPost();
setNewTitle("");
setNewContent("");
} catch (error) {
alert("データの新規登録ができません");
}
};
return(
<>
<div className={styles.container}>
<main className={styles.main}>
<div>
<form onSubmit={addPost}>
<div>
<label>タイトル</label><br/>
<input
type="text"
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)} />
</div>
<div>
<label>内容</label><br/>
<textarea value={newContent} rows={10} cols={40} onChange={(e) => setNewContent(e.target.value)}/>
</div>
<div>
<button type="submit">登録</button>
</div>
</form>
</div>
</main>
<footer className={styles.footer}>
</footer>
</div>
</>
)
}
では、コードを上から順に追って説明します。
最初にタイトル、内容の各値を状態管理するための記述を行います。
const [newTitle, setNewTitle] = useState("");
const [newContent, setNewContent] = useState("");
次にsupabase.tsを呼び出し、認可部分の実装を行います。
今回はinsert()
を使用し、postsテーブルにデータを挿入します。
引数には挿入したいデータカラムとそれに伴う登録フォームの変数を設定します。
insert()
を使用する方法は、下記のsupabase公式ドキュメントにも記載されていますので、ご興味のある方はご参照ください🙌
await indexPost()
に関しては、投稿機能の実装後に説明する一覧機能のメソッドを呼び出しています。登録した値が一覧画面に表示されるように設定しています。
投稿完了後は投稿フォームの値を空にしたいため、setNewTitle("")
とsetNewContent("")
によりタイトルと内容の変数の値を空に設定しています。
try-catch
構文を用いることにより、例外処理を行い、エラーが出た場合の対処を行います。
const addPost = async (e) => {
e.preventDefault();
try {
const { error } = await supabase.from("posts").insert([
{
title: newTitle,
content: newContent,
},
]);
if (error) throw error;
await indexPost();
setNewTitle("");
setNewContent("");
} catch (error) {
alert("データの新規登録ができません");
}
};
投稿機能に関わる全体のマークアップを行います。
タイトルと内容のフォームにonChange
イベントを設定し、入力した各値を保持します。
return (
<>
<form onSubmit={addPost}>
<div>
<label>タイトル</label><br/>
<input
type="text"
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
/>
</div>
<div>
<label>内容</label><br/>
<textarea value={newContent} rows={10} cols={40} onChange={(e) => setNewContent(e.target.value)}/>
</div>
<div>
<button type="submit">登録</button>
</div>
</form>
</>
)
一覧機能を実装しよう
次に一覧機能についての説明を行います。
下記がtop.tsx
の一覧機能における全体像です。
import Head from "next/head";
import styles from "../styles/Home.module.css";
import { supabase } from "../utils/supabase";
import { useState,useEffect } from 'react';
export default function Top(){
const [posts, setPosts] = useState([]);
useEffect(() => {
(async () => await indexPost())();
}, []);
const indexPost = async () => {
try {
const { data, error } = await supabase.from("posts").select("*");
if (error) throw error;
setPosts(data);
} catch (error) {
alert(error.message);
setPosts([]);
}
};
return(
<>
<div className={styles.container}>
<main className={styles.main}>
<div>
<h1>トップページ</h1>
<div>
<table>
<thead>
<tr>
<th>投稿日</th>
<th>タイトル</th>
<th>内容</th>
</tr>
</thead>
<tbody>
{posts.map((post) => (
<tr key={post.id}>
<td>{post.created_at.substr(0, 10)}</td>
<td>{post.title}</td>
<td>{post.content}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</main>
<footer className={styles.footer}>
</footer>
</div>
</>
)
}
一覧機能の実装方法について、コードを上から順に説明します。
最初に一覧表示をする投稿(posts)の値を状態管理するための記述を行います。
また、投稿内容の繰り返し表示を防ぐためにuseEffect
でindexPost()
を呼び出します。
const [posts, setPosts] = useState([]);
useEffect(() => {
(async () => await indexPost())();
},[]);
次にsupabase.tsを呼び出し、認可部分の実装を行います。
今回はselect()
を使用し、postsテーブルのデータを全選択します。
select()
を使用する方法は、下記のsupabase公式ドキュメントにも記載されていますので、ご興味のある方はご参照ください🙌
一覧表示するデータをマークアップの際に使用したいため、setPosts()
の変数にdata
を設定しています。try-catch
構文を用いることにより、例外処理を行い、エラーが出た場合の対処を行います。
const indexPost = async () => {
try {
const { data, error } = await supabase.from("posts").select("*");
if (error) throw error;
setPosts(data);
} catch (error) {
alert(error.message);
setPosts([]);
}
};
一覧機能に関わる全体のマークアップを行います。
posts変数には上記で設定したsetPost(data)
のdata
(挿入した投稿の値)が入っています。
そのため、postsを用いてmap
構文で値を出力します。
return(
<>
<div className={styles.container}>
<Head>
<title>トップページ</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<div>
<table>
<thead>
<tr>
<th>投稿日</th>
<th>タイトル</th>
<th>内容</th>
</tr>
</thead>
<tbody>
{posts.map((post) => (
<tr key={post.id}>
<td>{post.created_at.substr(0, 10)}</td>
<td>{post.title}</td>
<td>{post.content}</td>
</tr>
))}
</tbody>
</table>
</div>
</main>
<footer className={styles.footer}>
</footer>
</div>
</>
)
認可ができているか確認しよう!
RLSの設定と投稿機能の実装を終えました!
認可の設定が上手くいっているかを確認してみましょう♪
下記画像のデモは、以下の順ですすみます。
- ログイン済みの状態で投稿
- ログアウト
- ログアウト状態で投稿
「すべてのユーザーが投稿の一覧表示を見ることができる」「ログイン済みのユーザーのみがタイトルと内容を投稿できる状態になっている」ことを確認することができました😚成功です✌️
.
最後に
これで実装はおしまいです!
今回は、Next.js×Supabaseを用いたアプリ開発における投稿機能の実装とそれに伴うRLS(Row Level Security)の使用方法について説明しました。今後は主にフロントエンド周辺、コミュニティ運営、Tech PR(技術広報)の記事を中心に書いていく予定ですので、気になる方はぜひフォローをよろしくお願いします♪ではでは〜🐥
Twitterアカウント:https://twitter.com/kaho_eng
参考資料