wheatandcatの開発ブログ

React Nativeで開発しているペペロミア & memoirの技術系記事を投稿してます

SupabaseのAuthとDatabaseを使ってみた

作成したChrome拡張でデータをサーバー上から同期させる機能をSupabaseを使って実装してみたので紹介

PR

github.com

Supabaseとは?

supabase.com

  • SupabaseはFirebaseの代替として注目されているBaas
  • 以下のサービスを提供している
    • Authentication
    • Database
    • Storage
    • Edge Functions
  • 各サービスがモダンな作りになっているので全体的に痒いところに手が届く作りになっている

実装

実装は以下のPlasmoとSupabaseの連携のドキュメントがあるので参考に実装

docs.plasmo.com

今回は、Supabaseでログインすると、データ同期ができるような作りにしたので、まずはAuthenticationから紹介

Authentication

実装は簡単でコード的には以下のみでOK

■ options.tsx

  const handleOAuthLogin = async (provider: Provider, scopes = "email") => {
    await supabase.auth.signInWithOAuth({
      provider,
      options: {
        scopes,
        redirectTo: location.href
      }
    })
  }

  略)

  // ログイン後は以下から認証情報を取得できる
  const { data, error } = await supabase.auth.getSession()

Supabaseはかなりのサービスログインをサポートしているのでコンソールから有効にすれば簡単に利用可能。

Database

SQL実行周り

SupabaseのDatabaseはPostgresを提供している。

SQLの操作はSupabaseのWebコンソールから実行可能。

今回の実装ではサーバーに保存したユーザー自身の情報だけ取得できればOKなので、以下のようなポリシー を設定。

■ table作成

create table
  public.items (
    id integer not null default nextval('items_id_seq'::regclass),
    uuid uuid not null,
    title text not null,
    url text not null,
    favIconUrl text null,
    created timestamp with time zone not null default current_timestamp,
    constraint items_pkey primary key (id)
  ) tablespace pg_default;

■ ポリシーの設定

CREATE policy "All own items" 
ON items
FOR ALL
USING ( auth.uid() = uuid );

上記のように設定することで認証したuuidに対してのみSQLが発行されるようになる。

具体的には以下のようなSQLを実行した場合に実際のSQLのWhereにポリシーの設定が反映される

■ コード

  const { data: items, error } = await supabase.from("items").select()

■ 発行されるSQL

SELECT *
FROM items
WHERE auth.uid() = items.uuid

こんな感じで実行されるので、簡単なSQL実行ならクライアントから直接Databse接続してもセキュリティが担保が簡単にできる設計になっている

Databaseの型安全

SupabaseはTypeScriptの型変換をサポートしている。
以下の手順で変換可能。

以下のコマンドでコマンドラインでSupabaseにログイン

npx supabase login

以下にで初期設定

npx supabase init 
npx supabase link --project-ref <プロジェクトのID>

以下のコマンドTypeScriptの型情報を生成してくれる。

npx supabase gen types typescript --linked > schema.ts

以下が実際に作成されたファイル

schema.ts

export type Json =
  | string
  | number
  | boolean
  | null
  | { [key: string]: Json }
  | Json[]

export interface Database {
  graphql_public: {
    Tables: {
      [_ in never]: never
    }
    Views: {
      [_ in never]: never
    }
    Functions: {
      graphql: {
        Args: {
          operationName?: string
          query?: string
          variables?: Json
          extensions?: Json
        }
        Returns: Json
      }
    }
    Enums: {
      [_ in never]: never
    }
    CompositeTypes: {
      [_ in never]: never
    }
  }
  public: {
    Tables: {
      countries: {
        Row: {
          id: number
          name: string
        }
        Insert: {
          id?: number
          name: string
        }
        Update: {
          id?: number
          name?: string
        }
        Relationships: []
      }
      items: {
        Row: {
          created: string
          favIconUrl: string | null
          id: number
          title: string
          url: string
          uuid: string
        }
        Insert: {
          created?: string
          favIconUrl?: string | null
          id?: number
          title: string
          url: string
          uuid: string
        }
        Update: {
          created?: string
          favIconUrl?: string | null
          id?: number
          title?: string
          url?: string
          uuid?: string
        }
        Relationships: []
      }
    }
    Views: {
      [_ in never]: never
    }
    Functions: {
      [_ in never]: never
    }
    Enums: {
      [_ in never]: never
    }
    CompositeTypes: {
      [_ in never]: never
    }
  }
  storage: {
    Tables: {
      buckets: {
        Row: {
          allowed_mime_types: string[] | null
          avif_autodetection: boolean | null
          created_at: string | null
          file_size_limit: number | null
          id: string
          name: string
          owner: string | null
          public: boolean | null
          updated_at: string | null
        }
        Insert: {
          allowed_mime_types?: string[] | null
          avif_autodetection?: boolean | null
          created_at?: string | null
          file_size_limit?: number | null
          id: string
          name: string
          owner?: string | null
          public?: boolean | null
          updated_at?: string | null
        }
        Update: {
          allowed_mime_types?: string[] | null
          avif_autodetection?: boolean | null
          created_at?: string | null
          file_size_limit?: number | null
          id?: string
          name?: string
          owner?: string | null
          public?: boolean | null
          updated_at?: string | null
        }
        Relationships: [
          {
            foreignKeyName: "buckets_owner_fkey"
            columns: ["owner"]
            referencedRelation: "users"
            referencedColumns: ["id"]
          }
        ]
      }
      migrations: {
        Row: {
          executed_at: string | null
          hash: string
          id: number
          name: string
        }
        Insert: {
          executed_at?: string | null
          hash: string
          id: number
          name: string
        }
        Update: {
          executed_at?: string | null
          hash?: string
          id?: number
          name?: string
        }
        Relationships: []
      }
      objects: {
        Row: {
          bucket_id: string | null
          created_at: string | null
          id: string
          last_accessed_at: string | null
          metadata: Json | null
          name: string | null
          owner: string | null
          path_tokens: string[] | null
          updated_at: string | null
          version: string | null
        }
        Insert: {
          bucket_id?: string | null
          created_at?: string | null
          id?: string
          last_accessed_at?: string | null
          metadata?: Json | null
          name?: string | null
          owner?: string | null
          path_tokens?: string[] | null
          updated_at?: string | null
          version?: string | null
        }
        Update: {
          bucket_id?: string | null
          created_at?: string | null
          id?: string
          last_accessed_at?: string | null
          metadata?: Json | null
          name?: string | null
          owner?: string | null
          path_tokens?: string[] | null
          updated_at?: string | null
          version?: string | null
        }
        Relationships: [
          {
            foreignKeyName: "objects_bucketId_fkey"
            columns: ["bucket_id"]
            referencedRelation: "buckets"
            referencedColumns: ["id"]
          },
          {
            foreignKeyName: "objects_owner_fkey"
            columns: ["owner"]
            referencedRelation: "users"
            referencedColumns: ["id"]
          }
        ]
      }
    }
    Views: {
      [_ in never]: never
    }
    Functions: {
      can_insert_object: {
        Args: {
          bucketid: string
          name: string
          owner: string
          metadata: Json
        }
        Returns: undefined
      }
      extension: {
        Args: {
          name: string
        }
        Returns: string
      }
      filename: {
        Args: {
          name: string
        }
        Returns: string
      }
      foldername: {
        Args: {
          name: string
        }
        Returns: unknown
      }
      get_size_by_bucket: {
        Args: Record<PropertyKey, never>
        Returns: {
          size: number
          bucket_id: string
        }[]
      }
      search: {
        Args: {
          prefix: string
          bucketname: string
          limits?: number
          levels?: number
          offsets?: number
          search?: string
          sortcolumn?: string
          sortorder?: string
        }
        Returns: {
          name: string
          id: string
          updated_at: string
          created_at: string
          last_accessed_at: string
          metadata: Json
        }[]
      }
    }
    Enums: {
      [_ in never]: never
    }
    CompositeTypes: {
      [_ in never]: never
    }
  }
}

このファイルを以下のように設定

core/supabase.ts

import { createClient } from "@supabase/supabase-js";
import type { Database } from "../schema";

export const supabase = createClient<Database>(
  process.env.PLASMO_PUBLIC_SUPABASE_URL,
  process.env.PLASMO_PUBLIC_SUPABASE_KEY,
);

上記を設定することで以下のような補完が効くようになる。

まとめ

  • 上記の対応でログインからデータ登録/同期までの実装ができた
  • Supabaseは作りもモダンで、かなり良い
  • 値段的にはプロジェクト2個までは無料で使えるので、テスト開発に良い感じでした

supabase.com