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:
parent
13cfc37b37
commit
35da070893
|
|
@ -52,12 +52,14 @@ Technologien & Standards:
|
||||||
- Architektur: Domain-Driven Design (DDD). Halte Domänenlogik rein und getrennt von Infrastruktur.
|
- Architektur: Domain-Driven Design (DDD). Halte Domänenlogik rein und getrennt von Infrastruktur.
|
||||||
- Testing: JUnit 5, MockK, Testcontainers (Postgres, Keycloak).
|
- Testing: JUnit 5, MockK, Testcontainers (Postgres, Keycloak).
|
||||||
- API: REST, OpenAPI (SpringDoc).
|
- API: REST, OpenAPI (SpringDoc).
|
||||||
|
- **Sync-Strategie:** Implementierung von Delta-Sync APIs (basierend auf UUIDv7/Timestamps) für Offline-First Clients.
|
||||||
|
|
||||||
Regeln:
|
Regeln:
|
||||||
1. Nutze `val` und Immutability wo immer möglich.
|
1. Nutze `val` und Immutability wo immer möglich.
|
||||||
2. Implementiere Business-Logik in der Domain-Schicht, nicht im Controller.
|
2. Implementiere Business-Logik in der Domain-Schicht, nicht im Controller.
|
||||||
3. Nutze Testcontainers für Integrationstests.
|
3. Nutze Testcontainers für Integrationstests.
|
||||||
4. Beachte die Modul-Struktur: `:api` (Interfaces/DTOs), `:domain` (Core Logic), `:service` (Application/Infra).
|
4. Beachte die Modul-Struktur: `:api` (Interfaces/DTOs), `:domain` (Core Logic), `:service` (Application/Infra).
|
||||||
|
5. **KMP-Awareness:** Achte darauf, dass Code in `:api` und `:domain` Modulen KMP-kompatibel bleibt (keine Java-Dependencies). Nutze für JVM-spezifische Logik (z.B. Exposed) dedizierte Infrastruktur-Module.
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
21
backend/infrastructure/persistence/build.gradle.kts
Normal file
21
backend/infrastructure/persistence/build.gradle.kts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlinJvm)
|
||||||
|
alias(libs.plugins.kotlinJpa)
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.core.coreUtils)
|
||||||
|
implementation(projects.core.coreDomain)
|
||||||
|
implementation(projects.platform.platformDependencies)
|
||||||
|
|
||||||
|
// Exposed
|
||||||
|
implementation(libs.exposed.core)
|
||||||
|
implementation(libs.exposed.jdbc)
|
||||||
|
implementation(libs.exposed.dao)
|
||||||
|
implementation(libs.exposed.java.time)
|
||||||
|
implementation(libs.exposed.json)
|
||||||
|
implementation(libs.exposed.kotlin.datetime)
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
implementation(libs.slf4j.api)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,220 @@
|
||||||
|
package at.mocode.backend.infrastructure.persistence
|
||||||
|
|
||||||
|
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 Backend.
|
||||||
|
* Provides common database operations and configurations using Exposed.
|
||||||
|
*
|
||||||
|
* Moved from core:core-utils to avoid polluting the KMP frontend build.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -9,8 +9,6 @@ version = "1.0.0"
|
||||||
kotlin {
|
kotlin {
|
||||||
// Toolchain is now handled centrally in the root build.gradle.kts
|
// Toolchain is now handled centrally in the root build.gradle.kts
|
||||||
|
|
||||||
val enableWasm = providers.gradleProperty("enableWasm").orNull == "true"
|
|
||||||
|
|
||||||
// JVM target for backend usage
|
// JVM target for backend usage
|
||||||
jvm()
|
jvm()
|
||||||
|
|
||||||
|
|
@ -20,12 +18,10 @@ kotlin {
|
||||||
// no need for binaries.executable() in a library
|
// no need for binaries.executable() in a library
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional Wasm target for browser clients
|
// Wasm enabled by default
|
||||||
if (enableWasm) {
|
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||||
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
wasmJs {
|
||||||
wasmJs {
|
browser()
|
||||||
browser()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
|
|
||||||
|
|
@ -16,22 +16,21 @@ dependencies {
|
||||||
// === Project Dependencies ===
|
// === Project Dependencies ===
|
||||||
implementation(projects.backend.services.ping.pingApi)
|
implementation(projects.backend.services.ping.pingApi)
|
||||||
implementation(projects.platform.platformDependencies)
|
implementation(projects.platform.platformDependencies)
|
||||||
|
// NEU: Zugriff auf die verschobenen DatabaseUtils
|
||||||
|
implementation(projects.backend.infrastructure.persistence)
|
||||||
|
|
||||||
// === Spring Boot & Cloud ===
|
// === Spring Boot & Cloud ===
|
||||||
implementation(libs.bundles.spring.boot.service.complete)
|
implementation(libs.bundles.spring.boot.service.complete)
|
||||||
// WICHTIG: Da wir JPA (blockierend) nutzen, brauchen wir Spring MVC (nicht WebFlux)
|
// WICHTIG: Da wir JPA (blockierend) nutzen, brauchen wir Spring MVC (nicht WebFlux)
|
||||||
implementation(libs.spring.boot.starter.web)
|
implementation(libs.spring.boot.starter.web)
|
||||||
|
|
||||||
// KORREKTUR: Bundle aufgelöst, da Accessor fehlschlägt
|
// Service Discovery
|
||||||
// libs.bundles.spring.cloud.gateway -> spring-cloud-gateway
|
|
||||||
implementation(libs.spring.cloud.starter.gateway.server.webflux)
|
|
||||||
implementation(libs.spring.cloud.starter.consul.discovery)
|
implementation(libs.spring.cloud.starter.consul.discovery)
|
||||||
|
|
||||||
// === Database & Persistence ===
|
// === Database & Persistence ===
|
||||||
implementation(libs.bundles.database.complete)
|
implementation(libs.bundles.database.complete)
|
||||||
|
|
||||||
// === Resilience ===
|
// === Resilience ===
|
||||||
// KORREKTUR: Bundle aufgelöst
|
|
||||||
implementation(libs.resilience4j.spring.boot3)
|
implementation(libs.resilience4j.spring.boot3)
|
||||||
implementation(libs.resilience4j.reactor)
|
implementation(libs.resilience4j.reactor)
|
||||||
implementation(libs.spring.boot.starter.aop)
|
implementation(libs.spring.boot.starter.aop)
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,12 @@ kotlin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wasm support enabled?
|
||||||
|
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||||
|
wasmJs {
|
||||||
|
browser()
|
||||||
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
// Opt-in to experimental Kotlin UUID API across all source sets
|
// Opt-in to experimental Kotlin UUID API across all source sets
|
||||||
all {
|
all {
|
||||||
|
|
|
||||||
|
|
@ -1,90 +1,39 @@
|
||||||
// Dieses Modul stellt gemeinsame technische Hilfsfunktionen bereit,
|
|
||||||
// wie z.B. Konfigurations-Management, Datenbank-Verbindungen und Service Discovery.
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
alias(libs.plugins.kotlinSerialization)
|
alias(libs.plugins.kotlinSerialization)
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
// Toolchain is now handled centrally in the root build.gradle.kts
|
jvm()
|
||||||
|
js {
|
||||||
// Target platforms
|
browser()
|
||||||
jvm {
|
|
||||||
compilerOptions {
|
|
||||||
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
|
|
||||||
}
|
}
|
||||||
}
|
// Wasm support enabled?
|
||||||
|
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||||
js(IR) {
|
wasmJs {
|
||||||
browser {
|
browser()
|
||||||
testTask {
|
|
||||||
enabled = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
all {
|
|
||||||
languageSettings.optIn("kotlin.uuid.ExperimentalUuidApi")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
commonMain.dependencies {
|
sourceSets {
|
||||||
// Domain models and types (core-utils depends on core-domain, not vice versa)
|
commonMain {
|
||||||
api(projects.core.coreDomain)
|
dependencies {
|
||||||
|
implementation(projects.core.coreDomain)
|
||||||
api(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
api(libs.kotlinx.datetime)
|
implementation(libs.kotlinx.datetime)
|
||||||
// Async support (available for all platforms)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
api(libs.kotlinx.coroutines.core)
|
}
|
||||||
// Utilities (multiplatform compatible)
|
}
|
||||||
api(libs.bignum)
|
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
|
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.
|
* JVM-specific database utilities for the Core module.
|
||||||
* Provides common database operations and configurations.
|
|
||||||
*
|
*
|
||||||
* DEPRECATED / DISABLED:
|
* MOVED: The content of this file has been moved to :backend:infrastructure:persistence
|
||||||
* This file contains Exposed-specific code which is not compatible with the KMP frontend.
|
* to resolve KMP compatibility issues with the 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).
|
* 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
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
|
||||||
59
docs/Backend_Status_Report_01-2026.md
Normal file
59
docs/Backend_Status_Report_01-2026.md
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
# Architecture & Build Status Report
|
||||||
|
**Datum:** 09.01.2026
|
||||||
|
**Von:** Senior Backend Developer
|
||||||
|
**An:** Lead Software Architect
|
||||||
|
|
||||||
|
## 1. Executive Summary
|
||||||
|
Wir haben eine umfassende Stabilisierung der Projekt-Architektur durchgeführt. Kritische Versionskonflikte im Backend (Spring Boot vs. Spring Cloud) wurden behoben. Die Trennung zwischen Frontend (KMP) und Backend (JVM) wurde durch Refactoring des `core`-Bereichs strikt durchgesetzt, um "Pollution" durch JVM-Code im Frontend zu verhindern.
|
||||||
|
|
||||||
|
Der Build-Prozess ist derzeit noch durch spezifische **Kotlin/Wasm Kompilierungsfehler** blockiert, die aus der strikten Typisierung und dem JS-Interop von WebAssembly resultieren.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Durchgeführte Maßnahmen
|
||||||
|
|
||||||
|
### 2.1 Backend Architecture Alignment
|
||||||
|
* **Spring Cloud Konflikt gelöst:** Downgrade von `2025.1.0` (Oakwood, inkompatibel mit Boot 3.5) auf **`2025.0.1` (Northfields)**. Dies verhindert garantierte Laufzeitfehler (`NoSuchMethodError`).
|
||||||
|
* **Java 25 Optimierung:** Upgrade von Micrometer auf `1.16.1` für besseren Virtual Thread Support.
|
||||||
|
* **Exposed Versionierung:** Bestätigung der Nutzung von `1.0.0-rc-4` (statt der veralteten 0.61.0).
|
||||||
|
|
||||||
|
### 2.2 Modul-Hygiene & KMP Trennung
|
||||||
|
* **Refactoring `core:core-utils`:**
|
||||||
|
* Das Modul enthielt JVM-spezifischen Code (`DatabaseUtils.kt` mit Exposed-Abhängigkeiten), der den Frontend-Build (JS/Wasm) brach.
|
||||||
|
* **Lösung:** Erstellung eines neuen Moduls **`:backend:infrastructure:persistence`**. Der DB-Code wurde dorthin verschoben. `core:core-utils` ist nun ein reines KMP-Modul.
|
||||||
|
* **Zirkuläre Abhängigkeiten aufgelöst:**
|
||||||
|
* Das Modul `frontend:shared` hatte Abhängigkeiten zu Feature-Modulen und dem Design-System, was zu Zyklen führte.
|
||||||
|
* **Lösung:** `frontend:shared` wurde bereinigt und dient nun rein als Basis-Layer (Config, Utils).
|
||||||
|
|
||||||
|
### 2.3 Build-System (Gradle & KMP)
|
||||||
|
* **Wasm-Target Konsolidierung:**
|
||||||
|
* Um Inkonsistenzen bei der Dependency Resolution zu beheben, wurde das Target `wasmJs` **projektweit** in allen relevanten KMP-Modulen (`core`, `frontend`) aktiviert.
|
||||||
|
* Dies löste die `Unresolved platforms: [wasmJs]` Fehler.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Aktuelle Blocker (Wasm Compiler)
|
||||||
|
|
||||||
|
Obwohl die Dependency-Struktur nun sauber ist, scheitert der Compiler im `wasmJs` Target an spezifischen Interop-Problemen:
|
||||||
|
|
||||||
|
1. **Fehlende Referenzen (`Unresolved reference`):**
|
||||||
|
* `org.w3c.dom.Worker` und `kotlinx.browser.window` werden im Wasm-Kontext nicht gefunden.
|
||||||
|
* *Ursache:* Kotlin/Wasm benötigt möglicherweise explizite Imports oder externe Deklarationen für bestimmte DOM-APIs, die in Kotlin/JS implizit waren, oder die Standard-Bibliothek wird nicht korrekt eingebunden.
|
||||||
|
2. **JS-Interop Einschränkungen:**
|
||||||
|
* Fehler: `Type 'ERROR CLASS: Symbol not found for Worker' cannot be used as return type`.
|
||||||
|
* Kotlin/Wasm erlaubt keine komplexen `js("...")` Blöcke innerhalb von Funktionen und hat keinen `dynamic` Typ. Unsere ersten Fixes (Helper-Funktionen) waren ein Schritt in die richtige Richtung, aber die Typen (wie `Worker`) müssen dem Compiler bekannt gemacht werden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Nächste Schritte (Plan)
|
||||||
|
|
||||||
|
1. **Wasm-Build reparieren:**
|
||||||
|
* Prüfen, ob wir eine explizite Dependency (z.B. `kotlinx-browser` oder `kotlin-stdlib-wasm-js`) benötigen.
|
||||||
|
* Falls `Worker` in der Wasm-Stdlib fehlt: Definition einer `external class Worker` für Wasm erstellen, um dem Compiler den Typ bekannt zu machen.
|
||||||
|
2. **Backend-Verifikation ("Bauplan"):**
|
||||||
|
* Sobald der Build durchläuft (oder wir das Frontend temporär exkludieren), werde ich den **`ping-service`** starten.
|
||||||
|
* Ziel: Nachweis, dass Spring Context, Datenbank-Verbindung (JPA) und die neue Modul-Struktur (`backend:infrastructure:persistence`) zur Laufzeit funktionieren.
|
||||||
|
3. **Sync-Strategie:**
|
||||||
|
* Anschließend widmen wir uns der im Frontend-Report erwähnten "Offline-Sync"-Logik (basierend auf UUIDv7).
|
||||||
|
|
||||||
|
**Empfehlung:** Wir sollten den Wasm-Build-Fix priorisieren, da er aktuell das gesamte Projekt blockiert ("Fail Fast").
|
||||||
|
|
@ -7,7 +7,6 @@ plugins {
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
// Toolchain is now handled centrally in the root build.gradle.kts
|
// Toolchain is now handled centrally in the root build.gradle.kts
|
||||||
val enableWasm = providers.gradleProperty("enableWasm").orNull == "true"
|
|
||||||
|
|
||||||
jvm()
|
jvm()
|
||||||
js(IR) {
|
js(IR) {
|
||||||
|
|
@ -15,12 +14,10 @@ kotlin {
|
||||||
// nodejs()
|
// nodejs()
|
||||||
}
|
}
|
||||||
|
|
||||||
// WASM, nur wenn explizit aktiviert
|
// WASM enabled by default
|
||||||
if (enableWasm) {
|
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||||
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
wasmJs {
|
||||||
wasmJs {
|
browser()
|
||||||
browser()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@ plugins {
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
// Toolchain is now handled centrally in the root build.gradle.kts
|
// Toolchain is now handled centrally in the root build.gradle.kts
|
||||||
val enableWasm = providers.gradleProperty("enableWasm").orNull == "true"
|
|
||||||
|
// Wasm is now a first-class citizen in our stack, so we enable it by default
|
||||||
|
// val enableWasm = providers.gradleProperty("enableWasm").orNull == "true"
|
||||||
|
|
||||||
jvm()
|
jvm()
|
||||||
js {
|
js {
|
||||||
|
|
@ -19,10 +21,9 @@ kotlin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enableWasm) {
|
// Always enable Wasm to match the rest of the KMP stack
|
||||||
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||||
wasmJs { browser() }
|
wasmJs { browser() }
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain.dependencies {
|
commonMain.dependencies {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ plugins {
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
// Toolchain is now handled centrally in the root build.gradle.kts
|
// Toolchain is now handled centrally in the root build.gradle.kts
|
||||||
val enableWasm = providers.gradleProperty("enableWasm").orNull == "true"
|
|
||||||
|
|
||||||
jvm()
|
jvm()
|
||||||
js {
|
js {
|
||||||
|
|
@ -19,11 +18,10 @@ kotlin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enableWasm) {
|
// Wasm enabled by default
|
||||||
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||||
wasmJs {
|
wasmJs {
|
||||||
browser()
|
browser()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
|
@ -42,11 +40,9 @@ kotlin {
|
||||||
implementation(libs.sqldelight.driver.web)
|
implementation(libs.sqldelight.driver.web)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enableWasm) {
|
val wasmJsMain = getByName("wasmJsMain")
|
||||||
val wasmJsMain = getByName("wasmJsMain")
|
wasmJsMain.dependencies {
|
||||||
wasmJsMain.dependencies {
|
implementation(libs.sqldelight.driver.web)
|
||||||
implementation(libs.sqldelight.driver.web)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
commonTest.dependencies {
|
commonTest.dependencies {
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,12 @@ import org.w3c.dom.Worker
|
||||||
|
|
||||||
actual class DatabaseDriverFactory {
|
actual class DatabaseDriverFactory {
|
||||||
actual suspend fun createDriver(): SqlDriver {
|
actual suspend fun createDriver(): SqlDriver {
|
||||||
// Same as JS, we use a Web Worker for Wasm to support OPFS
|
// In Kotlin/Wasm, we cannot use the js() function inside a function body like in Kotlin/JS.
|
||||||
val worker = Worker(
|
// We need to use a helper function or a different approach.
|
||||||
js("""new URL("sqlite.worker.js", import.meta.url)""")
|
// However, for WebWorkerDriver, we need a Worker instance.
|
||||||
)
|
|
||||||
|
// Workaround for Wasm: Use a helper function to create the Worker
|
||||||
|
val worker = createWorker()
|
||||||
val driver = WebWorkerDriver(worker)
|
val driver = WebWorkerDriver(worker)
|
||||||
|
|
||||||
AppDatabase.Schema.create(driver).await()
|
AppDatabase.Schema.create(driver).await()
|
||||||
|
|
@ -17,3 +19,9 @@ actual class DatabaseDriverFactory {
|
||||||
return driver
|
return driver
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to create a Worker in Wasm
|
||||||
|
// Note: Kotlin/Wasm JS interop is stricter.
|
||||||
|
// We must return a type that Wasm understands as an external JS reference.
|
||||||
|
// 'Worker' from org.w3c.dom is correct, but we need to ensure the stdlib is available.
|
||||||
|
private fun createWorker(): Worker = js("new Worker(new URL('sqlite.worker.js', import.meta.url))")
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ version = "1.0.0"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
// Toolchain is now handled centrally in the root build.gradle.kts
|
// Toolchain is now handled centrally in the root build.gradle.kts
|
||||||
val enableWasm = providers.gradleProperty("enableWasm").orNull == "true"
|
|
||||||
|
|
||||||
jvm()
|
jvm()
|
||||||
|
|
||||||
|
|
@ -25,11 +24,10 @@ kotlin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enableWasm) {
|
// Wasm enabled by default
|
||||||
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||||
wasmJs {
|
wasmJs {
|
||||||
browser()
|
browser()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ plugins {
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
// Toolchain is now handled centrally in the root build.gradle.kts
|
// Toolchain is now handled centrally in the root build.gradle.kts
|
||||||
val enableWasm = providers.gradleProperty("enableWasm").orNull == "true"
|
|
||||||
|
|
||||||
jvm()
|
jvm()
|
||||||
js {
|
js {
|
||||||
|
|
@ -19,10 +18,9 @@ kotlin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enableWasm) {
|
// Wasm enabled by default
|
||||||
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||||
wasmJs { browser() }
|
wasmJs { browser() }
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain.dependencies {
|
commonMain.dependencies {
|
||||||
|
|
@ -52,11 +50,9 @@ kotlin {
|
||||||
implementation(libs.ktor.client.js)
|
implementation(libs.ktor.client.js)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enableWasm) {
|
val wasmJsMain = getByName("wasmJsMain")
|
||||||
val wasmJsMain = getByName("wasmJsMain")
|
wasmJsMain.dependencies {
|
||||||
wasmJsMain.dependencies {
|
implementation(libs.ktor.client.js)
|
||||||
implementation(libs.ktor.client.js)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,28 +2,35 @@ package at.mocode.frontend.core.network
|
||||||
|
|
||||||
import kotlinx.browser.window
|
import kotlinx.browser.window
|
||||||
|
|
||||||
@Suppress("UnsafeCastFromDynamic", "EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
|
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
|
||||||
actual object PlatformConfig {
|
actual object PlatformConfig {
|
||||||
actual fun resolveApiBaseUrl(): String {
|
actual fun resolveApiBaseUrl(): String {
|
||||||
// 1) Prefer a global JS variable (can be injected by index.html or nginx)
|
// 1) Prefer a global JS variable (can be injected by index.html or nginx)
|
||||||
val global =
|
val fromGlobal = getGlobalApiBaseUrl()
|
||||||
js("typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : (typeof self !== 'undefined' ? self : {}))")
|
|
||||||
val fromGlobal = try {
|
|
||||||
(global.API_BASE_URL as? String)?.trim().orEmpty()
|
|
||||||
} catch (_: dynamic) {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
if (fromGlobal.isNotEmpty()) return fromGlobal.removeSuffix("/")
|
if (fromGlobal.isNotEmpty()) return fromGlobal.removeSuffix("/")
|
||||||
|
|
||||||
// 2) Try window location origin (same origin gateway/proxy setup)
|
// 2) Try window location origin (same origin gateway/proxy setup)
|
||||||
|
// In Wasm, we can access a window directly if we are in the browser main thread.
|
||||||
|
// However, we need to be careful about exceptions.
|
||||||
val origin = try {
|
val origin = try {
|
||||||
window.location.origin
|
window.location.origin
|
||||||
} catch (_: dynamic) {
|
} catch (e: Throwable) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!origin.isNullOrBlank()) return origin.removeSuffix("/")
|
if (!origin.isNullOrBlank()) return origin.removeSuffix("/")
|
||||||
|
|
||||||
// 3) Fallback to the local gateway
|
// 3) Fallback to the local gateway
|
||||||
return "http://localhost:8081"
|
return "http://localhost:8081"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function for JS interop in Wasm
|
||||||
|
// Kotlin/Wasm does not support 'dynamic' type or complex js() blocks inside functions.
|
||||||
|
// We must use top-level external functions or simple js() expressions.
|
||||||
|
private fun getGlobalApiBaseUrl(): String = js("""
|
||||||
|
(function() {
|
||||||
|
var global = typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : (typeof self !== 'undefined' ? self : {}));
|
||||||
|
return (global.API_BASE_URL && typeof global.API_BASE_URL === 'string') ? global.API_BASE_URL : "";
|
||||||
|
})()
|
||||||
|
""")
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ version = "1.0.0"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
// Toolchain is now handled centrally in the root build.gradle.kts
|
// Toolchain is now handled centrally in the root build.gradle.kts
|
||||||
val enableWasm = providers.gradleProperty("enableWasm").orNull == "true"
|
|
||||||
|
|
||||||
jvm()
|
jvm()
|
||||||
|
|
||||||
|
|
@ -29,12 +28,10 @@ kotlin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WASM, nur wenn explizit aktiviert
|
// Wasm enabled by default
|
||||||
if (enableWasm) {
|
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||||
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
wasmJs {
|
||||||
wasmJs {
|
browser()
|
||||||
browser()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
|
@ -85,17 +82,14 @@ kotlin {
|
||||||
implementation(libs.ktor.client.js)
|
implementation(libs.ktor.client.js)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WASM SourceSet, nur wenn aktiviert
|
val wasmJsMain = getByName("wasmJsMain")
|
||||||
if (enableWasm) {
|
wasmJsMain.dependencies {
|
||||||
val wasmJsMain = getByName("wasmJsMain")
|
implementation(libs.ktor.client.js) // WASM verwendet JS-Client [cite: 7]
|
||||||
wasmJsMain.dependencies {
|
|
||||||
implementation(libs.ktor.client.js) // WASM verwendet JS-Client [cite: 7]
|
|
||||||
|
|
||||||
// Compose für shared UI components für WASM
|
// Compose für shared UI components für WASM
|
||||||
implementation(compose.runtime)
|
implementation(compose.runtime)
|
||||||
implementation(compose.foundation)
|
implementation(compose.foundation)
|
||||||
implementation(compose.material3)
|
implementation(compose.material3)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ version = "1.0.0"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
// Toolchain is now handled centrally in the root build.gradle.kts
|
// Toolchain is now handled centrally in the root build.gradle.kts
|
||||||
val enableWasm = providers.gradleProperty("enableWasm").orNull == "true"
|
|
||||||
|
|
||||||
jvm()
|
jvm()
|
||||||
|
|
||||||
|
|
@ -29,12 +28,10 @@ kotlin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WASM, nur wenn explizit aktiviert
|
// Wasm enabled by default
|
||||||
if (enableWasm) {
|
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||||
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
wasmJs {
|
||||||
wasmJs {
|
browser()
|
||||||
browser()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
|
@ -85,17 +82,14 @@ kotlin {
|
||||||
implementation(libs.ktor.client.js)
|
implementation(libs.ktor.client.js)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WASM SourceSet, nur wenn aktiviert
|
val wasmJsMain = getByName("wasmJsMain")
|
||||||
if (enableWasm) {
|
wasmJsMain.dependencies {
|
||||||
val wasmJsMain = getByName("wasmJsMain")
|
implementation(libs.ktor.client.js) // WASM verwendet JS-Client [cite: 7]
|
||||||
wasmJsMain.dependencies {
|
|
||||||
implementation(libs.ktor.client.js) // WASM verwendet JS-Client [cite: 7]
|
|
||||||
|
|
||||||
// Compose für shared UI components für WASM
|
// Compose für shared UI components für WASM
|
||||||
implementation(compose.runtime)
|
implementation(compose.runtime)
|
||||||
implementation(compose.foundation)
|
implementation(compose.foundation)
|
||||||
implementation(compose.material3)
|
implementation(compose.material3)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,121 +1,79 @@
|
||||||
@file:OptIn(ExperimentalKotlinGradlePluginApi::class, ExperimentalWasmDsl::class)
|
|
||||||
|
|
||||||
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
|
|
||||||
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
|
||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
|
||||||
import org.jetbrains.kotlin.gradle.dsl.KotlinJsCompile
|
|
||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shared Module: Gemeinsame Libraries und Utilities für alle Client-Features
|
|
||||||
* KEINE EXECUTABLE - ist eine Library für andere Module
|
|
||||||
*/
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
alias(libs.plugins.kotlinSerialization)
|
alias(libs.plugins.kotlinSerialization)
|
||||||
alias(libs.plugins.composeMultiplatform)
|
alias(libs.plugins.composeMultiplatform)
|
||||||
alias(libs.plugins.composeCompiler)
|
alias(libs.plugins.composeCompiler)
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
// Toolchain is now handled centrally in the root build.gradle.kts
|
jvm("desktop")
|
||||||
val enableWasm = providers.gradleProperty("enableWasm").orNull == "true"
|
|
||||||
|
|
||||||
// JVM Target für Desktop
|
js(IR) {
|
||||||
jvm()
|
browser()
|
||||||
|
binaries.executable()
|
||||||
// JavaScript Target für Web
|
|
||||||
js {
|
|
||||||
browser {
|
|
||||||
testTask {
|
|
||||||
enabled = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
|
|
||||||
// WASM, nur wenn explizit aktiviert
|
|
||||||
if (enableWasm) {
|
|
||||||
@OptIn(ExperimentalWasmDsl::class)
|
|
||||||
wasmJs { browser() }
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
commonMain.dependencies {
|
|
||||||
|
|
||||||
api(projects.core.coreUtils)
|
|
||||||
api(projects.core.coreDomain)
|
|
||||||
api(project(":frontend:core:domain"))
|
|
||||||
|
|
||||||
// Kotlinx core dependencies (coroutines, serialization, datetime)
|
|
||||||
// KORREKTUR: Zugriff auf Bundle korrigiert.
|
|
||||||
// In libs.versions.toml: [bundles] kotlinx-core = [...]
|
|
||||||
// Gradle Accessor: libs.bundles.kotlinx.core
|
|
||||||
// Falls das fehlschlägt, listen wir die Libs einzeln auf, um den Build zu fixen.
|
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
|
||||||
implementation(libs.kotlinx.serialization.json)
|
|
||||||
implementation(libs.kotlinx.datetime)
|
|
||||||
|
|
||||||
// HTTP Client
|
|
||||||
implementation(libs.ktor.client.core)
|
|
||||||
implementation(libs.ktor.client.contentNegotiation)
|
|
||||||
implementation(libs.ktor.client.serialization.kotlinx.json)
|
|
||||||
implementation(libs.ktor.client.logging)
|
|
||||||
implementation(libs.ktor.client.auth)
|
|
||||||
|
|
||||||
// Dependency Injection (Koin)
|
|
||||||
implementation(libs.koin.core)
|
|
||||||
implementation(libs.koin.compose)
|
|
||||||
implementation(libs.koin.compose.viewmodel)
|
|
||||||
|
|
||||||
// Network module (provides DI `apiClient`)
|
|
||||||
implementation(projects.frontend.core.network)
|
|
||||||
|
|
||||||
// Compose für shared UI components (common)
|
|
||||||
implementation(compose.runtime)
|
|
||||||
implementation(compose.foundation)
|
|
||||||
implementation(compose.material3)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
commonTest.dependencies {
|
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||||
implementation(libs.kotlin.test)
|
wasmJs {
|
||||||
implementation(libs.kotlinx.coroutines.test)
|
browser()
|
||||||
|
binaries.executable()
|
||||||
}
|
}
|
||||||
|
|
||||||
jvmMain.dependencies {
|
sourceSets {
|
||||||
implementation(libs.ktor.client.cio)
|
commonMain {
|
||||||
}
|
dependencies {
|
||||||
|
implementation(projects.frontend.core.domain)
|
||||||
|
// implementation(projects.frontend.core.designSystem) // REMOVED: Circular dependency
|
||||||
|
implementation(projects.frontend.core.navigation)
|
||||||
|
implementation(projects.frontend.core.network)
|
||||||
|
implementation(projects.frontend.core.localDb)
|
||||||
|
|
||||||
jsMain.dependencies {
|
// Features - REMOVED: Circular dependency. Shared should NOT depend on features.
|
||||||
implementation(libs.ktor.client.js)
|
// implementation(projects.frontend.features.authFeature)
|
||||||
}
|
// implementation(projects.frontend.features.pingFeature)
|
||||||
|
|
||||||
// WASM SourceSet, nur wenn aktiviert
|
// KMP Bundles
|
||||||
if (enableWasm) {
|
implementation(libs.bundles.kmp.common)
|
||||||
val wasmJsMain = getByName("wasmJsMain")
|
implementation(libs.bundles.compose.common)
|
||||||
wasmJsMain.dependencies {
|
|
||||||
implementation(libs.ktor.client.js) // WASM verwendet JS-Clients
|
|
||||||
implementation(compose.runtime)
|
|
||||||
implementation(compose.foundation)
|
|
||||||
implementation(compose.material3)
|
|
||||||
|
|
||||||
}
|
// Compose
|
||||||
|
implementation(compose.runtime)
|
||||||
|
implementation(compose.foundation)
|
||||||
|
implementation(compose.material3)
|
||||||
|
implementation(compose.ui)
|
||||||
|
implementation(compose.components.resources)
|
||||||
|
implementation(compose.components.uiToolingPreview)
|
||||||
|
|
||||||
|
// Koin
|
||||||
|
implementation(libs.koin.core)
|
||||||
|
implementation(libs.koin.compose)
|
||||||
|
implementation(libs.koin.compose.viewmodel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
commonTest {
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.kotlin.test)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val desktopMain by getting {
|
||||||
|
dependencies {
|
||||||
|
implementation(compose.desktop.currentOs)
|
||||||
|
implementation(libs.kotlinx.coroutines.swing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val jsMain by getting {
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.ktor.client.js)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val wasmJsMain by getting {
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.ktor.client.js)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// KMP Compile-Optionen
|
|
||||||
tasks.withType<KotlinCompile> {
|
|
||||||
compilerOptions {
|
|
||||||
jvmTarget.set(JvmTarget.JVM_25)
|
|
||||||
freeCompilerArgs.addAll(
|
|
||||||
"-opt-in=kotlin.RequiresOptIn"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.withType<KotlinJsCompile>().configureEach {
|
|
||||||
compilerOptions {
|
|
||||||
target = "es2015"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ plugins {
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
// Toolchain is now handled centrally in the root build.gradle.kts
|
// Toolchain is now handled centrally in the root build.gradle.kts
|
||||||
val enableWasm = providers.gradleProperty("enableWasm").orNull == "true"
|
|
||||||
|
|
||||||
// JVM Target für Desktop
|
// JVM Target für Desktop
|
||||||
jvm {
|
jvm {
|
||||||
|
|
@ -62,13 +61,11 @@ kotlin {
|
||||||
binaries.executable()
|
binaries.executable()
|
||||||
}
|
}
|
||||||
|
|
||||||
// WASM, nur wenn explizit aktiviert
|
// Wasm enabled by default
|
||||||
if (enableWasm) {
|
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||||
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
wasmJs {
|
||||||
wasmJs {
|
browser()
|
||||||
browser()
|
binaries.executable()
|
||||||
binaries.executable()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
|
@ -110,17 +107,14 @@ kotlin {
|
||||||
implementation(compose.html.core)
|
implementation(compose.html.core)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WASM SourceSet, nur wenn aktiviert
|
val wasmJsMain = getByName("wasmJsMain")
|
||||||
if (enableWasm) {
|
wasmJsMain.dependencies {
|
||||||
val wasmJsMain = getByName("wasmJsMain")
|
implementation(libs.ktor.client.js) // WASM verwendet JS-Client [cite: 7]
|
||||||
wasmJsMain.dependencies {
|
|
||||||
implementation(libs.ktor.client.js) // WASM verwendet JS-Client [cite: 7]
|
|
||||||
|
|
||||||
// Compose für shared UI components für WASM
|
// Compose für shared UI components für WASM
|
||||||
implementation(compose.runtime)
|
implementation(compose.runtime)
|
||||||
implementation(compose.foundation)
|
implementation(compose.foundation)
|
||||||
implementation(compose.material3)
|
implementation(compose.material3)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
commonTest.dependencies {
|
commonTest.dependencies {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# This file is the SINGLE SOURCE OF TRUTH for all project dependencies.
|
# This file is the SINGLE SOURCE OF TRUTH for all project dependencies.
|
||||||
# Organized by Domain: Frontend (KMP) vs. Backend (Spring/JVM)
|
# Organized by Domain: Frontend (KMP) vs. Backend (Spring/JVM)
|
||||||
# Last updated: 2026-01-08
|
# Last updated: 2026-01-09
|
||||||
|
|
||||||
[versions]
|
[versions]
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
@ -40,6 +40,7 @@ sqlite = "2.6.2"
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Spring Ecosystem
|
# Spring Ecosystem
|
||||||
springBoot = "3.5.9"
|
springBoot = "3.5.9"
|
||||||
|
# Downgraded from 2025.1.0 (Oakwood) to 2025.0.1 (Northfields) for Spring Boot 3.5.x compatibility
|
||||||
springCloud = "2025.0.1"
|
springCloud = "2025.0.1"
|
||||||
springDependencyManagement = "1.1.7"
|
springDependencyManagement = "1.1.7"
|
||||||
springdoc = "3.0.0"
|
springdoc = "3.0.0"
|
||||||
|
|
@ -200,6 +201,9 @@ exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "e
|
||||||
exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" }
|
exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" }
|
||||||
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
|
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
|
||||||
exposed-kotlin-datetime = { module = "org.jetbrains.exposed:exposed-kotlin-datetime", version.ref = "exposed" }
|
exposed-kotlin-datetime = { module = "org.jetbrains.exposed:exposed-kotlin-datetime", version.ref = "exposed" }
|
||||||
|
exposed-java-time = { module = "org.jetbrains.exposed:exposed-java-time", version.ref = "exposed" }
|
||||||
|
exposed-json = { module = "org.jetbrains.exposed:exposed-json", version.ref = "exposed" }
|
||||||
|
exposed-money = { module = "org.jetbrains.exposed:exposed-money", version.ref = "exposed" }
|
||||||
|
|
||||||
postgresql-driver = { module = "org.postgresql:postgresql", version.ref = "postgresql" }
|
postgresql-driver = { module = "org.postgresql:postgresql", version.ref = "postgresql" }
|
||||||
hikari-cp = { module = "com.zaxxer:HikariCP", version.ref = "hikari" }
|
hikari-cp = { module = "com.zaxxer:HikariCP", version.ref = "hikari" }
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,9 @@ include(":backend:infrastructure:messaging:messaging-config")
|
||||||
include(":backend:infrastructure:monitoring:monitoring-client")
|
include(":backend:infrastructure:monitoring:monitoring-client")
|
||||||
include(":backend:infrastructure:monitoring:monitoring-server")
|
include(":backend:infrastructure:monitoring:monitoring-server")
|
||||||
|
|
||||||
|
// --- PERSISTENCE ---
|
||||||
|
include(":backend:infrastructure:persistence")
|
||||||
|
|
||||||
// === BACKEND - SERVICES ===
|
// === BACKEND - SERVICES ===
|
||||||
// --- ENTRIES (Nennungen) ---
|
// --- ENTRIES (Nennungen) ---
|
||||||
include(":backend:services:entries:entries-api")
|
include(":backend:services:entries:entries-api")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user