✨【v2】TypeScriptでFirestoreを扱いやすくするライブラリ「BlueSpark」を作った (web/admin/React Hooks対応)


@yarnaimo です ✋

最近 RSS と Twitter から情報を収集するツールを作っているのですが、その副産物として TypeScript 向け Firestore ライブラリ BlueSpark ができたので紹介します。

11/13 追記: v2.1.0 更新に伴い API がわかりやすくなりました。

概要

TypeScript の Firestore ライブラリはすでにいくつか存在していますが、web と admin のどちらか片方にしか対応していなかったり、TimestampDate など読み取りと書き込みの際で型が異なるフィールドの扱いが難しいといった課題がありました。

BlueSpark はモデル定義とコレクション定義を分けたことでこれらの課題をクリアしつつ覚えやすい設計になっています。

yarnaimo/bluespark
https://github.com/yarnaimo/bluespark

1. インストール

yarn add bluespark
# or
npm i -S bluespark

2. 準備

i. 初期化

まず initializeApp() で初期化します。(admin の場合は必要に応じて読み替えてください)

import firebase, { firestore } from 'firebase/app'
import 'firebase/firestore'
import { Blue, Spark } from 'bluespark'

const app = firebase.initializeApp({
    apiKey: '### FIREBASE API KEY ###',
    authDomain: '### FIREBASE AUTH DOMAIN ###',
    projectId: '### CLOUD FIRESTORE PROJECT ID ###',
})

const dbInstance = app.firestore()

ii. モデル定義

Blue.Interface で型を定義しモデルを作成します。
読み取りと書き込みで型が異なる場合は Blue.IO を使います。例えば date フィールドで読み取りが Timestamp、書き込みが Date | FieldValue となる場合以下のようにします。

Blue.Timestamp / Blue.FieldValue などは web と admin の union type です。

ここではルートに users コレクションがあり、各 userposts サブコレクションを持っている構造とします。

type IUser = Blue.Interface<{ name: string }>

type IPost = Blue.Interface<{
    number: number
    date: Blue.IO<Blue.Timestamp, Date | Blue.FieldValue>
    text: string
    tags: string[]
}>

// users
const User = Spark<IUser>()({
    root: true,
    collection: db => db.collection('users'),
})

// users/{user}/posts
const Post = Spark<IPost>()({
    root: false,
    collection: user => user.collection('posts'),
})

iii. コレクション定義

次に ii で作ったモデルを使ってコレクションを定義します。

const createCollections = <F extends Blue.Firestore>(db: F) => {
    return {
        users: User(db),
        _postsIn: <D extends Blue.DocRef>(userRef: D) => Post(userRef),
    }
}

const db = createCollections(dbInstance)
const dbAdmin = createCollections(dbInstanceAdmin) // admin の場合

3. 使い方 (基本 API)

定義したコレクションとモデルを使ってデータの読み取り/書き込みをしていきます。

i. ドキュメント/クエリを取得する

// `users/userId` を取得
const user = await db.users.getDoc({ doc: 'userId' })

// `users/userId/posts` の中から `number` が 3 を超えるものを取得
const _posts = await db._postsIn(user._ref).getQuery({
    q: q => q.where('number', '>', 3),
})

// クエリの取得結果は array と map で取得できます
const firstPost = _posts.array[0]
const postA = _posts.map.get('postId')!

この場合、firstPostpostA の型は以下のようになります。

type _ = {
    _createdAt: Blue.Timestamp
    _updatedAt: Blue.Timestamp
    _id: string
    _ref: Blue.DocRef
    number: number
    date: Blue.Timestamp
    text: string
    tags: string[]
}

_id にはドキュメントの id、_ref にはドキュメントのリファレンスが入ります。
_createdAt_updatedAt については後ほど説明します。

decoder を渡すと自動的にデータを変換することができます。

const _posts = await db._postsIn(user._ref).getQuery({
    decoder: (post: IPost['_D']) => ({
        ...post,
        number: String(post.number),
    }),
})

この場合、各 postnumber フィールドは string 型になります。

ii. ドキュメントを作成する

ドキュメントの作成には .create() を使います。
_createdAt_updatedAt フィールドに ServerTimestamp が自動的にセットされます。

await db._postsIn(user._ref).create('postId', {
    number: 17,
    date: firestore.FieldValue.serverTimestamp(), // または Date
    text: 'text',
    tags: ['a', 'b'],
})

iii. ドキュメントを更新する

ドキュメントの更新には .update() を使います。
実行すると _updatedAt フィールドが自動的に更新されます。

await db._postsIn(user._ref).update('postId', {
    text: 'new-text',
})

4. 使い方 (React Hooks)

BlueSpark は React Hooks でも簡単に使うことができます。この機能には react-firebase-hooks を使用しています。

const { data: user, loading, error } = useSDoc({
    model: db.users,
    doc: 'userId',
})

const _posts = await useSCollection({
    model: db._postsIn(user._ref),
    q: q => q.where('number', '>', 3),
})

const { array, map, query, loading, error } = _posts

読み込み中の場合 loadingtrue が、エラーの場合 errorError が入ります。

まとめ

BlueSpark を使うと Firestore がより使いやすくなります。ぜひ試してみてください!!🙌