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

**追記: v7（v6は欠番）についてのエントリは[こちら](http://kesin.hatenablog.com/entry/2020/04/27/150000)**

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

https://github.com/Kesin11/Firestore-simple

[CHANGELOG](https://github.com/Kesin11/Firestore-simple/releases/tag/v5.0.0)

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

- [Firestoreをもっと手軽に使えるfirestore-simpleがバージョン2になりました](http://kesin.hatenablog.com/entry/firestore_simple_v2)
- [TypeScriptからFirestoreを使いやすくするfirestore-simple v4をリリースしました](https://qiita.com/Kesin11/items/c2a52e4e33d6f8e83723)

# 新機能

## collectionGroup

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

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

```ts
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](https://twitter.com/hecateball)さんの[Firestore Mastery](https://shiodaifuku.booth.pm/items/1503228)を読んだ際にbatchを積極的に利用すべきだと思い直しました。

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

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

batch.set({ ... })

await batch.commit()
```

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

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

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

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

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

```ts
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より便利だと思いますが、単にパフォーマンスの問題で単一のコレクションに対して配列でまとめて`add`や`delete`したいというケースは多いと思います。

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

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

# BREAKING CHANGES

## updateの引数の型のバグ修正

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

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

実際にどのようなケースで問題だったかは修正の[pull-req](https://github.com/Kesin11/Firestore-simple/pull/87)を見てもらうと早いと思います。

## fetchByQueryの削除

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

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

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

# 今後の方向性

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

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

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

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

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