Firebase Cloud Functions by Kotlin/JS 2021

Kotlin code を Kotlin/JS で JavaScript に transpile して Firebase Cloud Functions に deploy するまでの軌跡です。


# 環境
この記事は次の環境で実行した情報を記載しています。

* Apple M1
* macOS BigSur 11.6


Firebase Project については作成済みであることを前提としています。


# Set up
IntelliJ IDEA を使って Kotlin/JS を setup します。

## Firebase init
任意の場所に `firebase init` で local に Firebase Project を setup します。

```zsh
% firebase init

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

You're about to initialize a Firebase project in this directory:

  /path/to/firebase-project

? Which Firebase features do you want to set up for this directory? Press Space 
to select features, then Enter to confirm your choices. Functions: Configure a C
loud Functions directory and its files

=== Project Setup

First, let's associate this project directory with a Firebase project.
You can create multiple project aliases by running firebase use --add, 
but for now we'll just set up a default project.

? Please select an option: Use an existing project
? Select a default Firebase project for this directory: your-project-id (Your Project Name)
i  Using project your-project-id (Your Project Name)

=== Functions Setup

A functions directory will be created in your project with sample code
pre-configured. Functions can be deployed with firebase deploy.

? 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
✔  Wrote functions/package.json
✔  Wrote functions/index.js
✔  Wrote functions/.gitignore
? Do you want to install dependencies with npm now? No

i  Writing configuration info to firebase.json...
i  Writing project information to .firebaserc...
i  Writing gitignore file to .gitignore...

✔  Firebase initialization complete!
```

`firebase init` 直後は次の状態になっています。

```zsh
% ls -aR 
.		..		.DS_Store	.firebaserc	.gitignore	firebase.json	functions

./functions:
.		..		.gitignore	index.js	package.json
```

※ `functions/` 下の file は一旦不要なので全て削除しておきます。


## Install IntelliJ IDEA
Kotlin/JS の開発をするために JetBrains 社の web site から IntelliJ IDEA Community を [download](https://www.jetbrains.com/ja-jp/idea/download/#section=mac) します。 
PC の architecture 毎に dmg が用意されているため、適宜選択して download して install してください。

https://www.jetbrains.com/ja-jp/idea/download/#section=mac


## New Project
Install した IntelliJ IDEA を開き、
Menu > File > New > Project を選択します。

* 左の一覧から `Kotlin` を選択します。
* Location を `/path/to/firebase-project/functions` に設定します。
* Project Template から `Node.JS Application` を選択します。
* Project JDK を 16 に設定します。 (17 だと test 実行時に error が発生しました)

![スクリーンショット 2021-12-11 21.06.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95517/0083dba4-893c-3fd5-606f-462c3b075395.png)


* Kotlin/JS Compiler を IR に設定します。
* お好みで Use experimental Node.js API (kotlinx-nodejs) を有効にしてください。

![スクリーンショット 2021-12-11 20.32.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95517/fbe9c8e3-d1d6-c2e1-0d67-6d04c755a3da.png)

https://kotlinlang.org/docs/js-ir-compiler.html


Project を作成すると、 `src/` 配下に `Main.kt` や `GreetingTest.kt` が sample として用意されています。

![スクリーンショット 2021-12-11 20.38.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95517/defe3c7a-44ea-cc3e-39f2-31551e2dbc85.png)


## Build
Menu > Build > Build Project もしくは `./gradrew build` で build すると、
`functions/build/js` に workspace として出力されます。
Kotlin で実装した module は `functions/build/js/packages/{module name}/` に出力されます。

![スクリーンショット 2021-12-12 10.53.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95517/7a1a043a-f878-780b-ebd4-0796a86a0144.png)

Workspaces について

https://classic.yarnpkg.com/lang/en/docs/workspaces/

# Build Configuration
Kotlin の build を構成します。

## firebase.json
functions の source は default が `functions/` なので、
出力した `functions/build/js/packages/{module name}/` に変更します。
また、 `runtime` を設定しないと deploy で error が出るので node version を指定します。

```firebase.json
{
  "functions": {
    "source": "functions/build/js/packages/{module name}/",
    "runtime": "nodejs14"
  }
}
```


## Dependencies
利用する Library を Project に設定します。
`serialization` や `firebase-admin` についてはお好みでどうぞ。

```build.gradle.kts
plugins {
    kotlin("js") version "1.6.0"
    kotlin("plugin.serialization") version "1.5.31"
}

...

dependencies {
    // JS, Basic Libraries
    implementation(kotlin("stdlib-js", "1.6.0"))
    implementation("org.jetbrains.kotlinx", "kotlinx-nodejs", "0.0.7") // Experimental
    implementation("org.jetbrains.kotlinx", "kotlinx-coroutines-core-js", "1.6.0-RC")
    implementation("org.jetbrains.kotlinx", "kotlinx-serialization-json", "1.3.1")

    // Firebase
    implementation(npm("firebase-admin", "^10.0.0"))
    implementation(npm("firebase-functions", "^3.16.0"))

    // Test
    testImplementation(kotlin("test", "1.6.0"))
    testImplementation(npm("firebase-functions-test", "^0.3.3"))
}
```


# Implementation
準備が整ったところで Kotlin で Functions の実体を実装していきます。

Kotlin で実装するとはいえ、 JS の資産 (npm) を使うことになるため、 Kotlin と JS のつなぎこみが必要になります。
JS の code は `js` 関数で Kotlin に埋め込むことができます。

```Main.kt
// firebase-functions module を JS code で require します。
private val functions = js("require(\"firebase-functions\")")

val log = js("require(\"firebase-functions/v1\").logger")
val structuredData = json("structuredData" to true)
```

また、外部で定義済みの変数などは `external` 修飾子で宣言することができます。

```Main.kt
// Functions の exports が定義されていることを宣言します。
private external val exports: dynamic
```

onCall は次のような signature です。

```Main.kt
suspend fun greeting(data: Json, context: Json): Json {
    log.debug("greeting")
    log.debug(data, structuredData)
    return json(
        "result" to "Hello, ${data["greeting"]}",
    )
}
```

main 関数で export します。

```Main.kt
fun main() {
    log.debug("main")
    exports.greeting = functions.https.onCall(::greeting)
}
```

https://firebase.google.com/docs/functions/callable?hl=ja#write_and_deploy_the_callable_function


Build して Firebase Local Emulator を起動するか、 Firebase に deploy すると、
`greeting` が反映されて呼び出せるようになります。

```zsh
% curl -i http://{host}/{your-project-id}/us-central1/greeting \
   --header "Content-Type: application/json" \
   -d '{"data":{"greeting":"Firebase!"}}'

HTTP/1.1 200 OK
x-powered-by: Express
vary: Origin
content-type: application/json; charset=utf-8
content-length: 40
date: Sun, 12 Dec 2021 03:50:44 GMT
connection: close

{"result":{"result":"Hello, Firebase!"}}
```


## HttpsError
Coming soon


# Test
Coming soon


# Repository
Coming soon


# 参考
次の情報を参考にさせていただきました。 🙏

## Firebase

https://firebase.google.com/docs/functions/callable?hl=ja

https://firebase.google.com/docs/functions/unit-testing?hl=ja


https://firebase.google.com/docs/reference/functions/common_providers_https.callablecontext

https://firebase.google.com/docs/reference/functions/common_providers_https.authdata

https://firebase.google.com/docs/reference/admin/node/firebase-admin.app

https://firebase.google.com/docs/reference/node/firebase.functions.HttpsError

https://firebase.google.com/docs/reference/android/com/google/firebase/functions/FirebaseFunctionsException.Code


## Kotlin/JS

https://kotlinlang.org/docs/js-project-setup.html

https://kotlinlang.org/docs/js-modules.html

https://kotlinlang.org/docs/js-running-tests.html


## Kotlin Coroutine

https://developer.android.com/kotlin/coroutines/coroutines-adv?hl=ja

https://qiita.com/kawmra/items/ee4acb7db61f70dec9a8

https://blog.jetbrains.com/ja/kotlin/2021/06/kotlin-coroutines-1-5-0-released-ja/#globalscope

https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/

https://www.seeds-std.co.jp/blog/creators/2019-10-16-130000/

https://qiita.com/ikemura23/items/fb8caeba4c35fcd85644

https://qiita.com/kaleidot725/items/9b91c8f3817d98b60076