refactor(build): enable Wasm by default and refactor modules for improved KMP compatibility

Enabled Wasm target across all relevant modules and removed conditional enablement logic. Refactored `core:core-utils` to move JVM-specific code to a new `backend:infrastructure:persistence` module for strict KMP compliance. Updated dependencies, adjusted Gradle configurations, and resolved circular dependencies.
This commit is contained in:
2026-01-09 14:36:10 +01:00
parent 13cfc37b37
commit 35da070893
22 changed files with 513 additions and 526 deletions
+6
View File
@@ -20,6 +20,12 @@ kotlin {
}
}
// Wasm support enabled?
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
wasmJs {
browser()
}
sourceSets {
// Opt-in to experimental Kotlin UUID API across all source sets
all {
+30 -81
View File
@@ -1,90 +1,39 @@
// Dieses Modul stellt gemeinsame technische Hilfsfunktionen bereit,
// wie z.B. Konfigurations-Management, Datenbank-Verbindungen und Service Discovery.
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
}
kotlin {
// Toolchain is now handled centrally in the root build.gradle.kts
// Target platforms
jvm {
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
jvm()
js {
browser()
}
}
js(IR) {
browser {
testTask {
enabled = false
}
}
}
sourceSets {
all {
languageSettings.optIn("kotlin.uuid.ExperimentalUuidApi")
// Wasm support enabled?
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
wasmJs {
browser()
}
commonMain.dependencies {
// Domain models and types (core-utils depends on core-domain, not vice versa)
api(projects.core.coreDomain)
api(libs.kotlinx.serialization.json)
api(libs.kotlinx.datetime)
// Async support (available for all platforms)
api(libs.kotlinx.coroutines.core)
// Utilities (multiplatform compatible)
api(libs.bignum)
sourceSets {
commonMain {
dependencies {
implementation(projects.core.coreDomain)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.coroutines.core)
}
}
commonTest {
dependencies {
implementation(libs.kotlin.test)
}
}
jvmMain {
dependencies {
// Removed Exposed dependencies to make this module KMP compatible
// implementation(libs.exposed.core)
// implementation(libs.exposed.jdbc)
}
}
}
commonTest.dependencies {
implementation(libs.kotlin.test)
}
jvmMain.dependencies {
// JVM-specific dependencies - access to central catalog
api(projects.platform.platformDependencies)
// Database Management (JVM-specific)
// Exposed dependencies restored for backend compatibility
api(libs.exposed.core)
api(libs.exposed.dao)
api(libs.exposed.jdbc)
api(libs.exposed.kotlin.datetime)
api(libs.flyway.core)
api(libs.flyway.postgresql)
api(libs.hikari.cp)
// Service Discovery (JVM-specific)
api(libs.spring.cloud.starter.consul.discovery)
// Logging (JVM-specific)
api(libs.kotlin.logging.jvm)
// Jakarta Annotation API
api(libs.jakarta.annotation.api)
// JSON Processing
api(libs.jackson.module.kotlin)
api(libs.jackson.datatype.jsr310)
}
jvmTest.dependencies {
// Testing (JVM-specific)
implementation(projects.platform.platformTesting)
implementation(libs.junit.jupiter.api)
implementation(libs.junit.jupiter.engine)
implementation(libs.junit.jupiter.params)
implementation(libs.junit.platform.launcher)
implementation(libs.mockk)
implementation(libs.assertj.core)
implementation(libs.kotlinx.coroutines.test)
runtimeOnly(libs.postgresql.driver)
}
}
}
tasks.named<Test>("jvmTest") {
useJUnitPlatform()
}
@@ -1,225 +1,10 @@
package at.mocode.core.utils
// import at.mocode.core.domain.model.ErrorCodes
// import at.mocode.core.domain.model.ErrorDto
// import at.mocode.core.domain.model.PagedResponse
// import org.jetbrains.exposed.sql.*
// import org.jetbrains.exposed.sql.statements.BatchInsertStatement
// import org.jetbrains.exposed.sql.transactions.transaction
// import java.sql.SQLException
// import java.sql.SQLTimeoutException
/**
* JVM-specific database utilities for the Core module.
* Provides common database operations and configurations.
*
* DEPRECATED / DISABLED:
* This file contains Exposed-specific code which is not compatible with the KMP frontend.
* It has been commented out to allow the frontend build to succeed.
* If backend services need this, it should be moved to a backend-specific module (e.g. :backend:common).
* MOVED: The content of this file has been moved to :backend:infrastructure:persistence
* to resolve KMP compatibility issues with the frontend.
*
* Please use `at.mocode.backend.infrastructure.persistence.DatabaseUtils` instead.
*/
/*
inline fun <T> transactionResult(
database: Database? = null,
crossinline block: Transaction.() -> T
): Result<T> {
return try {
val result = transaction(database) { block() }
Result.success(result)
} catch (e: SQLTimeoutException) {
Result.failure(
ErrorDto(
code = ErrorCodes.DATABASE_TIMEOUT,
message = "Datenbank-Operation wegen Timeout fehlgeschlagen"
)
)
} catch (e: SQLException) {
// Robustere Fehlerbehandlung über SQLSTATE (Postgres)
val mapped = when (e.sqlState) {
// unique_violation
"23505" -> ErrorCodes.DUPLICATE_ENTRY
// foreign_key_violation
"23503" -> ErrorCodes.FOREIGN_KEY_VIOLATION
// check_violation
"23514" -> ErrorCodes.CHECK_VIOLATION
else -> ErrorCodes.DATABASE_ERROR
}
Result.failure(
ErrorDto(
code = mapped,
message = "Datenbank-Operation fehlgeschlagen"
)
)
} catch (e: Exception) {
Result.failure(
ErrorDto(
code = ErrorCodes.TRANSACTION_ERROR,
message = "Transaktion fehlgeschlagen"
)
)
}
}
inline fun <T> writeTransaction(
database: Database? = null,
crossinline block: Transaction.() -> T
): Result<T> = transactionResult(database, block)
inline fun <T> readTransaction(
database: Database? = null,
crossinline block: Transaction.() -> T
): Result<T> = transactionResult(database, block)
fun Query.paginate(page: Int, size: Int): Query {
require(page >= 0) { "Page number must be non-negative" }
require(size > 0) { "Page size must be positive" }
return limit(size).offset(start = (page * size).toLong())
}
fun <T> Query.toPagedResponse(
page: Int,
size: Int,
transform: (ResultRow) -> T
): PagedResponse<T> {
// Validate input parameters
require(page >= 0) { "Page number must be non-negative" }
require(size > 0) { "Page size must be positive" }
// Calculate the total count first (executes a COUNT query)
val totalCount = this.count()
// If there are no results, return an empty page
if (totalCount == 0L) {
return PagedResponse.create(
content = emptyList(),
page = page,
size = size,
totalElements = 0,
totalPages = 0,
hasNext = false,
hasPrevious = page > 0
)
}
// Calculate total pages - use ceil division to ensure we round up
val totalPages = ((totalCount + size - 1) / size).toInt()
// Ensure the requested page exists (if page is beyond available pages, return the last page)
val adjustedPage = if (page >= totalPages) (totalPages - 1).coerceAtLeast(0) else page
// Then apply pagination and transform results
val content = this.paginate(adjustedPage, size).map(transform)
return PagedResponse.create(
content = content,
page = adjustedPage,
size = size,
totalElements = totalCount,
totalPages = totalPages,
hasNext = adjustedPage < totalPages - 1,
hasPrevious = adjustedPage > 0
)
}
object DatabaseUtils {
fun tableExists(tableName: String, database: Database? = null): Boolean {
return try {
transaction(database) {
// Postgres-spezifischer, robuster Ansatz über to_regclass
val valid = tableName.trim()
if (!valid.matches(Regex("^[A-Za-z_][A-Za-z0-9_]*$"))) return@transaction false
exec("SELECT to_regclass('$valid')") { rs ->
if (rs.next()) rs.getString(1) else null
} != null
}
} catch (e: Exception) {
false
}
}
@JvmName("createIndexIfNotExistsArray")
fun createIndexIfNotExists(
tableName: String,
indexName: String,
columns: Array<String>,
unique: Boolean = false,
database: Database? = null
): Result<Unit> = createIndexIfNotExists(tableName, indexName, *columns, unique = unique, database = database)
@JvmName("createIndexIfNotExistsVararg")
fun createIndexIfNotExists(
tableName: String,
indexName: String,
vararg columns: String,
unique: Boolean = false,
database: Database? = null
): Result<Unit> {
return transactionResult(database) {
// Einfache Sanitization + Quoting der Identifier
fun quoteIdent(name: String): String {
require(name.matches(Regex("^[A-Za-z_][A-Za-z0-9_]*$"))) { "Ungültiger Identifier: $name" }
return "\"$name\""
}
val uniqueStr = if (unique) "UNIQUE" else ""
val qTable = quoteIdent(tableName)
val qIndex = quoteIdent(indexName)
val cols = columns.map { quoteIdent(it) }.joinToString(", ")
val sql = "CREATE $uniqueStr INDEX IF NOT EXISTS $qIndex ON $qTable ($cols)"
exec(sql)
Unit
}
}
fun executeRawSql(sql: String, database: Database? = null): Result<Unit> = transactionResult(database) {
exec(sql)
Unit
}
fun executeUpdate(sql: String, database: Database? = null): Result<Int> = transactionResult(database) {
// Nutzt Exposed PreparedStatementApi, kein AutoCloseable
val ps = this.connection.prepareStatement(sql, false)
ps.executeUpdate()
}
inline fun <T> batchInsert(
table: Table,
data: Iterable<T>,
crossinline body: BatchInsertStatement.(T) -> Unit
): Result<List<ResultRow>> {
return transactionResult {
table.batchInsert(data) { item ->
body(item)
}
}
}
}
fun <T> ResultRow.getOrNull(column: Column<T>): T? {
return try {
this[column]
} catch (e: Exception) {
null
}
}
fun ResultRow.toMap(): Map<String, Any?> {
val result = mutableMapOf<String, Any?>()
this.fieldIndex.forEach { (expression, _) ->
try {
when (expression) {
is Column<*> -> result[expression.name] = this[expression]
else -> result[expression.toString()] = this[expression]
}
} catch (e: Exception) {
// Ignore columns that can't be read and log the error if needed
// You could add logging here in a production environment
}
}
return result
}
*/