Runner in the High

技術のことをかくこころみ

elm-firestore v10.0.0をリリースした

引き続きチビチビと作り続けていたelm-firestoreだが、とうとうv10をリリースした。

elm-firestoreとはなんぞや、という記事はここで書いていた。

izumisy.work

変更点

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 ↑ドキュメントの一覧取得でページサイズ指定してもページネーショントークンが返されない