【Firebase + TypeScript】Cloud Functionsについての備忘録

# 概要

こちらの記事を購入して初めてCloud Functionsについて学んだ内容を備忘録として残しています

https://zenn.dev/tentel/books/a96e4a64d3a672911f

# Cloud Functions for Firebaseとは

https://firebase.google.com/docs/functions

> Cloud Functions for Firebase はサーバーレス フレームワークで、Firebase の機能と HTTPS リクエストによってトリガーされたイベントに応じて、バックエンド コードを自動的に実行できます。JavaScript または TypeScript コードは Google のクラウドに保存され、マネージド環境で実行されます。独自のサーバーを管理およびスケーリングする必要はありません。


# 環境構築

## プロジェクトの作成

https://firebase.google.com/docs/functions/get-started?hl=ja




![スクリーンショット 2021-12-26 1.40.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555632/529c891e-2bde-c287-6173-5cdb377984b6.png)

## `Cloud Firestore`の作成

![スクリーンショット 2021-12-26 1.43.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555632/fa38fd85-5b29-7e98-fdee-33cb1a8a69b4.png)

## Firebase CLI導入

```bash
npm install -g firebase-tools
```

## プロジェクトディレクトリを作成

```bash
# 任意のプロジェクトディレクトリを作成
mkdir functions-test
cd functions-test

# プロジェクトを作成したGoogleアカウントでログイン
firebase login
```

## Firebaseプロジェクトを初期化

```bash:/Document/functions-test
firebase init

     ######## #### ########  ######## ########     ###     ######  ########
     ##        ##  ##     ## ##       ##     ##  ##   ##  ##       ##      
     ######    ##  ########  ######   ########  #########  ######  ######  
     ##        ##  ##    ##  ##       ##     ## ##     ##       ## ##      
     ##       #### ##     ## ######## ########  ##     ##  ######  ########

? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confirm your choices. 
 ( ) Database: Configure Firebase Realtime Database and deploy rules
 (*) Firestore: Deploy rules and create indexes for Firestore
 (*) Functions: Configure and deploy Cloud Functions
 ( ) Hosting: Configure and deploy Firebase Hosting sites
>(*) Storage: Deploy Cloud Storage security rules
 ( ) Emulators: Set up local emulators for Firebase features
 ( ) Remote Config: Get, deploy, and rollback configurations for Remote Config

? Please select an option: (Use arrow keys)
> Use an existing project 

Select a default Firebase project for this directory:
Firestore Setup

? What file should be used for Firestore Rules? (firestore.rules)
? What file should be used for Firestore indexes? (firestore.indexes.json)
? What language would you like to use to write Cloud Functions?
  JavaScript
> TypeScript

? Do you want to use ESLint to catch probable bugs and enforce style? (Y/n)
# 今回はESLintは導入しないので"n"

=== Storage Setup

? What file should be used for Storage Rules? (storage.rules)
+  Firebase initialization complete!

# コードを最新にする
npm install firebase-admin@latest firebase-functions@latest
npm install -g firebase-tools
```

## 403エラーが発生する場合は以下を参考にしてください

https://zenn.dev/naonao70/articles/42f3e1d7e24d88

## npmのアップデートを検証する

https://www.npmjs.com/package/npm-check-updates

```bash:/Document/functions-test
# アップデート検証ツールをインストール
npm install -g npm-check-updates

# 検証開始
ncu

# package.jsonをアップデート
ncu -u

# 最新バージョンをインストール
npm install
```

# Cloud Functionsをデプロイする

## サンプルメソッドのコメントアウトを解除する

```ts:functions/src/index.ts
import * as functions from "firebase-functions";

// Start writing Firebase Functions
// https://firebase.google.com/docs/functions/typescript

export const helloWorld = functions.https.onRequest((request, response) => {
  // FirebaseConsole（GUIの管理画面）のFunctionsにログを出力
  functions.logger.info("Hello logs!", { structuredData: true });
  // response.sendでリクエスト元に値を返す
  response.send("Hello from Firebase!");
});
```

## Cloud Functions のデプロイをする

作成したプロジェクトを従量制に変更してください

```bash:/Document/functions-test
firebase deploy --only functions
```

### デプロイ結果
![スクリーンショット 2021-12-26 2.12.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555632/d42497e4-3bf7-1ea4-cb74-e5922132f0ce.png)
先ほどの関数の結果が画像のURLに表示されます
![スクリーンショット 2021-12-26 2.13.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555632/2b37b203-67db-8a82-070d-916a7cdbd540.png)

# Firebase Local Emulator Suiteの導入

https://firebase.google.com/docs/emulator-suite

> Firebase Local Emulator Suite は、Cloud Firestore、Realtime Database、Cloud Storage、Authentication、Cloud Functions、Pub/Sub、Firebase Hosting を使用してアプリをローカルでビルドおよびテストするデベロッパー向けの高度なツールセットです。

```bash:/Document/functions-test
firebase init emulators

=== Emulators Setup
? Which Firebase emulators do you want to set up? Press Space to select emulators, then Enter to confirm your choices. 
 (*) Authentication Emulator
 (*) Functions Emulator
>(*) Firestore Emulator
 ( ) Database Emulator
 ( ) Hosting Emulator
 ( ) Pub/Sub Emulator
 (*) Storage Emulator
# -------------------------------------------------------------------------
# 上記のように選択してEnter
# -------------------------------------------------------------------------
? Which port do you want to use for the auth emulator? (9099)
? Which port do you want to use for the functions emulator? (5001)
? Which port do you want to use for the firestore emulator? (8080)
? Which port do you want to use for the storage emulator? 9199
# -------------------------------------------------------------------------
? Would you like to enable the Emulator UI? Yes
? Which port do you want to use for the Emulator UI (leave empty to use any available port)? 
? Would you like to download the emulators now? No
+  Firebase initialization complete!
```

## ビルドする

```bash:/Document/functions-test
cd functions/
npm run build
```

## エミュレーターを起動

```bash:/Document/functions-test
firebase emulators:start

┌─────────────────────────────────────────────────────────────┐
│ ✔  All emulators ready! It is now safe to connect your app. │
│ i  View Emulator UI at http://localhost:4000                │
└─────────────────────────────────────────────────────────────┘

┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator       │ Host:Port      │ View in Emulator UI             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ localhost:9099 │ http://localhost:4000/auth      │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Functions      │ localhost:5001 │ http://localhost:4000/functions │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore      │ localhost:8080 │ http://localhost:4000/firestore │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Storage        │ localhost:9199 │ http://localhost:4000/storage   │
└────────────────┴────────────────┴─────────────────────────────────┘
  Emulator Hub running at localhost:4400
  Other reserved ports: 4500

Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.

```

![スクリーンショット 2021-12-26 2.34.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555632/dff13dfa-0807-8b63-47ab-b5564d4ed2ca.png)

# Cloud Firestoreにエクスポート・インポートする

## 保存（エクスポート）

```bash:/Document/functions-test/functions
firebase emulators:export ed
```

## 値をエミュレータに読み込む(インポート）

```bash:/Document/functions-test/functions
firebase emulators:start --import=ed
```

# HTTPリクエストをトリガーに Cloud Firestore の値を返す

## Cloud Firestore

> Google の柔軟でスケーラブルな NoSQL クラウド データベースを使用して、クライアント側開発とサーバー側開発のデータを保存、同期します。

https://firebase.google.com/docs/firestore

## Cloud Firestore にデータを追加する

![スクリーンショット 2021-12-26 19.20.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555632/9f020759-93de-a57f-a6e8-9bcc291e7dc1.png)

## Cloud Firestoreからデータを返却する処理を追加

https://firebase.google.com/docs/admin/setup

```ts:functions/src/index.ts
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";

// firebase-adminを初期化
admin.initializeApp();

export const getBooks = functions.https.onRequest(async (request, response) => {
    try {
        // firestoreのインスタンスを取得
        const db = admin.firestore();

        //インスタンスからコレクションを取得
        const ref = await db.collection('books').get();
        response.send(ref.docs.map(book => book.data()));
    } catch (e) {
        console.error(e);
        response.status(500).send(e);
    }
});
```

## ビルドする

```bash:/Document/functions-test/functions
npm run build
```

## エミュレータ上のFirestoreにデータを追加

```bash:/Document/functions-test
firebase emulators:start

# 8080ポートが既に使用されているエラーが出る場合
firestore: Port 8080 is not open on localhost, could not start Firestore Emulator.
⚠  firestore: To select a different host/port, specify that host/port in a firebase.json config file:
      {
        // ...
        "emulators": {
          "firestore": {
            "host": "HOST",
            "port": "PORT"
          }
        }
      }
i  emulators: Shutting down emulators.

Error: Could not start Firestore Emulator, port taken.

# 8080ポートが何に使われているかを確認
sudo lsof -i:8080 
java    4190 xxxxx  147u  IPv6 0x372f84f782df4f69      0t0  TCP localhost:http-alt (LISTEN)

# プロセス終了
kill 4190

# 再度エミュレーターを起動する
firebase emulators:start

✔  functions[us-central1-getBooks]: http function initialized (http://localhost:5001/functions-test-6696a/us-central1/getBooks).

┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator       │ Host:Port      │ View in Emulator UI             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ localhost:9099 │ http://localhost:4001/auth      │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Functions      │ localhost:5001 │ http://localhost:4001/functions │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore      │ localhost:8080 │ http://localhost:4001/firestore │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Storage        │ localhost:9199 │ http://localhost:4001/storage   │
└────────────────┴────────────────┴─────────────────────────────────┘
```

## Firestoreにアクセスしコレクションを追加する
![スクリーンショット 2021-12-26 21.02.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555632/7900ca14-d5cb-e69a-3f2a-e34bbd30925e.png)

## Cloud Firestoreからデータを返却する処理を実行する

```bash
 curl http://localhost:5001/functions-test-6696a/us-central1/getBooks
[{"ins_ts":{"_seconds":1640519638,"_nanoseconds":801000000},"title":"CloudFunctions入門","price":1000}]%        
```

# Cloud Firestore トリガー
> Cloud Functions を使用すると、クライアント コードを更新することなく、Cloud Firestore 内のイベントを処理できます。Cloud Firestore の変更は、DocumentSnapshot インターフェースまたは Admin SDK を使用して行うことができます。

https://firebase.google.com/docs/functions/firestore-events?hl=ja

https://qiita.com/ryo2132/items/5f8013248dc6596b59bb

> イベントタイプ	トリガー
`onCreate`	ドキュメントが最初に書き込まれたときにトリガーされます。
`onUpdate`	すでに存在するドキュメントの値が変更されたときにトリガーされます。
`onDelete`	データを含むドキュメントが削除されたときにトリガーされます。
`onWrite`	`onCreate`、`onUpdate` または `onDelete` がトリガーされたときにトリガーされます。

## onCreate

```ts:functions/src/index.ts
// 以下を追加する

export const store = functions.https.onRequest(async (request, response) => {
    if (request.method !== 'POST') {
        response.status(400).send('【不正】リクエストタイプが不正です。');
    }

    const body = request.body;

    try {
        const db = admin.firestore()
        await db.collection('books').add({body})
        response.send("Complete")
    } catch (e) {
        console.error(e);
        response.status(500).send(e)
    }
});

export const onCreateByBook = functions.firestore.document('books/{isbn}').onCreate(async (snapshot, context) => {
        const storedData = snapshot.data();
        const isbn = context.params.isbn
        const title = storedData.body.title
        const price = storedData.body.price
        console.log(`【新着本追加】ISBN: ${isbn}, タイトル: ${title}, 価格: ${price}`);
    }
)
```

## ビルド

```bash:/Document/functions-test/functions
npm run build
```

## エミュレーターを起動する

```bash:/Document/functions-test/functions
firebase emulators:start
i  emulators: Starting emulators: auth, functions, firestore, storage
⚠  functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: database, hosting, pubsub
⚠  Your requested "node" version "14" doesn't match your global version "12"
i  firestore: Firestore Emulator logging to firestore-debug.log
⚠  ui: Emulator UI unable to start on port 4000, starting on 4001 instead.
i  ui: Emulator UI logging to ui-debug.log
i  functions: Watching "/Users/habanaoki/Documents/study/functions-test/functions" for Cloud Functions...
✔  functions[us-central1-getBooks]: http function initialized (http://localhost:5001/functions-test-6696a/us-central1/getBooks).
✔  functions[us-central1-store]: http function initialized (http://localhost:5001/functions-test-6696a/us-central1/store).
✔  functions[us-central1-onCreateByBook]: firestore function initialized.

┌─────────────────────────────────────────────────────────────┐
│ ✔  All emulators ready! It is now safe to connect your app. │
│ i  View Emulator UI at http://localhost:4001                │
└─────────────────────────────────────────────────────────────┘

┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator       │ Host:Port      │ View in Emulator UI             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ localhost:9099 │ http://localhost:4001/auth      │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Functions      │ localhost:5001 │ http://localhost:4001/functions │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore      │ localhost:8080 │ http://localhost:4001/firestore │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Storage        │ localhost:9199 │ http://localhost:4001/storage   │
└────────────────┴────────────────┴─────────────────────────────────┘
  Emulator Hub running at localhost:4400
  Other reserved ports: 4500

Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.
```

## 登録処理

```bash:
curl -X POST -H "Content-Type: application/json" -d '{"price":500,"title":"sample"}' http://localhost:5001/functions-test-6696a/us-central1/store
```

## Firestore・FunctionsのURLにアクセスする

### Firestoreに登録したデータが表示される
![スクリーンショット 2021-12-26 23.22.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555632/5ef97d54-2d01-ea53-e5e5-57eb85770353.png)

`oncreate`をトリガーしてログを表示されていれば成功
![スクリーンショット 2021-12-26 23.21.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555632/c3535238-554c-9cf7-bfaa-eed7dd8263fb.png)

## onUpdate

> document()で指定したコレクションのドキュメントの更新をトリガーに関数が実行される。

```ts:functions/src/index.ts
// 以下を追加後ビルドします
export const onUpdateByBook = functions.firestore.document('books/{isbn}').onUpdate(async (change, context) => {
        const before = change.before.data()
        const after = change.after.data()
        console.log(`【変更前】: ${before.body.price}, [変更後]: ${after.body.price}`);
    }
)
```
## FireStoreから任意のコレクションを更新します
![スクリーンショット 2021-12-26 23.34.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555632/ab519f82-3a65-dcc7-d9d4-a05ee98c9e86.png)

### 更新処理をトリガーできれば成功
![スクリーンショット 2021-12-26 23.43.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555632/a7efbdad-517e-efc6-1f79-a3c65b2cd68b.png)

## onDelete

> document()で指定したコレクションからのドキュメントの削除をトリガーに関数が実行される。

```ts:functions/src/index.ts
// 以下を追加後ビルドします
exports.onDeleteByBook = functions.firestore.document('books/{isbn}').onDelete(async (snapshot, context) => {
    const data = snapshot.data()
    const isbn = context.params.isbn
    const title = data.body.title
    const price = data.body.price
    console.log(`【削除】ISBN: ${isbn}, タイトル: ${title}, 価格: ${price}`);
})
```

## FireStoreから任意のコレクションを削除します
![スクリーンショット 2021-12-26 23.34.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555632/ab519f82-3a65-dcc7-d9d4-a05ee98c9e86.png)

### 削除処理をトリガーできれば成功

![スクリーンショット 2021-12-26 23.48.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555632/ee236d24-dc13-5457-9ba2-b81b83c3e6a8.png)