Next.jsでSupabaseを使う

## はじめに
以下のドキュメントに基づいて解説します。
また、データベーステーブルの作成及びRLSポリシーの設定は完了している前提とします。

https://supabase.com/docs/guides/getting-started/quickstarts/nextjs

RLSポリシーについては、以下参照。

https://aiv.co.jp/docs/blog/supabase-row-level-security-rls

## 環境変数の設定
supabseプロジェクトのページから以下の２つの変数を取得し、`.env.local`ファイルに記述する
```.env.local
NEXT_PUBLIC_SUPABASE_URL=<SUBSTITUTE_SUPABASE_URL>
NEXT_PUBLIC_SUPABASE_ANON_KEY=<SUBSTITUTE_SUPABASE_ANON_KEY>
```

## Supabaseパッケージのインストール
以下をターミナルで実行し、パッケージをインストールする
```bash
npm install @supabase/supabase-js
```

## Supabaseクライアントの作成
Next.jsプロジェクト内でSupabaseのクライアントを作成していく

1. プロジェクトのルートディレクトリに`utils/supabase`ディレクトリを作成
1. `utils/supabase`ディレクトリに`server.tsx`, `client.tsx`を作成

```typescript:utils/supabase/client.tsx
import { createBrowserClient } from "@supabase/ssr";
import { Database } from "@/types/database.types";

export const createClient = () => {

    createBrowserClient<Database>(
        process.env.NEXT_PUBLIC_SUPABASE_URL!,
        process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    );
};
```

```typescript:utils/supabase/server.tsx

import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { cookies } from "next/headers";
import { Database } from "@/types/database.types";

export const createClient = async() => {

  const cookieStore = await cookies();  

  return createServerClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          return cookieStore.get(name)?.value;
        },
        set(name: string, value: string, options: CookieOptions) {
          try {
            cookieStore.set({ name, value, ...options });
          } catch (error) {
            // The `set` method was called from a Server Component.
            // This can be ignored if you have middleware refreshing
            // user sessions.
          }
        },
        remove(name: string, options: CookieOptions) {
          try {
            cookieStore.set({ name, value: "", ...options });
          } catch (error) {
            // The `delete` method was called from a Server Component.
            // This can be ignored if you have middleware refreshing
            // user sessions.
          }
        },
      },
    },
  );
};
```

# 使用する
## サーバサイドで使用する
```typescript:page.tsx
import { createClient } from '@/utils/supabase/server';
export default async function Instruments() {
  const supabase = await createClient();
  const { data: instruments } = await supabase.from("instruments").select();
  return <pre>{JSON.stringify(instruments, null, 2)}</pre>
}
```

## クライアントサイドで使用する
```typescript:page.tsx
'use client'

import { createClient } from '@/utils/supabase/client';
export default async function Instruments() {
  const supabase = await createClient();
  const { data: instruments } = await supabase.from("instruments").select();
  return <pre>{JSON.stringify(instruments, null, 2)}</pre>
}
```

## クエリの書き方
### `.from(tableName) `
対象のテーブルを指定します。
```js
supabase.from('users')
```

### `.select(columns)`
カラムを指定します。`*`で全カラム指定。

```js
supabase.from('users').select('*')
supabase.from('users').select('id, name')
supabase.from('users').select('profile(id, bio)') // リレーションの取得
```
### `.insert(data)`
新しいレコードを挿入します。

```js
const { data, error } = await supabase
  .from('users')
  .insert([{ name: 'Taro', age: 30 }])
```
### `.update(data)`
レコードを更新します。`.eq()`などで条件を指定する必要があります。

```js
const { data, error } = await supabase
  .from('users')
  .update({ age: 31 })
  .eq('id', 1)
```
### `.delete()`
レコードを削除します。.eq()などで条件を指定する必要があります。

```js
const { data, error } = await supabase
  .from('users')
  .delete()
  .eq('id', 1)
```

### 比較系フィルター( `.eq(column, value) .neq() .gt() .gte() .lt() .lte()` )
フィルター条件を指定します。

```js
.select('*').eq('id', 1)       // id = 1
.select('*').neq('age', 20)    // age != 20
.select('*').gt('age', 20)     // age > 20
.select('*').gte('age', 20)    // age >= 20
.select('*').lt('age', 30)     // age < 30
.select('*').lte('age', 30)    // age <= 30
```

### セット・範囲系フィルタ( `.in(column, [values]) .notIn() .is() .not()` )
指定カラムが配列内要素との一致・不一致によってフィルターします

`.in()`    : 配列に含まれる
`.notIn()` : 配列に含まれない
`.is()`	   : nullの比較
`.not()`   : 否定（特殊系）

```js
.select('*').in('id', [1, 2, 3])         // id == 1 or 2 or 3
.select('*').notIn('id', 'in', '(1, 2)') // id != 1 and id != 2
.select('*').is('deleted_at', null)      // deleted_at == null
.select('*').not(id, 'eq', 0)            // status != 0
```
### 部分一致検索・パターンマッチ( `.like(column, pattern) .ilike() .textSearch() .match()` )
部分一致・全部一致で検索します（%をワイルドカードに使用）

```js
// '太郎'に部分一致
.select('*').like('name', '%太郎%')
// '@gmail.com'に大文字小文字無視の部分一致
.select('*').ilike('email', '%@gmail.com')
// patternに全部一致するフルテキスト検索
.select('*').textSearch('description', 'search term')
// name == 'Taro' and age == 30 に一致するデータ
.select('*').match({ name: 'Taro', age: 30 })
```
### `.order(column, { ascending: true/false })`
`ascending: true/false`で（降順・昇順）並び替えを行います

```js
// `created_at`カラムについて昇順で並び替える
.select('*').order('created_at', { ascending: false })
```

### `.limit(count)`
取得件数を制限します

```js
// 10件のみ取得する
.select('*').limit(10)
```

### `.single()` / `.maybeSingle()`

`.single()`     ：1件だけ取得することを期待（なければエラー）
`.maybeSingle()`：1件取得できれば返す（なくてもエラーにならない）

```js
// id == 1のデータを1件だけ取得する(なければエラー)
.select('*').eq('id', 1).single()
```

### `.rpc(function_name, params)`

Supabaseのストアドプロシージャ（PostgreSQL関数）を呼び出します

```js
const { data, error } = await supabase
  .rpc('get_user_posts', { user_id: 1 })
```

### 複数条件の組み合わせ例
```js
const { data, error } = await supabase
  .from('users')
  .select('*')
  .gt('age', 20)
  .lt('age', 50)
  .like('name', '%太郎%')
  .order('created_at', { ascending: false })
  .limit(10)
```
:::note info
Supabaseはバックエンドで PostgREST を使用しており、クエリチェーンで指定された条件（.eq(), .gt(), .like()など）は、最終的に1つのHTTPクエリパラメータ群に変換されてサーバーに送信されます。

このため、以下のように順番を変えても、SQLに変換されたときには同じ意味のWHERE句になります。
:::
:::note warn 
### ただし順序が意味を持つケースもある
順不同が前提ですが、以下のような非フィルター系メソッド（副作用があるもの）が絡む場合は、順序に注意が必要です。

❌ 良くない例
```js
// 更新前にフィルターせずに .update() を呼ぶと全件対象になる可能性
supabase
  .from('users')
  .update({ status: 'active' })  // ← この時点で全件対象になる
  .eq('country', 'Japan')
```
✅ 正しい順序
```js
// フィルターしてから更新
supabase
  .from('users')
  .eq('country', 'Japan')
  .update({ status: 'active' })
```
:::