Firebase Functionsを関数ごとにファイル分割 with TypeScript

正解っぽい方法を見つけたので[追記](https://qiita.com/nekomimi-daimao/items/eea046dca2ab669a7fa9#追記)しました。（2019/01/25）

---

[Udemy](https://www.udemy.com)の正月セールであれもこれもと買っていたらクレジットカードにとんでもねえ請求[^1]が来たので憂さ晴らしに書きます。

## 目的
[Firebase Functionsで関数ごとにファイルを分割し高速化とメンテナンス性向上も目指す]
(https://uyamazak.hatenablog.com/entry/2018/10/22/113000)

これがやりたい。
しかしながら動的型付け言語イヤなので静的型付け言語でやりたい。つまりTypeScriptでやりたい。

## コード

サンプルをコピペして愚直に書いてみます。全部同じディレクトリに置いておきます。
ランタイムは`Node.js 6`です。

```typescript:index.ts
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';

admin.initializeApp();

const funcs = {
    fromHttps: './fromHttps',
    fromDb: './fromDb'
};

const loadFunctions = (funcsObj) => {
    console.log('loadFunctions ' + process.env.FUNCTION_NAME);
    for (const name in funcsObj) {
        if (!process.env.FUNCTION_NAME || process.env.FUNCTION_NAME === name) {
            exports[name] = require(funcsObj[name])
        }
    }
};

loadFunctions(funcs);

export const inIndex = functions.https.onRequest((request, response) => {
    console.log('inIndex');
    response.status(200).send('inIndex');
});

console.log('index loaded');

```

```typescript:fromDb.ts
import * as functions from 'firebase-functions';

export const fromDb = functions.database.ref('/onamae/{name}/name')
    .onCreate((snapshot: functions.database.DataSnapshot, context: functions.EventContext) => {
        const name: string = snapshot.val();
        console.log('fromDb' + name);
        return snapshot.ref.parent.child('uppercase').set(name.toUpperCase());
    });

console.log('fromDb loaded');
```

```typescript:fromHttps.ts
import * as functions from 'firebase-functions';

export const fromHttps = functions.https.onRequest((request, response) => {
    const name: string = request.body.name;
    console.log('fromHttps' + name);
    response.status(200).send(name + '!');
});

console.log('fromHttps loaded');
```

各ファイルに計3つの関数を定義しました。

| function | file | trigger |
|:-:|:-:|:-:|
| inIndex | index.ts | https |
| fromHttps | fromHttps.ts | https |
| fromDb | fromDb.ts | RealtimeDatabase |

## デプロイその１
とりあえずデプロイ。
`npm run deploy`

```terminal
> functions@ deploy /Users/●●●/Firebase/testBed/functions
> firebase deploy --only functions


=== Deploying to '●●●'...

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

> functions@ lint /Users/●●●/Firebase/testBed/functions
> tslint --project tsconfig.json

no-unused-variable is deprecated. Since TypeScript 2.9. Please use the built-in compiler checks instead.
Running command: npm --prefix "$RESOURCE_DIR" run build

> functions@ build /Users/●●●/Firebase/testBed/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 (15.37 KB) for uploading
✔  functions: functions folder uploaded successfully
i  functions: creating Node.js 6 function fromHttps-fromHttps(us-central1)...
i  functions: creating Node.js 6 function fromDb-fromDb(us-central1)...
i  functions: creating Node.js 6 function inIndex(us-central1)...
⚠  functions[fromHttps-fromHttps(us-central1)]: Deployment error.
Function failed on loading user code. Error message: Node.js module defined by file lib/index.js is expected to export function named fromHttps.fromHttps
⚠  functions[fromDb-fromDb(us-central1)]: Deployment error.
Function failed on loading user code. Error message: Node.js module defined by file lib/index.js is expected to export function named fromDb.fromDb
✔  functions[inIndex(us-central1)]: Successful create operation. 
Function URL (inIndex): https://[trigger url]/inIndex


Functions deploy had errors with the following functions:
	fromDb-fromDb
	fromHttps-fromHttps


To try redeploying those functions, run:
    firebase deploy --only functions:fromDb-fromDb,functions:fromHttps-fromHttps


To continue deploying other features (such as database), run:
    firebase deploy --except functions

Error: Functions did not deploy properly.

npm ERR! Darwin 18.2.0
npm ERR! argv "/Users/●●●/.nodebrew/node/v6.11.5/bin/node" "/Users/●●●/.nodebrew/current/bin/npm" "run" "deploy"
npm ERR! node v6.11.5
npm ERR! npm  v3.10.10
npm ERR! code ELIFECYCLE
npm ERR! functions@ deploy: `firebase deploy --only functions`
npm ERR! Exit status 1
npm ERR! 
npm ERR! Failed at the functions@ deploy script 'firebase deploy --only functions'.
npm ERR! Make sure you have the latest version of node.js and npm installed.
npm ERR! If you do, this is most likely a problem with the functions package,
npm ERR! not with npm itself.
npm ERR! Tell the author that this fails on your system:
npm ERR!     firebase deploy --only functions
npm ERR! You can get information on how to open an issue for this project with:
npm ERR!     npm bugs functions
npm ERR! Or if that isn't available, you can get their info via:
npm ERR!     npm owner ls functions
npm ERR! There is likely additional logging output above.

npm ERR! Please include the following file with any support request:
npm ERR!     /Users/●●●/Firebase/testBed/functions/npm-debug.log
```

**死んだ！**
<img width="1331" alt="スクリーンショット 2019-01-22 23.43.01.png" src="https://qiita-image-store.s3.amazonaws.com/0/146245/3be139d7-e621-08be-cc9a-49570a1c1222.png">
**でもなんかいる……**

関数の横のハンバーガーメニューから「使用状況の詳細な統計情報」を押すことでGoogle Cloud PlatformのCloud Functionに移動することができます。
<img width="903" alt="スクリーンショット 2019-01-22 23.48.20.png" src="https://qiita-image-store.s3.amazonaws.com/0/146245/a11799e6-01bf-dfca-e2b2-2305519d913b.png">
**やはり死んでいる**

## 関数テスト・その１
そうはいうものの生きてるかもしれないので関数をテストしてみます。

### inIndex
```terminal
curl https://[trigger url]/inIndex

---

inIndex
```
生きてる。

### fromHttps

```terminal
curl https://[trigger url]/fromHttps-fromHttps -X POST -H "Content-Type: application/json" -d '{"name": "whoseyards"}'

---

Error: could not handle the request
```
死んでる。

### fromDb
<img width="221" alt="スクリーンショット 2019-01-23 0.23.50.png" src="https://qiita-image-store.s3.amazonaws.com/0/146245/af931c28-ad10-4157-b2e5-d7d6df813e3c.png">
しばらく待ってもなにも起こらず。死んでる。

## 中間考察
見事に他ファイルに分割した関数だけ死んで、` index.ts`に記載した関数だけが生き残りました。
やはり`JavaScript`でやるしかない……？
ここでfirebase cloud functionsのログを見ます。別ファイルに分割した関数名だけが*繰り返されている*のが目に入ります。

<img width="1243" alt="スクリーンショット 2019-01-23 0.08.37.png" src="https://qiita-image-store.s3.amazonaws.com/0/146245/33033ac5-62db-4454-b621-1bb34b20aa9a.png">


## デプロイその２
とりあえず前方一致にしてみる……。

```typescript:index.ts
const loadFunctions = (funcsObj) => {
    console.log('loadFunctions ' + process.env.FUNCTION_NAME);
    for (const name in funcsObj) {
        // 全文じゃなくて前方一致にする
        // if (!process.env.FUNCTION_NAME || process.env.FUNCTION_NAME === name) {
        if (!process.env.FUNCTION_NAME || process.env.FUNCTION_NAME.startsWith(name)) {
            exports[name] = require(funcsObj[name])
        }
    }
};
```

`npm run deploy`

```terminal
> functions@ deploy /Users/●●●/Firebase/testBed/functions
> firebase deploy --only functions


=== Deploying to '●●●'...

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

> functions@ lint /Users/●●●/Firebase/testBed/functions
> tslint --project tsconfig.json

no-unused-variable is deprecated. Since TypeScript 2.9. Please use the built-in compiler checks instead.
Running command: npm --prefix "$RESOURCE_DIR" run build

> functions@ build /Users/●●●/Firebase/testBed/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 (15.89 KB) for uploading
✔  functions: functions folder uploaded successfully
i  functions: updating Node.js 6 function fromHttps-fromHttps(us-central1)...
i  functions: updating Node.js 6 function fromDb-fromDb(us-central1)...
i  functions: updating Node.js 6 function inIndex(us-central1)...
✔  functions[fromDb-fromDb(us-central1)]: Successful update operation. 
✔  functions[inIndex(us-central1)]: Successful update operation. 
✔  functions[fromHttps-fromHttps(us-central1)]: Successful update operation. 

✔  Deploy complete!
```

デプロイ成功。

## 関数テスト・その２

### inIndex
```terminal
curl https://[trigger url]/inIndex

---

inIndex
```
引き続き生きてる。

### fromHttps

```terminal
curl https://[trigger url]/fromHttps-fromHttps -X POST -H "Content-Type: application/json" -d '{"name": "whoseyards"}'

---

whoseyards!
```
生きてる。

### fromDb
<img width="293" alt="スクリーンショット 2019-01-23 0.24.16.png" src="https://qiita-image-store.s3.amazonaws.com/0/146245/e0def64a-e1e9-571f-54d5-70f27f9ed787.png">
変化中のスクリーンショットが取れなかったんですが、生きてます。

## 結論
`process.env.FUNCTION_NAME`で実行される関数名が取得できるのでそれを利用して読み込むファイルを限定する、というのが[元記事](https://uyamazak.hatenablog.com/entry/2018/10/22/113000)と[元々記事](https://tech.ginco.io/post/ginco-engineer-meetup-2018-cloud-functions/)の要点なわけですが、**なんか-増えてる**のでうまくいかなかったようです。
`process.env.FUNCTION_NAME = 関数名-関数名`
前方一致だけだと関数名の前方が同じ場合に死ぬので、後方一致も条件に含めておいたほうがより安全です。
ちなみに別ファイルに分割した関数に対しての個別関数デプロイも通ります。
`firebase deploy --only functions:fromHttps`

## 感想
**で、なんでふえてるの……？**
Firebase・GCP・Node.js・TypeScript・JavaScriptのどれもよくわからんのでどこに原因があるかよくわからない！　詳しい人書いといて！

TypeScriptで書かれた実戦に投入できるレベルのCloudFunctionのサンプルが見つからないのですが、やっぱり主流はJavaScriptなんでしょうか。
Firebaseを重点的に勉強中なのですが、詳しいことを学ぼうとすると[Firebase公式Youtubeチャンネル](https://www.youtube.com/user/Firebase)を見る羽目になります。重要なこと喋ってるのに再生数少ないのはそういうこと[^2]なんだなって……。

個人的な願望なのですが、動的型付け言語だとIDEとかコンパイラが面倒見てくれなくてめんどくさいので、静的型付け言語でCloudFunction書きたいです。Gradual Typingじゃなくてガチガチの静的型付け言語のやつ。Kotlinとか。PythonとGoがβらしいので、βが取れたら頑張ってGoを覚えようかと思っています。

このまま進めば`firebase-admin`とか`firebase-function`をTypeScriptでglobalに格納する話……になる予定です。正直`namespace`がよくわからないすぎるので、いつになるかわかりませんが。

おしまい。

## 参考
[Firebase Functionsで関数ごとにファイルを分割し高速化とメンテナンス性向上も目指す]
(https://uyamazak.hatenablog.com/entry/2018/10/22/113000)
[GincoにおけるCloud Functionsの利用とその高速化](https://tech.ginco.io/post/ginco-engineer-meetup-2018-cloud-functions/)
[TypeScriptで複数ファイル構成する2つの方法](https://teppeis.hatenablog.com/entry/2014/05/typescript-external-modules)

## 追記
> なぜ関数名が上記のようになってしまっているかというと、require(funcObj["fromHttps"]) で { fromHttps: <fromHttpsの関数本体> } のようなオブジェクトが返るので、
> exports.fromHttps = { fromHttps: <fromHttpsの関数本体> }
のようにexportされてしまうためです。

いただいたコメントを読んで
> exports.fromHttps = { fromHttps: \<fromHttpsの関数本体> }

ということはつまりその関数本体をexportsとかいうやつにそのままぶちこめばなんかうまくいくのでは？
と思って以下の変更を加えたら期待通りの動作になりました。[^3]

`export` → `module.exports`

```typescript:fromHttps.ts
import * as functions from 'firebase-functions';

// before
// export const fromHttps = functions.https.onRequest((request, response) => {
// after
module.exports = functions.https.onRequest((request, response) => {
    const name: string = request.body.name;
    console.log('fromHttps' + name);
    response.status(200).send(name + '!');
});

// module.exportsから公開されるのは1ファイル内で1つだけ
// = 1関数1ファイルが強制される！ はず！

console.log('fromHttps loaded');
```

<img width="268" alt="スクリーンショット 2019-01-25 1.37.29.png" src="https://qiita-image-store.s3.amazonaws.com/0/146245/676d67bf-679d-58b8-9e67-334505031c84.png">
関数名も繰り返してない。よろしい。

## 追記の参考
[Node.jsのexportsとmodule.exportsの違いについてのメモ](https://qiita.com/toshihirock/items/e98363a4c99950be5abc)


[^1]: やばい。
[^2]: 「わからないことがあったらstackoverflowに聞いてくれ！　あそこは素晴らしいサイトだよ！」とか朗らかに言っててつらい
[^3]: なんでかはよくわからん。