Cloud functions for Firebase メモ

#Cloud functions for Firebaseとは
Cloud functions for FirebaseはFirebaseの機能やHTTPS要求によってトリガされたイベントに応答して、自動的に用意したバックエンドコードを実行することができます。これらのバックエンドコードはGoogleのクラウドに保存され管理＆自動実行されるので、自分でサーバー管理及び拡張する必要はありません。

##現在サポートしているFirebaseの機能
- Realtime Database Triggers
    - Writes to Realtime Database
- Firebase Authentication Triggers
    - New user authentication
- Google Analytics for Firebase Triggers
    - Analytics conversion events
- Cloud Storage Triggers
    - Uploads to your storage bucket
- HTTP Triggers
    - Incoming HTTPS requests
- Cloud Pub/Sub Triggers

##サポートしている言語
現在はnode.jsのみサポート、今後他の言語もサポートする予定

##環境構築

-  nodeのインストール

```text:terminal
brew install node
```

- Firebase CLIのインストール

```text:terminal
sudo npm install -g firebase-tools
```

- 任意のフォルダを作成

```text:terminal
mkdir functions-firebase
cd functions-firebase
```

- firebase にログイン

```text:terminal
firebase login
```

- プロジェクトの初期化

```text:terminal
firebase init
```

- Functions: Configure and deploy Cloud Functionsを選択
- GCPプロジェクトの選択
- Do you want to install dependencies with npm now? Yesを選択

##デプロイ
index.jsを開いてください。そこにHTTPリクエストのイベントに応答するサンプルコードがコメントアウトされています。そのコメントアウトを解除し、function名を"sayHelloWorld"にしてください。

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

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

exports.sayHelloWorld = functions.https.onRequest((request, response) => {
 response.send("Hello from Firebase!");
});
```

```text:terminal
firebase deploy
```

デプロイが終わるとfunction URLが返ってくるので、ブラウザでアクセスしてください。

```text:terminal
Function URL (sayHelloWorld): https://us-central1.../satHelloWorld
```

ブラウザに"Hello from Firebase!"が表示されればデプロイ成功です。

##Firebaseコンソールにログを出してみる

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

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

exports.sayHelloWorld = functions.https.onRequest((request, response) => {
 console.log("Hello from Firebase!");   
 response.send("Hello from Firebase!");
});
```

```text:terminal
firebase deploy
```

またブラウザでFunction URLにアクセスしたらFirebaseコンソールのログを見てみます。

```
Firebaseコンソール > functions > ログ　
```

"Hello from Firebase!" が表示されれば成功です。

##Authentication Trigger 
Authentication Triggerではユーザーが作成されたときや削除された時のイベントを取ることができます。こちらのTriggerを利用すればユーザーが会員登録をした際に自動返信メールを出すことができます。

###ユーザーが作成された時
新しいユーザーが作成されたらデータベースにUserデータを作成し、デフォルトのユーザーアイコンの画像URLを挿入する。

```js:index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase)

const ref = admin.database().ref()

//Authentication Trigger
exports.createUserAccount = functions.auth.user().onCreate(event => {
    const uid = event.data.uid
    const email =  event.data.email
    const photoUrl = event.data.photoUrl || 
    'photo_url'
    const newUserRef = ref.child(`/users/${uid}`)
    return newUserRef.set({
        photoUrl: photoUrl,
        email: email
    })
})
```

###ユーザーが削除された時
ユーザーが削除されたら削除済みフラグ("isDeleted: true")をユーザーデータに自動的にセットさせる

```js:index.js
exports.cleanupUserData = functions.auth.user().onDelete(event => {
    const uid = event.data.uid
    const userRef = ref.child(`/users/${uid}`)
    return userRef.update({isDeleted: true})
})
```

##Realtime Database Trigger
Realtime Database Triggerではデータベースの書き込みなどのイベントを取得することが出来ます。

###regionをJapanに書き換えするサンプル

```js:index.js
//Realtime Database Trigger
exports.changePost = functions.database
    .ref('/posts/{pushId}')
    .onWrite(event => {
        const post = event.data.val()
        if (post.chnaged) {
            return
        }
        post.chnaged = true
        post.region = "Japan"
        return event.data.ref.set(post)
})
```
fibreaseコンソールで/posts/{pushId}のregionに適当な値をいれて、データベース見ましょう。自動的にJapanに置き換わります。

##Cloud Storage Trigger
CloudStorageにファイルが追加された時のイベントを取得することができます。下記のサンプルコードは"/image/"ディレクトリに画像が追加された時、自動的にサムネイル画像を生成します。

###下準備
package.jsonがあるディレクトリで以下のコマンドを実行し必要なライブラリを導入します。

```text:terminal
npm install --save @google_cloud/storage
npm install --save child-process-promise
```

package.jsonにこれらが入っていれば成功です。

```json:package.json
"dependencies": {
    "@google-cloud/storage": "^1.2.0",
    "child-process-promise": "^2.2.1"
}
```

###画像が追加されたら自動的にサムネイルを生成するコード

```js:index.js
const functions = require('firebase-functions')
const gcs = require('@google-cloud/storage')();
const spawn = require('child-process-promise').spawn

//Cloud Storage Trigger
exports.generateThumbnail = functions.storage.object()
    .onChange(event => {
        const object = event.data
        const filePath = object.name
        const fileName = filePath.split('/').pop()
        const fileBucket = object.bucket
        const bucket = gcs.bucket(fileBucket)
        const tempFilePath = `/tmp/${fileName}`

        if (fileName.startsWith('thumb_')){
            console.log('Already a Thumbnail.')
            return 
        }

        if(!object.contentType.startsWith('image/')){
            console.log('This is not an image.')
        }

        if(object.resourceState === 'not_exists'){
            console.log('This is a deletion event.')
        }

        return bucket.file(filePath).download({
            destination: tempFilePath
        }).then(() => {
            console.log('Image downloaded locally to', tempFilePath)
            return spawn('convert', [tempFilePath, '-thumbnail', '200x200>',
                tempFilePath])
        }).then(() => {
            console.log('Thumbnail created')
            const thumbFilePath = filePath.replace(/(\/)?([^\/]*)$/,
            '$1thumb_$2')
            
            return bucket.upload(tempFilePath, {
                destination: thumbFilePath
            })
        })
    })
```

###さらに応用
サムネイル画像が追加されたらRealtime Databaseに画像のURLを追加します。

###下準備
Firebaseコンソールの歯車アイコン＞プロジェクトの設定＞サービスアカウント＞新しい秘密鍵の生成をクリックしjsonファイルを落としindex.jsと同じディレクトリにいれます。

###画像のURLをRealtime Databaseに保存するサンプルコード

```js:index.js
const functions = require('firebase-functions')
const gcs = require('@google-cloud/storage')({keyFilename: 
    'Firebaseコンソールでダウンロードしたjsonファイル.json'})
const spawn = require('child-process-promise').spawn
const admin = require('firebase-admin')
admin.initializeApp(functions.config().firebase)

//Cloud Storage Trigger
exports.generateThumbnail = functions.storage.object()
    .onChange(event => {
        const object = event.data
        const filePath = object.name
        const fileName = filePath.split('/').pop()
        const fileBucket = object.bucket
        const bucket = gcs.bucket(fileBucket)
        const tempFilePath = `/tmp/${fileName}`
        const ref = admin.database().ref()
        const file = bucket.file(filePath)
        const thumbFilePath = filePath.replace(/(\/)?([^\/]*)$/,
            '$1thumb_$2')

        if (fileName.startsWith('thumb_')){
            console.log('Already a Thumbnail.')
            return 
        }

        if(!object.contentType.startsWith('image/')){
            console.log('This is not an image.')
        }

        if(object.resourceState === 'not_exists'){
            console.log('This is a deletion event.')
        }

        return bucket.file(filePath).download({
            destination: tempFilePath
        }).then(() => {
            console.log('Image downloaded locally to', tempFilePath)
            return spawn('convert', [tempFilePath, '-thumbnail', '200x200>',
                tempFilePath])
        }).then(() => {
            console.log('Thumbnail created')
                        
            return bucket.upload(tempFilePath, {
                destination: thumbFilePath
            })
        }).then(() => {
            const config = {
                action: 'read',
                expires: '03-09-2491'
            }
            const thumbFile = bucket.file(thumbFilePath)
            return Promise.all([
                thumbFile.getSignedUrl(config),
                file.getSignedUrl(config)
            ])
        }).then(results => {
            const thumbResult = results[0]
            const originalResult = results[1]
            const thumbFileUrl = thumbResult[0]
            const fileUrl = originalResult[0]
            return ref.child('posts').push({path: fileUrl, thumbnail: thumbFileUrl})
        })
    })
```