🌵

Supabase EdgeFunctionでAPI作成してみた

2023/03/28に公開

Firebaseの代替

firebaseの代替として、名前をしばしばみるSupabaseです。
今回、Supabaseの以下サービスを用いて、APIを作成してみました。

  • Edge Function
  • Storage
  • Database

元々Flutterでアプリケーションを作成している最中でして、そのバックエンドの役割として、今回作成をしています。以下画像が私が現在作成中のアプリケーションのアーキテクチャです。

Edge Functionの作成

基本的にはチュートリアル通り実装いただければできる想定です。
以下画像の右上にあるTerminal instructionsに記載されたコマンドを実行します。

私の場合は、ローカルPCにテキトーにフォルダを1つ作成しました。

準備・関数作成

  1. package.jsonの作成
npm init -y
  1. Supabaseのinstall
npm i @supabase/supabase-js

https://www.npmjs.com/package/@supabase/supabase-js

  1. 関数の作成(xxxxxxxxxxは関数名ですので設定してください)
npx supabase functions new xxxxxxxxxx
  1. functions/xxxxxxxxxxというフォルダが作成されます。
    index.tsとtypescriptのファイルがありますが、デフォルトでそうなっています。

デプロイ・実行

  1. デプロイ(yyyyyyyyyyyyyyyyyは、各自で異なります)
npx supabase functions deploy hello-world --project-ref yyyyyyyyyyyyyyyyy
  1. 実行(zzzzzzzzzzzzzzにはanon keyが設定されています)
curl -L -X POST 'https://xxxxxxxxxxxxxxxxx.functions.supabase.co/hello-world' -H 'Authorization: Bearer zzzzzzzzzzzzzz' --data '{"name":"Functions"}'

以下のようなレスポンスが出力されます

'{"name":" Hello Functions"}'
  1. Supabaseのコンソールから、ログの確認を行います。

Database・Storageからデータ取得

Databaseの作成

  • テーブルを作成
    • 方法1:SQLで作成いただく方法がやりやすい方、SQLの形で残しておきたい方はSQL Editorから進めてみてください。
    • 方法2:画面上でポチポチしながらやりたい方はTable EditorNew Tableから
      • この際に次手順のRLSも設定してしまってよいかと思います。

  • RLSの有効化
    そもそもRLSってどういうもの?といった話は以下記事が非常に参考になりました

https://labs.septeni.co.jp/entry/2022/03/28/193223

  • RLS有効化作業

https://supabase.com/docs/guides/auth/row-level-security

  • 方法1でテーブル作成された方は、引き続きSQLを実行します。
    alter table public."your_table_name"
    enable row level security;
    
  • 方法2でテーブル作成された方は、Enabled Row Level Securityにチェックを入れておきます。
  • データ取得が行えるようなポリシーを作成
    • 例えば、データ選択(SQLでいうselect句)ができるようにPolicyを作成する場合(insertやupdateは行わず、selectのみ(取得)のみするテーブルという形で進めさせていただきます。)
      1. メニューからAuthenticationを選択して、ConfigurationPoliciesから対象のテーブルを表示します。(私の場合は、moviesテーブルを用意しているので、moviesと表示されています)
      2. New Policyというボタンをクリックすると、 以下のように、どういった形でPolicyを作成したいか選択する画面に切り替わります。
      3. Get started quicklyを選択します。(ある程度はTemplateに頼ることにします)
      4. Enable read access to everyone文字通りにはなりますが、全ユーザへのアクセスを許可します。怖っっ、となるかもしれませんが、ただGETリクエストを実行するだけではデータ取得できません。
        上記のEdge Function作成時のリクエスト内容にAuthorizationというHeaderを設定しています。Header未設定のリクエストをしても、401エラーが発生してデータ取得はできません。
      5. 以下の画面の状態(特に何も変更しない)でReviewをクリックします。このとき、Target rolesに何も設定しない状態で進めます。

      6. Reviewing policyということで、以下のクエリを発行しますがよろしいですか?といった確認画面です。問題なければ、Save Policyをクリックします。
      7. 対象のテーブルのPolicyを確認すると、以下のように、Aplied topublicの状態になっているはずです。
      8. ここからは、少し掘りこんだ内容になりますが、Aplied toauthenticatedにしてみます。(publicでない形にしたい)
      9. リクエストの結果は以下の通りです。認証に関する設定をしていないので、何も取得できません。
      10. Aplied toanonにしてみます。(publicでない形にしたい)
      11. Project API keysをリクエストに設定できているので、情報が取得できます。

Storageの作成

  • バケット作成
    • Supabaseのコンソール上から、Storageを選択後、New bucketをクリックすると、以下画面が表示されるので、バケット名を入力して、バケットを作成します。認証なしにPublicAccessはされるべきではない、と思った方はprivateのまま作成してください。
  • ポリシー作成
    • StorageのメニューにConfigurationと見出しがあり、そこにPoliciesとあるはずなので、そこで設定をします。それなりに細かく設定ができそうですので、適宜設定を行います。

これで、DatabaseとStorageの準備ができましたので、実装に入ります。

全体の実装の流れ

基本的には以下を参照いただく形で問題ないかと思います。
https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/read-storage/index.ts#L5-L7
上記ファイル内の以下実装部分がデータ取得部分になるので、その部分を適宜修正いただく形で進めます。

const { data, error } = await supabaseClient.storage.from('my-bucket').download('sample.txt')

私の場合は、DBのあるテーブルに画像ファイル名のカラムを用意しており、そのファイル名の値を用いて、ストレージから画像URLを取得する実装を行っています。

それでは、Database・Storageそれぞれのデータ取得について、見ていきます。
先に共有しますが、javaScriptの実装は以下ドキュメントに一通り記載されています。

https://supabase.com/docs/reference/javascript/installing

Databaseからデータ取得したい場合

一例を挙げますが、sampleというテーブルを作成して、idの昇順で取得する場合の実装

  const { data, error } = await supabase
    .from('sample')
    .select('*')
    .order('id', { ascending: true });

Storageからデータ取得したい場合

画像を画面に表示する場合は、画像のURLが必要になります。

ある画像のURLを取得する方法を例として挙げます。demoバケットから、1.pngという画像ファイルを取得する場合
createSignedUrlgetPublicUrlの違い

  • バケットの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
    これらの知識事項は、以下記事あたりが参考になるかと思います。

https://qiita.com/yana1316/items/34ee351adb6091ea7fc1
https://qiita.com/nnishimura/items/1f156f05b26a5bce3672

データ取得実装StatefulWidgetをもちいて、initState内の処理でデータ取得を行っています。
supabaseBaseのUrl・supabaseKeyに関する情報は、.envファイルから読み込む形です。

各処理実装

main.dart
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()));
}
supabase_utils.dart
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',
    },
  );
}
getData.dart
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