FirestoreにJSON形式のデータをimportする(Subcollection込みで)

# 概要
JSON形式のデータをFirestoreにimportしたい。
Subcollection込みで。

# firestore-import-export 
https://github.com/dalenguyen/firestore-import-export
いかにもな名前で期待感大ですが、
1. Firestoreでsubcollectionを含むcollectionを作成。
2. Firestoreからexportする(下記コマンド)と、JSONでエクスポートされる (出力ファイルをexport.jsonとする)
3. foo_collectionとbar_subcollectoinを削除する。
4. export.jsonをimportコマンド(下記参照)で取り込もうとするとsubcollectionがある場合はimportできない。orz

```Bash
# exportコマンド
node export.js foo_collection bar_subcollection
```
```Bash
# importコマンド
node import.js export.json
```


# stackoverflow
賢い人っているんですねぇ。さらっと回答しています。
https://stackoverflow.com/questions/46640981/how-to-import-csv-or-json-to-firebase-cloud-firestore
General Solutionのセクション

コードを引用(Javascriptです)

```JavaScript
const admin = require('../functions/node_modules/firebase-admin');
const serviceAccount = require("./service-key.json");

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: "https://<your-database-name>.firebaseio.com"
});

const data = require("./fakedb.json");

/**
 * Data is a collection if
 *  - it has a odd depth
 *  - contains only objects or contains no objects.
 */
function isCollection(data, path, depth) {
  if (
    typeof data != 'object' ||
    data == null ||
    data.length === 0 ||
    isEmpty(data)
  ) {
    return false;
  }

  for (const key in data) {
    if (typeof data[key] != 'object' || data[key] == null) {
      // If there is at least one non-object item in the data then it cannot be collection.
      return false;
    }
  }

  return true;
}

// Checks if object is empty.
function isEmpty(obj) {
  for(const key in obj) {
    if(obj.hasOwnProperty(key)) {
      return false;
    }
  }
  return true;
}

async function upload(data, path) {
  return await admin.firestore()
    .doc(path.join('/'))
    .set(data)
    .then(() => console.log(`Document ${path.join('/')} uploaded.`))
    .catch(() => console.error(`Could not write document ${path.join('/')}.`));
}

/**
 *
 */
async function resolve(data, path = []) {
  if (path.length > 0 && path.length % 2 == 0) {
    // Document's length of path is always even, however, one of keys can actually be a collection.

    // Copy an object.
    const documentData = Object.assign({}, data);

    for (const key in data) {
      // Resolve each collection and remove it from document data.
      if (isCollection(data[key], [...path, key])) {
        // Remove a collection from the document data.
        delete documentData[key];
        // Resolve a colleciton.
        resolve(data[key], [...path, key]);
      }
    }
```
読めばほぼ自明ですが、ちょっと補足。

## importするライブラリ名
```JavaScript
const admin = require('../functions/node_modules/firebase-admin');
//　設定によっては、↓かも
const admin = require('firebase-admin');
```

## service account key
```JavaScript
// GCPではいつもの、serive account keyの設定。
const serviceAccount = require("./service-key.json");
```

## import対象のFirestoreの指定
<your-database-name>　のところは、デフォルトだとプロジェクト名になっているはず。

```JavaScript
  databaseURL: "https://<your-database-name>.firebaseio.com"
```

## importするデータの指定
```JavaScript
    // 読み込むJSON
    const data = require("./fakedb.json");
```


## importするJSONの例

```JSON
{
  "collection_name": {
    "document_identifier1" : {
      "field1": "hoge",
      "field2": 1,
      "array": ["element1","element2","element3"],
      "subcollection_name": {
        "document_identifier1_1": {
          "field3" : "foo",
          "field4" : 4
        },
        "document_identifier1_2": {
          "field3" : "foobar",
          "field4" : 5
        }
      }
    },
    "document_identifier2" : {
      "field1": "hogehoge",
      "field2": 2,
      "array": ["element4","element5","element6"],
      "subcollection_name": {
        "document_identifier2_1": {
          "field3" : "bar",
          "field4" : 6
        }
      }
    }
  }
}
```

document_identifierN は 何でもよいです。
Firestoreのデフォルトだと自動で生成されるランダムな20文字のものです。
→ 重複しなければ、 1 などにしても問題なしです。

# あとがき
JSONやYAMLで取り込む機能は、公式が用意してしてくれてもいいのになぁ。

2020/11/08追記
良さそう
https://github.com/matsu0228/fsrpl
公式の実装はまだかなぁ。