0
0

Kotlin MultipatformとFirestoreでNode.jsアプリ

Last updated at Posted at 2024-07-09

概要/日記

Kotlin Multiplatform (for Javascript)と firebase-kotlin-SDK を使用し Node.js アプリを作る。
サンプルアプリはDB上の要求リストがあれば即座に応じてOSコマンドを実行しその結果を格納する。

これの続き:

前提

  • Ubuntu 22.04 on Windwos 11 wsl2
  • Firebaseプロジェクト・Firestoreが有効であること
  • 認証無し。実験用

Node.js用プロジェクト構成

KtNodeSvr/build.gradle.kts

plugins {
    kotlin("multiplatform") version "2.0.0"
    kotlin("plugin.serialization") version "2.0.0"
}

repositories {
    mavenCentral()
}

kotlin {
    js {
        nodejs { }
        binaries.executable()
    }

    sourceSets {
        jsMain.dependencies {
            implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") // https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core
            implementation("dev.gitlive:firebase-firestore:1.13.0") // https://mvnrepository.com/artifact/dev.gitlive/firebase-firestore
            implementation(npm("child_process", "1.0.2"))
            implementation(project(":shared"))
        }
    }
}
settings.gradle.kts
rootProject.name = "KtMpApp"

include(":KtNodeSvr")
include(":KtHTMLFirestoreApp")
include(":shared")
KtNodeSvr/src/jsMain/kotlin/KtNodeSvr.kt
import dev.gitlive.firebase.Firebase
import dev.gitlive.firebase.FirebaseOptions
import dev.gitlive.firebase.firestore.*
import dev.gitlive.firebase.initialize
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine


external val process: dynamic

val options = FirebaseOptions(
    apiKey = process.env.APPKEY as String,
    projectId = "xxxxxx",
    databaseUrl = "https://xxxxxxxxxxxxx.firebaseio.com",
    applicationId = "1:xxxxxxxxxxx:web:xxxxxxxxxxxxxxxxxxxx",
)

val app = Firebase.initialize(Unit, options)
val db = Firebase.firestore(app).apply {
    settings = firestoreSettings(settings) { cacheSettings = memoryCacheSettings { } }
}

external fun require(module: String): dynamic

suspend fun main() = runCatching {
    val args = (process.argv as Array<String>).drop(2)
    val (tg, pw) = args
    val refRqs = db.collection("fireshell").document(tg).collection("requests")

    refRqs.orderBy("time", Direction.ASCENDING).where { "isComplete" notEqualTo true }.limit(10).snapshots.collect {
        it.documents.forEach { ds ->
            val req = ds.data<Request>()
            runCatching {
                println("Run: ${req.cmd}")
                val res = spawn(req.cmd)
                ds.reference.set<Request>(req.copy(isComplete = true, result = res))
            }.onFailure {
                ds.reference.set<Request>(req.copy(isComplete = true, exception = it.message))
            }
        }
    }
}.onFailure { println(it.stackTraceToString()) }.getOrElse { }

suspend fun spawn(cmdLine: String) = suspendCoroutine { cont ->
    var r = 0
    val child_process = require("child_process")
    val (cmd, args) = cmdLine.split(" ").let { it.first() to it.drop(1).toTypedArray() }
    val ls = child_process.spawn(cmd, args)
    val stdout = mutableListOf<String>()
    val stderr = mutableListOf<String>()
    ls.stdout.on("data") { data -> stdout.add("$data") }
    ls.stderr.on("data", { data -> stderr.add("$data") })
    ls.on("close") { c -> if (r++ == 0) cont.resume(SpawnResult(c, stdout.joinToString(), stderr.joinToString())) }
    ls.on("error") { err -> if (r++ == 0) cont.resumeWithException(Exception("Error: spawn($cmdLine):${err}")) }
}
shared/src/jsMain/kotlin/DataType.kt
import dev.gitlive.firebase.firestore.Timestamp
import dev.gitlive.firebase.firestore.Timestamp.Companion.now
import kotlinx.serialization.Serializable

@Serializable
data class SpawnResult(
    val exitCode: Int,
    val stdout: String,
    val stderr: String,
)

@Serializable
data class Request(
    val cmd: String,
    val time: Timestamp = now(),
    val isComplete: Boolean = false,
    val result: SpawnResult? = null,
    val exception: String? = null,
)

ソースコード

Firestore側の設定

Firestore → ルール
service cloud.firestore {
  match /databases/{db}/documents/fireshell/{document=**} {
    allow read, write: 
        // if request.resource.data.password == resource.data.password;
        if true;
	}
}

このルールは指定コレクションについて無制限のアクセスを許す。テスト目的でのみ使用すること。
(APIKEYはクライアントアプリに組み込まれるため秘匿されない)

Firestore→インデクス
コレクションID: requests
フィールド:
1. time=Accending(昇順)
2. isComplete=Accending(昇順)
クエリのスコープ: コレクション

複合インデクスは複数条件での列挙に必須

ビルド/実行

sh gradlew kotlinNpmInstall
sh gradlew :KtNodeSvr:jsDevelopmentExecutableCompileSync
export APPKEY=<Firebase-App-Key>
node build/js/packages/KtMpApp-KtNodeSvr/kotlin/KtMpApp-KtNodeSvr.js default password
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0