作成したChrome拡張でデータをサーバー上から同期させる機能をSupabaseを使って実装してみたので紹介
PR
Supabaseとは?
- SupabaseはFirebaseの代替として注目されているBaas
- 以下のサービスを提供している
- Authentication
- Database
- Storage
- Edge Functions
- 各サービスがモダンな作りになっているので全体的に痒いところに手が届く作りになっている
実装
実装は以下のPlasmoとSupabaseの連携のドキュメントがあるので参考に実装
今回は、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
以下が実際に作成されたファイル
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 } } }
このファイルを以下のように設定
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個までは無料で使えるので、テスト開発に良い感じでした