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

[@yarnaimo](https://twitter.com/yarnaimo) です ✋

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

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

## 概要

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

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

> **yarnaimo/bluespark**<br>https://github.com/yarnaimo/bluespark

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">TypeScript向けFirestoreライブラリ BlueSparkを作りました！！🎉<br><br>✅ web / admin どちらにも対応<br>✅ シンプルなモデル定義<br>✅ 読み取り/書き込みの型を分けられる (読み取りはTimestamp / 書き込みはDate | FieldValue など)<br><br>yarn add bluespark でインストールできます！<a href="https://t.co/gAiyR1bcIn">https://t.co/gAiyR1bcIn</a> <a href="https://t.co/QV1RhDBlpY">pic.twitter.com/QV1RhDBlpY</a></p>&mdash; やまいも (@yarnaimo) <a href="https://twitter.com/yarnaimo/status/1185901785497473024?ref_src=twsrc%5Etfw">October 20, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## 1. インストール

```sh
yarn add bluespark
# or
npm i -S bluespark
```

## 2. 準備

### i. 初期化

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

```ts
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` コレクションがあり、各 `user` が `posts` サブコレクションを持っている構造とします。

```ts
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 で作ったモデルを使ってコレクションを定義します。

```ts
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. ドキュメント/クエリを取得する

```ts
// `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')!
```

この場合、`firstPost` と `postA` の型は以下のようになります。

```ts
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 を渡すと自動的にデータを変換することができます。

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

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

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

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

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

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

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

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

## 4. 使い方 (React Hooks)

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

```ts
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
```

読み込み中の場合 `loading` に `true` が、エラーの場合 `error` に `Error` が入ります。

## まとめ

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