Upgrade to Pro — share decks privately, control downloads, hide ads and more …

TypeScriptから使いやすいFirestore-simpleを紹介します(2020年版)

 TypeScriptから使いやすいFirestore-simpleを紹介します(2020年版)

2020/06/26 Firebase Realtime Meetup
https://firebase-community.connpass.com/event/175985/

Kenta Kase

June 26, 2020
Tweet

More Decks by Kenta Kase

Other Decks in Programming

Transcript

  1. TypeScriptから使いやすいFirestore-simpleを紹介します(2020年版)
    Firebase Realtime Meetup (2020/06/26)
    @Kesin11

    View Slide

  2. ⾃⼰紹介
    @Kesin11 (GitHub/Twitter)
    本業
    組織横断でテストやCI/CDの調査・サポートなど
    (おそらく普通よりは)CIマニアの気がある
    Firebaseはプライベート開発がメイン
    2019年のアドベントカレンダーでエミュレータを使ったテストの書き⽅を紹介
    Firebase Emulator Suiteをフル活⽤してTDDで開発しよう
    2

    View Slide

  3. はじめに
    この発表はFirestoreの機能は⼀通り知っている前提で話します
    質問はYouTubeのコメントにお願いします!
    発表中に答えきれなかったらTwitterの#FJUGあたりで回答するかも?
    放送後はFJUGのSlackで質問して頂ければ回答できます
    3

    View Slide

  4. TypeScriptでFirestoreのコード書くときどんな感じのコードになりますか?

    View Slide

  5. 多分こんな感じ?
    type User = {
    name: string,
    age: number,
    }
    const alice: User = { name: 'alice', age: 20 }
    const docRef = await firestore.collection('users').add(alice)
    const snapshot = await firestore.collection('users').doc(docRef.id).get()
    const user = snapshot.data() as User //
    ここでキャストして型を付ける
    毎回collectionを書くのが⾯倒くさい
    毎回data()を書くのが⾯倒くさい
    毎回キャストで型を付けるのが⾯倒くさい
    気軽にキャストしていくのは型ミスに繋がる
    5

    View Slide

  6. ⾃分でUserCollectionクラスを作ろう
    class UserCollection {
    collection: CollectionReference
    constructor (firestore: Firestore) {
    this.collection = firestore.collection('users')
    }
    async fetch (userId: string) {
    const snapshot = await this.collection.doc(userId).get()
    return snapshot.data() as User
    }
    }
    const userCollection = new UserCollection(firestore)
    // id='alice'
    を渡すだけ
    const user = await userCollection.fetch('alice')
    6

    View Slide

  7. ⾃分でUserCollectionクラスを作ろう
    初期の頃に⾃分が取っていたアプローチ
    collectionやdata()を毎回書かなくて済む
    キャストする場所が集約される
    コレクション毎に似たようなクラスを作る必要がある?
    汎⽤化したクラスを作れば全コレクションで使いまわせそう
    Firebaseプロジェクト毎に汎⽤化したクラスをコピペで持ってくる?
    -> ライブラリ化しよう
    7

    View Slide

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

    View Slide

  9. Firestore-simpleの思想
    . 冗⻑なコードを省く
    . 強⼒な型推論と補完
    . 素のFirestoreのAPIから乖離しすぎない
    . 間違いを起こしやすい機能へのサポート
    9

    View Slide

  10. 1. 冗⻑なコードを省く

    View Slide

  11. サンプルケース:
    FirestoreとTypeScript側でキー名や型が微妙に違う場合
    Firestore側
    /users : { name: string, age: number, created_at: Timestamp }
    TypeScript側
    User: { name: string, age: number, createdAt: Date }
    こういうケース、たまにありませんか?
    (むしろ皆さんが普段どうやっているのか知りたい)
    Firestore-simpleはこういう処理を上⼿く実現できます
    11

    View Slide

  12. 素のFirestoreで書く場合
    type User = {
    name: string,
    age: number,
    createdAt: Date, // js
    のDate
    型で扱いたい
    }
    const snapshot = await firestore.collection('users').doc(docRef.id).get()
    const data = snapshot.data()
    if (!data) return
    const user = {
    name: data.name,
    age: data.age,
    createdAt: data.created_at.toDate() //
    キー名を変換しつつ、Date
    型に変換
    } as User //
    型を揃えたのでここでキャスト
    12

    View Slide

  13. Firestore-simpleで書いた場合
    ライブコーディングします

    View Slide

  14. Firestore-simpleで書いた場合
    type User = {
    // id
    は必須プロパティ
    id: string, name: string, age: number,
    createdAt: Date,
    }
    type UserFirestore = {
    name: string, age: number,
    created_at: Date,
    }
    const firestoreSimple = new FirestoreSimple(firestore)
    const userCollection = firestoreSimple.collection({
    path: 'users',
    decode: (doc) => {
    return {
    id: doc.id,
    name: doc.name,
    age: doc.age,
    createdAt: doc.created_at.toDate()
    }
    }
    })
    const user = await userCollection.fetch('alice')
    // => { id: 'alice', name: 'alice',
    // => age: 20, createdAt: 2020-06-20T13:12:54.816Z }
    14

    View Slide

  15. Firestore-simpleで書いた場合
    collectionのpathを指定するのは⼀度だけ
    fetchした結果にUser型が⾃動で付く
    Firestore -> js での変換処理(decode)を書くのも⼀度だけ
    decode: (doc) => {}
    の中で doc
    も型推論されているので補完が効く(今回は
    詳しい説明は省略)
    逆⽅向のjs -> Firestoreの変換は encode: (obj) => {}
    (今回は省略)
    型に id
    を必須にしているのは data()
    をなくしたゆえの制約
    id
    と他のプロパティを同じようにアクセス可能にするため
    先の例: user.id
    (FirestoreのdocumentId), user.name
    , user.age
    15

    View Slide

  16. その他よく使うと思われる機能のショートハンド
    fetchAll
    bulkAdd
    , bulkSet
    , bulkDelete
    batchによる⼀括更新処理のラッパー
    // batch
    でset
    await userCollection.bulkSet([
    { id: 'alice', name: 'alice', age: 20 },
    { id: 'bob', name: 'bob', age: 22 },
    ])
    // colletion
    の全ドキュメントを取得
    const users: User[] = await userCollection.fetchAll()
    // batch
    でdelete
    await userCollection.bulkDelete(users.map((user) => user.id))
    16

    View Slide

  17. 2. 強⼒な型推論と補完

    View Slide

  18. 現代ではwithConverterを使うという⼿がある
    data()
    の結果に型が付く。Firestore <-> jsの変換も可能
    Firestore-simpleの開発を始めた2018年頃にはなかった
    const userConverter = {
    toFirestore (user: User): FirebaseFirestore.DocumentData {
    return {
    name: user.name,
    age: user.age,
    created_at: user.createdAt
    }
    },
    fromFirestore ( data: FirebaseFirestore.DocumentData): User {
    return {
    name: data.name,
    age: data.age,
    createdAt: data.created_at.toDate()
    }
    }
    }
    const userCollection = firestore.collection('users').withConverter(userConverter)
    await userCollection.doc('alice').set({ name: 'alice', age: 20, createdAt: new Date() })
    const snapshot = await userCollection.doc('alice').get()
    const user: User | undefined = snapshot.data()
    18

    View Slide

  19. Firestoreを扱う上でバグが起きやすいのは?
    (個⼈的には)キー名のtypoという凡ミスが多い
    docRef.update({ nama: 'bob' })
    collection('user').where('aga', '>=', '20')
    これらはwithConverterでは防ぐことができない
    withConverterを通したとしても update
    や where
    はキー名までチェックしてくれない
    19

    View Slide

  20. Firestore-simpleでは型から抽出されるキー名で補完が可能
    ライブコーディングでお⾒せします
    SpeakerDeckで⾒てる⽅は過去記事のGIFアニメをどうぞ
    TypeScriptからFirestoreを使いやすくするfirestore-simple v4をリリースしました

    View Slide

  21. 3. 素のFirestoreのAPIから乖離しすぎない

    View Slide

  22. ActiveRecord⾵にはしない
    //
    擬似コードです
    const user = new User
    user.name = 'alice'
    user.age = 20
    await user.save() //
    ここでDB
    に書き込まれる
    const alice = await User.findBy({ name: 'alice') // DB
    からデータ取得
    素のFirestoreと使い⽅がかなり異なる
    何かエラーが起きた場合にユーザーがデバッグするのが⼤変になりそう
    Firestoreのアップデートに追従して機能追加していくのも⼤変そう
    22

    View Slide

  23. 外しやすいライブラリを⽬指す
    あくまでFirestoreの補助
    使い⽅を⼤きく変えるライブラリは外しにくくなる
    外しにくい=採⽤しにくい
    個⼈的なポリシー
    jsでまだドラフト中の新しい構⽂は使わない(デコレータとか)
    そもそもFirestoreがもっと便利になれば⾃作して⾃分が使う理由もなくなる
    withConverterが来たときは開発をやめるか⼀瞬悩みましたが、⾃分にとってはまだ
    代替に⾄るものではなかった
    23

    View Slide

  24. 4. 間違いを起こしやすい機能へのサポート
    batch
    transaction

    View Slide

  25. batch
    const userCollectionRef = firestore.collection('users')
    const batch = firestore.batch()
    batch.set(userCollectionRef.doc('alice'), { name: 'alice', age: 20 })
    batch.set(userCollectionRef.doc('bob'), { name: 'bob', age: 22 })
    await batch.commit()
    個⼈的に気に⼊らない点
    transactionと挙動が似ているのに、runTransactionとAPIが全く異なる
    set, update, deleteの使い⽅が普通と異なる
    addが存在しない
    doc()
    とidを空にすると裏でランダム値を⼊れてくれるのでaddは実現可能
    なぜ⽤意してくれない・・・
    25

    View Slide

  26. Firestore-simpleによるbatchのサポート
    単に配列を⼀括処理したい場合は bulkAdd
    , bulkSet
    , bulkdDelete
    runTransactionと同じように書ける runBatch
    も⽤意
    await firestoreSimple.runBatch(async (_batch) => {
    await userCollection.set({ id: 'alice', name: 'alice', age: 20 })
    await userCollection.set({ id: 'bob', name: 'bob', age: 20 })
    await userCollection.add({ name: 'random', age: 20 })
    }) // <- runBatch
    を抜けるタイミングで⾃動的にbatch.commit()
    される
    runBatch
    の中ではsetなどはすべて⾃動的にbatch内処理として扱われる
    うっかりbatch外処理として実⾏してしまうミスは起きない
    26

    View Slide

  27. Firestore-simpleによるtransactionのサポート
    runTransactionもrunBatchと同じ改良がされている
    add, set, update, deleteは⾃動的にtransaction処理として扱われる
    transactionはgetも同様
    ⼀部の処理だけtransaction外で⾏ってしまうという危険なミスは起きない
    await firestoreSimple.runTransaction(async (_tx) => {
    await userCollection.fetch('alice')
    await userCollection.fetch('bob')
    await userCollection.set({ id: 'alice', name: 'alice', age: 20 })
    await userCollection.set({ id: 'bob', name: 'bob', age: 22 })
    }) // <- runTransaction
    を抜けるタイミングで⾃動的にtransaction.commit()
    される
    27

    View Slide

  28. Firestoreの新しめの機能もカバーしてます
    FieldValue.serverTimestamp
    FieldValue.increment
    CollectionGroup
    where INなど
    Firestore Emulator
    Firestore-simple⾃体のテストがemulatorで実⾏されているのでバッチリ

    View Slide

  29. 最近のv7のアップデート

    View Slide

  30. 最近のv7のアップデート
    Web SDKにも対応しました!
    v6以前とはインストール⽅法から変わりました
    npm i firestore-simple
    はDEPRECATED
    Admin SDK, Cloud Function⽤
    npm i @firestore-simple/admin
    Web SDK⽤
    npm i @firestore-simple/web
    30

    View Slide

  31. Firestore-simpleのターゲットユーザー
    TypeScriptで型をちゃんと付けたい
    使い⽅が素のFirestoreから⼤きく変わらない⽅が好み
    UIフレームワークとは疎結合にしたい
    Firestore層のテストを書きたい
    Firestore-simple⾃体のテストコードをぜひ参考に
    31

    View Slide

  32. 今後の予定
    Web SDKの⼀部機能のサポートができていないので対応したい
    getやonSnapshotなどWeb SDKのみ存在するオプションにまだ未対応
    APIドキュメント⽣成
    TypeScriptで流⾏りのドキュメント⽣成ツールを知りたい
    Firestoreに新機能が来たら対応したい
    今年はIOがなかったのでいつ来るのだろう
    32

    View Slide

  33. 付録1
    ドキュメント
    https://github.com/Kesin11/Firestore-simple/blob/master/README.md
    サンプルコード(AdminとWebで別々にあります)
    https://github.com/Kesin11/Firestore-
    simple/tree/master/packages/admin/example
    https://github.com/Kesin11/Firestore-
    simple/tree/master/packages/web/example

    View Slide

  34. 付録2
    過去記事
    v7をリリースしたので改めてfirestore-simpleを紹介します
    firestore-simple v5をリリースしました
    TypeScriptからFirestoreを使いやすくするfirestore-simple v4をリリースしました
    Firestoreをもっと⼿軽に使えるfirestore-simpleがバージョン2になりました
    Firestoreをもっと⼿軽に使えるfirestore-simpleを作った

    View Slide