ユーザーに見えない処理はすべてCloud Functionsに任せよう

## TL;DR
Firebaseでのアプリ開発において、Viewに影響しない処理はCloud Functionsに投げてしまおうという話です。
Storageのお掃除や、Authenticationの設定など。

## なぜCloud Functionsを使うのか？
本来、**クライアントは画面や操作に影響する処理のみを扱うべき**だと思います。

そうではない処理はサーバーに任せてしまった方が良き。

（ユーザー数の多いアプリケーションの場合、サーバーへの負荷分散のためあえてクライアント側に処理を任せることもあると思いますが。）

Cloud Functionsはデータベースやストレージに変更があった際など、**イベントをトリガーに実行できるので「後でこれやっておいて〜」**という処理を実装するのに便利です。

弊社ではザックリ以下のように分類しています。

 - すぐにレスポンスが欲しい → Swiftで実装
 - 後から処理してくれれば良い → Cloud Functionsで実装

UI/UXの観点からどちらで実装するか選べば良いかなと思います。

## Cloud Functionsを使う準備
※Firebaseを使ったことがある方向けに端折って書いています。

1. `firebase init`する際に`Functions`を選択、`JavaScript`か`TypeScript`を選ぶ
2. `firebase init`完了後に作成された`functions`フォルダ内で`npm install -S firebase-functions firebase-admin`


```typescript:functions/src/index.ts
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp(); // 最近、optionsを指定する必要がなくなりましたね
```

## Cloud Functionsでよく実装する機能たち
いくつか例をあげていきます。

### 特定ユーザーにAdmin権限の付与

```typescript:functions/src/index.ts
exports.addAdminClaim = functions.database.ref('admin/{adminId}')
    .onCreate((snap: any, context: any) => {
        const uid = context.params.adminId;
        modifyAdmin(uid, true);
    });

const modifyAdmin = (uid: string, isAdmin: boolean) => {
    admin.auth().setCustomUserClaims(uid, {admin: isAdmin}).then(() => {
        console.log(`Successfully changed [${uid}]'s authentication.`)
    }).catch((error) => {
        console.error(`Failed to change [${uid}]'s authentication. `, error)
    })
};
```

Firebase Authで作成したユーザーのUIDを、adminモデルに追加した時（onCreate）、カスタム権限を設定して管理者扱いにできます。

### 動画アップロード時にサムネイルを自動生成
この記事がめちゃくちゃ参考になりました。

[Firebase Storage に動画ファイルがアップロードされたら Cloud Functions で自動的にサムネイルを作成する](https://qiita.com/imamurh/items/b8586a4c6a4b0084b555)

### 画像アップロード時にキャッシュを設定

```typescript:functions/src/index.ts
exports.setMetadata = functions.storage.object().onFinalize((object: any) => {
    const filePath = object.name;
    const { contentType } = object;
    const fileRef = bucket.file(filePath);
    const newMetadata = {
        cacheControl: 'public,max-age=86400',
    };

    // ここでcontentTypeに応じてメタデータを変えてみたりとか
    if (contentType.startsWith('image/')) {
        fileRef.setMetadata(newMetadata).then(() => {
            console.log('Set Metadata'); // eslint-disable-line
            return true;
        }).catch((err) => {
            console.log(err); // eslint-disable-line
            return false;
        });
    }
});
```

画像ファイルにキャッシュを設定して次回の表示を高速化できます。

サンプルコードは86400秒＝24時間で設定してみました。
（要件に応じてチューニングしてください）

### データ削除時にひもづく画像/動画/ファイルをCloud Storageから削除

```typescript:functions/src/index.ts
exports.fileDelete = functions.database.ref('hoge/${id}')
    .onDelete((snap: any, context: any) => {
        // idと削除対象のファイル名を同一にしている場合
        const id = context.params.id;
        // fugaフォルダのidと同じ名前のjpegファイルを削除
        return admin.storage().bucket().file(`/fuga/${id}.jpeg`).delete();
    });
```

detabaseのデータ削除をトリガーに、データIDと同じファイルをCloud Storageから削除します。
クライアント側ではDBのデータ削除のみ書けばOKです。

## Cloud Functionsのまとめ
「何かを削除した時に一緒にこれも消したい」とか「何かを作った時に、設定値を追加しておきたい」といった処理をさせるには便利だな〜という印象です。

デバイスにPush通知を送るときにCloud Functionsは必須なので、iOS/Androidエンジニアの人もある程度かけるようになるとハッピーですね。