Supabase EdgeFunctionでAPI作成してみた
Firebaseの代替
firebaseの代替として、名前をしばしばみるSupabase
です。
今回、Supabaseの以下サービスを用いて、APIを作成してみました。
- Edge Function
- Storage
- Database
元々Flutterでアプリケーションを作成している最中でして、そのバックエンドの役割として、今回作成をしています。以下画像が私が現在作成中のアプリケーションのアーキテクチャです。
Edge Functionの作成
基本的にはチュートリアル通り実装いただければできる想定です。
以下画像の右上にあるTerminal instructions
に記載されたコマンドを実行します。
私の場合は、ローカルPCにテキトーにフォルダを1つ作成しました。
準備・関数作成
-
package.json
の作成
npm init -y
- Supabaseのinstall
npm i @supabase/supabase-js
- 関数の作成(xxxxxxxxxxは関数名ですので設定してください)
npx supabase functions new xxxxxxxxxx
-
functions/xxxxxxxxxx
というフォルダが作成されます。
※index.ts
とtypescriptのファイルがありますが、デフォルトでそうなっています。
デプロイ・実行
- デプロイ(yyyyyyyyyyyyyyyyyは、各自で異なります)
npx supabase functions deploy hello-world --project-ref yyyyyyyyyyyyyyyyy
- 実行(zzzzzzzzzzzzzzには
anon key
が設定されています)
curl -L -X POST 'https://xxxxxxxxxxxxxxxxx.functions.supabase.co/hello-world' -H 'Authorization: Bearer zzzzzzzzzzzzzz' --data '{"name":"Functions"}'
以下のようなレスポンスが出力されます
'{"name":" Hello Functions"}'
- Supabaseのコンソールから、ログの確認を行います。
Database・Storageからデータ取得
Databaseの作成
- テーブルを作成
-
方法1:SQLで作成いただく方法がやりやすい方、SQLの形で残しておきたい方は
SQL Editor
から進めてみてください。
-
方法2:画面上でポチポチしながらやりたい方は
Table Editor
のNew Table
から- この際に次手順のRLSも設定してしまってよいかと思います。
- この際に次手順のRLSも設定してしまってよいかと思います。
-
方法1:SQLで作成いただく方法がやりやすい方、SQLの形で残しておきたい方は
- RLSの有効化
そもそもRLSってどういうもの?といった話は以下記事が非常に参考になりました
- RLS有効化作業
-
方法1でテーブル作成された方は、引き続きSQLを実行します。
alter table public."your_table_name" enable row level security;
-
方法2でテーブル作成された方は、
Enabled Row Level Security
にチェックを入れておきます。 - データ取得が行えるようなポリシーを作成
- 例えば、データ選択(SQLでいう
select
句)ができるようにPolicyを作成する場合(insertやupdateは行わず、selectのみ(取得)のみするテーブルという形で進めさせていただきます。)- メニューから
Authentication
を選択して、Configuration
のPolicies
から対象のテーブルを表示します。(私の場合は、moviesテーブルを用意しているので、movies
と表示されています)
-
New Policy
というボタンをクリックすると、 以下のように、どういった形でPolicyを作成したいか選択する画面に切り替わります。
-
Get started quickly
を選択します。(ある程度はTemplateに頼ることにします)
-
Enable read access to everyone
文字通りにはなりますが、全ユーザへのアクセスを許可します。怖っっ、となるかもしれませんが、ただGETリクエストを実行するだけではデータ取得できません。
上記のEdge Function作成時のリクエスト内容にAuthorization
というHeaderを設定しています。Header未設定のリクエストをしても、401エラーが発生してデータ取得はできません。
- 以下の画面の状態(特に何も変更しない)でReviewをクリックします。このとき、
Target roles
に何も設定しない状態で進めます。
-
Reviewing policy
ということで、以下のクエリを発行しますがよろしいですか?といった確認画面です。問題なければ、Save Policy
をクリックします。
- 対象のテーブルのPolicyを確認すると、以下のように、
Aplied to
がpublic
の状態になっているはずです。
- ここからは、少し掘りこんだ内容になりますが、
Aplied to
をauthenticated
にしてみます。(publicでない形にしたい)
- リクエストの結果は以下の通りです。認証に関する設定をしていないので、何も取得できません。
-
Aplied to
をanon
にしてみます。(publicでない形にしたい)
-
Project API keys
をリクエストに設定できているので、情報が取得できます。
- メニューから
- 例えば、データ選択(SQLでいう
Storageの作成
- バケット作成
- Supabaseのコンソール上から、
Storage
を選択後、New bucket
をクリックすると、以下画面が表示されるので、バケット名を入力して、バケットを作成します。認証なしにPublicAccessはされるべきではない、と思った方はprivateのまま作成してください。
- Supabaseのコンソール上から、
- ポリシー作成
- Storageのメニューに
Configuration
と見出しがあり、そこにPolicies
とあるはずなので、そこで設定をします。それなりに細かく設定ができそうですので、適宜設定を行います。
- Storageのメニューに
これで、DatabaseとStorageの準備ができましたので、実装に入ります。
全体の実装の流れ
基本的には以下を参照いただく形で問題ないかと思います。
上記ファイル内の以下実装部分がデータ取得部分になるので、その部分を適宜修正いただく形で進めます。const { data, error } = await supabaseClient.storage.from('my-bucket').download('sample.txt')
私の場合は、DBのあるテーブルに画像ファイル名のカラムを用意しており、そのファイル名の値を用いて、ストレージから画像URLを取得する実装を行っています。
それでは、Database・Storageそれぞれのデータ取得について、見ていきます。
先に共有しますが、javaScriptの実装は以下ドキュメントに一通り記載されています。
Databaseからデータ取得したい場合
一例を挙げますが、sample
というテーブルを作成して、idの昇順で取得する場合の実装
const { data, error } = await supabase
.from('sample')
.select('*')
.order('id', { ascending: true });
Storageからデータ取得したい場合
画像を画面に表示する場合は、画像のURLが必要になります。
ある画像のURLを取得する方法を例として挙げます。demoバケットから、1.png
という画像ファイルを取得する場合
※createSignedUrl
とgetPublicUrl
の違い
- バケットのVisibilityがPublic:
getPublicUrl
- そうでない(Private):
createSignedUrl
let response = await supabase.storage
.from('demo')
.createSignedUrl(`1.png`, 300)
// 複数画像かつPublicでない場合
// .createSignedUrls(['folder/avatar1.png', 'folder/avatar2.png'], 60)
最終的には・・・
断片的にコードを出しても、わかりにくいんですけど・・・ってなるかもしれないので、
リクエストを受け取ってから、データを返す一連の実装を以下に記載しました。
実装内容
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
//
interface IMovie {
year: number;
title: String;
subTitle: String;
description: String;
edSong: String;
edSongUrl: String;
imageUrl: String;
}
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers":
"authorization, x-client-info, apikey,content-type",
};
serve(async (req) => {
if (req.method === "OPTIONS") {
return new Response("ok", { headers: corsHeaders });
}
const supabase = createClient(
Deno.env.get("SUPABASE_URL") ?? "",
Deno.env.get("SUPABASE_ANON_KEY") ?? "",
{
global: { headers: { Authorization: req.headers.get("Authorization") } },
}
);
// 映画情報をmoviesテーブルから取得
const { data, error } = await supabase
.from("movies")
.select()
.order("year", { ascending: true });
const imageUrlArr: String[] = [];
// moviesテーブルのうち、画像URLのカラムの情報を配列に設定
// movies/movie_list:Storageのパス
data.forEach((mov) => {
imageUrlArr.push(`movies/movie_list/${mov.image_file_name}`);
});
// 映画の画像URL一覧取得をStorageから取得
const imageUrls = await supabase.storage
.from('conan')
.createSignedUrls(imageUrlArr, 120);
// 映画情報格納
const movies: IMovie[] = [];
data.forEach((mov, index) => {
const movie: IMovie = {
year: mov.year,
title: mov.title,
subTitle: mov.subtitle,
description: mov.description,
edSong: mov.ed_song,
edSongUrl: mov.ed_song_url,
imageUrl: imageUrls.data[index]['signedUrl'],
};
movies.push(movie);
});
const returnData = {
movies: movies,
};
return new Response(JSON.stringify(returnData), {
headers: {
...corsHeaders,
"Content-Type": "text/plain; charset=utf-8",
},
status: 200,
});
});
アプリケーション側からの呼び出し
私の場合は、Flutterのアプリケーションから呼び出しを行っています。
Flutterとは関係しませんが前提知識について
異なるドメイン(FirebaseのホスティングサービスからとSupabaseのEdge Functionを呼び出す)、request headerにAuthorizationが含まれるため、以下事項に注意して実装が必要です。
- CORS
- Preright request
これらの知識事項は、以下記事あたりが参考になるかと思います。
データ取得実装StatefulWidgetをもちいて、initState内の処理でデータ取得を行っています。
supabaseBaseのUrl・supabaseKeyに関する情報は、.env
ファイルから読み込む形です。
各処理実装
import 'package:flutter_dotenv/flutter_dotenv.dart';
Future<void> main() async {
await dotenv.load(fileName: '.env'); //環境変数用のファイル
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(const ProviderScope(child: MyApp()));
}
import 'package:supabase_flutter/supabase_flutter.dart';
class Utils {
static String supabaseUrl = dotenv.get('SUPABASE_URL'); //環境変数読み込み
static String supabaseKey = dotenv.get('SUPABASE_KEY'); //環境変数読み込み
static SupabaseClient supabaseClient =
SupabaseClient(supabaseUrl, supabaseKey);
static SupabaseStorageClient storageClient = SupabaseStorageClient(
'$supabaseUrl/storage/v1',
{
'Authorization': 'Bearer $supabaseKey',
},
);
}
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:conan/supabase_utils.dart';
// APIから情報取得
Future<void> readData() async {
final apiResponse = await http.get(
Uri.parse(
'https://xxxxxxxxxxxxxxxxx.functions.supabase.co/xxxxxxxxxx',
),
headers: {'Authorization': 'Bearer ${Utils.supabaseKey}'},
);
final responseData = json.decode(apiResponse.body);
setState(() {
// 用意した変数に値を設定
});
}
void initState() {
super.initState();
readData();
}
まとめ
Supabaseに関して、Edge Function・Database・Storageを用いたAPI作成の方法について、
簡単ではありますが、作り方を記載してみました。
Discussion