引き続きチビチビと作り続けていたelm-firestoreだが、とうとうv10をリリースした。
I finally have just rolled out elm-firestore v10.0.0! It has Query opertion support and fixes interface of transaction-related functions. Plus, integration tests with Firestore Emulator were also introduced, so way more reliable than before!🥳 https://t.co/GvQPHrQEVz
— IzumiSy™ (@sy_izumi) 2021年4月21日
elm-firestoreとはなんぞや、という記事はここで書いていた。
変更点
Experimentalなライブラリということもあって毎バージョンいつもドラスティックな変更を加えているが、今回特筆すべきは以下の3つ。
- トランザクション関連関数の整備
- クエリオペレーションのサポート
- Firestore Emulatorとの統合テストの追加
特にクエリオペレーションに関してはissueでも要望されており、たしかにFirestoreでクエリ系が使えないと検索処理も実装できずプロダクトでは使い物にならないところがあった。
また、トランザクション関連の関数に関しても Transaction
型は存在しているものの、それを使った更新処理を書くことが実際的には用意されている関数のインターフェイス上不可能というかなりちぐはぐな状態であった。しかし今回ようやく関連する関数群を整備&Firestore Emulatorとの結合テストも追加したことでトランザクション周りの実装まで動作を担保した状態になったと言える。
トランザクション関連関数
FirestoreのREST APIにおけるトランザクション周りのインターフェイスはUnit of Work*1的なものになっているのでelm-firestoreでもその雰囲気を踏襲した。パイプで更新/削除をトランザクションに積み上げて最後にコミットするようなスタイル。
model.firestore |> Firestore.commit (transaction |> Firestore.updateTx "users/user1" newUser1 |> Firestore.updateTx "users/user2" newUser2 |> Firestore.deleteTx "users/user3" |> Firestore.deleteTx "users/user4" ) |> Task.attempt Commited
また、更新系以外にも getTx
, listTx
, runQueryTx
などのTxな取得系関数も増やした。
以下は統合テストからの抜粋になるが「usersコレクション配下から一覧取得したuserドキュメントの名前をアトミックに全更新する」みたいな取得系関数と組み合わせたコードは以下のようになる。
model |> Firestore.begin |> Task.andThen (\transaction -> model |> Firestore.path "users" |> Firestore.listTx transaction (Codec.asDecoder codec) ListOptions.default |> Task.map (\{ documents } -> ( transaction, documents )) ) |> Task.andThen (\( transaction, documents ) -> Firestore.commit (List.foldr (\{ name, fields } -> Firestore.updateTx ("users/" ++ Firestore.id name) (Codec.asEncoder codec { name = fields.name ++ "txUpdated" , age = fields.age } ) ) transaction documents ) model ) |> Task.attempt RanTestListTx
クエリオペレーション
とりあえず CompositeFilter
, FieldFilter
, UnaryFilter
の3つをサポートした。
この中ではCompositeFilterのインタフェースのみ特殊で、いわゆるNon-empty list的なインタフェースにしている。これはFirestoreのCompositeFilterの前提条件が「少なくともひとつはフィルタを与えなければならない」ため。
-- usersコレクションからageが10以上かつ30より小さなメンバをクエリ Query.new |> Query.from "users" |> Query.where_ (Query.compositeFilter Query.And (Query.fieldFilter "age" Query.GreaterThanOrEqual (Query.int 10)) [ Query.fieldFilter "age" Query.LessThan (Query.int 30) ] )
もしフィルタのパラメタを単なる配列にしてしまうと、フィルタとして空リストを与えた場合にエラーが返ってきてしまう。Elmなので実行時エラーにはならずResult型にはなるものの、型やインタフェース実装で守れるところはこのようにできるだけ守っておくほうが設計としては望ましい。
Firestore Emulatorとの統合テスト
Firebaseの製品のほとんどはエミュレータ用いてローカルで動かすことができるので、それを利用してelm-firestoreの統合テストを追加した。
やっていることはシンプルで、elm-firestoreを利用したWorkerという小さなテスト用のElmアプリケーションを別都用意し、テストランナー(ava.js)で実行とアサーションをやっている。Elmには Platform#worker
というヘッドレスアプリケーション用のランタイム実行関数が用意されているため、それを使ってテスト用アプリを作る。あとはポート経由でテスト用アプリと通信しテストの実行と結果の受け取りをする。詳しい実装はリポジトリの integration_tests
ディレクトリ配下にすべてある。
テストランナーにava.jsを使っているのは、Firestoreとの通信をモックせずIOを発生させるテストが多いため並列実行との相性がいいテストランナーを選択したかったという背景から。最初はmochaを使っていたがthisコンテキスト関連の黒魔術が多くあまり体験が良くなかったこともあり乗り換えた。
ところでElmとは話が逸れるが、エミュレータのREST APIにはバグがまだたくさんあるようで、elm-firestoreのテストを書く過程でクリティカルなバグに2件遭遇している。本番では動くので問題ないが、2021年4月時点ではエミュレータとプロダクションの挙動が完全に一致しているとは思わないほうが無難だと思われる。
github.com ↑トランザクション内でドキュメントの取得系メソッドを呼ぶとエミュレータが実行時エラーで止まる
github.com ↑ドキュメントの一覧取得でページサイズ指定してもページネーショントークンが返されない