Firestore に json ファイルからデータを一括インポートする

# 概要

Firebase の Firestore を使っていると、データを一括で Firestore に流し込みたくなるときがあります。
Google Cloud の機能としてインポート/エクスポートがサポートされていますが、こちらは一度エクスポートしたデータをインポートすることしかできません。
データの初期値やマスターデータなど固定のデータを流し込むときはどうすれば良いのでしょうか。
その方法を記述します。

# 先に結論

結論から書くと、以下のコードを実行することでインポートすることができます。
詳細は下に書いています。

```typescript
import admin from "firebase-admin";
import importer from "firestore-export-import";
import fs from "fs";

// Firebaseの秘密鍵JSONファイルを読み込み認証情報としてセットする
const serviceAccount = JSON.parse(fs.readFileSync('./secret/secret-key.json', 'utf8'));
admin.initializeApp({
  credential: admin.credential.cert(serviceAccount)
});
// data下のディレクトリ以下のファイルを全部対象にする
fs.readdir('./data/', async function (err, files) {
  if (err) {
    throw err;
  }
  for (const file of files) {
    try {
      // インポート
      await importer.restore('./data/' + file);
    } catch (e) {
      // 1つのファイルが失敗しても処理を止めない
      console.log(e);
    }
  }
});

```


# 方法

`firestore-export-import` というツールを使ってインポーターを作ることで実現できます。

今回は以下のディレクトリ構成でインポーターを作ることにしました。

```bash
data/ # インポートするデータを置く場所
  users.json
secret/
  secret-key.json # Firebase の秘密鍵
src/
  import_firestore.ts # インポーター
package.json
tsconfig.json
```

### package.json, tsconfig.json の記述

`package.json` に必要となるパッケージと scripts を書いておきます。そして `npm install` します。

```json:package.json
{
    "name": "tools",
    "type": "module",
    "scripts": {
        "import": "./node_modules/.bin/ts-node-esm src/import_firestore.ts"
    },
    "devDependencies": {
        "firebase-admin": "^11.4.1",
        "firestore-export-import": "^1.3.5",
        "ts-node": "^10.9.1",
        "typescript": "^4.9.4"
    }
}
```

`tsconfig.json` は好きにしてもらって良いと思いますが、私の設定は以下です。

```json:tsconfig.json
{
    "compilerOptions": {
        "module": "esnext",
        "noImplicitReturns": true,
        "noUnusedLocals": true,
        "moduleResolution": "node",
        "outDir": "lib",
        "sourceMap": true,
        "strict": true,
        "target": "es2020"
    },
    "compileOnSave": true,
    "include": [
        "src"
    ]
}
```

### Firebase の秘密鍵の作成

あらかじめ Firebase コンソールから秘密鍵を作成しておきます。
「プロジェクトの設定」>「サービス アカウント」>「新しい秘密鍵の生成」を押すと生成できます。
それを secret 以下に置いてください(管理は自己責任で)。

![スクリーンショット 2022-12-30 21.25.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27159/05e00909-9a53-05da-8e3f-eb0c17490a1d.png)

### インポートする JSON ファイルを作成

次に data 下にjsonファイルを配置していきます。
今回は users.json を作って以下のように書きました。1つの JSON ファイルに1つのコレクションを書きます。

```json
{
    "User": {
        "1": {
            "name": "山田太郎",
            "description": "taro yamada"
        },
        "2": {
            "name": "鈴木花子",
            "description": "hanako suzuki"
        },
        "3": {
            "name": "佐藤佳子",
            "description": "yoshiko sato"
        }
    }
}
```

### インポーターの作成

続いて tools 下に import_firestore.ts を作って以下のように記述します。
data 下の JSON　ファイル全てを読みに行ってインポートするプログラムです。

```typescript
import admin from "firebase-admin";
import importer from "firestore-export-import";
import fs from "fs";

// Firebase秘密鍵JSONファイルを読み込み認証情報としてセットする
const serviceAccount = JSON.parse(fs.readFileSync('./secret/secret-key.json', 'utf8'));
admin.initializeApp({
  credential: admin.credential.cert(serviceAccount)
});
// data下のディレクトリ以下のファイルを全部対象にする
fs.readdir('./data/', async function (err, files) {
  if (err) {
    throw err;
  }
  for (const file of files) {
    try {
      // インポート
      await importer.restore('./data/' + file);
    } catch (e) {
      // 1つのファイルが失敗しても処理を止めない
      console.log(e);
    }
  }
});

```

### 実行

それでは実行してみます。

```bash
$ npm run import
```

すると...プログラムが成功すると静かに実行終了します。
Firebaseコンソールでimportが成功しているか見てみます。

![スクリーンショット 2022-12-30 21.22.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27159/68279aee-4906-6433-85a8-2edb3e141bd9.png)

見事インポート成功していました！

# Tips: サブコレクションを追加したい場合

コレクション内にサブコレクションを追加したい場合は少し特殊な JSON ファイルの書き方が必要になります。
例えば、キャラクターのコレクション内でスキルのサブコレクションを持ちたい場合、以下のように記述します。

```json
{
    "character": {
        "Ken": {
            "name": "ケン",
            "subCollection": {
                "character/Ken/skill": {
                    "1": {
                        "name": "パンチ"
                    },
                    "2": {
                        "name": "キック"
                    }
                }
            }
        },
        "Ryu": {
            "name": "リュウ",
            "subCollection": {
                "character/Ryu/skill": {
                    "1": {
                        "name": "波動拳"
                    },
                    "2": {
                        "name": "昇竜拳"
                    }
                }
            }
        },
        "Aoi": {
            "name": "アオイ",
            "subCollection": {
                "character/Aoi/skill": {
                    "1": {
                        "name": "かわす"
                    },
                    "2": {
                        "name": "合気道"
                    }
                }
            }
        }
    }
}
```

これをインポートして Firebase コンソールで見てみると...

![スクリーンショット 2022-12-30 21.35.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27159/06860436-600c-0b9b-1731-e6ca55bda93a.png)

サブコレクションまでインポート成功していました！

# Tips: ローカルの Firestore にデータをインポートしたい場合

エミューレーターで起動したローカルの Firestore にデータをインポートしたい場合はどうすればよいか。
`FIRESTORE_EMULATOR_HOST` という環境変数がセットされていた場合、自動でそっちへ向くようになっている。
（クレデンシャルの設定があっても無視されるのでコードは一切変える必要がない）
環境変数セット後、インポートを実行すればローカルの firestore にデータが入る。

```bash
$ export FIRESTORE_EMULATOR_HOST="localhost:5003"
$ npm run import
```