サーバサイドKotlinでのGraphQLのグローバルIDの変換処理
やりたいこと・背景
サーバサイドKotlin + GraphQLでAPIを作成しています。 GraphQLを書いているとアプリケーション内部のドメインに紐づくモデルを、GraphQLスキーマに変換することは日常的にあるかなと思いますが、このときモデルのIDをGraphQLのグローバルIDに変換するのが手間だと思います。 Kotlinはこのあたり柔軟に書けるのでコードとして書いてみました。
GraphQLのグローバルIDについて、詳細を知りたい方は以下を参照。
GraphQL Global Object Identification Specification
グローバルIDのフォーマット
今回のコードでは、GraphQLのグローバルフォーマットは、<Model名>:<Id(int)>
としてこれをBase64でエンコードしたものをクライアントに返しています。
例:
- Userモデル id=123の場合
エンコード前文字列: User:123
エンコード後文字列: VXNlcjoxMjM=
使用しているライブラリ
- Getting Started | GraphQL Kotlin
- Ktor: Build Asynchronous Servers and Clients in Kotlin | Ktor Framework
※ 基本的なコードの構成は、ライブラリに依存するものではありませんが、該当のライブラリのクラスやメソッドをつかっているため記載しています。
コード全体
// GraphQLのライブラリとしてgraphql-kotlinを使用しているためグローバルIDは当該のクラスを使用する import com.expediagroup.graphql.generator.scalars.ID // ktorにbase64エンコード/デコード処理が公開されていたため使用(何でも良い) import io.ktor.util.decodeBase64String import io.ktor.util.encodeBase64 import <独自のパッケージ名>.ModelId // ドメインに紐づくモデルのID // 各モデルごとに継承して使う open class ModelId<T>(val value: T) { override fun toString() = value.toString() override fun hashCode(): Int = value.hashCode() override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is ModelId<*>) return false return other.value == value } } // Userモデル用に継承したID class UserId(value: Int) : ModelId<Int>(value) data class User( val id: UserId, val mane: String, ) // モデルID => GraphQLのグローバルID変換 fun ModelId<Int>.toId(): ID { try { // モデル毎 のクラス名を取得(ex: Userモデルなら UserId) val cName = this::class.simpleName!! // suffixが "Id"であることを決め打ちしてモデル名を取得 // ※ このあたりはサンプルなので適当にしています。 val modelName = cName.substring(0, cName.length - 2) // 「モデル名:id値」で文字列にしてエンコードする return ID("$modelName:${this.value}".encodeBase64()) } catch (e: Throwable) { throw IllegalStateException("Encode ID Failure.") } } // GraphQLのグローバルID => モデルID変換 fun <T : ModelId<Int>> ID.toModelId(): T { try { // base64からモデルをデコード val decodedId = value.decodeBase64String() // :で分割してモデル名を取得 val modelAndId = decodedId.split(":") // モデル名 + Id でモデル毎のIdクラスの名前を作る(ex: Userモデルなら UserId) val cName = "${modelAndId.first()}Id" // リフレクションでインスタンスを生成する val fullClass = Class.forName("<独自のモデルのパッケージ名>.$cName") val constructor = fullClass.getConstructor(Int::class.java) return constructor.newInstance(modelAndId.last().toInt()) as T } catch (e: Throwable) { throw IllegalArgumentException("Parse ModelId Failure.") } }
使い方
internal class ModelIdExtensionsKtTest : StringSpec({ "toId: Should return Id by Model." { UserId(123).toId() shouldBe ID("UserId:123".encodeBase64()) } "toModelId: Should return ModelId from ID." { val modelId = ID("UserId:123".encodeBase64()).toModelId<UserId>() modelId.value shouldBe 123 } })
感想
このあたり、TypeScriptなど、拡張が手軽にできない言語だと、サクッと書けないのでKotlinは便利です