firestore-simple v5をリリースしました


追記: v7(v6は欠番)についてのエントリはこちら

FirestoreをTypeScriptから使いやすくするfirestore-simpleのv5をリリースしました。v4は2019/06にリリースだったので、半年も経ってしまったのですね・・・。

CHANGELOG

firestore-simpleの基本的な使い方や、コンセプトは過去の記事を見てもらうのが早いです。

新機能

collectionGroup

collectionGroup機能は、2019年のFirestoreの新機能の中でも最もインパクトがあった機能ではないでしょうか。

時間がかかってしまいましたが、firestore-simpleからでもcollectionGroupを使えるようになりました。

const ROOT_PATH = 'example/ts_admin_collection_group'
const firestore = admin.firestore()

interface Review {
  id: string,
  userId: string,
  text: string,
  created: Date,
}

const userNames = ['alice', 'bob', 'john']
const collectionId = 'review'

// CollectionGroup用のDAOを作成
const firestoreSimple = new FirestoreSimple(firestore)
const reviewCollectionGroup = firestoreSimple.collectionGroup<Review>({
  collectionId: 'review',
  decode: (doc) => {
    return {
      id: doc.id,
      userId: doc.userId,
      text: doc.text,
      created: doc.created.toDate() // timestamp型をDate型に変換
    }
  }
})

// CollectionGroupのドキュメントを取得
const reviews = await reviewCollectionGroup.fetch()

FirestoreSimple.Collectionと同様に、ジェネリクスに型の情報を渡すことで fetchしたときのオブジェクトに型がちゃんと付きます。

もちろん、decodeも使えるのでサンプルコードのようにfetchしたときにFirestoreのtimestamp型を自動的にDate型に変換するということも可能です。

runBatch

今年のはじめぐらいまではFirestoreにおけるbatchの必要性を理解していなかったのですが、FJUGでのFirestore利用事例を聞いたり、hecateballさんのFirestore Masteryを読んだ際にbatchを積極的に利用すべきだと思い直しました。

ただ元々のFirestoreのbatchのAPIは以下のようにbatch()でbatchオブジェクトを生成し、このオブジェクトのset()などを呼び出して最後にcommit()するという、通常のドキュメント更新とは少々異なる使い方を強制されます。

const batch = admin.firestore().batch()

batch.set({ ... })

await batch.commit()

このbatchのAPIは単純なのですが、それゆえに以下のようなミスや問題が起きやすいと自分は考えています。

  • batch.set()のつもりが誤って通常のset()を使ってしまう(delete()なども同様)
  • 最後にbatch.commit()を実行するのを忘れてしまう
  • set()などを別のメソッドで行う場合、batchオブジェクトを引き回す必要がある

firestore-simpleではrunBatchというrunTransactionと似たような使い方ができるオリジナルのメソッドを導入することでこれらの問題を解決します。

runBatchrunTransactionと同様にcallback関数を引数に取ります。そしてこのcallback関数の中で行ったadd, set, update, deleteは自動的にbatch実行として扱われ、このcallback関数を正常に抜けたタイミングで自動的にcommit()されます。

つまり、batchで処理したい更新ロジックをrunBatchの内部に書くだけで済むため、どの処理がbatchとして更新されるのかコードを読むのが非常に簡単になります。

const ROOT_PATH = 'example/ts_admin_batch'

const firestore = admin.firestore()

interface User {
id: string,
name: string,
rank: number,
}
const userNames = ['bob', 'alice', 'john', 'meary', 'king']

const firestoreSimple = new FirestoreSimple(firestore)
const userDao = firestoreSimple.collection<User>({ path: `${ROOT_PATH}/user` })

await firestoreSimple.runBatch(async (_batch) => {
  let rank = 1
  for (const name of userNames) {
    await userDao.add({ name, rank }) // runBatch内でのadd()は内部的にbatch.add()に変換されて実行される
    rank += 1
  }
  console.dir(await userDao.fetchAll()) // まだcommit()されていないため、このタイミングではfetchAllした結果は空
})
// <- runBatchのcallbackを抜けた直後にbatch.commit()が実行される

let users = await userDao.orderBy('rank').fetch()
console.dir(users)
// [ { id: '5ouYv8W5yiku0ztWtqA0', name: 'bob', rank: 1 },
// { id: 'PvoggKKdthKQgAFXtQb3', name: 'alice', rank: 2 },
// { id: '6E9oCximPhqN9uvNnPVi', name: 'john', rank: 3 },
// { id: 'PLv2JLyBRpkq7Xn6F23g', name: 'meary', rank: 4 },
// { id: 'PIWntOJi4YM1IkpG4lLG', name: 'king', rank: 5 } ]

// update(), delete()も同様にrunBatchの中では自動的にbatchに対しての実行に変換される
// 今回は登場しないが、set()も可能
await firestoreSimple.runBatch(async (_batch) => {
  let rank = 0
  for (const user of users) {
    if (user.rank < 4) {
      // rankを0スタートの順列に変更
      await userDao.update({ id: user.id, rank })
    } else {
      // ただし、元々rank 4以上だったドキュメントは削除
      await userDao.delete(user.id)
    }
    rank += 1
  }
})
// <- ここでbatch.commit()によりupdateとdeleteがまとめて反映される

users = await userDao.orderBy('rank').fetch()
console.dir(users)
// [ { id: '5ouYv8W5yiku0ztWtqA0', name: 'bob', rank: 0 },
// { id: 'PvoggKKdthKQgAFXtQb3', name: 'alice', rank: 1 },
// { id: '6E9oCximPhqN9uvNnPVi', name: 'john', rank: 2 } ]

bulkAdd追加

runBatchだけでも元々のFirestoreより便利だと思いますが、単にパフォーマンスの問題で単一のコレクションに対して配列でまとめてadddeleteしたいというケースは多いと思います。

firestore-simpleでは以前からこのようなユースケースのために、シンプルに配列を受け取ってbatchで更新してくれるbulkSet, bulkDeleteというオリジナルの機能を用意していました。

今回、同様の機能であるbulkAddも追加しました。

BREAKING CHANGES

updateの引数の型のバグ修正

以前はupdateの引数はジェネリクスのTでしたが、これは誤りでSが正でした。js側とFirestore側でキー名が異なるなどの理由でencode, decodeを利用してキー名を変換している場合において、updateencode後のキー名を渡そうとすると型の不一致でエラーになっていました。

firestore-simpleはadd, setでFirestoreに更新を行う前にencodeが実行されるのですが、いくつかの要因によりupdateだけはencodeを通さずに直接Firestore側のドキュメントのキー名を指定する必要があります。ですが、このバグによりencodeで変換後のキー名を渡そうとすると型エラーとなってしまっていため、そのようなキーで実質的にupdateを実行できませんでした。

実際にどのようなケースで問題だったかは修正のpull-reqを見てもらうと早いと思います。

fetchByQueryの削除

FirestoreSimple.collectionに存在したfetchByQueryを削除しました。

collectionGroupを実装するにあたり、Query周りを修正する必要があったのですがその過程で内部的に不要となったので削除しました。

Queryが関係するwhereorderByを使う場合にはFirestoreSimple.collection.whereなどを使っていると思いますので、元々使うケースはほとんどなかったはずです。

今後の方向性

今回のcollectionGroupとrunBatchの追加により、FirestoreのAdmin SDKに存在する機能はほぼカバーできたかなと思います。自分が欲しかったfirestore-simpleオリジナルの機能も一旦実装し終えたので、Firestore自体に新機能が追加されない限りおそらく大幅な機能追加はないと思います。

ただ、サンプルとしてexampleに置いてあるコードが少々古くなっているので直したり、たまに失敗する不安定なテストをFirestoreのエミュレータを使うようにするなど内部的な改善の予定は残っていたりします。

一方で、2019年にWeb SDK対応の手を付けることが結局できなかったので、2020年はこれを優先的に行いたいと考えています。

おそらく、Web SDKに対応させるためにはfirestore-simpleのパッケージ構造を変える必要があるだろうと考えています。AdminとWebでクラス名を分けるとか、monorepoにするなど方法はいくつかあるはずですが、その方法をこれから検討していこうと思います。

というわけで、firestore-simpleはこれからも開発を続けていく予定です。皆さんも2020年はさらにFirestoreを使いこなしてバリバリ開発していきましょう!