Bubbleのプラグインで、サーバーActionから、JSONオブジェクトを返す方法

# はじめに
Bubbleのプラグインを作ろうとしていて、わからなくて数日試行錯誤してしまったので、結論を書いておきます。時間をかけて試行錯誤し、少しいじるとすぐ動かなくなり、またBubbleのサーバー処理はデバッグがすごく大変なので、自分用に記録を残しておきたいという思いが強いです。

## 参考にしたBubble Forumの記事
[Output an object for server-side/client-side - Tips - Bubble Forum](https://forum.bubble.io/t/output-an-object-for-server-side-client-side/59419)

こちらの投稿をもとに、私が試行錯誤した内容です。


# 課題
まず、Bubbleは、ノーコード or ローコードでアプリが作れるSaaSです。私は、そこで使用するためのプラグインを作ろうと思いました。

作ろうとしてみたプラグインは、Supabaseからデータを抽出するというもの。

SupabaseのAPI呼び出しは、難なくできました。Bubbleを使わず、Make.comで作ってみたんですが、サラっと取得できました。
難しかったのは、BubbleからAPI呼び出しをして、その結果をクライアント側に返すということ。それが今回の課題で、この記事です。

ちなみにAPI callsだけで済む要件なら、それがベストです。今回は、サーバーサイドのActionを使いたかった。

**課題：Bubbleのプラグインで、Supabaseへアクセスしてデータを抽出し、BubbleのWorkflowで扱えるようにする**

## 制約１：サーバーサイドのActionsの処理
プラグインからSupabaseへのアクセスは、APIキーなどを渡すため、クライアントサイドではなく、サーバーサイドで動かす必要があります。そうしないと、BubbleアプリのユーザーへAPIキーが漏れてしまう。

また、引数などを柔軟に扱える"Actions"で行いたいということも制約です。（API callsではないという意味）

## 制約２：任意のテーブル、任意の列
プラグインとして提供するからには、プラグイン利用者（＝アプリ開発者）の持つ、様々なテーブルや列にアクセスできないといけません。「test_table」から「col1」と「col2」を取り出すことがハードコーディングされていては、使い勝手が悪いということです。当たり前です。

## つまり
サーバーサイドの戻り値として、呼び出し側が指定した自由な形を返したい。クエリを投げたら、↓こんなレスポンスを得たい。（Google Analyticsの日ごとのユーザー数、セッション数です）
```sql
SELECT date, users, sessions FROM ga_data_daily_summary;
```
```json
[
    {
        "date": "2024-11-22",
        "users": 100,
        "sessions": 200
    },
    {
        "date": "2024-11-23",
        "users": 110,
        "sessions": 210
    }
]
```
※ 後述しますが、最終的にはこの形ではない形式で、この内容を得ます。


# サーバーアクションから、JSONを返す設定方法
ではいきなり解です。

## １．APIを定義する
**API callsの本来の機能は使わない**のですが、ここで、戻り値の型を定義します。

ここで設定する `API Name`と `Name` は、後で出てきます。`ResponseType (Supabase)`のような形で、画面のElementなどの型として使われます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/d2dedf95-696b-dc65-7a9b-a6a190a3b793.png)

ここでは、APIのURL等は指定しなくて、いきなり一番下の `Manually enter API response` をクリックして、「こんなオブジェクトが返るよ」という指定をします。（参考記事では、APIを呼ぶステップがありますが、省略可能です。やってもいいです。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/f8748f30-43a2-0447-3410-7107acf1315f.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/4b1d5d80-bcd2-fa13-17c9-bdf8d16913a7.png)
私の場合は↑こうしました。

このオブジェクトを１つ返す。`db_records`の中に`columns`があって、`columns`にクエリ結果の１行が指定された列の順番に入っていると。

実際はこういうイメージですが、この入力欄では繰り返しが省略されます。
```json
{
    "db_recrods": [
        {
            "columns": [
                "2024-11-22",
                "100",
                "200"
            ]
        },
        {
            "columns": [
                "2024-11-23",
                "110",
                "210"
            ]
        }
    ]
}
```

SAVEボタンを押すと、「あなたが入力したのはこの形ですね？」っていう書き直された画面が出てきます。ここに表示されている情報がとても重要なので、しつこく書きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/d978054d-e339-2910-dd3c-09bf6bbec18b.png)

① `ResponseType`型は、こういう型です
② `db_records`というキーを持ち、型はリスト（配列）です
③ このリスト（配列）を、`ResponseType db_record`型と呼びます
④ `ResponseType db_record`型のリストの１要素は、`columns`というキー値を持ち、リスト（配列）です
⑤ `columns`のリストの１要素は、全部text型です。

本当は、`columns`には、`[文字列、数値、数値]`のように、汚いですがいろいろ入れたい。JavaScriptならできそう。でも、全部そろってる必要があります。これは、httpサーバーがクライアントへ返すものだからしかたないのかもしれません。

### 注意
よく見ると、私は**Manually enter API responseで"db_records"と入力したのに、③では"db_record"と"s"を取られています**。また、ResponseTypeとはスペース区切り。

もし私が、"ResponseType"でなく"Response Type"、"db_records"でなく"db records"と名付けたすると、"Response Type db record"という型になり、固有名詞なのか一般名詞なのかまったくわからなくなるので、非常に注意が必要です。returnとかvalueとかrecordとかそういう単語は、Bubble用語としてもいっぱい使われるので、自分用の"db_records"のような単語にした方が良いです。（それでも勝手に"s"を取ったりしますが）

## ２．Actionsを作る
### 2.1. 設定項目
Get RecordsというActionを作ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/93c74678-fa76-eb50-e38f-629de452049e.png)

① 名前は、"Get Records"
② Action typeは、"Server side"。（APIキーを扱うため）
テーブル名、列名のFieldは割愛

次からがキモ。

③ "RR_TYPE"という名前でFiledを定義し、④Captionは"Return Record Type"、⑤Editorは、"App Type"
⑥ 戻り値の変数名は"RR"、⑦Captionは"Returned Records"、⑧Key Typeは、"as RR_TYPE"

③～⑤と、⑥～⑧の塊が重要です。
まずなかなか思いつかないけどField（入力引数）として、戻り値を定義します。⑤で`App Type`と指定します。
そうすると、次のReturn values（戻り値）として、Key Type（戻り値の型）を`as RR_TYPE`と指定できます。

ステップ１でAPI callsを作りましたが、それを⑤で受けて、⑧で利用する感じです。

ちなみに③と⑥が急に全部大文字ですが、これは私の都合です。この名前は表に出ない項目名（開発用の変数）だと思っているのですが、万が一どこかに出てきたらすぐわかるように、ちょっと目立つ変数名にしてあります。

### 2.2. Action code（function）
Supabaseから抽出する方法は割愛します。ここでは、レコードを取得して、こんな形のオブジェクトになりますという、functionの戻り値の型だけ。

```javascript
async function(properties, context) {
  /* ～ Supabaseの処理は割愛 ～ */

  return {
    "RR": {
      "_p_db_recrods": [
        {
          "_p_columns": [
            "2024-11-22",
            "100",
            "200"
          ]
        },
        {
          "_p_columns": [
            "2024-11-23",
            "110",
            "210"
          ]
        }
      ]
    }
  };
}                      
```
- 戻り値は、オブジェクトで、Return valuesで定義した"RR"というキー値に入れる
- "RR"の値は、API callsで定義した型をちょっと変える
    - **キー名にすべて、`_p_`をつける**

この２点ですかね。
どちらもなかなかわかりづらいですが、特に２点目は、どうしてこれを導出できたのか（公式HPにはない）わかりません。とにかくここで、`_p_`をつけて返すと、API callsで定義した型に違反したことにはならないまま、プラグイン利用者（APP開発者）がこの変数にアクセスできるようになるんです。

知っていればすぐかもしれませんが、なかなかの難所です。

ここまでがプラグインを作る側の話で、次はついでに、アプリ開発者が利用する方法です。

# ３．アプリ側の利用方法
## 3.1.Designの設定（UI）
DBからレコードを取り出し表示するので、Repeating GroupというElementを使います。

そのプロパティの１つ目の、"Type of content"で、`ResponseType db_record (Supabase)`を選びます。困ったことに、`ResponseType (Supabase)`というのもあるので注意です。切れて末尾まで見られませんが、一度選択すると、カーソルを入れられるので、カーソルを右に移動すると末尾まで見れたりコピーできたりします。（プロパティダイアログの幅は、たぶん変更不可。すごい仕様）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/50c0369f-0324-62be-61d4-2ddcbb114903.png)

つまりこの繰り返しのハコは、API callsの設定時に確認画面で表示された型の③`ResponseType db_recrod`です、ということを設定しました。ハコの１つ１つの中からは、`columns`を取り出せる状態です。

実際、Repeating groupの中にテキストを置くと、下記のように設定できます。`Current cell's ResponseType db_record (Supabase)'s columns:first item`。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/24472d99-afb9-0d6e-f510-0b442e4834b8.png)

## 3.2.Workflowの設定
私は、ボタンを押したらDBから取り出す、という処理にしましたが、トリガーはなんでもいいでしょう。

Workflowでは２段階です。１段階目は、プラグインを使ってデータを取り出す。２段階目は、それをRepeating groupへ設定する。

### 3.2.1.プラグインを使ってデータを取り出す
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/06bd10da-6bd3-3293-8d42-315b2de0246e.png)

テーブル名とか列名とかを入力することにしてますが、それは置いといてここで大事なのは、"Return Record Type"です。このネーミングは、私が2.1の④でつけたものです。必要に応じて変えてください。

そしてその型は`ResponseType (Supabase)`です。さっきのRepeating groupで使ったのとは別の方です。これは、functionの戻り値としては、API callsで定義した`Response Type`の全体を受け取りますという意味です。（ここは１つにした方が絶対いい。うまいやり方がありそう🤔）


### 3.2.2.Repeating groupへ設定する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/667e666a-e545-833f-69ba-d138e8ad441c.png)

"rg2"というのは、Repeating groupの名前です。変な名前ですみません。

そこへ、`Result of step1 (Get Records (test...)'s Returned Record's db_records`を設定しています。マルカッコ閉じが１つないのは、Workflowの項目名が途中からマルカッコ閉じも含めて省略されているからですが、わかりづらい。

とにかく長いですね。
①3.2.1の結果の、②Returned Recordsの、③db_recrods。順に説明します。

①は、Workflowの１つ目の項目名が `Get Records (testing)`（プロパティのタイトルに出てる）でしたので、その結果という意味。

②は、Actionsの設定項目で設定した項目名でした。2.1の⑦の名前です。

③は、関数から、`_p_db_records`と返したオブジェクトです。`{columns:[]}`のリストです。それが3.1で作ったRepeating groupの型と一致するので、値を設定することができます。




# 別解：サーバーサイドから`JSON.stringify`した文字列を返す
別解というか、これが真の正解なんじゃないかと思ったりします。そしてプラグイン利用者・開発者が、`JSON.parse`する。

試行錯誤の闇の中にいるときに気づきましたが、~~意地になって~~なんとか今回の課題をクリアした感じです:innocent:


# おわりに
今回の技術を使いたい人はどれだけいるのかわかりませんが、一部の人には刺さるかもしれませんので、書いてみました。もとにした投稿が2019年の投稿記事なので、今回の件が、いまさら公式に対応されたり、逆に使えなくなったりすることはない気がします。

Bubbleは、ノーコードっぽい気の利かせ方をして、なかなかのエンジニア泣かせです。
- "records"と名付けた変数を"record"に変えてくれたり
    - その名前、どこから持ってきてるの？ってなる
- Captionに設定した項目が、ユーザーの画面上に出てきてそれを選択する必要があったり
    - 注釈だと思ってたらそれがユーザーの項目名で、変数名は完全に裏方に徹する名前だったり
- 一般的にはパラメーターと呼ぶものが、Fieldっていう呼び名だったり

今回はもはやノーコードでもローコードでもないんですが、これはプラグイン作成だからかな。いや利用者も、戻り値をstringからdateやnumberへ変換する必要がある。一般的に利用してもらうには、もう少し工夫必要そうです。

というかそもそも、私がSupabaseからデータを取り出したいAppを作るとしたら、プラグインなんて使わず、Appから直接SupabaseのAPIを呼び出すな。それすらできない人が、Supabaseからデータ取り出したくなるかな。ま、私の想像を超えるいろんな事情があるだろうから、もう少し作りこんで一応公開してみよう。

ではよきBubblyライフを～。