Firebase Emulatorで動作確認しながらREST APIを自作してFirestoreにデータを保存させ、functionをデプロイする

# (2)CloudFunctions for Firebaseに触れる

## 概要
前回まででフロントエンドのReactアプリとfirebaseの設定とfunctionsの雛形の作成が完了しました。
前回：https://qiita.com/sugimo-ne/items/c363342a0d83c7ab82b4
今回はfunctionsをどのように作成して動作させるのか、簡単にRESTAPIの作成とCloud Firestoreとの連携もしてみたいと思います。


## functionsに触れる。

https://firebase.google.com/docs/emulator-suite/connect_and_prototype#prototype_and_test

こちらの公式ドキュメントに沿ってサンプルのコードを動かしていきます。


### firebase functionsエンドポイント作成とレスポンスの設定をしてみる。
まずその前に、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!");
// });

```

自動で雛形が作成されています。
firebase emulatorが起動されていない場合、
```
$ firebase emulators:start
```
で起動させ、
index.jsのコメントアウトを外してみてください。
コンソールにfunctionのローカルのエンドポイントが出力されるかとおもます。

*スクショ

　http://localhost:5001/プロジェクト名/us-central1/helloWorld

こちらのエンドポイントブラウザからアクセスしてみましょう。

![スクリーンショット 2022-08-20 17.42.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/652033/b008caa7-60cc-c838-f2ed-1e3c79ddfeee.png)



functions/index.jsの
```
exports.helloWorld = functions.https.onRequest((request, response) => {
  functions.logger.info("Hello logs!", {structuredData: true});
  response.send("Hello from Firebase!");
});
```
helloWorldが実行されてresponse.sendで指定した文字列がリクエストに対するレスポンスとして帰ってきています。

試しに
```
exports.helloWorld
```
こちらの記述を
```
exports.sample
```
と書き換え、


```
response.send("Hello from Firebase!");
```
を
```
response.send("yaa");
```
と書き換えてみます。

先ほどと変わって

　http://localhost:5001/プロジェクト名/us-central1/sample

こちらにアクセスするとyaaという文字列が返却されているのがわかるかと思います。




(request, response)を引数に取り、responseのメソッドを使用したものを関数を与え、
返り値を変数に格納し、exportすると変数名に対応したアクセスがあった際に実行されるためresponseの内容が返ってくるようになります。

### 公式のサンプルを動かす。

次にcloudfunctionsでfirebaseの提供するサービスと連携するscriptを見てみます。

https://firebase.google.com/docs/emulator-suite/connect_and_prototype?hl=ja#prototype_and_test

公式のサンプルに従ってローカルで起動しているcloudfirestoreと連携したscriptを動かしてみます。

functions/index.jsの中身をサンプルに入れ替えます。
```
  const functions = require('firebase-functions');

  exports.makeUppercase = functions.firestore.document('/messages/{documentId}')
      .onCreate((snap, context) => {
        const original = snap.data().original;
        console.log('Uppercasing', context.params.documentId, original);
        const uppercase = original.toUpperCase();
        return snap.ref.set({uppercase}, {merge: true});
      });
  
```
中身を見ていくと

```functions.firestore.document()```の引数にfirestoreのcollectionのドキュメントまでのパスを与え、```.onCreate```関数でドキュメントが作成された際の処理を記述しています。
ここではmessagesコレクションに作成されたドキュメントのoriginalフィールドの値を取得し、すべて英数字の大文字に変換し同じドキュメント内のussercaseフィールドに保存する処理が記述されています。


firebase emulatorにアクセスし、firestoreのタブを開きます。
![スクリーンショット 2022-08-20 18.41.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/652033/5c5afea9-2f2a-88e7-bb58-69dcae1b48b4.png)


[start collection]ボタンを押して、

![スクリーンショット 2022-08-20 19.38.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/652033/ddbc031f-0dea-f304-c695-eb52192d7ff8.png)
collectionIDをmessages

DocumentIDをデフォルト値

Fieldをoriginal

Valueをtest
![スクリーンショット 2022-08-20 19.38.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/652033/8d99e601-37f9-c1eb-14d4-775a3099c4ce.png)

としてsaveします。


そうするとローカルのEmulatorでデータベースの値が更新されているのが確認できます。
![スクリーンショット 2022-08-20 19.39.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/652033/6a3e8507-6b1c-cd7c-fd90-603adcbb70aa.png)


### functionsにexpressを使用してREST APIを作成し、データをローカルのfirestoreに保存する。
これまでやった内容をもとに
RESTAPIを作成してfirestoreのデータを追加してみましょう。

```
$ cd functions
$ npm install --save-dev firebase-admin express cors
```

必要なパッケージ類をインストールします。

#### ソースコード

**functions/admin.js**

```
const admin = require("firebase-admin");
admin.initializeApp();

const db = admin.firestore();

module.exports = {
  db,
};

```
admin.jsにfirebase-admin(firebaseをサーバサイドで利用するためのsdk)を使うための初期設定を行なっています。
ローカルのemulatorに対して操作を行う際には```initializeApp()```の引数は与えなくて大丈夫です。
controllerで使用するdbもインスタンス化してexportします。

**functions/controllers/TestController.js**
```
const {db} = require("../admin");

const messagePost = async (req, res) => {
    try{
        console.log(req.body)
        const {text} = req.body;
        const ref = db.collection("messages");
        const docRef = await ref.add({
            original: text
        })

        const docSnapshot = await docRef.get();
        const newMessage = {
            id: docSnapshot.id,
            ...docSnapshot.data(),
          };

        res.json({
            message: "success",
            data:newMessage
        })
    }catch(e){
        console.log(e)
        res.json({
            message: "failed",
            error:e
        })
    }
}

module.exports = {
    messagePost
}
```
messagePost関数はexpressのrouterでルーティングにマッチしたpostリクエストを受け取った際に発火させます。
リクエストボディのtextを受け取ってadmin sdkを使用してfirestoreにデータを保存する処理を記述しています。

これによってtextデータを含めたリクエストがきた際に、
messagesコレクションに新しく、
**original: textの中身**
のフィールドを持ったドキュメントが追加されるようになります。

**functions/routes.js**
```
const {Router} = require("express");
const router = new Router();

const {messagePost} = require("./controllers/TestController");

router.route("/test").post(messagePost);

module.exports = router;

```

routes.jsではexpressのルーティング機能を利用するための設定を行なっています。
controllerのmessagePost関数を読み込んで
/testに対してpostリクエストがあった際にmessagePostを呼び出します。



**functions/index.js**

```
const functions = require("firebase-functions");
const express = require("express");
const cors = require("cors");
const app = express();

app.use(cors({origin: true}));

const router = require("./routes");
app.use("/", router);

exports.api = functions.https.onRequest(app);

exports.makeUppercase = functions.firestore.document("/messages/{documentId}")
    .onCreate((snap, context) => {
      const original = snap.data().original;
      console.log("Uppercasing", context.params.documentId, original);
      const uppercase = original.toUpperCase();
      return snap.ref.set({uppercase}, {merge: true});
    });
```

index.jsで
 - expressの初期化
 - corsの設定
 - ミドルウェアの設定(routes.jsからrouterを読み込みmidlleware化する。)
 - apiモジュールのexport

を追加しました。
これによって/api/*にhttpリクエストがきた際に、
expressのrouterがルーティングにマッチした処理を行います。

ソースコードの編集は以上です。

#### 動作確認

動作確認にはPOSTMANを利用して、
bodyに**text**を含めて作成したAPIに対してリクエストを送信します。

すると正しく処理ができた場合にレスポンスとして作成されたデータのがjson形式でレスポンスとして返ってきていることがわかります。

Emulatorで確認しみましょう。


![スクリーンショット 2022-08-20 22.54.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/652033/bb85a44c-8dc0-57f0-b4d7-9ae7ecd2cf0b.png)

作成したAPIを呼び出してデータが保存されていることと、
公式のサンプルコードの処理が走ってuppercaseフィールドに、大文字に変換された値が保存されているのがわかります。

#### デプロイしてみる。

実際にデプロイして動作確認を行います、見てデプロイの仕方を覚えるだけでも大丈夫ですが興味のある方は、従量課金の設定が必須となるので任意で進めてみてください。

![スクリーンショット 2022-08-20 23.09.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/652033/316d2d64-444c-1169-140f-8a8598dbc24c.png)

firebaseコンソールでsparkプランとなっている場合firebaseの関数はデプロイできないので従量課金プランに変更します。

その後/functionsより一つ上の階層で
```
$ firebase deploy --only functions
```
とコマンドを実行します。
するとcliでデプロイ処理が行われます。
lintの設定で引っかかるとデプロイできないので引っかかった方はコードを修正したりeslint.rcの内容を編集するなどしてください。

![スクリーンショット 2022-08-20 23.29.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/652033/34aaf347-3a9e-1ea7-abf2-716435c42157.png)

こちらが出たら成功です。

firebaseのコンソールを開き、
![スクリーンショット 2022-08-20 23.31.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/652033/e2eac57e-bfb7-d5ff-9bf1-1d8e808f1f9d.png)
Functionsを開きます。
![スクリーンショット 2022-08-20 23.32.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/652033/baecac19-9865-9837-f3c0-de7a0d25b85c.png)
二つの関数が作成されていて
api関数のトリガーの部分のリクエストにurlが貼ってあるのでそのurlをコピーします。

その後、postmanのエンドポイントをコピーしたurlに変えてpostしてみます。
![スクリーンショット 2022-08-20 23.34.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/652033/3cd5a5f3-8108-42f3-32a8-5be5f379df22.png)

レスポンスが正常に返ってきたらfirebaseのコンソールを確認します。
![スクリーンショット 2022-08-20 23.36.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/652033/032484ec-8f63-97a6-29c3-9bc7781116b4.png)

ローカルのEmulatorではなく本番のfirebaseにデータが保存されているのがわかります。

## まとめ

ここまででfirebase上でAPIを自作し、firestoreにデータを保存させたり、
firestoreにデータが保存されるのをトリガーに処理を走らせることができるようになりました。

フロントエンドでアプリを作って連携させたり、応用すればそれなりに動くものが作れるようになるはずです。

次回からはいよいよ今回作るタスク管理アプリの概要から実装まで順を追って行なっていきたいと思います。