REPL上でのFirestoreのデータ操作をちょっと便利にするOSSを作った
何を作った?
Firestore のデータ内容を確認したり修正したりするときに、いちいち Firebase のコンソール上でぽちぽちやるのが面倒でした。
だからといって、Node.js の REPL で Firebase Admin SDK を使うと、Admin SDK の初期化が手間だったり Node.js の REPL が標準で Top-level await に対応していなかったりとこちらも準備が大変です。
そこで、Node.js の REPL を拡張して、Firestore のデータ操作を簡単に行えるカスタム REPL を作りました。
名前は Firepl
(Firebase + REPL)🔥
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 を作れました。
REPL 部分はわずか数行です。
#!/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 の初期化部分は以下です。
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 })()
と即時関数で包む必要がありました。
Node.js 10 系から起動時のオプションで--experimental-repl-await
を指定することで、Top-level await を使えるようにはなったのですが、いちいちオプションを付けるのは面倒です。
そこで今回はnode-repl-awaitというライブラリを利用して、REPL の eval を拡張することで Top-level await を実現しています。
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