【Firebase】CLI 経由で Firestore に大量のテストデータを投入する

## はじめに
アドベントカレンダー初投稿です！
業務で Firestore を利用したアプリ開発をしていて、ページネーションを実装するにあたって大量のテストデータを用意したくなったので勉強がてら実装してみました。。。

この投稿は [Firebase Advent Calendar 2018](https://qiita.com/advent-calendar/2018/firebase) 16日目の記事です

## やりたいこと
- firestore の users コレクションにテストデータを投入する
- 各ドキュメントはランダムに生成されたデータが入っている
- 多めの件数でも任意に指定して書き込めるようにする（最大10,000件）
- テストデータだけを全件消去できる
- 以上すべてをコマンドラインツールとして実現する（引数で制御したい）

## 環境構築

以下の前提で進める

-  `firebase init` でディレクトリを生成済
-  `firebase-admin` を使うために functions ディレクトリ内でコードを書く
-  TypeScript を使用
-  パッケージマネージャーは yarn

#### 必要なパッケージをインストール

```bash:/functions
$ yarn add ts-node faker commander
```

#### typescript ファイルを作成

```bash
functions
└── cli
    ├── index.ts
    └── user.ts
```

注：functions としてデプロイするわけではないのでビルドの対象には含めなくてよい

#### CLI 実行用の run scripts を作成

```json:package.json
{
  "scripts": {
    "cli": "ts-node cli/index.ts"
  }
}
```

## モックデータ用関数の実装

- ランダムなデータを作成するために [faker](https://github.com/marak/Faker.js/) を利用
- 一括書き込みの上限が500件なので、再帰関数で500件ずつ書き込む  
  参考：[トランザクションと一括書き込み](https://firebase.google.com/docs/firestore/manage-data/transactions?hl=ja)
- 作成したモックデータのみ消せるように、`isMock` フィールドを用意した

```typescript:user.ts
import * as admin from 'firebase-admin'
import * as faker from 'faker'

// 共通化するために Admin SDK の初期化は index.ts で行う
const db = admin.firestore()
const batchSize = 500

// 作成用関数
export const createUsers = async (n: number) => {
  const usersRef = await db.collection('users')

  // 再帰関数
  const excuteBatch = async (size: number) => {
    if (size === 0) {
      return
    }
    // 500件を超えないようにする
    const length = Math.min(size, batchSize)
    const batch = db.batch()
    for (let i = 0; i < length; i = i + 1) {
      const user = {
        firstName: faker.name.firstName(),
        lastName: faker.name.lastName(),
        createdAt: admin.firestore.FieldValue.serverTimestamp(),
        updatedAt: admin.firestore.FieldValue.serverTimestamp(),
        isMock: true // 削除時のクエリ用フィールド
      }
      batch.create(usersRef.doc(), user)
    }
    const results = await batch.commit()
    // 書き込みが成功した分を引いた残りの件数を代入
    await excuteBatch(size - results.length)
  }
  await excuteBatch(n)
}

// 削除用関数
export const deleteUsers = async () => {
  // モックデータのみを500件ずつ取得
  const query = await db
    .collection('users')
    .where('isMock', '==', true)
    .limit(batchSize)

  // 再帰関数
  const executeBatch = async () => {
    const snapshot = await query.get()
    if (snapshot.size === 0) {
      return
    }
    const batch = db.batch()
    snapshot.docs.forEach(doc => {
      batch.delete(doc.ref)
    })
    await batch.commit()
    await executeBatch()
  }
  await executeBatch()
}
```

削除用の関数の実装は公式ドキュメントを参考にした
[Cloud Firestore からデータを削除する#コレクションを削除する](https://firebase.google.com/docs/firestore/manage-data/delete-data?hl=ja#collections)

ドキュメントでは `process.nextTick` を使っているが、上記の実装では async/await で制御できているから要らない？（マサカリお待ちしています）

## CLI ツールの実装

`index.ts` でそのまま呼び出して実行しても良かったが、CLI ツールとして実装した
[commander](https://github.com/tj/commander.js/) というライブラリを利用（大したことはしないのでなんでも良い）
バージョン情報や、ヘルプコマンドなど必要なものはよしなに設定してくれたので中々良かった

```typescript:index.ts
import * as admin from 'firebase-admin'
import * as program from 'commander'

// user.ts を import する前に Admin SDK を初期化する
admin.initializeApp()
admin.firestore().settings({ timestampsInSnapshots: true })

import { createUsers, deleteUsers } from './user'

program.version('1.0.0', '-v, --version')
program
  .command('user')
  .option('-d, --delete', 'delete only the created documents')
  .option('-n, --number <n>', 'A number of test documents', parseInt, 0)
  .description('create test user documents')
  .action(cmd => {
    if (cmd.number > 10000) {
      return console.error('The number must be 10000 or less')
    }
    const promise = cmd.delete ? deleteUsers() : createUsers(cmd.number)
    promise.then(() => console.log('Command has completed')).catch(console.error)
  })
program.parse(process.argv)

console.log('Firestore Mocking CLI')
```

## テストデータ作成

初めに作成した run scripts 経由で実行する

```
$ yarn cli user -n 1000

yarn run v1.12.3
$ ts-node test/cli.ts user -n 1000
Firestore Mocking CLI
Command has completed
✨  Done in 6.71s.
```

## テストデータ削除

```
$ yarn cli user -d

yarn run v1.12.3
$ ts-node test/cli.ts user -d
Firestore Mocking CLI
Command has completed
✨  Done in 5.73s.
```

## おわりに
無料枠の制限にひっかからないように気をつけましょう…
2018/12/15 現在は以下の通りです

<img width="593" alt="firebase_2018-12-14_23-09-07.png" src="https://qiita-image-store.s3.amazonaws.com/0/199914/f4ec7cc0-ae46-4103-24d9-5ba0ec07b07b.png">

そもそも一括書き込みやトランザクションは1回の実行で1件？500件？どちらなんでしょう:thinking:

[追記] 
@mono0926 さんよりコメントいただきました

> 500ドキュメント扱った場合は、いかなる方法でも500件のコストです。

書き込み件数の節約のためにバッチやトランザクションを使うことはできないということですね！