diff --git a/backend/infrastructure/persistence/src/main/kotlin/at/mocode/backend/infrastructure/persistence/DatabaseUtils.kt b/backend/infrastructure/persistence/src/main/kotlin/at/mocode/backend/infrastructure/persistence/DatabaseUtils.kt index 18d2904c..a08db3c1 100644 --- a/backend/infrastructure/persistence/src/main/kotlin/at/mocode/backend/infrastructure/persistence/DatabaseUtils.kt +++ b/backend/infrastructure/persistence/src/main/kotlin/at/mocode/backend/infrastructure/persistence/DatabaseUtils.kt @@ -4,10 +4,11 @@ import at.mocode.core.domain.model.ErrorCodes import at.mocode.core.domain.model.ErrorDto import at.mocode.core.domain.model.PagedResponse import at.mocode.core.utils.Result +import org.jetbrains.exposed.v1.core.Column import org.jetbrains.exposed.v1.core.ResultRow import org.jetbrains.exposed.v1.core.Table -import org.jetbrains.exposed.v1.core.Column import org.jetbrains.exposed.v1.core.statements.BatchInsertStatement +import org.jetbrains.exposed.v1.core.statements.StatementType import org.jetbrains.exposed.v1.jdbc.Database import org.jetbrains.exposed.v1.jdbc.JdbcTransaction import org.jetbrains.exposed.v1.jdbc.Query @@ -28,9 +29,13 @@ inline fun transactionResult( crossinline block: JdbcTransaction.() -> T ): Result { return try { - val result = transaction(database) { block() } + // Wir nutzen hier explizit Exposed JDBC Transaktionen. + // Der Cast ist sicher, solange wir nur JDBC Databases verwenden (was wir tun). + val result = transaction(database) { + this.block() + } Result.success(result) - } catch (e: SQLTimeoutException) { + } catch (_: SQLTimeoutException) { Result.failure( ErrorDto( code = ErrorCodes.DATABASE_TIMEOUT, @@ -40,26 +45,25 @@ inline fun transactionResult( } 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 + "40001" -> ErrorCodes.DATABASE_ERROR // serialization_failure / deadlock + "08000", "08003", "08006" -> ErrorCodes.DATABASE_ERROR // connection errors else -> ErrorCodes.DATABASE_ERROR } Result.failure( ErrorDto( code = mapped, - message = "Datenbank-Operation fehlgeschlagen" + message = "Datenbank-Operation fehlgeschlagen: ${e.message}" ) ) } catch (e: Exception) { Result.failure( ErrorDto( code = ErrorCodes.TRANSACTION_ERROR, - message = "Transaktion fehlgeschlagen" + message = "Transaktion fehlgeschlagen: ${e.message}" ) ) } @@ -87,14 +91,11 @@ fun Query.toPagedResponse( size: Int, transform: (ResultRow) -> T ): PagedResponse { - // 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(), @@ -107,23 +108,30 @@ fun Query.toPagedResponse( ) } - // 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 + if (page >= totalPages) { + return PagedResponse.create( + content = emptyList(), + page = page, + size = size, + totalElements = totalCount, + totalPages = totalPages, + hasNext = false, + hasPrevious = totalPages > 0 + ) + } - // Then apply pagination and transform results - val content = this.paginate(adjustedPage, size).map(transform) + val content = this.paginate(page, size).map(transform) return PagedResponse.create( content = content, - page = adjustedPage, + page = page, size = size, totalElements = totalCount, totalPages = totalPages, - hasNext = adjustedPage < totalPages - 1, - hasPrevious = adjustedPage > 0 + hasNext = page < totalPages - 1, + hasPrevious = page > 0 ) } @@ -131,10 +139,10 @@ object DatabaseUtils { fun tableExists(tableName: String, database: Database? = null): Boolean { return transactionResult(database) { - // Postgres-spezifischer, robuster Ansatz über to_regclass val valid = tableName.trim() if (!valid.matches(Regex("^[A-Za-z_][A-Za-z0-9_]*$"))) return@transactionResult false - exec("SELECT to_regclass('$valid')") { rs -> + + this.exec("SELECT to_regclass('$valid')", explicitStatementType = StatementType.SELECT) { rs -> if (rs.next()) rs.getString(1) else null } != null }.fold( @@ -161,7 +169,6 @@ object DatabaseUtils { database: Database? = null ): Result { 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\"" @@ -172,20 +179,27 @@ object DatabaseUtils { 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) + + this.exec(sql, explicitStatementType = StatementType.CREATE) Unit } } fun executeRawSql(sql: String, database: Database? = null): Result = transactionResult(database) { - exec(sql) + this.exec(sql, explicitStatementType = StatementType.OTHER) Unit } fun executeUpdate(sql: String, database: Database? = null): Result = transactionResult(database) { - // Nutzt Exposed PreparedStatementApi, kein AutoCloseable - val ps = this.connection.prepareStatement(sql, false) - ps.executeUpdate() + // Exposed 1.0.0: prepareStatement returns PreparedStatementApi which is NOT AutoCloseable + // and executeUpdate() might be missing on the interface or requires casting. + // We use the safe way via try-finally and closeIfPossible() + val stmt = this.connection.prepareStatement(sql, false) + try { + stmt.executeUpdate() + } finally { + stmt.closeIfPossible() + } } inline fun batchInsert( @@ -218,8 +232,7 @@ fun ResultRow.toMap(): Map { 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 + // Spalten ignorieren } } return result diff --git a/backend/services/events/events-infrastructure/src/main/kotlin/at/mocode/events/infrastructure/persistence/VeranstaltungTable.kt b/backend/services/events/events-infrastructure/src/main/kotlin/at/mocode/events/infrastructure/persistence/VeranstaltungTable.kt index b1637e7b..61f33cab 100644 --- a/backend/services/events/events-infrastructure/src/main/kotlin/at/mocode/events/infrastructure/persistence/VeranstaltungTable.kt +++ b/backend/services/events/events-infrastructure/src/main/kotlin/at/mocode/events/infrastructure/persistence/VeranstaltungTable.kt @@ -1,9 +1,10 @@ package at.mocode.events.infrastructure.persistence import at.mocode.core.domain.model.SparteE -import org.jetbrains.exposed.dao.id.UUIDTable -import org.jetbrains.exposed.sql.kotlin.datetime.date -import org.jetbrains.exposed.sql.kotlin.datetime.timestamp +import org.jetbrains.exposed.v1.core.dao.id.java.UUIDTable +import org.jetbrains.exposed.v1.core.kotlin.datetime.date +import org.jetbrains.exposed.v1.core.kotlin.datetime.timestamp +import org.jetbrains.exposed.v1.core.javaUUID /** * Database table definition for events (Veranstaltung) in the event-management context. @@ -24,7 +25,8 @@ object VeranstaltungTable : UUIDTable("veranstaltungen") { // Location and Organization val ort = varchar("ort", 255) - val veranstalterVereinId = uuid("veranstalter_verein_id") + // Migration to Exposed 1.0.0: Use javaUUID for java.util.UUID compatibility + val veranstalterVereinId = javaUUID("veranstalter_verein_id") // Event Details val sparten = text("sparten") // JSON array of SparteE values diff --git a/backend/services/horses/horses-infrastructure/src/main/kotlin/at/mocode/horses/infrastructure/persistence/HorseTable.kt b/backend/services/horses/horses-infrastructure/src/main/kotlin/at/mocode/horses/infrastructure/persistence/HorseTable.kt index 5088c03b..09dd27ce 100644 --- a/backend/services/horses/horses-infrastructure/src/main/kotlin/at/mocode/horses/infrastructure/persistence/HorseTable.kt +++ b/backend/services/horses/horses-infrastructure/src/main/kotlin/at/mocode/horses/infrastructure/persistence/HorseTable.kt @@ -2,9 +2,10 @@ package at.mocode.horses.infrastructure.persistence import at.mocode.core.domain.model.PferdeGeschlechtE import at.mocode.core.domain.model.DatenQuelleE -import org.jetbrains.exposed.dao.id.UUIDTable -import org.jetbrains.exposed.sql.kotlin.datetime.date -import org.jetbrains.exposed.sql.kotlin.datetime.timestamp +import org.jetbrains.exposed.v1.core.dao.id.java.UUIDTable +import org.jetbrains.exposed.v1.core.kotlin.datetime.date +import org.jetbrains.exposed.v1.core.kotlin.datetime.timestamp +import org.jetbrains.exposed.v1.core.javaUUID /** * Database table definition for horses in the horse-registry context. @@ -21,8 +22,8 @@ object HorseTable : UUIDTable("horses") { val farbe = varchar("farbe", 100).nullable() // Ownership and Responsibility - val besitzerId = uuid("besitzer_id").nullable() - val verantwortlichePersonId = uuid("verantwortliche_person_id").nullable() + val besitzerId = javaUUID("besitzer_id").nullable() + val verantwortlichePersonId = javaUUID("verantwortliche_person_id").nullable() // Breeding Information val zuechterName = varchar("zuechter_name", 255).nullable() diff --git a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/AltersklasseTable.kt b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/AltersklasseTable.kt index 5e255b15..4c316708 100644 --- a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/AltersklasseTable.kt +++ b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/AltersklasseTable.kt @@ -1,8 +1,9 @@ package at.mocode.masterdata.infrastructure.persistence -import org.jetbrains.exposed.sql.Table -import org.jetbrains.exposed.sql.kotlin.datetime.datetime -import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime +import org.jetbrains.exposed.v1.core.Table +import org.jetbrains.exposed.v1.core.kotlin.datetime.datetime +import org.jetbrains.exposed.v1.core.kotlin.datetime.CurrentDateTime +import org.jetbrains.exposed.v1.core.javaUUID /** * Exposed-Tabellendefinition für die Altersklasse-Entität (Altersklassendefinitionen). @@ -11,7 +12,7 @@ import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime * entsprechend der AltersklasseDefinition Domain-Entität. */ object AltersklasseTable : Table("altersklasse") { - val id = uuid("id").autoGenerate() + val id = javaUUID("id").autoGenerate() val altersklasseCode = varchar("altersklasse_code", 50).uniqueIndex() val bezeichnung = varchar("bezeichnung", 200) val minAlter = integer("min_alter").nullable() @@ -19,7 +20,7 @@ object AltersklasseTable : Table("altersklasse") { val stichtagRegelText = varchar("stichtag_regel_text", 500).nullable() val sparteFilter = varchar("sparte_filter", 50).nullable() // Enum as string val geschlechtFilter = char("geschlecht_filter").nullable() - val oetoRegelReferenzId = uuid("oeto_regel_referenz_id").nullable() + val oetoRegelReferenzId = javaUUID("oeto_regel_referenz_id").nullable() val istAktiv = bool("ist_aktiv").default(true) val createdAt = datetime("created_at").defaultExpression(CurrentDateTime) val updatedAt = datetime("updated_at").defaultExpression(CurrentDateTime) diff --git a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/BundeslandTable.kt b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/BundeslandTable.kt index d1040431..4246bc37 100644 --- a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/BundeslandTable.kt +++ b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/BundeslandTable.kt @@ -1,8 +1,9 @@ package at.mocode.masterdata.infrastructure.persistence -import org.jetbrains.exposed.sql.Table -import org.jetbrains.exposed.sql.kotlin.datetime.datetime -import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime +import org.jetbrains.exposed.v1.core.Table +import org.jetbrains.exposed.v1.core.kotlin.datetime.datetime +import org.jetbrains.exposed.v1.core.kotlin.datetime.CurrentDateTime +import org.jetbrains.exposed.v1.core.javaUUID /** * Exposed-Tabellendefinition für die Bundesland-Entität (Bundesländer/Regionen). @@ -11,8 +12,8 @@ import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime * Verwaltungseinheiten entsprechend der BundeslandDefinition Domain-Entität. */ object BundeslandTable : Table("bundesland") { - val id = uuid("id").autoGenerate() - val landId = uuid("land_id").references(LandTable.id) + val id = javaUUID("id").autoGenerate() + val landId = javaUUID("land_id").references(LandTable.id) val oepsCode = varchar("oeps_code", 10).nullable() val iso3166_2_Code = varchar("iso_3166_2_code", 10).nullable() val name = varchar("name", 100) diff --git a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/LandTable.kt b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/LandTable.kt index 4182bd8e..fdad0fe5 100644 --- a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/LandTable.kt +++ b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/LandTable.kt @@ -1,8 +1,9 @@ package at.mocode.masterdata.infrastructure.persistence -import org.jetbrains.exposed.sql.Table -import org.jetbrains.exposed.sql.kotlin.datetime.datetime -import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime +import org.jetbrains.exposed.v1.core.Table +import org.jetbrains.exposed.v1.core.kotlin.datetime.datetime +import org.jetbrains.exposed.v1.core.kotlin.datetime.CurrentDateTime +import org.jetbrains.exposed.v1.core.javaUUID /** * Exposed-Tabellendefinition für die Land-Entität (Länderstammdaten). @@ -11,7 +12,7 @@ import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime * der LandDefinition Domain-Entität. */ object LandTable : Table("land") { - val id = uuid("id").autoGenerate() + val id = javaUUID("id").autoGenerate() val isoAlpha2Code = varchar("iso_alpha2_code", 2).uniqueIndex() val isoAlpha3Code = varchar("iso_alpha3_code", 3).uniqueIndex() val isoNumerischerCode = varchar("iso_numerischer_code", 3).nullable() diff --git a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/PlatzTable.kt b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/PlatzTable.kt index 564c9da3..54e313f4 100644 --- a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/PlatzTable.kt +++ b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/PlatzTable.kt @@ -1,8 +1,9 @@ package at.mocode.masterdata.infrastructure.persistence -import org.jetbrains.exposed.sql.Table -import org.jetbrains.exposed.sql.kotlin.datetime.datetime -import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime +import org.jetbrains.exposed.v1.core.Table +import org.jetbrains.exposed.v1.core.kotlin.datetime.datetime +import org.jetbrains.exposed.v1.core.kotlin.datetime.CurrentDateTime +import org.jetbrains.exposed.v1.core.javaUUID /** * Exposed-Tabellendefinition für die Platz-Entität (Turnierplätze/Wettkampfstätten). @@ -11,8 +12,8 @@ import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime * entsprechend der Platz Domain-Entität. */ object PlatzTable : Table("platz") { - val id = uuid("id").autoGenerate() - val turnierId = uuid("turnier_id") // Foreign key to tournament (not enforced here as tournament might be in different module) + val id = javaUUID("id").autoGenerate() + val turnierId = javaUUID("turnier_id") // Foreign key to tournament (not enforced here as tournament might be in different module) val name = varchar("name", 200) val dimension = varchar("dimension", 50).nullable() val boden = varchar("boden", 100).nullable() diff --git a/docs/01_Architecture/Reference/Architektonische-Evolution-Migrationsleitfaden_Exposed-1-0-0-rc-4_zu_1-0-0_01-2026.md b/docs/01_Architecture/Reference/Architektonische-Evolution-Migrationsleitfaden_Exposed-1-0-0-rc-4_zu_1-0-0_01-2026.md index 638541c5..67466792 100644 --- a/docs/01_Architecture/Reference/Architektonische-Evolution-Migrationsleitfaden_Exposed-1-0-0-rc-4_zu_1-0-0_01-2026.md +++ b/docs/01_Architecture/Reference/Architektonische-Evolution-Migrationsleitfaden_Exposed-1-0-0-rc-4_zu_1-0-0_01-2026.md @@ -96,19 +96,39 @@ Das Handling von JSONB-Spalten in SQLite wurde vereinheitlicht. 5. JSON/SQLite: - Verhalten von `jsonb()` mit `castToJsonFormat` prüfen und ggf. deaktivieren. -## 3. Test-Matrix +## 3. Troubleshooting & Lessons Learned (Update 2026-02-02) + +### 3.1 Low-Level JDBC Zugriff (`exec`, `executeUpdate`) + +Bei der Migration von `DatabaseUtils.kt` traten Probleme mit der Auflösung von `exec` und `executeUpdate` auf. + +* **Problem:** Die generische `Transaction` Klasse bietet in Exposed 1.0.0 keinen direkten Zugriff mehr auf `exec` mit `ResultSet`-Verarbeitung oder `executeUpdate`. Diese Methoden sind nun spezifischer in `JdbcTransaction` oder `PreparedStatementApi` verortet. +* **Lösung:** + * **Explizite `JdbcTransaction`:** Unsere Transaction-Wrapper (`transactionResult`) wurden angepasst, um `JdbcTransaction` als Receiver (`this`) zu erzwingen. + * **`exec` mit `StatementType`:** Aufrufe von `exec` müssen nun den `explicitStatementType` Parameter (z.B. `StatementType.SELECT`) nutzen, damit der Compiler die korrekte Überladung wählt. + * **`executeUpdate` Workaround:** Da `PreparedStatementApi` in Exposed 1.0.0 nicht `AutoCloseable` ist und `executeUpdate` teilweise schwer aufzulösen war, nutzen wir `try-finally` mit `closeIfPossible()` und greifen bei Bedarf auf die native JDBC Connection zu. + +```kotlin +// Beispiel für korrekten Low-Level Zugriff in Exposed 1.0.0 +transactionResult(database) { + // 'this' ist JdbcTransaction + this.exec("SELECT ...", explicitStatementType = StatementType.SELECT) { rs -> ... } +} +``` + +## 4. Test-Matrix - Unit: DSL-Typen (UUID, Zeittypen), Mappings, einfache Inserts/Selects. - Integration: JDBC/Hikari Konfiguration, `batchInsert`, Upsert/Ignore-Pfade. - E2E (Docker): CRUD-Flows über den Service hinweg; Metriken/Health. -## 4. Rollback-Plan +## 5. Rollback-Plan - `git revert` des Version-Bumps in `libs.versions.toml` und ggf. betroffener Anpassungs-Commits. - Rebuild; E2E-Smoketest durchführen. - Dokumentenstatus auf `ARCHIVED` setzen oder Nachtrag mit „Rollback erfolgt“ ergänzen. -## 5. Diagramm (Flow) +## 6. Diagramm (Flow) ```mermaid flowchart TD diff --git a/docs/05_Backend/Guides/Database_Best_Practices.md b/docs/05_Backend/Guides/Database_Best_Practices.md new file mode 100644 index 00000000..269b2aa7 --- /dev/null +++ b/docs/05_Backend/Guides/Database_Best_Practices.md @@ -0,0 +1,58 @@ +--- +type: Guide +status: DRAFT +owner: Backend Developer +date: 2026-02-02 +--- + +# Database Best Practices & Exposed 1.0.0 + +Dieser Guide beschreibt den korrekten Umgang mit der Datenbank-Schicht in unseren Backend-Services, basierend auf JetBrains Exposed 1.0.0. + +## 1. Architektur-Prinzipien + +* **Trennung:** Datenbank-Zugriffe gehören ausschließlich in die `infrastructure/persistence` Schicht. Services nutzen Repositories (Interfaces), keine direkten Exposed-Aufrufe. +* **Transaktionen:** Jede geschäftliche Operation sollte in einer Transaktion laufen. Nutze dafür die Helper aus `DatabaseUtils.kt`. + +## 2. Nutzung von `DatabaseUtils` + +Wir haben zentrale Wrapper für Transaktionen, um Fehlerbehandlung und Logging zu vereinheitlichen. + +### 2.1 Transaktionen starten + +Nutze immer `transactionResult` (oder die Aliase `readTransaction` / `writeTransaction`), um Exposed-Code auszuführen. + +```kotlin +fun findUser(id: UUID): Result = readTransaction { + // 'this' ist hier eine JdbcTransaction + UserTable.select { UserTable.id eq id } + .map { ... } + .singleOrNull() +} +``` + +**Wichtig:** Der Lambda-Receiver ist `JdbcTransaction`. Das ermöglicht Zugriff auf Low-Level JDBC Funktionen, falls nötig. + +### 2.2 Low-Level SQL (`exec`, `executeUpdate`) + +Vermeide rohes SQL, wo immer möglich. Wenn es sein muss (z.B. für Performance-Optimierungen oder spezielle Postgres-Features), beachte folgende Regeln für Exposed 1.0.0: + +* **`exec`:** Nutze immer `explicitStatementType`. + ```kotlin + this.exec("SELECT 1", explicitStatementType = StatementType.SELECT) { rs -> ... } + ``` +* **`executeUpdate`:** Nutze die Helper-Methode `DatabaseUtils.executeUpdate`, da sie sich um das korrekte Schließen von Statements kümmert (Exposed `PreparedStatementApi` ist nicht `AutoCloseable`). + +## 3. Exposed 1.0.0 Besonderheiten + +* **UUIDs:** Nutze `Table.javaUUID()` für `java.util.UUID` Spalten. `Table.uuid()` ist für `kotlin.uuid.Uuid` reserviert. +* **JSONB:** Bei SQLite wird JSON automatisch gewrappt. Prüfe `castToJsonFormat` Flag. + +## 4. Fehlerbehandlung + +`DatabaseUtils` fängt `SQLException` ab und mappt sie auf unsere Domain-Fehler (`ErrorDto`): +* Duplicate Key -> `ErrorCodes.DUPLICATE_ENTRY` +* Foreign Key -> `ErrorCodes.FOREIGN_KEY_VIOLATION` +* Timeout -> `ErrorCodes.DATABASE_TIMEOUT` + +Wirf keine rohen Exceptions aus Repositories. diff --git a/docs/99_Journal/2026-01-29_Session_Log_Roadmap_Update.md b/docs/99_Journal/2026-01-29_Session_Log_Roadmap_Update.md new file mode 100644 index 00000000..9f5c2454 --- /dev/null +++ b/docs/99_Journal/2026-01-29_Session_Log_Roadmap_Update.md @@ -0,0 +1,41 @@ +--- +type: Journal +status: COMPLETED +owner: Lead Architect +date: 2026-01-29 +participants: + - Lead Architect +--- + +# Session Log: 29. Jänner 2026 - Roadmap Update & Phase 4 Kickoff + +## Zielsetzung +Aktualisierung der MASTER ROADMAP nach erfolgreichem Abschluss der "Tracer Bullet" Phase und Vorbereitung auf den Start der fachlichen Implementierung (Phase 4). + +## Durchgeführte Arbeiten + +### 1. Status-Review +* Der "Tracer Bullet" (Ping-Service) ist erfolgreich durch den gesamten Stack implementiert. +* Kritische technische Hürden (SQLDelight Async, Docker Networking, ArchUnit) sind genommen. +* Das Projekt ist bereit für die Skalierung auf echte Fach-Domänen. + +### 2. Roadmap Update +* **Phase 1-3:** Als "ABGESCHLOSSEN" markiert. +* **Phase 4 (Production Packaging & Domain Start):** Als "AKTUELL" definiert. +* **Neue Aufgaben:** + * **DevOps:** Dockerisierung des Frontends. + * **Backend:** Erstellung des `event-service`. + * **Frontend:** Erstellung des `events`-Features. + * **Architecture:** Sicherstellung der ArchUnit-Abdeckung für neue Module. + +### 3. Architecture Review Vorbereitung +* Die bestehenden ArchUnit-Tests (`BackendArchitectureTest`, `FrontendArchitectureTest`) wurden analysiert. +* **Erkenntnis:** + * Backend-Tests erfordern manuelle Erweiterung der Package-Liste (`at.mocode.events..`). + * Frontend-Tests erfordern strikte Einhaltung der Package-Struktur (`at.mocode..feature`). +* Diese Anforderungen wurden explizit in die Roadmap-Tasks aufgenommen. + +## Ergebnis & Status +* Die Roadmap ist aktuell und spiegelt den Projektfortschritt wider. +* Die Aufgaben für die spezialisierten Agenten sind klar definiert. +* Der Fokus liegt nun auf der Erstellung der ersten echten Fachlichkeit ("Events"). diff --git a/docs/99_Journal/2026-01-30_Refactoring_Exposed_Ktor.md b/docs/99_Journal/2026-01-30_Refactoring_Exposed_Ktor.md new file mode 100644 index 00000000..14285595 --- /dev/null +++ b/docs/99_Journal/2026-01-30_Refactoring_Exposed_Ktor.md @@ -0,0 +1,37 @@ +--- +type: Journal +status: COMPLETED +owner: Lead Architect +date: 2026-01-30 +participants: + - Lead Architect +--- + +# Session Log: 30. Jänner 2026 - Refactoring Exposed & Ktor + +## Zielsetzung +Durchführung der strategischen Migration auf Exposed 1.0.0 (Stable) und Ktor 3.4.0, basierend auf den technischen Analyseberichten. + +## Durchgeführte Arbeiten + +### 1. Versions-Update (`libs.versions.toml`) +* **Exposed:** Aktualisiert von `1.0.0-rc-4` auf `1.0.0`. +* **Ktor:** Aktualisiert von `3.3.3` auf `3.4.0`. +* **Neue Dependency:** `ktor-server-routing-openapi` hinzugefügt (für Backend OpenAPI Fix). + +### 2. Exposed Migration (Backend) +* **Problem:** `DatabaseUtils.kt` enthielt veraltete/falsche Imports (`org.jetbrains.exposed.v1...`), die nicht mit Exposed 1.0.0 kompatibel sind. +* **Lösung:** Die Imports wurden auf die Standard-Exposed-Packages (`org.jetbrains.exposed.sql...`) korrigiert. +* **UUID-Thematik:** Die Tabellen-Definitionen (`VeranstaltungTable`, etc.) nutzen weiterhin `UUIDTable`. Es besteht das Risiko, dass Exposed 1.0.0 hier auf `kotlin.uuid.Uuid` gewechselt hat. Dies muss beim nächsten Build verifiziert werden. Falls Kompilierfehler auftreten, müssen die Tabellen auf `javaUUID` bzw. `JavaUUIDTable` (falls existent) migriert werden. + +### 3. Ktor Migration (Frontend & Backend) +* **Frontend:** Das Build-Skript `frontend/core/network/build.gradle.kts` nutzt separate `js` und `wasmJs` Blöcke, daher war keine Umbenennung von `jsAndWasmShared` notwendig. +* **Backend:** Die Dependency `ktor-server-routing-openapi` wurde im Katalog bereitgestellt. Da die Backend-Module (Events, Horses, Masterdata) Ktor Server nutzen, aber keine explizite OpenAPI-Nutzung im Code gefunden wurde (wahrscheinlich SpringDoc), wurde hier kein Code geändert. + +## Offene Punkte / Risiken +* **UUID Kompatibilität:** Die Verwendung von `UUIDTable` im Backend muss gegen Exposed 1.0.0 getestet werden. Es ist möglich, dass hier Breaking Changes zur Laufzeit oder Compile-Zeit auftreten. +* **Ktor JS Target:** Die separate Konfiguration von `js` und `wasmJs` ist funktional, aber das neue `web` Target wäre zukunftssicherer. + +## Nächste Schritte +1. **Build & Test:** Ausführen des kompletten Builds (`./gradlew build`), um Kompilierfehler (insb. UUIDs) zu identifizieren. +2. **Runtime Test:** Starten des Backends und Prüfung der Datenbank-Interaktion. diff --git a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/ping/feature/data/PingApiKoinClient.kt b/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/ping/feature/data/PingApiKoinClient.kt index 2eef23a4..2dc86ee5 100644 --- a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/ping/feature/data/PingApiKoinClient.kt +++ b/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/ping/feature/data/PingApiKoinClient.kt @@ -37,9 +37,9 @@ class PingApiKoinClient(private val client: HttpClient) : PingApi { return client.get("/api/ping/secure").body() } - override suspend fun syncPings(lastSyncTimestamp: Long): List { + override suspend fun syncPings(since: Long): List { return client.get("/api/ping/sync") { - url.parameters.append("lastSyncTimestamp", lastSyncTimestamp.toString()) + url.parameters.append("lastSyncTimestamp", since.toString()) }.body() } }