🔥

REPL上でのFirestoreのデータ操作をちょっと便利にするOSSを作った

5 min read

何を作った?

Firestore のデータ内容を確認したり修正したりするときに、いちいち Firebase のコンソール上でぽちぽちやるのが面倒でした。
だからといって、Node.js の REPL で Firebase Admin SDK を使うと、Admin SDK の初期化が手間だったり Node.js の REPL が標準で Top-level await に対応していなかったりとこちらも準備が大変です。

そこで、Node.js の REPL を拡張して、Firestore のデータ操作を簡単に行えるカスタム REPL を作りました。

名前は Firepl(Firebase + REPL)🔥

https://github.com/kawamataryo/firepl

Firebase のサービスアカウントを指定して起動すると Firestore の初期化を自動で行い、REPL 上にてfirepl.db ですぐ Firestore を操作できます。
さらに firepl.getData を使うと少し面倒な snapshot の取り回しを抽象化して目的のデータを取得できます。Node.js 標準 REPL の薄いラッパーなので、データの修正・加工も素の JS で手軽に行えます。最高便利。

使い方

最初に Firebase Admin SDK の初期化に使うサービスアカウントを Firebase のコンソールから取得します。

プロジェクトの設定 > サービスアカウント でアクセスできます。

ダウロードしたサービスアカウントを適当な場所に配置して、そのパスを firepl の起動時引数に与えると起動します。

# npx で使う場合
$ npx firepl -c path/to/serviceAccountKey.json

# global installして使う場合
$ npm i -g firepl
$ firepl -c path/to/serviceAccountKey.json

REPL 内にてfirepl.dbでサービスアカウントに指定したプロジェクトの Firestore にアクセス出来ます。コレクションやドキュメントの操作も通常の Admin SDK と同様です。

🔥 > docRef = firepl.db.collection("user").doc("documentId")

collectionReference または、documentReference の内容を取得したいときは、firepl.getDataが使えます。snapShot を介さずデータを扱えます。

🔥 > data = await firepl.getData(docRef)

技術的なポイント

Node.jsのREPLをそのまま利用する

当初 REPL を作るのはハードル高そうと尻込みしていたのですが、Node.js の以下ドキュメントページにあるとおり、既存の REPL を拡張することで、とても手軽に独自の REPL を作れました。

https://nodejs.org/en/knowledge/REPL/how-to-create-a-custom-repl/

REPL 部分はわずか数行です。

📦 src/main.ts

#!/usr/bin/env node

import * as repl from "pretty-repl";
import { asyncEval } from "./lib/eval";
import { Firepl } from "./lib/firepl";
import { getArgs } from "./lib/args";

const args = getArgs();
const firepl = new Firepl(args.credentials);

console.log("Welcome to Firepl.\nexit using Ctrl+Z or Ctrl+C or type .exit\n");

const customRepl = repl.start({ prompt: "🔥 > ", eval: asyncEval });
customRepl.context.firepl = firepl;

repl.start で prompt や eval を指定して、ReplServer を作り、その context に REPL 内で使えるグローバルな状態を定義しています。

replの import がpretty-replとなっているのは、REPL 内でシンタックスハイライトが効かせようと以下ライブラリを利用しているためです。通常のreplでも動作は変わりません。
https://www.npmjs.com/package/pretty-repl

この REPL の肝となる Firestore の初期化部分は以下です。

📦 src/lib/firepl.ts

import * as admin from "firebase-admin";
import { firestore } from "firebase-admin";
import { join } from "path";

export class Firepl {
  db: firestore.Firestore;

  constructor(serviceAccountPath: string) {
    const serviceAccount = require(join(process.cwd(), serviceAccountPath));

    admin.initializeApp({
      credential: admin.credential.cert(serviceAccount),
    });
    this.db = admin.firestore();
  }

  async getData(
    ref: firestore.CollectionReference | firestore.DocumentReference
  ) {
    try {
      const snapShot = await ref.get();
      if ("docs" in snapShot) {
        return snapShot.docs.map((d) => d.data());
      }
      return snapShot.data();
    } catch (e) {
      console.error(e);
    }
  }
}

コマンド引数で指定されたサービスアカウントを利用してadmin.initializeApp()で Firebase Admin の初期化を行っています。
この Firepl クラスは前述の REPL の作成時に初期化し REPL の context に設定しています。

「サービスアカウントをライブラリに渡すのは不安。.」という意見もあると思います(自分もあります)。ただ、本当にここでしか利用してないので、安心して使ってください🙏

Top-level awaitへの対応

通常の Node.js の REPL は Top-level await に対応しておらず、async/await なコードを扱う場合、(async () => { await xxx })()と即時関数で包む必要がありました。

https://github.com/nodejs/node/issues/13209

Node.js 10 系から起動時のオプションで--experimental-repl-awaitを指定することで、Top-level await を使えるようにはなったのですが、いちいちオプションを付けるのは面倒です。

そこで今回はnode-repl-awaitというライブラリを利用して、REPL の eval を拡張することで Top-level await を実現しています。

https://www.npmjs.com/package/node-repl-await

📦 src/lib/eval.ts

import { processTopLevelAwait } from "node-repl-await";
import * as vm from "vm";
import * as repl from "repl";
import { Context } from "vm";

function isRecoverableError(error: Error) {
  if (error.name === "SyntaxError") {
    return /^(Unexpected end of input|Unexpected token)/.test(error.message);
  }
  return false;
}

export const asyncEval = async (
  code: string,
  context: Context,
  filename: string,
  callback: (...args: any[]) => any
) => {
  code = processTopLevelAwait(code) || code;

  try {
    const result = await vm.runInNewContext(code, context);
    callback(null, result);
  } catch (e) {
    if (e instanceof Error && isRecoverableError(e)) {
      callback(new repl.Recoverable(e));
    } else {
      console.log(e);
    }
  }
};

これでいちいち即時関数に包むことなく REPL 上で await が手軽に使えます。

おわりに

全コードで百数行のとても簡易な OSS ですが、自分的に便利に使えて大満足です。
何か不具合ありましたら、気軽に Issue や PullRequest を送ってください。

また、実は自分が認知していないだけでもっと便利に Firebase のデータを確認したり操作したりするツールもありそうです。何かご存知でしたらコメントもらえると嬉しいです🙏

Discussion

ログインするとコメントできます