Firebaseエミュレータでcloud functions、storage、datastoreを使った

Firebaseエミュレータでcloud functions、storage、Firestoreを使ったときのメモ

# やりたいこと
・パソコンで作成したユーザリスト(csv)をstorageにインポート
・storageインポートをトリガにしてcloud functions実行
・cloud functionsにて、
　　- storageに格納されたcsvからユーザと年齢を取り出す
　　- ユーザと年齢にuuidを追加し、Firestoreに格納
　　- すでに同じユーザがいる場合は追加しない
・以上をFirebaseのエミュレータで動かす

### 苦労したこと
・apiの情報が少ない。ネットに情報はあるが断片的。。
・ネットに情報があってもapiが古かったりする
・cloud functions/storage/datastoreの情報を見つけたと思ったらFirebaseではなくGCPだったりする

### なぜFirebaseにしたか
・エミュレータがあるからです

# 作業開始
●インストール
```
npm install firebase
npm install -g firebase-tools
npm install @firebase/firestore
```
●エミュレータ初期化。cloud storage、cloud functions、cloud datastoreを有効化
```
firebase init emulators
```
●cloud functions作成
```
firebase init functions
cd functions
```
```javascript:cloud functionsのindex.ts
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
import { v4 as uuidv4 } from 'uuid';

type dataType = {
  user: string;
  age: number;
  id?: string;
}

//Firebaseコンソールにログインしてプロジェクト情報を持ってくる
//エミュレータを動かすだけなのにFirestoreを使うにはプロジェクト情報が必要
const firebaseConfig = {
  apiKey: "xxxxxxxxxxxxxxxxx",
  authDomain: "xxxxxxxxxxxxxxxxxm",
  projectId: "xxxxxxxxxxxxxxxxx",
  storageBucket: "xxxxxxxxxxxxxxxxx",
  messagingSenderId: "xxxxxxxxxxxxxxxxx",
  appId: "xxxxxxxxxxxxxxxxx",
  measurementId: "xxxxxxxxxxxxxxxxx"
};

//storageにファイルアップロードをトリガにして関数が呼ばれる
//バケット情報が渡される
exports
.cf_test = functions.region("asia-northeast1").storage
          .bucket(process.env.BUCKET_NAME).object()
          .onFinalize(async (object) => {

          const fileBucket = object.bucket;
          const filePath = object.name;
          let dataList:dataType[] = [];
          //この初期設定しないとエミュレータが動かない
          admin.initializeApp(firebaseConfig);

          //バケット配下をファイル検索。prefixで絞り込む
          (await admin.storage()
          .bucket(fileBucket).getFiles({prefix:filePath}).then((files) => {
            files[0].forEach((file) => {
              //ファイル名チェック
              if(filePath === file.name) {
                //csvの中身を読み取って解析
                file.createReadStream().on('data', function(chunk) {
                  //バイト配列→文字列変換→文字列を改行で分割
                  Buffer.from(chunk).toString().split("\r\n").forEach((data) => {
                    const tmp = data.split(",");
                    dataList.push({user: tmp[0], age: Number(tmp[1])});
                  });
                  //Firestoreにデータ格納
                  writeToDatastore(dataList);
                })
              }
            })
          }))
        });

//Firestoreにデータ格納
function writeToDatastore(dataList: dataType[]) {
  const db = admin.firestore();
  dataList.forEach((data) => {
    //すでにuserが存在するかチェック。whereで条件指定し、マッチした数が0より大きいなら存在
    db.collection("hoge-collection")
    .where("user", "==", data.user).get().then((val) => {
      if(val.size > 0) {
        //同じuserが登録済なので追加しない
        functions.logger.log("this user is already added=" + data.user);
      } else {
        //同じuserが存在しないので追加する
        const inputData: dataType = {
          user: data.user, age: data.age, id: uuidv4()
        }
        db.collection("hoge-collection").doc()
        .set(inputData).then(() => {
          functions.logger.log("add user=" + data.user);
        });
      }
    });
  })
}
```
●cloud functionsをビルド
```
npm run build
```
●エミュレータ起動
```:cloud functionsのフォルダの親フォルダで実行
firebase emulators:start
```
●storageにcsvを入れる
```:csv
user01, 10
user02, 20
user03, 30
user04, 40
```
![1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726391/8e7fb5e6-5bff-97e9-e422-f7f1ecb93c25.jpeg)

●cloud functionsが走った。storage内のcsvを読み込んで解析成功
![2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726391/606d2bcb-e561-7eb3-a7a8-271bbac215c7.jpeg)

●Firestoreにデータが追加された
![3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726391/ca089b94-0161-be8d-0610-5d4afc43e206.jpeg)

# 参考
[apiリファレンス](https://googleapis.dev/nodejs/storage/latest/index.html)
[接頭辞フィルタ](https://cloud.google.com/storage/docs/samples/storage-list-files-with-prefix?hl=ja)
[Firebaseのcloud functions](https://firebase.google.com/docs/functions/gcp-storage-events?hl=ja)