世界一簡単なCloud Functions for Firebase入門

# 概要
これまで、自分はサーバーレス（AWS LamndaやCloud Functions For Firebase）という技術について触ったことがなく、どんなことができるかもいまいち知り得ないという状態でした。

そこで今回、軽く入門してみようと思い、簡易的なAPIの開発を通じて勉強していこうと思います。

今回のAPIの開発は専用のリポジトリを作成していますので、よかったら参考にしてみてください！
[専用のリポジトリ](https://github.com/msy7822-ux/learn-cloud-functions)

今回やることは、大まかに以下の通りです。

1. firebaseのプロジェクトを新規開設
2. 必要なライブラリをインストールし、セットアップを行う
3. cloud functionsで何らかの値をレスポンスとして返してくれるエンドポイントを実装
4. firebase deployする

上記の一連の流れとは別に、追加でfirestoreへのデータの追加・読み出しを行うAPIの実装なども行っていきたいと思ってます。

また、今後追加でfirestoreと連携させ、firestoreに格納されたデータなどをjson形式で返してくれるエンドポイントなんかも取り入れようと思っています。


<!-- 具体的な実装については適宜まとめておく。 -->

# コンソールからFirebaseのプロジェクトを作成する
### プロジェクト名を決める
下記のプロジェクトを新規作成する画面で、何らかのプロジェクト名を入力して下さい。
今回、cloud functionのチュートリアルということで、自分は`cloud-function-tutorial`と名付けました。
![](https://storage.googleapis.com/zenn-user-upload/c7525a26fdee-20220522.png)


名前の入力が完了したら、そのまま「続行」のボタンをクリックします。

### Google Analyticsの設定をする
今回のプロジェクトではGoogle Analyticsを使用する予定はありませんので、有効のチェックを外していただいて大丈夫です。
![](https://storage.googleapis.com/zenn-user-upload/008c1fe51bb9-20220522.png)

最後に「プロジェクトを作成」するボタンをクリックいただくと、プロジェクトの作成が行われます。
![](https://storage.googleapis.com/zenn-user-upload/e72bd2990b38-20220522.png)


下記のようにプロジェクトのダッシュボードの画面に遷移したら、無事プロジェクトの作成は完了です。
![](https://storage.googleapis.com/zenn-user-upload/35e1d96c2a84-20220522.png)

# 必要なライブラリ等のインストールおよびセットアップを行う
今回、cloud functionをいじる際に必要なライブラリや実行環境は下記の通りです。
- Node.js
- npm
- firebase-tools

#### firebase-toolsというライブラリに関して
cliでfirebaseを使うのに必要になります。
こちらをインストールすることで、`firebase`コマンドが使えるようになり、様々なことがcliでできるようになります。

今回は、`npm`と`Node.js`はインストール済みという前提で進めさせていただきます！🙇‍♂️

### firebase-toolsをインストールする
下記のコマンドを実行して、firebase-toolsというライブラリをインストールします。
```bash: bash
$ npm install -g firebase-tools
```

下記のように、`firebase`コマンドが実行できたらOKです。
```bash: bash
$ firebase --version
11.0.0
```

### cliからfirebaseにログインする
クラウド上のfirebaseのアカウントとローカルのcli上のfirebaseとを紐づけるために、cliからfirebaseにログインします。

下記のコマンドでログインします。
```bash: bash
$ firebase login
```

※ログイン時に下記のような質問があると思いますが、`n（No）`でOKです！
これはただのFirebaseがCLIの使用状況とエラーレポート情報を収集できるようにするかどうかの確認なのでNoで大丈夫です。

```bash: bash
? Allow Firebase to collect CLI usage and error reporting information?
```

# firebaseのプロジェクトの初期化を行う
```bash: bash
$ firebase init firestore
$ firebase init functions
```

上記の二つのコマンドを実行して、プロジェクトの初期化を行います。

この二つのコマンドをどちらも実行時にいくつかの質問に答えつつ、進めていってください。
基本的には、`useing a exists project`（文言あってなかったらごめんなさい）みたいなやつを選んでいただければOKです！

そちらを選ぶと、実際に既存のプロジェクトの中からどのプロジェクトを使用するか聞かれるので、先ほど作ったばかりのプロジェクトを選択してください。

またFunctionsの方では、Functionsに記述する言語を`JavaScript`か`TypeScript`のどちらにするか質問されると思います。

今回のチュートリアルは、「世界一簡単」を目指していきたいので`JavaScript`を選択します。

上記の二つのコマンドの両方とも、下記のような文言が表示されれば初期化が無事完了したことになります。
```bash: bash
✔  Firebase initialization complete!
```

# 実際にAPIのエンドポイントを実装する
この段階で、ディレクトリ構成は下記のようになっています。
```bash: bash
$ tree .
.
├── .firebaserc
├── .gitignore
├── README.md
├── firebase.json
├── firestore.indexes.json
├── firestore.rules
└── functions
    ├── .eslintrc.js
    ├── .gitignore
    ├── index.js
    └── package.json
```

このtree構造の中の`functions/index.js`というファイルに実際のAPIのエンドポイントとなる関数を実装していきます。

今の時点では、`functions/index.js`ファイルは下記のようになっています。
```js: functions/index.js
const functions = require("firebase-functions");

// // Create and Deploy Your First Cloud Functions
// // https://firebase.google.com/docs/functions/write-firebase-functions
//
// exports.helloWorld = functions.https.onRequest((request, response) => {
//   functions.logger.info("Hello logs!", {structuredData: true});
//   response.send("Hello from Firebase!");
// });
```

まずは、デフォルトで表記されているエンドポイントのサンプルコードを動かしてみたいと思います。
こちらのコメントになっている部分のコメントを外して下記のように書き換えます。

```js: functions/index.js
const functions = require("firebase-functions");

// Create and Deploy Your First Cloud Functions
// https://firebase.google.com/docs/functions/write-firebase-functions

exports.helloWorld = functions.https.onRequest((request, response) => {
  functions.logger.info("Hello logs!", {structuredData: true});
  response.send("Hello from Firebase!");
});
```

次にfunctionsディレクトリに移動し、firebaseのemulatorsを起動します。
```bash: bash
$ cd functions
$ npm run serve
```
下記のような画面になれば、無事起動完了です。
![](https://storage.googleapis.com/zenn-user-upload/a9610721c5e7-20220523.png)

こちらのエンドポイントを`curl`コマンドを叩いてみます。
※ `helloWorld`はfunctionの名前です。
```bash: bash
$ curl http://localhost:5001/${プロジェクトID}/${リージョン}/helloWorld
```

このコマンドの結果として、`Hello from Firebase!`という文字列が表示されれば成功です。

# firebase deployする
一旦ここまでで、このfunctionをデプロイしてみたいと思います。

デプロイに際して、下記のコマンドを実行してください。

```bash: bash
$ firebase deploy --only functions
```

このコマンドを実行すると、下記のようなエラーメッセージが表示されるかと思います。
![](https://storage.googleapis.com/zenn-user-upload/642e54e6957c-20220523.png)

[【Firebase】Cloud Functionsが従量課金プランでしか使えなくなるらしい](https://qiita.com/azukiazusa/items/edd6ca9cba2d48c4c3e2)というこちらの記事によると、どうやら
> 要約するとこんな感じ
>
> 1. Cloud FunctionsがNode.js 10になってNode.js 8が非推奨になるよ
> 2. いまNode.js 8使ってる人はアップデートしてね
> 3. Node.js 10はBlaza(従量課金）プランでしか使えないからSpark(無料)プラン使ってる人はアップデートしてね
>ってことらしい

とのことらしいです。
ということで、コンソールからFunctionsのプランを`Spark`から`Blaze`にアップグレードしてください。

無料枠分はちゃんと適応されるぽいので、よっぽどのことがない限りはお金の支払いが発生することはないでしょう。

再度、上記のデプロイコマンドを実行してみてください。

```bash: bash
✔  Deploy complete!
```
このような文言が出力されれば、無事デプロイ完了です。


# firestoreへのデータの追加・読み出しをするAPIを作ってみる

### テストデータを用意する

今回は、まずIDが`test`というcollectionを作成して、そのcollectionが存在していることを前提に進めていこうと思います。

**※最初のこちらのcollectionと、初期データのに手動で作成いただくことになります。**

![](https://storage.googleapis.com/zenn-user-upload/e83ca4b96c33-20220524.png)

初期データには、`id`フィールドと`name`フィールドを作成して任意の値を設定してください。

![](https://storage.googleapis.com/zenn-user-upload/8e5d3ad9fd4c-20220524.png)

### admin sdkからfirestoreを使えるように設定する
```js: functions/index.js
const admin = require("firebase-admin");

// Admin SDKでfireStoreを使う
admin.initializeApp(functions.config().firebase);

// データベースの参照を取得する
const fireStore = admin.firestore();
```

### firestoreからデータを取得して返すエンドポイントを実装する
以下は実装は、firestoreの`test`というcollectionから任意のdocument IDのドキュメントのデータを取得して、レスポンスとしてクライアントに返すというエンドポイントです。

```js: functions/index.js
exports.getFirestore = functions.https.onRequest((req, res) => {
  // パラメータを取得
  const params = req.body;
  // パラメータから任意のdocument IDを取得する
  const documentId = params.documentId;

  if (documentId) {
    // 'test'というcollectionの中の任意のdocumentに格納されているデータを取得する
    const testRef = fireStore.collection('test');
    testRef.doc(documentId).get().then((doc) => {
      if (doc.exists) {
        res.status(200).send(doc.data());
      } else {
        res.status(200).send("document not found");
      }
    });
  } else {
    res.status(400).send({errorMessaage: 'document id not found'});
  }
});
```

以下のcurlコマンドを実行して、エンドポイントを確認してみてください。
```bash: bash
$ curl http://localhost:5001/${プロジェクトID}/${リージョン}/getFirestore --data 'documentId=${任意のdcument ID}'
```

### firestoreのcollectionに任意のIDのドキュメントを作成するエンドポイントを実装する

以下の実装は、任意のデータ（パラメータ）を渡すとそれをfirestoreの任意のIDのdocumentを作成しデータを格納したのち、格納したデータをレスポンスとして返すというエンドポイントです。

```js: functions/index.js
// 渡されたパラメータのスキーマをチェックする
const validateParamsSchema = (params) => {
  const hasId = 'id' in params;
  const hasName = 'name' in params;
  const hasDocumentId = 'documentId' in params;

  return hasId && hasName && hasDocumentId;
};

// firestoreに任意のデータを保存する
exports.saveFirestore = functions.https.onRequest((req, res) => {
  const params = req.body;
  // パラメータのスキーマのチェック
  if (!validateParamsSchema(params)) {
    res.status(400).send({errorMessaage: 'パラメータが不正です'});
  } else {
    const db = fireStore;
    // 'test'というcollectionがある前提で任意のドキュメントIDのdocumentを生成する
    db.doc(`test/${params.documentId}`).set({
      id: params.id,
      name: params.name,
    });

    // 非同期的に保存したデータを参照する
    db.collection('test')
        .doc(params.documentId)
        .onSnapshot((doc) => {
	  // 取得したデータをレスポンスとして返す
          res.status(200).send(doc.data());
        });
  }
});
```

こちらに関しても同様に、以下のコマンドでエンドポイントの確認をしてみてください。
各パラメータの値は任意の値でOKです。

```bash: bash
curl http://localhost:5001/${プロジェクトID}/${リージョン}/saveFirestore --data 'id=7&name=test 7&documentId=custome doc7'
```

# 再度、firebaseをdeployする
下記のコマンドを実行して、firestoreとcloud functionsをdeployします。
```bash: bash
$ firebase deploy --only functions
$ firebase deploy --only firestore
```

deployが成功したら、以上で本チュートリアルは終了になります。
お疲れ様でした。

何か不明点などがあればコメントを頂けますと幸いです。



# 参考サイト情報
- [firebase initでエラーが起こる。](https://qiita.com/HorikawaTokiya/items/2ed026a1d43f09ad089a)
- [【Firebase】Cloud Functionsが従量課金プランでしか使えなくなるらしい](https://qiita.com/azukiazusa/items/edd6ca9cba2d48c4c3e2)
- [【firestore公式ドキュメント】Cloud Firestore データモデル](https://firebase.google.com/docs/firestore/data-model?hl=ja)
- [Firestoreでコレクションだけ持ってるドキュメントを一覧で取得できなくてハマったらFirestoreの構造を実感できた](https://uyamazak.hatenablog.com/entry/2018/08/08/131918)
- [Firebase - Firestore](https://note.yuuniworks.com/study/firestore.html)