🔥

Google I/O 2024で発表されたFirebase Data ConnectをVSCodeのエミュレーターで試してみた

2024/06/29に公開
1

執筆時点でIDXを使った記事は見かけるものの、VSCodeを使ってローカルで試している日本語記事は自分はまだ見かけていないので、おそらく日本語では初の記事なんじゃないかと思います。

少なくともZennでは自分が最初の1人でした。

Firebase Data Connectとは

Google I/O 2024で発表されたFirebaseの新機能です。

これを使うことで、GraphQLを介して、Cloud SQL For PostgreSQLへアクセスしデータのCRUDが可能になるようです。

Getting Start

記事執筆時点では限定公開プレビュー版なので、利用するためには限定公開プレビューへの申し込みが必要です。

申し込みはFirebaseプロジェクトの管理画面上から可能です。

ただし申し込み後すぐに使えるわけではなく、Googleさんの方で手続きをして頂いたのちに利用可能となります。

自分の場合は申し込みから使えるようになるまで3週間ほどかかりました。

Cloud SQLインスタンスの用意

利用可能になっていると、「始める」というボタンが表示されるのでこちらをクリック。

設定画面へ移動します。ロケーションには日本では東京・大阪が選択できるようです。これはおそらく、Cloud SQLのインスタンスのロケーションと同様に選択できるみたいです。

と思ったらやっぱりそうで、次の選択肢にCloud SQLのインスタンスについての設定が出てきました。

あとはCloud SQLのインスタンスIDとデータベース名を入力すれば、データソースの作成が始まり…

おぉ〜🙌 デモを触っていく準備が整いました🙌

ローカル接続用のPostgreSQLコンテナを用意

先回りした話ですが、Data ConnectはVSCodeの拡張機能を使ってローカルに作成したPostgreSQLデータベースに接続して色々デモを行うことができます。

今回は検証なので、簡単に済ませます。適当にDockerで作っちゃいます。

docker-compose.yml
version: '3'

services:
  db:
    image: postgres:14
    container_name: postgres_pta
    ports:
      - 5432:5432
    volumes:
      - db-store:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: 'user'
      POSTGRES_PASSWORD: 'postgres'
volumes:
  db-store:
docker-compose up -d --build

あとはTablePlusなどを使って接続確認します。

接続できていればOKです。

VSCodeに開発用の拡張機能を追加

まず、Firebase StorageからData Connect開発用のVSIXをインストールし、VSCodeへ拡張機能を追加します。VSIXは以下のリンクからインストールができます。

https://firebasestorage.googleapis.com/v0/b/firemat-preview-drop/o/vsix%2Ffirebase-vscode-latest.vsix?alt=media

その後、以下の画像の箇所(メニューバーの、表示 > 拡張機能からでも選択可能)から、「VSIX からインストールする」を選択する。

で、あらかじめインストールしておいたVSIXを選択し、以下のように表示されれば設定完了です。

ローカルプロジェクトでData Connectを使うための設定

Firebase CLIでプロジェクトを初期化します。

https://firebase.google.com/docs/cli?authuser=0&hl=ja

Firebase CLIを既にインストールしている方も、Data Connect用のコマンドが入っている最新版へアップデートする必要があります。

npm install -g firebase-tools

ここがハマりポイントで、ドキュメントには拡張機能を選択し、「Run firebase init」をクリックすればいいとあるのですが、その場合ターミナルに表示される選択肢に「Data Connect」は表示されません。

ので、コマンドでfirebase init dataconnectを実行することでセットアップを始めることができます。

このとき、Which would you like to set up local files for?ではプレビューの申し込み時に指定したプロジェクトを選択する必要があります。

またWhat is the connection string of the local Postgres instance you would like to use with the Data Connect emulator?のように聞かれるので、ローカルでのPostgreSQL接続識別子を教えてあげます。

先のDocker環境だとpostgresql://user:postgres@localhost:5432/postgres?sslmode=disableです。またプロジェクトの選択時には、あらかじめData Connectの公開プレビュー時に申し込んでおいたプロジェクトを選択する必要があります。

=== Dataconnect Setup
i  dataconnect: ensuring required API firebasedataconnect.googleapis.com is enabled...
✔  dataconnect: required API firebasedataconnect.googleapis.com is enabled
i  dataconnect: ensuring required API sqladmin.googleapis.com is enabled...
⚠  dataconnect: missing required API sqladmin.googleapis.com. Enabling now...
✔  dataconnect: required API sqladmin.googleapis.com is enabled
i  dataconnect: ensuring required API compute.googleapis.com is enabled...
✔  dataconnect: required API compute.googleapis.com is enabled
? Your project already has existing services. Which would you like to set up local files for? asia-northeast2/data-connect-demo
? What is the connection string of the local Postgres instance you would like to use with the Data Connect emulator? postgresql://user:postgres@localhost:5432/postgres?sslmode=disable
✔  Wrote dataconnect/dataconnect.yaml
✔  Wrote dataconnect/schema/schema.gql
✔  Wrote dataconnect/default-connector/connector.yaml
✔  Wrote dataconnect/default-connector/queries.gql
✔  Wrote dataconnect/default-connector/mutations.gql

✔  If you'd like to generate an SDK for your new connector, run firebase init dataconnect:sdk

i  Writing configuration info to firebase.json...
i  Writing project information to .firebaserc...

✔  Firebase initialization complete!

セットアップが完了するとdataconnectフォルダが作成され、GraphQLの雛形を用意してくれます。

そして、Firebaseプロジェクトの初期化が完了したら、Data Connectを有効にします。

firebase experiments:enable dataconnect

あとは以下のように.firebasercができていれば準備OKです。

.firebaserc
{
  "projects": {
    "default": "xxxx"
  },
  "targets": {},
  "etags": {},
  "dataconnectEmulatorConfig": {
    "postgres": {
      "localConnectionString": "postgresql://user:postgres@localhost:5432/postgres?sslmode=disable"
    }
  }
}

Data Connect それ自体を試していく

ローカルのエミュレーターで試してみる

今回SchemaはData Connectがデモで用意してくれているものを使うことにします。

schame.gql
# Example schema for simple email app
type User @table(key: "uid") {
	uid: String!
	name: String!
	address: String!
}

type Email @table {
	subject: String!
	sent: Date!
	text: String!
	from: User!
}

type Recipient @table(key: ["email", "user"]) {
	email: Email!
	user: User!
}

type EmailMeta @table(key: ["user", "email"]) {
	user: User!
	email: Email!
	labels: [String]
	read: Boolean!
	starred: Boolean!
	muted: Boolean!
	snoozed: Date
}

最初はこのファイルはコメントアウトされていたのですが、コメントを外すすと同時にいろんなファイルができあがりました。

これがたぶんドキュメントに書かれてる以下のことのようです。

メールスキーマを編集すると、スキーマ拡張、クエリ、ミューテーション、フィルタ、テーブル リレーションが自動的に生成されます。生成されたコードは、次の 2 つの方法で表示できます。
・Firebase 拡張機能 UI の [FDC Explorer] パネルで、生成された暗黙的なクエリとミューテーションのリストを確認できます。
・ローカルで生成されたすべてのコードは、.dataconnect/schema ディレクトリのソースで確認できます。

二つの方法のうち後者は確認できたのですが、前者は確認できませんでした。自分の設定が悪い or バグ?

ただ、この時点でPostgreSQLへ接続してみると、Schemaに定義されている内容でテーブルができてる!!すごい!!

一方でコードの方でコメントアウトしたスキーマを見てみると、Add DataRead Dataのように表示されています。

これをクリックすると自動的にGraphQLのクエリを作成してくれました!便利。

User_insert.gql
# This is a file for you to write an un-named mutation. 
# Only one un-named mutation is allowed per file.
mutation {
	user_insert(data: {
		uid: "" # String
		address: "" # String
		name: "" # String
	})
}
User_read.gql
# This is a file for you to write an un-named queries. 
# Only one un-named query is allowed per file.
query {
  users{
    uid
    name
    address
  }
}

で、それぞれを試してみようと思います。まずは参照から。どうやらローカルと本番それぞれで実行できるようです。期待値としては、まだデータがはいはずなので何も返ってこないことです。

実行すると、DATA CONNECT EXECUTIONというタブに結果が表示されます。期待通りの結果ですね。

では次はデータを登録してみようと思います。同様に表示されてるRun(Local)をクリックすると以下のように表示されました。

どうやら成功したっぽいので、再度参照をしてみると…?

データができてるっぽい🙌 DBの方も確認すると…?

select * from "user";

当たり前だけど、ちゃんとできてますね 🎉

本番で確認

拡張機能Deployボタンを押すとデプロイができるようなので、押す。

続けてOKを押すと…

デプロイが流れます。見た感じ、テーブルを作ってくれているようですね。

firebase deploy --only dataconnect:data-connect-demo:schema,dataconnect:data-connect-demo:default-connector

=== Deploying to 'xxx'...

i  deploying dataconnect
i  dataconnect: ensuring required API firebasedataconnect.googleapis.com is enabled...
✔  dataconnect: required API firebasedataconnect.googleapis.com is enabled
i  dataconnect: ensuring required API sqladmin.googleapis.com is enabled...
✔  dataconnect: required API sqladmin.googleapis.com is enabled
i  dataconnect: ensuring required API compute.googleapis.com is enabled...
✔  dataconnect: required API compute.googleapis.com is enabled
i  dataconnect: Preparing to deploy
i  dataconnect: Successfully prepared schema and connectors
i  dataconnect: Checking for CloudSQL resources...
i  dataconnect: Found existing instance data-connect-demo.
i  dataconnect: Database data-connect-demo not found, creating it now...
i  dataconnect: Database data-connect-demo created.
i  dataconnect: Releasing Data Connect schemas...
⚠  dataconnect: Your new schema is incompatible with the schema of your CloudSQL database. The following SQL statements will migrate your database schema to match your new Data Connect schema.
/** Install Postgres extension "uuid-ossp"*/
CREATE EXTENSION IF NOT EXISTS "uuid-ossp"
/** create "user" table*/
CREATE TABLE "public"."user" (
  "uid" text NOT NULL,
  "address" text NOT NULL,
  "name" text NOT NULL,
  PRIMARY KEY ("uid")
)
/** create "email" table*/
CREATE TABLE "public"."email" (
  "id" uuid NOT NULL DEFAULT uuid_generate_v4 (),
  "from_uid" text NOT NULL,
  "sent" date NOT NULL,
  "subject" text NOT NULL,
  "text" text NOT NULL,
  PRIMARY KEY ("id"),
  CONSTRAINT "email_from_uid_fkey" FOREIGN KEY ("from_uid") REFERENCES "public"."user" ("uid")
)
/** create "email_meta" table*/
CREATE TABLE "public"."email_meta" (
  "user_uid" text NOT NULL,
  "email_id" uuid NOT NULL,
  "labels" text[] NULL,
  "muted" boolean NOT NULL,
  "read" boolean NOT NULL,
  "snoozed" date NULL,
  "starred" boolean NOT NULL,
  PRIMARY KEY ("user_uid", "email_id"),
  CONSTRAINT "email_meta_email_id_fkey" FOREIGN KEY ("email_id") REFERENCES "public"."email" ("id"),
  CONSTRAINT "email_meta_user_uid_fkey" FOREIGN KEY ("user_uid") REFERENCES "public"."user" ("uid")
)
/** create "recipient" table*/
CREATE TABLE "public"."recipient" (
  "email_id" uuid NOT NULL,
  "user_uid" text NOT NULL,
  PRIMARY KEY ("email_id", "user_uid"),
  CONSTRAINT "recipient_email_id_fkey" FOREIGN KEY ("email_id") REFERENCES "public"."email" ("id"),
  CONSTRAINT "recipient_user_uid_fkey" FOREIGN KEY ("user_uid") REFERENCES "public"."user" ("uid")
)
? Would you like to execute these changes against data-connect-demo? Execute changes
Logged in as kengo071225@gmail.com
Executing: 'SET ROLE "firebaseowner_data-connect-demo_public"'
Executing: 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp"'
Executing: 'CREATE TABLE "public"."user" ("uid" text NOT NULL, "address" text NOT NULL, "name" text NOT NULL, PRIMARY KEY ("uid"))'
Executing: 'CREATE TABLE "public"."email" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "from_uid" text NOT NULL, "sent" date NOT NULL, "subject" text NOT NULL, "text" text NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "email_from_uid_fkey" FOREIGN KEY ("from_uid") REFERENCES "public"."user" ("uid"))'
Executing: 'CREATE TABLE "public"."email_meta" ("user_uid" text NOT NULL, "email_id" uuid NOT NULL, "labels" text[] NULL, "muted" boolean NOT NULL, "read" boolean NOT NULL, "snoozed" date NULL, "starred" boolean NOT NULL, PRIMARY KEY ("user_uid", "email_id"), CONSTRAINT "email_meta_email_id_fkey" FOREIGN KEY ("email_id") REFERENCES "public"."email" ("id"), CONSTRAINT "email_meta_user_uid_fkey" FOREIGN KEY ("user_uid") REFERENCES "public"."user" ("uid"))'
Executing: 'CREATE TABLE "public"."recipient" ("email_id" uuid NOT NULL, "user_uid" text NOT NULL, PRIMARY KEY ("email_id", "user_uid"), CONSTRAINT "recipient_email_id_fkey" FOREIGN KEY ("email_id") REFERENCES "public"."email" ("id"), CONSTRAINT "recipient_user_uid_fkey" FOREIGN KEY ("user_uid") REFERENCES "public"."user" ("uid"))'
Executing: 'GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA "public" TO PUBLIC'
i  dataconnect: Schemas released.
i  dataconnect: Releasing connectors...
✔  dataconnect: Deployed connector projects/xxx/locations/asia-northeast2/services/data-connect-demo/connectors/default-connector
i  dataconnect: Connectors released.
✔  dataconnect: Deploy complete!

✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/xxx/overview

CloudSQL側から見ても、テーブルを作るためのクエリが発行されたのがわかります。

同じくローカルのVSCodeからも試せるのですがスクショを貼るとプロジェクトIDが見えてしまうのもあって、デプロイの確認がてら本番はFirebase側から確認してみようと思います。

見てみると、作成した.gqlのクエリがちゃんとでてますね。

試しに実行してみます。

当然データがないので、結果は空です。

GraphQLから渡したクエリが実際はどのようなSQLで流れているのか?

Data Connectのリリースを聞いた時に、自分が一番疑問に思ったことを解消しようかと思います。

先ほどFirebaseから実行したクエリを見てみます。ちゃんとログには上がってきてますね。

(ローカルのVSCodeから実行してみた場合は、CloudSQLのログに載ってこないのかも🤔 自分は表示されませんでした)

SELECT
  json_build_object($1,
    (
    SELECT
      COALESCE(json_agg(j), $2)
    FROM (
      SELECT
        jsonb_build_object($3,
          "uid",
          $4,
          "name",
          $5,
          "address" ) AS j
      FROM
        "public"."user"
      LIMIT
        $6) AS x))

どうやらこういうクエリが吐かれているようです。GraphQLの指定によって柔軟に使えるようになってるということっぽいですね🤔

一方で、ListSentのように「とあるユーザーが送信したメール」の一覧を取得するようなこちらのGraphQLを発行すると…

query ListSent($uid: String) @auth(level: PUBLIC) {
  emails(where: {from: {uid: {eq: $uid}}}) {
    id
    subject
    sent
    content: text
    sender: from {
      name
      address
      uid
    }
    to: recipients_on_email {
      user {
        name
        address
        uid
      }
    }
  }
}

ちょっと自分にはすぐに理解するのが難しいSQLが吐かれていました😅

SELECT
  json_build_object($4,
    (
    SELECT
      COALESCE(json_agg(j), $5)
    FROM (
      SELECT
        jsonb_build_object($6,
          REPLACE(CAST(( "id" ) AS text), $7, $8),
          $9,
          "subject",
          $10,
          "sent",
          $11,
          "text",
          $12,
          (
          SELECT
            jsonb_build_object($13,
              "_user"."name",
              $14,
              "_user"."address",
              $15,
              "_user"."uid" )
          FROM
            "public"."user" AS "_user"
          WHERE
            ( "_user"."uid" ) = ( "email"."from_uid" )),
          $16,
          (
          SELECT
            COALESCE(json_agg(j), $17)
          FROM (
            SELECT
              jsonb_build_object($18,
                (
                SELECT
                  jsonb_build_object($19,
                    "_recipient_user"."name",
                    $20,
                    "_recipient_user"."address",
                    $21,
                    "_recipient_user"."uid" )
                FROM
                  "public"."user" AS "_recipient_user"
                WHERE
                  ( "_recipient_user"."uid" ) = ( "_recipient"."user_uid" ))) AS j
            FROM
              "public"."recipient" AS "_recipient"
            WHERE
              ( "_recipient"."email_id" ) = ( "email"."id" )
            LIMIT
              $22) AS x)) AS j
      FROM
        "public"."email"
      WHERE
        CASE
          WHEN ( $1 ::boolean) THEN (EXISTS ( SELECT * FROM "public"."user" AS "_ref_user" WHERE ( "_ref_user"."uid" ) = ( "email"."from_uid" ) AND CASE
              WHEN ( $2 ::boolean) THEN (( "_ref_user"."uid" )

どうやらjoinとかを使わずうまくリレーションを取得しているようです。ただ、こちらで好きにチューニングをガンガンかけたいような状況に陥った場合には困りそうだなと思いました。

現状は「どのようなGraphQLを書けば、どういうSQLができるか?」というドキュメントまでは用意されていないようなので、これは正式公開版に期待ですね。

一応ファイルの中には以下のように、<field>_on_<foreign_key_field>と書けば簡単に他テーブルのデータを抜けるよいう旨のことは書かれてます。

queries.gql
# Logged in users should be able to see their inbox though, so we use USER
query ListInbox @auth(level: USER) {
  # where allows you to filter lists
  # Here, we use it to filter to only emails that are sent to the logged in user.
  emails(where: {
    users_via_Recipient: {
      exist: { uid: { eq_expr: "auth.uid" }
    }}
  }) {
    id subject sent
    content: text # Select the `text` field but alias it as `content` in the response.
    sender: from { name address uid }
    # <field>_on_<foreign_key_field> makes it easy to grab info from another table
    # Here, we use it to grab all the recipients of the email.
    to: recipients_on_email {
      user { name address uid }
    }
  }
}

アプリに組み込んでみるのには…?

こちらももちろん試すつもりでいたのですが、結果としては検証できませんでした。

Firebaseのドキュメントにはサンプルが載っているのですが、

const { initializeApp } = require("firebase/app");
const {
  connectDataConnectEmulator,
  getDataConnect,
} = require("firebase/data-connect");
const { listEmails, connectorConfig } = require("@email-manager/emails");

// TODO: Replace the following with your app's Firebase project configuration
const firebaseConfig = {
  //...
};

const app = initializeApp(firebaseConfig);
const dc = getDataConnect(app, connectorConfig);

// Remove the following line to connect directly to production
connectDataConnectEmulator(dc, "localhost", 9399);

listEmails().then(res => {
  console.log(res.data.emails);
  process.exit(0);
});
    

firebase/data-connectパッケージが最新のfirebase-js-sdkパッケージに入っていないので、使えなさそうでした…

https://github.com/firebase/firebase-js-sdk

そもそも限定公開中ということは、このパッケージをnpmとかのリポジトリに公開できるわけないのでいなくて当たり前な気がしなくもない🤔

実装はサンプルを見る限りはそんなに難しいことはなさそうで、これまでのFirestoreなどと同じようにSDKから呼んでくるだけみたいです。

listEmailsのような関数自体はconnecter.yamlに以下のように書いておけば自動的に作ってくれました。この点は便利ですね。

connecter.yaml
connectorId: "default-connector"
authMode: "PUBLIC" 
# ## Here's an example of how to add generated SDKs.
# ## You'll need to replace the outputDirs with ones pointing to where you want the generated code in your app.
generate:
  javascriptSdk:
    outputDir: "../../src/js-email-generated"
    package: "@firebasegen/my-connector"
    packageJSONDir: "../../package.json"

おわりに

ローカルでの開発体験や、GraphSQLがそのままSQLとして使えている状況には素直に感動しました。

ただ単純なデータを取る場合は良いのですが、複雑なデータ抽出が必要になったりチューニングが必要になった際には困りそうだと思いました。

一方で、これまでFireStoreなどNOSQLで作っていた場合に比べて、本格的なバックエンドが必要になったとしてもPostgreSQLであることには変わらないのでデータ移行の心配がないのはよさそうです。

まだまだ限定公開版なのでこれからさらに良くなりそうですが、クエリの書き方のドキュメントが充実してきたら自分はかなり使ってみたいです。

ただ料金がCloudSQLのそれに準拠するので、高いですね…

あとたぶん、ローカルのエミュレーターで動かしてみるだけなら誰でもできそうな気がする。

📢 Kobe.tsというTypeScriptコミュニティを主催しています

フロント・バックエンドに限らず、周辺知識も含めてTypeScriptの勉強会を主催しています。

毎朝オフラインでもくもくしたり、神戸を中心に関西でLTもしています。

盛り上がってる感を出していきたいので、良ければメンバーにだけでもなってください😣

https://kobets.connpass.com/

Kobe.ts

Discussion

Yabuya IshbakYabuya Ishbak

Thank you so much for this post. It is very useful given the fact that there isn't enough documentation, yet.