🐦

Kotlin で kintone をカスタマイズする

に公開

概要

kintone は、JavaScript でカスタマイズをすることができます。

@kintone/rest-api-client を用いることで、kintone REST API を使った実装が簡単になります。

https://github.com/kintone/js-sdk/tree/main/packages/rest-api-client

また、Kotlin は Multiplatform で動作するため、JavaScript にトランスパイルすることで、ブラウザで動作させることができます。

Kotlin/JavaScript (Kotlin/JS) lets you transpile your Kotlin code, the Kotlin standard library, and any compatible dependencies to JavaScript. This way, your Kotlin applications can run in any environment that supports JavaScript.

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

本記事では、Kotlin を使った kintone カスタマイズにトライします。

Kotlin 用の @kintone/rest-api-client を自作し、それを使った実装例を示します。

詳細

kintone REST API を呼び出す方法は、下記が考えられます。他にもありますが代表的なものを記載します。

  1. window.fetch() を用いて、kintone REST API を呼び出す。
  2. kintone JavaScript API を用いて、kintone REST API リクエストを送信する
  3. HTTP 通信用ライブラリを用いて、kintone REST API を呼び出す

1, 2 はブラウザ上での実行を前提としています。
今回は、1 の方針で進めます。

環境

ディレクトリ構成
.
├── gradlew
├── .gitignore
├── build
│   ├── kotlin-webpack
│   │   └── js
│   │       └── productionExecutable
│   │           ├── kintone-rest-api-client-kt.js
│   │           └── kintone-rest-api-client-kt.js.map
│   ├── ...
├── src
│   └── jsMain
│       └── kotlin
│           ├── kintone
│           │   └── client
│           │       ├── record
│           │       │   ├── Record.kt
│           │       │   ├── RecordApiImpl.kt
│           │       │   ├── RecordId.kt
│           │       │   ├── RecordResponse.kt
│           │       │   ├── RecordApi.kt
│           │       │   └── FieldType.kt
│           │       ├── kintone
│           │       │   ├── KintoneApi.kt
│           │       │   └── kintone.kt
│           │       └── KintoneClient.kt
│           └── main.kt
├── gradlew.bat
├── package.json
├── build.gradle.kts
├── settings.gradle.kts
├── README.md
└── gradle.properties

build.gradle.kts
plugins {
    kotlin("multiplatform") version "2.3.0"
}

repositories {
    mavenCentral()
}

kotlin {
    js(IR) {
        browser {
            binaries.executable()
        }
    }

    sourceSets {
        val jsMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.10.2")
            }
        }
    }
}

ソースコード解説

kintone.kt

kintone は kintone の画面上では、グローバル変数として参照できるため、Kotlin 側でも変数として定義します。

external val kintone: dynamic

KintoneApi.kt

kintone REST API を呼び出す Wrapper クラスです。

class KintoneApi {

    companion object {
        suspend fun call(
            path: String,
            method: String,
            body: dynamic = js("{}")
        ): dynamic {
            val url = kintone.api.url(path, true)

            return kintone.api(
                url,
                method,
                body
            ).unsafeCast<Promise<dynamic>>().await()
        }
    }
}

RecordApi.kt

今は、1件のレコードを取得する API だけですが、将来的に拡張できます。

interface RecordApi {
    suspend fun getRecord(
        appId: AppId,
        recordId: RecordId
    ): Record
}

RecordApiImpl.kt

js() の中で、Kotlin 側の文字列が展開できません。
そのため、事前に空オブジェクトを用意して、プロパティを追加しています。

class RecordApiImpl : RecordApi {

    override suspend fun getRecord(
        appId: AppId,
        recordId: RecordId
    ): Record {
        val body = js("{}")
        body.app = appId.value
        body.id = recordId.value
        val response = KintoneApi.call(
            path = "/k/v1/record.json",
            method = "GET",
            body = body
        )

        return RecordResponse(
            record = response.record.unsafeCast<Map<String, RawField>>()
        ).parse()
    }
}

RecordId.kt

型安全のため、value class を用います。

value class AppId(val value: Int)
value class RecordId(val value: Int)

FieldType.kt

enum class FieldType {
    __ID__,
    RECORD_NUMBER,
    CREATOR,
    MODIFIER,
    CREATED_TIME,
    UPDATED_TIME,
    SINGLE_LINE_TEXT,
}

Record.kt

サンプルなので、フィールド型を絞っています。

typealias FieldCode = String

data class Record(
    val id: RecordId,
    val fields: Map<FieldCode, FieldValue>
)

sealed interface FieldValue {
    data class RecordNumber(val value: Int) : FieldValue
    data class User(
        val code: String,
        val name: String
    ) : FieldValue
    data class DateTime(val value: String) : FieldValue
    data class SingleLineText(val value: String?) : FieldValue
    data class Number(val value: String?) : FieldValue
}

RecordResponse.kt

Enum Class と when 式の組み合わせによって、網羅性をコンパイラレベルでチェックできます。
Kotlin のメリットが活かせるのがポイントです。

class RecordResponse(
    val record: Map<String, RawField>
) {
    fun parse(): Record {
        val fields = mutableMapOf<FieldCode, FieldValue>()
        var recordId: RecordId? = null

        val _record = this.record // record は Kotlin の Map 型であり、JavaScript のオブジェクトではない。一度変数に入れないと JS 側で認識不可。
        
        // JS 側で認識できるようにキャストする必要がある
        js("Object.entries(_record)").unsafeCast<Array<Array<dynamic>>>().forEach { (fieldCode, raw) ->
            val fieldType = enumValues<FieldType>().firstOrNull { it.name == raw.type }
            when (fieldType) {
                FieldType.__ID__ -> {
                    recordId = RecordId(raw.value.toString().toInt())
                }

                FieldType.RECORD_NUMBER -> {
                    fields[fieldCode] =
                        FieldValue.RecordNumber(raw.value.toString().toInt())
                }

                FieldType.CREATOR, FieldType.MODIFIER -> {
                    fields[fieldCode] = FieldValue.User(
                        code = raw.value.code as String,
                        name = raw.value.name as String
                    )
                }

                FieldType.CREATED_TIME, FieldType.UPDATED_TIME -> {
                    fields[fieldCode] = FieldValue.DateTime(raw.value as String)
                }

                FieldType.SINGLE_LINE_TEXT -> {
                    fields[fieldCode] = FieldValue.SingleLineText(raw.value as String?)
                }

                else -> {}
            }
        }

        return Record(
            id = recordId ?: error("Record ID not found"),
            fields = fields
        )
    }

}

data class RawField(
    val type: String,
    val value: dynamic
)

Main.kt

client 用のインスタンスを作成し、レコード情報を取得します。

Kotlin coroutine の仕組みを利用した並行処理も簡単に実装できます。

fun main() {
    val client = KintoneClient()

    kintone.events.on("app.record.detail.show") { event: dynamic ->
        console.log(event)
        MainScope().launch {
            val appId = AppId(kintone.app.getId())

            val recordDeferred = async {
                val recordId = RecordId((event.recordId as Number).toInt())
                client.record.getRecord(appId, recordId)
            }
            val recordDeferred2 = async {
                val recordId = RecordId(2)
                client.record.getRecord(appId, recordId)
            }

            val (record, record2) = listOf(recordDeferred, recordDeferred2).awaitAll()
            console.log("recordId: ", record.id.value)
            val singleLineText = record.fields["string"] as? FieldValue.SingleLineText
            console.log("single line text: ", singleLineText?.value)

            console.log("record2Id: ", record2.id.value)
        }
        event
    }
}

実行方法

./gradlew jsBrowserProductionWebpack

上記コマンド実行後、./build/kotlin-webpack/js/productionExecutable/kintone-rest-api-client-kt.js を kintone に JavaScriptカスタマイズとしてアップロードします。

実行結果

Dveloper tool から、ログ出力されていることが分かります。
また、Network タブを確認すると、並行処理されていました。

※ 事前に、フィールドコード string のフィールドを配置しています。

まとめ

本記事では、Kotlin を使った kintone カスタマイズ例を示しました。
また、Kotlin 用の @kintone/rest-api-client を自作しました。

個人的には、開発環境やボイラーテンプレートができてしまえば、JS よりも Kotlin で開発している方が体験が良かったです。
手軽さは JS ですが、型安全性に関しては Kotlin に軍配が上がります。

今後は、自作ライブラリを拡張したり、UI 操作に関する記事を書いたりしたいと思います。

Discussion