🔥

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

2021/09/12に公開

何を作った?

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 の肝となる 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