Cloud Functions for Firebase をつかってみる。ついでにBabelとかTypeScriptのトランスパイルも。

[Cloud Functions for Firebase](https://firebase.google.com/docs/functions/?hl=ja)はいわゆる AWS Lambdaの Firebase 版です。サーバレスで、サーバ側にロジックを構築できるわけですが、[FirebaseやFirestoreが便利すぎる件](https://qiita.com/masatomix/items/5316acdb5980289e5cc2)にくわえて、さらにそれらの処理をトリガーにロジックがうごかせるわけなので、、ちゃんと勉強しておいた方がよさそうです。。



ちなみに、サイトにある[ユースケース](https://firebase.google.com/docs/functions/use-cases?hl=ja)を見ると

- Realtime Database の書き込みをトリガーに、ユーザに通知を投げるとか、
- 画像のアップロードをトリガーに、画像の加工を行うとか
- GitHub webhook APIをつかって、commitのpushをトリガーにSlackに通知を投げる

なんて事ができそうです。HTTPSの受信をトリガーに出来るので、簡単なRESTサーバとかも構築できますね。。


という事で作業メモ。


## やってみる

### Hello World

firebase-toolsのインストール

```bash
$ npm install -g firebase-tools
$ source ~/.bash_profile
$ firebase --version
6.3.0
```

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

```bash
$ mkdir fb_function_samples  && cd $_
```

Firebaseにログインしておく

```bash
$ firebase login
? Allow Firebase to collect anonymous CLI usage and error reporting information?
 No

Visit this URL on any device to log in:
https://accounts.google.com/o/oauth2/auth?client_id=...   // OAuthでアクセスの認可まち

Waiting for authentication...

✔  Success! Logged in as masatomix@example.com
```

プロジェクト作成

```bash
$  firebase init functions

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

You're about to initialize a Firebase project in this directory:
```
質問には基本デフォルトでよさげ

```bash
? Select a default Firebase project for this directory: [create a new project]
? What language would you like to use to write Cloud Functions? JavaScript
? Do you want to use ESLint to catch probable bugs and enforce style? No
? Do you want to install dependencies with npm now? Yes

✔  Firebase initialization complete!

Project creation is only available from the Firebase Console
Please visit https://console.firebase.google.com to create a new project, then run firebase use --add
```

いわれるままに、コマンドを実行(どのFirebase上のプロジェクトにデプロイする？とかを指定してるぽい)

```
$ firebase use --add
? Which project do you want to add? sandbox-xxxxxxx
? What alias do you want to use for this project? (e.g. staging) test_env

Created alias test_env for sandbox-xxxxxxx.
Now using alias test_env (sandbox-xxxxxxx)
```

結果、プロジェクト構成はこんな感じ

```bash
$ pwd
/..../fb_function_samples
$ tree -a
.
├── .firebaserc
├── .gitignore
├── firebase.json
└── functions
    ├── .gitignore
    ├── index.js
    ├── node_modules/.... 割愛
    ├── package-lock.json
    └── package.json
```

さあ index.jsをいじってみます。

```javascript:functions/index.js(以下のように修正)
$ cat 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) => {
 response.send("Hello from Firebase!");
});

exports.echo = functions.https.onRequest((request, response) => {
    const task = request.body
    console.log(JSON.stringify(task))
    // const firestore = admin.firestore()
    // const ref = firestore.collection("todos")
     response.send(JSON.stringify(task));
});
```

HTTPのリクエストの受信をトリガーに動く、メソッド helloworldとechoを作成しました。

サーバへデプロイ

```bash
$ firebase deploy --only functions

=== Deploying to 'sandbox-xxxxxxx'...

i  deploying functions
i  functions: ensuring necessary APIs are enabled...
✔  functions: all necessary APIs are enabled
i  functions: preparing functions directory for uploading...
i  functions: packaged functions (42.39 KB) for uploading
✔  functions: functions folder uploaded successfully
i  functions: creating Node.js 6 function helloWorld(us-central1)...
i  functions: creating Node.js 6 function echo(us-central1)...
✔  functions[echo(us-central1)]: Successful create operation.
Function URL (echo): https://us-central1-sandbox-xxxxxxx.cloudfunctions.net/echo
✔  functions[helloWorld(us-central1)]: Successful create operation.
Function URL (helloWorld): https://us-central1-sandbox-xxxxxxx.cloudfunctions.net/helloWorld

✔  Deploy complete!

Please note that it can take up to 30 seconds for your updated functions to propagate.
Project Console: https://console.firebase.google.com/project/sandbox-xxxxxxx/overview

```

さあ、デプロイが完了したので、 firebase loginしたコンソールで、curlで呼び出してみます

```bash
$ curl https://us-central1-sandbox-xxxxxxx.cloudfunctions.net/helloWorld
Hello from Firebase!

$ cat request.json
{
  "id": "001",
  "name": "こんにちは",
  "isDone": true
}

$ curl -X POST -H "Content-Type:application/json" \
  --data-binary  @request.json \
  https://us-central1-sandbox-xxxxxxx.cloudfunctions.net/echo | jq
{
  "id": "001",
  "name": "こんにちは",
  "isDone": true
}
$
```

よさそうです。


### requireでなくてimportをつかいたい
[vue-cliで vue.jsのアプリを書いている](https://qiita.com/masatomix/items/24142b084b745ce816a9)と、ES2015形式(?)でコードを書いてますよね。外部モジュールを``const functions = require('firebase-functions');`` ではなく `` import * as functions from 'firebase-functions'`` って書きたい、ってヤツです。

- [ES2015のモジュール機能(import/export)](https://qiita.com/ozaki25/items/9723cb3c1c72845157d5)
- [# CommonJS と ES6の import/export で迷うなら](https://qiita.com/rooooomania/items/4c999d93ae745e9d8657)
- [Babel CLI（babelコマンド）の使い方](https://mae.chab.in/archives/2845)
 
ってことで、babelを入れてトランスパイルすることで、ES2015形式でかけるようにしてみます。


#### babel インストール他

```bash
$ cd functions
$ npm install --save-dev babel-cli babel-preset-es2015
$ cd ../
```

firebase.json にpredeployを追加(ココに書いておくと、deploy前に実行されます)

```bash
$ cat firebase.json
{
    "functions": {
      "predeploy": "npm --prefix \"$RESOURCE_DIR\" run build" // buildはこのあと定義します
    }
}
```

package.json にbuildのスクリプトの定義を追加

```json
$ cat  functions/package.json
{
  "name": "functions",
  "description": "Cloud Functions for Firebase",
  "scripts": {
    "build": "mkdir -p dist && ./node_modules/.bin/babel ./*.js --out-file ./dist/index.js",    // buildの定義を追加
    "serve": "firebase serve --only functions",
    "shell": "firebase functions:shell",
    "start": "npm run shell",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log"
  },
  "main": "dist/index.js",  // デプロイするファイルをココで指定
  "engines": {
    "node": "8" // ついでにnodeを8にしてみる
  },
  "dependencies": {
    "firebase-admin": "~6.0.0",
    "firebase-functions": "^2.1.0"
  },
  "private": true,
  "devDependencies": { // これらは自動で追加されたはず
    "babel-cli": "^6.26.0",
    "babel-preset-es2015": "^6.24.1"
  }
}
```

babel設定ファイル作成

```bash
$ cat functions/.babelrc
{
  "presets": [
    [
      "es2015",
      {
        "targets": {
          "node": "current"
        }
      }
    ]
  ]
}
```

ディレクトリ構成は以下のように。

```bash
$ tree -a
.
├── .firebaserc
├── .gitignore
├── firebase.json
└── functions
    ├── .babelrc
    ├── .gitignore
    ├── index.js
    ├── node_modules/.... 割愛
    ├── package-lock.json
    └── package.json
```

デプロイ

```bash
$ firebase deploy --only functions

=== Deploying to 'sandbox-xxxxxxx'...

i  deploying functions
Running command: npm --prefix "$RESOURCE_DIR" run build

> functions@ build /..../fb_function_samples/functions
> mkdir -p dist && ./node_modules/.bin/babel ./*.js --out-file ./dist/index.js

✔  functions: Finished running predeploy script.
i  functions: ensuring necessary APIs are enabled...
✔  functions: all necessary APIs are enabled
i  functions: preparing functions directory for uploading...
i  functions: packaged functions (55.53 KB) for uploading
✔  functions: functions folder uploaded successfully
i  functions: updating Node.js 8 function helloWorld(us-central1)...
i  functions: updating Node.js 8 function echo(us-central1)...
✔  functions[helloWorld(us-central1)]: Successful update operation.
✔  functions[echo(us-central1)]: Successful update operation.

✔  Deploy complete!

Please note that it can take up to 30 seconds for your updated functions to propagate.
Project Console: https://console.firebase.google.com/project/sandbox-xxxxxxx/overview
$
```

ログを見ると、babelのトランスパイルが動いてからデプロイされていますね。。
curlで実行出来ることを確認してください。


#### ちゃんとES2015の文法に書き直して、再ラン

さっきはindex.jsをもとのファイルそのままつかいましたが、以下のようにトランスパイルが必要なコードにしてみます。

```javascript:functions/index.js
import * as functions from 'firebase-functions'

// // Create and Deploy Your First Cloud Functions
// // https://firebase.google.com/docs/functions/write-firebase-functions
//
export const helloWorld = functions.https.onRequest((request, response) => {
  response.send("Hello from Firebase!")
})

export const echo = functions.https.onRequest((request, response) => {
  const task = request.body
  console.log(JSON.stringify(task))
  // const firestore = admin.firestore()
  // const ref = firestore.collection("todos")
  response.send(JSON.stringify(task))
})
```
も一度デプロイしてみましょう。ちなみに dist/index.js をみても、ちゃんと変換されてそうですね。。


### っていうかTypeScriptつかえば？ってことかな

いまいち JavaScriptのES2015とTypeScriptの関係がよく分かってないのですが、ES2015でbabelで前提でコードを書くのと、Gradual Typing なTypeScript で書くのって、構文おなじっぽいですよね？ってことで、そもそも初めのプロジェクト作成で、JavaScriptでなくてTypeScriptを選べばいいんじゃんね？っておもいました。。

ということで、別のTypeScriptプロジェクトを作成します。index.jsは後でつかうのでどっかに取っておきましょう。

### TypeScriptをつかってみる

プロジェクト作成(こんどはTypeScriptをえらびますよ)

```bash
$ mkdir fb_function_samples  && cd $_
$ firebase init functions


? Select a default Firebase project for this directory: [create a new project]
? What language would you like to use to write Cloud Functions? TypeScript    ← TypeScriptをえらぶ
? Do you want to use TSLint to catch probable bugs and enforce style? No      ← いったんNo
? Do you want to install dependencies with npm now? Yes

✔  Firebase initialization complete!

Project creation is only available from the Firebase Console
Please visit https://console.firebase.google.com to create a new project, then run firebase use --add
```

```bash
$ firebase use --add
? Which project do you want to add? sandbox-xxxxxxx
? What alias do you want to use for this project? (e.g. staging) test_env

Created alias test_env for sandbox-xxxxxxx.
Now using alias test_env (sandbox-xxxxxxx)
$
```

ディレクトリ構成

```bash
$ tree -a
.
├── .firebaserc
├── .gitignore
├── firebase.json
└── functions
    ├── .gitignore
    ├── node_modules/.... 割愛
    ├── package-lock.json
    ├── package.json
    ├── src
    │   └── index.ts
    └── tsconfig.json
```

さて、待避しておいた functions/index.js の内容を functions/src/index.ts へ上書きして、firebase deploy します

```bash
$ firebase deploy --only functions

=== Deploying to 'sandbox-xxxxxxx'...

i  deploying functions
Running command: npm --prefix "$RESOURCE_DIR" run build

> functions@ build /..../fb_function_samples_ts/functions
> tsc

✔  functions: Finished running predeploy script.
i  functions: ensuring necessary APIs are enabled...
✔  functions: all necessary APIs are enabled
i  functions: preparing functions directory for uploading...
i  functions: packaged functions (43.74 KB) for uploading
✔  functions: functions folder uploaded successfully
i  functions: updating Node.js 8 function helloWorld(us-central1)...
i  functions: updating Node.js 8 function echo(us-central1)...
✔  functions[helloWorld(us-central1)]: Successful update operation.
✔  functions[echo(us-central1)]: Successful update operation.

✔  Deploy complete!

Please note that it can take up to 30 seconds for your updated functions to propagate.
Project Console: https://console.firebase.google.com/project/sandbox-xxxxxxx/overview
```

さきほどのbabelではpredeployにbabelの処理を手動で追加しましたが、TypeScriptで作成したプロジェクトにはあらかじめ tsc(TypeScriptのトランスパイラ？)が事前に動くように設定されていますね。
(っていうかさっきbabelの処理を入れる際に、TypeScriptのプロジェクトを参考にしたので当たり前なのですが :-))


ちなみにトランスパイルするときのチェック処理などは、functions/tsconfig.json の定義に則って行われるみたいなので、必要に応じて設定ファイルを見直してください。

参考:
http://js.studio-kingdom.com/typescript/project_configuration/tsconfig_json


さて、curlでやってみて、おなじ結果が得られることを確認しましょう。

```bash
$ curl https://us-central1-sandbox-xxxxxxx.cloudfunctions.net/helloWorld
Hello from Firebase!

$ cat request.json
{
  "id": "001",
  "name": "こんにちは",
  "isDone": true
}

$ curl -X POST -H "Content-Type:application/json" \
  --data-binary  @request.json \
  https://us-central1-sandbox-xxxxxxx.cloudfunctions.net/echo | jq
{
  "id": "001",
  "name": "こんにちは",
  "isDone": true
}
$
```



なんだか遠回りした気がしますが、Vue.jsでつかってるJavaScriptとおなじ文法で、Functionsを記述することができるようになりました！


おつかれさまでした。