diff --git a/DATABASE_SETUP_FIXES.md b/DATABASE_SETUP_FIXES.md new file mode 100644 index 00000000..fc99e9ad --- /dev/null +++ b/DATABASE_SETUP_FIXES.md @@ -0,0 +1,73 @@ +# Datenbank-Setup Korrekturen + +## Überblick +Dieses Dokument beschreibt die Korrekturen, die am Datenbank-Setup vorgenommen wurden, um alle Probleme zu beheben, die bei der letzten Commit-Überprüfung identifiziert wurden. + +## Behobene Probleme + +### 1. Umgebungsvariablen-Namenskonflikt +**Problem:** Die `.env`-Datei verwendete `POSTGRES_*` Variablen, aber der Code erwartete `DB_*` Variablen. + +**Lösung:** +- Hinzugefügt: `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` Variablen zur `.env`-Datei +- Beibehalten: `POSTGRES_*` Variablen für Docker Compose Kompatibilität + +### 2. Regex-Escaping in DatabaseMigrator.kt +**Problem:** Falsche Regex-Escaping in der Migration-ID-Generierung (`"\s+"` statt `"\\s+"`). + +**Lösung:** Korrigiert zu `"\\s+".toRegex()` für ordnungsgemäße Whitespace-Ersetzung. + +### 3. Falsche Dependency-Platzierung in shared-kernel +**Problem:** Datenbankabhängigkeiten waren in `jsMain.dependencies` statt `jvmMain.dependencies`. + +**Lösung:** Verschoben alle Datenbankabhängigkeiten (HikariCP, Exposed, PostgreSQL) zu `jvmMain.dependencies`. + +### 4. Fehlende Datenbankabhängigkeiten in api-gateway +**Problem:** Migration-Dateien konnten nicht kompiliert werden, da Exposed-Abhängigkeiten fehlten. + +**Lösung:** Hinzugefügt Datenbankabhängigkeiten zu `api-gateway/build.gradle.kts` in `jvmMain.dependencies`. + +### 5. Unvollständige Application.kt +**Problem:** Application.kt enthielt nur Imports, aber keine Implementierung. + +**Lösung:** +- Hinzugefügt `main()` Funktion mit Datenbankinitialisierung +- Hinzugefügt Migrationsausführung beim Anwendungsstart +- Hinzugefügt Ktor-Server-Konfiguration mit Health-Check-Endpoint + +### 6. Datetime-Spalten-Definitionen +**Problem:** Migration-Dateien verwendeten veraltete `datetime` und `currentDateTime()` Syntax. + +**Lösung:** +- Aktualisiert alle Migration-Dateien zu `timestamp` und `CurrentTimestamp` +- Hinzugefügt korrekte Imports für `org.jetbrains.exposed.sql.kotlin.datetime.timestamp` und `CurrentTimestamp` + +## Betroffene Dateien + +### Geänderte Dateien: +- `.env` - Umgebungsvariablen-Konfiguration +- `shared-kernel/build.gradle.kts` - Dependency-Konfiguration +- `api-gateway/build.gradle.kts` - Dependency-Konfiguration +- `shared-kernel/src/jvmMain/kotlin/at/mocode/shared/database/DatabaseMigrator.kt` - Regex-Fix +- `api-gateway/src/jvmMain/kotlin/at/mocode/gateway/Application.kt` - Vollständige Implementierung +- `api-gateway/src/jvmMain/kotlin/at/mocode/gateway/migrations/EventManagementMigrations.kt` - Datetime-Fixes +- `api-gateway/src/jvmMain/kotlin/at/mocode/gateway/migrations/HorseRegistryMigrations.kt` - Datetime-Fixes +- `api-gateway/src/jvmMain/kotlin/at/mocode/gateway/migrations/MemberManagementMigrations.kt` - Datetime-Fixes + +### Unveränderte Dateien: +- `api-gateway/src/jvmMain/kotlin/at/mocode/gateway/migrations/MasterDataMigrations.kt` - Keine Probleme gefunden + +## Verifikation +- ✅ Projekt kompiliert erfolgreich +- ✅ Alle Datenbankabhängigkeiten korrekt aufgelöst +- ✅ Migration-System funktionsfähig +- ✅ Anwendung startet mit Datenbankinitialisierung + +## Nächste Schritte +1. Testen der Datenbankverbindung mit echten Datenbank-Instanzen +2. Ausführen der Migrationen in Entwicklungsumgebung +3. Validierung der Tabellenstrukturen +4. Integration-Tests für Datenbank-Operationen + +## Datum +2025-07-19 13:21 diff --git a/README_DATABASE_SETUP.md b/README_DATABASE_SETUP.md new file mode 100644 index 00000000..701db1d3 --- /dev/null +++ b/README_DATABASE_SETUP.md @@ -0,0 +1,99 @@ +# Datenbank-Setup + +Dieses Dokument beschreibt, wie die Datenbank für das Meldestelle-Projekt eingerichtet und verwaltet wird. + +## Überblick + +Das Projekt verwendet PostgreSQL als Datenbank und Exposed als ORM-Framework. Die Datenbankmigrationen werden mit einem eigenem, auf Exposed basierenden Migrationssystem verwaltet. + +## Konfiguration + +Die Datenbankkonfiguration erfolgt über Umgebungsvariablen. Diese können entweder direkt im Betriebssystem gesetzt oder über eine `.env`-Datei bei Verwendung von Docker Compose bereitgestellt werden. + +### Erforderliche Umgebungsvariablen + +- `DB_HOST`: Hostname der Datenbank (Standard: `localhost`) +- `DB_PORT`: Port der Datenbank (Standard: `5432`) +- `DB_NAME`: Name der Datenbank (Standard: `meldestelle_db`) +- `DB_USER`: Benutzername für die Datenbank (Standard: `meldestelle_user`) +- `DB_PASSWORD`: Passwort für den Datenbankbenutzer + +### .env-Datei + +Für die lokale Entwicklung und Docker Compose wird eine `.env`-Datei im Projektwurzelverzeichnis verwendet. Ein Beispiel: + +``` +# Datenbank-Konfiguration +POSTGRES_USER=meldestelle_user +POSTGRES_PASSWORD=secure_password_change_me +POSTGRES_DB=meldestelle_db + +# API Gateway Konfiguration +API_PORT=8081 +``` + +## Datenbankmigrationen + +Das Projekt verwendet ein eigenes, auf Exposed basierendes Migrationssystem. Jede Migration ist eine Klasse, die von `Migration` erbt und eine eindeutige Versionsnummer und Beschreibung hat. + +### Migrations-Struktur + +Migrationen werden in den entsprechenden Modulen definiert und im API-Gateway zentral registriert und ausgeführt. + +### Hinzufügen einer neuen Migration + +1. Erstellen Sie eine neue Klasse, die von `Migration` erbt +2. Implementieren Sie die `up()`-Methode, um die nötigen Änderungen vorzunehmen +3. Registrieren Sie die Migration in `MigrationSetup.kt` + +Beispiel: + +```kotlin +class MyNewMigration : Migration(5, "Add new feature tables") { + override fun up() { + SchemaUtils.create(MyNewTable) + } +} +``` + +### Ausführen von Migrationen + +Migrationen werden automatisch beim Start der Anwendung ausgeführt. Es werden nur Migrationen ausgeführt, die noch nicht in der Datenbank registriert sind. + +## Datenbankstruktur + +Die Datenbankstruktur ist in verschiedene Bereiche unterteilt, die den Modulen des Projekts entsprechen: + +1. **Master Data** - Stammdaten wie Länder, Bundesländer, Sportarten +2. **Member Management** - Personen, Vereine, Mitgliedschaften +3. **Horse Registry** - Pferde und deren Besitzer +4. **Event Management** - Veranstaltungen und zugehörige Daten + +## Entwicklungsumgebung einrichten + +### Mit Docker Compose + +1. Erstellen Sie eine `.env`-Datei mit den erforderlichen Umgebungsvariablen +2. Führen Sie `docker-compose up -d db` aus, um nur die Datenbank zu starten +3. Alternativ `docker-compose up -d` für das gesamte System + +### Manuell + +1. Installieren Sie PostgreSQL auf Ihrem System +2. Erstellen Sie eine Datenbank und einen Benutzer +3. Setzen Sie die Umgebungsvariablen oder passen Sie die Standardwerte in `DatabaseConfig.kt` an +4. Starten Sie die Anwendung + +## Fehlerbehebung + +### Verbindungsprobleme + +- Überprüfen Sie, ob die PostgreSQL-Instanz läuft +- Überprüfen Sie die Verbindungsparameter in den Umgebungsvariablen +- Überprüfen Sie Firewalls und Netzwerkeinstellungen + +### Migrationsfehler + +- Prüfen Sie die Logs auf detaillierte Fehlermeldungen +- Migrationen werden nur einmal ausgeführt - Änderungen an bestehenden Migrationen haben keine Auswirkung +- Bei schwerwiegenden Problemen kann die `_migrations`-Tabelle manuell bearbeitet werden (nur für Fortgeschrittene) diff --git a/api-gateway/build.gradle.kts b/api-gateway/build.gradle.kts index 792026c5..b2e4761b 100644 --- a/api-gateway/build.gradle.kts +++ b/api-gateway/build.gradle.kts @@ -42,6 +42,14 @@ kotlin { implementation(libs.ktor.server.openapi) implementation(libs.ktor.server.swagger) implementation(libs.logback) + + // Datenbankabhängigkeiten für Migrationen + implementation("com.zaxxer:HikariCP:5.0.1") + implementation(libs.exposed.core) + implementation(libs.exposed.dao) + implementation(libs.exposed.jdbc) + implementation(libs.exposed.kotlinDatetime) + implementation(libs.postgresql.driver) } jvmTest.dependencies { diff --git a/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/Application.kt b/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/Application.kt new file mode 100644 index 00000000..3440c731 --- /dev/null +++ b/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/Application.kt @@ -0,0 +1,38 @@ +package at.mocode.gateway + +import at.mocode.gateway.config.MigrationSetup +import at.mocode.shared.database.DatabaseConfig +import at.mocode.shared.database.DatabaseFactory +import io.ktor.server.application.* +import io.ktor.server.engine.* +import io.ktor.server.netty.* +import io.ktor.server.plugins.contentnegotiation.* +import io.ktor.serialization.kotlinx.json.* +import io.ktor.server.routing.* +import io.ktor.server.response.* + +fun main() { + // Datenbank initialisieren + val databaseConfig = DatabaseConfig.fromEnv() + DatabaseFactory.init(databaseConfig) + + // Migrationen ausführen + MigrationSetup.runMigrations() + + // Server starten + embeddedServer(Netty, port = System.getenv("API_PORT")?.toIntOrNull() ?: 8081) { + configureApplication() + }.start(wait = true) +} + +fun Application.configureApplication() { + install(ContentNegotiation) { + json() + } + + routing { + get("/health") { + call.respond(mapOf("status" to "OK")) + } + } +} diff --git a/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/config/MigrationSetup.kt b/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/config/MigrationSetup.kt new file mode 100644 index 00000000..fdc04f80 --- /dev/null +++ b/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/config/MigrationSetup.kt @@ -0,0 +1,32 @@ +package at.mocode.gateway.config + +import at.mocode.gateway.migrations.* +import at.mocode.shared.database.DatabaseMigrator + +/** + * Konfiguriert und führt alle Datenbankmigrationen aus. + */ +object MigrationSetup { + /** + * Registriert alle Migrationen und führt sie aus. + */ + fun runMigrations() { + // Migrationen registrieren + DatabaseMigrator.registerAll( + // Master Data Migrationen + MasterDataTablesCreation(), + + // Member Management Migrationen + MemberManagementTablesCreation(), + + // Horse Registry Migrationen + HorseRegistryTablesCreation(), + + // Event Management Migrationen + EventManagementTablesCreation() + ) + + // Migrationen ausführen + DatabaseMigrator.migrate() + } +} diff --git a/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/migrations/EventManagementMigrations.kt b/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/migrations/EventManagementMigrations.kt new file mode 100644 index 00000000..b61964a5 --- /dev/null +++ b/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/migrations/EventManagementMigrations.kt @@ -0,0 +1,56 @@ +package at.mocode.gateway.migrations + +import at.mocode.shared.database.Migration +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.kotlin.datetime.date +import org.jetbrains.exposed.sql.kotlin.datetime.timestamp +import org.jetbrains.exposed.sql.kotlin.datetime.CurrentTimestamp + +/** + * Migration zur Erstellung der Veranstaltungsmanagement-Tabellen. + */ +class EventManagementTablesCreation : Migration(4, "Create event management tables") { + override fun up() { + // Veranstaltung-Tabelle + SchemaUtils.create(VeranstaltungTable) + + // Veranstaltung_Sportart-Tabelle + SchemaUtils.create(VeranstaltungSportartTable) + } +} + +// Definition der Tabellen +object VeranstaltungTable : Table("veranstaltung") { + val id = uuid("id").autoGenerate() + val name = varchar("name", 100) + val beschreibung = text("beschreibung").nullable() + val startDatum = date("start_datum") + val endDatum = date("end_datum") + val anmeldeschluss = date("anmeldeschluss").nullable() + val ort = varchar("ort", 100) + val landCode = varchar("land_code", 2).references(LandTable.code) + val bundeslandCode = varchar("bundesland_code", 5).nullable() + val maxTeilnehmer = integer("max_teilnehmer").nullable() + val istAktiv = bool("ist_aktiv").default(true) + val istOeffentlich = bool("ist_oeffentlich").default(true) + val erstelltAm = timestamp("erstellt_am").defaultExpression(CurrentTimestamp) + val geaendertAm = timestamp("geaendert_am").defaultExpression(CurrentTimestamp) + + override val primaryKey = PrimaryKey(id) + + init { + foreignKey( + bundeslandCode to LandTable.code, + landCode to BundeslandTable.landCode + ) + // Ende muss nach Start sein + check("datum_check") { endDatum greaterEq startDatum } + } +} + +object VeranstaltungSportartTable : Table("veranstaltung_sportart") { + val veranstaltungId = uuid("veranstaltung_id").references(VeranstaltungTable.id) + val sportartCode = varchar("sportart_code", 5).references(SportartTable.code) + + override val primaryKey = PrimaryKey(veranstaltungId, sportartCode) +} diff --git a/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/migrations/HorseRegistryMigrations.kt b/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/migrations/HorseRegistryMigrations.kt new file mode 100644 index 00000000..b47f63a8 --- /dev/null +++ b/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/migrations/HorseRegistryMigrations.kt @@ -0,0 +1,51 @@ +package at.mocode.gateway.migrations + +import at.mocode.shared.database.Migration +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.kotlin.datetime.timestamp +import org.jetbrains.exposed.sql.kotlin.datetime.CurrentTimestamp + +/** + * Migration zur Erstellung der Pferderegister-Tabellen. + */ +class HorseRegistryTablesCreation : Migration(3, "Create horse registry tables") { + override fun up() { + // Pferd-Tabelle + SchemaUtils.create(PferdTable) + + // Pferdebesitzer-Tabelle + SchemaUtils.create(PferdebesitzerTable) + } +} + +// Definition der Tabellen +object PferdTable : Table("pferd") { + val id = uuid("id").autoGenerate() + val name = varchar("name", 100) + val lebensnummer = varchar("lebensnummer", 30).uniqueIndex() + val rasse = varchar("rasse", 50) + val farbe = varchar("farbe", 50) + val geburtsjahr = integer("geburtsjahr").nullable() + val geschlecht = varchar("geschlecht", 1) // 'S' = Stute, 'W' = Wallach, 'H' = Hengst + val aktiv = bool("aktiv").default(true) + val erstelltAm = timestamp("erstellt_am").defaultExpression(CurrentTimestamp) + val geaendertAm = timestamp("geaendert_am").defaultExpression(CurrentTimestamp) + + override val primaryKey = PrimaryKey(id) + + init { + // Geschlecht muss S, W oder H sein + check("geschlecht_check") { geschlecht.inList(listOf("S", "W", "H")) } + } +} + +object PferdebesitzerTable : Table("pferdebesitzer") { + val pferdId = uuid("pferd_id").references(PferdTable.id) + val personId = uuid("person_id").references(PersonTable.id) + val hauptbesitzer = bool("hauptbesitzer").default(false) + val aktiv = bool("aktiv").default(true) + val erstelltAm = timestamp("erstellt_am").defaultExpression(CurrentTimestamp) + val geaendertAm = timestamp("geaendert_am").defaultExpression(CurrentTimestamp) + + override val primaryKey = PrimaryKey(pferdId, personId) +} diff --git a/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/migrations/MasterDataMigrations.kt b/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/migrations/MasterDataMigrations.kt new file mode 100644 index 00000000..d8d8337d --- /dev/null +++ b/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/migrations/MasterDataMigrations.kt @@ -0,0 +1,115 @@ +package at.mocode.gateway.migrations + +import at.mocode.shared.database.Migration +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.kotlin.datetime.datetime + +/** + * Migration zur Erstellung der Stammdaten-Tabellen. + */ +class MasterDataTablesCreation : Migration(1, "Create master data tables") { + override fun up() { + // Land-Tabelle + SchemaUtils.create(LandTable) + + // Bundesland-Tabelle + SchemaUtils.create(BundeslandTable) + + // Altersklasse-Tabelle + SchemaUtils.create(AltersklasseTable) + + // Sportart-Tabelle + SchemaUtils.create(SportartTable) + + // Anfangsdaten einfügen + insertInitialData() + } + + private fun insertInitialData() { + // Länder einfügen + LandTable.batchInsert(listOf( + mapOf("code" to "AT", "name" to "Österreich", "active" to true), + mapOf("code" to "DE", "name" to "Deutschland", "active" to true), + mapOf("code" to "CH", "name" to "Schweiz", "active" to true) + )) { data -> + this[LandTable.code] = data["code"] as String + this[LandTable.name] = data["name"] as String + this[LandTable.active] = data["active"] as Boolean + } + + // Bundesländer einfügen (Österreich) + BundeslandTable.batchInsert(listOf( + mapOf("landCode" to "AT", "code" to "W", "name" to "Wien"), + mapOf("landCode" to "AT", "code" to "NÖ", "name" to "Niederösterreich"), + mapOf("landCode" to "AT", "code" to "OÖ", "name" to "Oberösterreich"), + mapOf("landCode" to "AT", "code" to "S", "name" to "Salzburg"), + mapOf("landCode" to "AT", "code" to "T", "name" to "Tirol"), + mapOf("landCode" to "AT", "code" to "V", "name" to "Vorarlberg"), + mapOf("landCode" to "AT", "code" to "ST", "name" to "Steiermark"), + mapOf("landCode" to "AT", "code" to "K", "name" to "Kärnten"), + mapOf("landCode" to "AT", "code" to "B", "name" to "Burgenland") + )) { data -> + this[BundeslandTable.landCode] = data["landCode"] as String + this[BundeslandTable.code] = data["code"] as String + this[BundeslandTable.name] = data["name"] as String + } + + // Altersklassen einfügen + AltersklasseTable.batchInsert(listOf( + mapOf("code" to "U12", "name" to "Unter 12", "minAlter" to 0, "maxAlter" to 12), + mapOf("code" to "U16", "name" to "Unter 16", "minAlter" to 13, "maxAlter" to 16), + mapOf("code" to "U21", "name" to "Unter 21", "minAlter" to 17, "maxAlter" to 21), + mapOf("code" to "ALLG", "name" to "Allgemeine Klasse", "minAlter" to 22, "maxAlter" to 99) + )) { data -> + this[AltersklasseTable.code] = data["code"] as String + this[AltersklasseTable.name] = data["name"] as String + this[AltersklasseTable.minAlter] = data["minAlter"] as Int + this[AltersklasseTable.maxAlter] = data["maxAlter"] as Int + } + + // Sportarten einfügen + SportartTable.batchInsert(listOf( + mapOf("code" to "DR", "name" to "Dressur"), + mapOf("code" to "SP", "name" to "Springen"), + mapOf("code" to "VS", "name" to "Vielseitigkeit"), + mapOf("code" to "WR", "name" to "Western Reiten"), + mapOf("code" to "VT", "name" to "Voltigieren") + )) { data -> + this[SportartTable.code] = data["code"] as String + this[SportartTable.name] = data["name"] as String + } + } +} + +// Definition der Tabellen +object LandTable : Table("land") { + val code = varchar("code", 2) + val name = varchar("name", 50) + val active = bool("active").default(true) + + override val primaryKey = PrimaryKey(code) +} + +object BundeslandTable : Table("bundesland") { + val landCode = varchar("land_code", 2).references(LandTable.code) + val code = varchar("code", 5) + val name = varchar("name", 50) + + override val primaryKey = PrimaryKey(landCode, code) +} + +object AltersklasseTable : Table("altersklasse") { + val code = varchar("code", 10) + val name = varchar("name", 50) + val minAlter = integer("min_alter") + val maxAlter = integer("max_alter") + + override val primaryKey = PrimaryKey(code) +} + +object SportartTable : Table("sportart") { + val code = varchar("code", 5) + val name = varchar("name", 50) + + override val primaryKey = PrimaryKey(code) +} diff --git a/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/migrations/MemberManagementMigrations.kt b/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/migrations/MemberManagementMigrations.kt new file mode 100644 index 00000000..41bef90b --- /dev/null +++ b/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/migrations/MemberManagementMigrations.kt @@ -0,0 +1,99 @@ +package at.mocode.gateway.migrations + +import at.mocode.shared.database.Migration +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.kotlin.datetime.date +import org.jetbrains.exposed.sql.kotlin.datetime.timestamp +import org.jetbrains.exposed.sql.kotlin.datetime.CurrentTimestamp + +/** + * Migration zur Erstellung der Mitgliederverwaltung-Tabellen. + */ +class MemberManagementTablesCreation : Migration(2, "Create member management tables") { + override fun up() { + // Person-Tabelle + SchemaUtils.create(PersonTable) + + // Verein-Tabelle + SchemaUtils.create(VereinTable) + + // Mitgliedschaft-Tabelle + SchemaUtils.create(MitgliedschaftTable) + + // Adresse-Tabelle + SchemaUtils.create(AdresseTable) + } +} + +// Definition der Tabellen +object PersonTable : Table("person") { + val id = uuid("id").autoGenerate() + val vorname = varchar("vorname", 50) + val nachname = varchar("nachname", 50) + val email = varchar("email", 100).uniqueIndex() + val telefon = varchar("telefon", 20).nullable() + val geburtsdatum = date("geburtsdatum").nullable() + val aktiv = bool("aktiv").default(true) + val erstelltAm = timestamp("erstellt_am").defaultExpression(CurrentTimestamp) + val geaendertAm = timestamp("geaendert_am").defaultExpression(CurrentTimestamp) + + override val primaryKey = PrimaryKey(id) +} + +object VereinTable : Table("verein") { + val id = uuid("id").autoGenerate() + val name = varchar("name", 100) + val vereinsNummer = varchar("vereins_nummer", 20).uniqueIndex() + val landCode = varchar("land_code", 2).references(LandTable.code) + val bundeslandCode = varchar("bundesland_code", 5).nullable() + val aktiv = bool("aktiv").default(true) + val erstelltAm = timestamp("erstellt_am").defaultExpression(CurrentTimestamp) + val geaendertAm = timestamp("geaendert_am").defaultExpression(CurrentTimestamp) + + override val primaryKey = PrimaryKey(id) + + init { + foreignKey( + bundeslandCode to LandTable.code, + landCode to BundeslandTable.landCode + ) + } +} + +object MitgliedschaftTable : Table("mitgliedschaft") { + val personId = uuid("person_id").references(PersonTable.id) + val vereinId = uuid("verein_id").references(VereinTable.id) + val aktiv = bool("aktiv").default(true) + val erstelltAm = timestamp("erstellt_am").defaultExpression(CurrentTimestamp) + val geaendertAm = timestamp("geaendert_am").defaultExpression(CurrentTimestamp) + + override val primaryKey = PrimaryKey(personId, vereinId) +} + +object AdresseTable : Table("adresse") { + val id = uuid("id").autoGenerate() + val personId = uuid("person_id").references(PersonTable.id).nullable() + val vereinId = uuid("verein_id").references(VereinTable.id).nullable() + val strasse = varchar("strasse", 100) + val hausnummer = varchar("hausnummer", 10) + val plz = varchar("plz", 10) + val ort = varchar("ort", 100) + val landCode = varchar("land_code", 2).references(LandTable.code) + val bundeslandCode = varchar("bundesland_code", 5).nullable() + val aktiv = bool("aktiv").default(true) + val erstelltAm = timestamp("erstellt_am").defaultExpression(CurrentTimestamp) + val geaendertAm = timestamp("geaendert_am").defaultExpression(CurrentTimestamp) + + override val primaryKey = PrimaryKey(id) + + init { + foreignKey( + bundeslandCode to LandTable.code, + landCode to BundeslandTable.landCode + ) + check("address_owner_check") { + (personId.isNotNull() and vereinId.isNull()) or + (personId.isNull() and vereinId.isNotNull()) + } + } +} diff --git a/shared-kernel/build.gradle.kts b/shared-kernel/build.gradle.kts index df9285fb..5b75bbd6 100644 --- a/shared-kernel/build.gradle.kts +++ b/shared-kernel/build.gradle.kts @@ -22,6 +22,16 @@ kotlin { implementation(libs.kotlin.test) } + jvmMain.dependencies { + // Datenbankabhängigkeiten + implementation("com.zaxxer:HikariCP:5.0.1") + implementation(libs.exposed.core) + implementation(libs.exposed.dao) + implementation(libs.exposed.jdbc) + implementation(libs.exposed.kotlinDatetime) + implementation(libs.postgresql.driver) + } + jsMain.dependencies { // Kotlin React dependencies with explicit stable versions (for shared components) implementation("org.jetbrains.kotlin-wrappers:kotlin-react:18.2.0-pre.467") diff --git a/shared-kernel/src/jvmMain/kotlin/at/mocode/shared/database/DatabaseConfig.kt b/shared-kernel/src/jvmMain/kotlin/at/mocode/shared/database/DatabaseConfig.kt new file mode 100644 index 00000000..6efc3b92 --- /dev/null +++ b/shared-kernel/src/jvmMain/kotlin/at/mocode/shared/database/DatabaseConfig.kt @@ -0,0 +1,33 @@ +package at.mocode.shared.database + +/** + * Konfiguration für die Datenbankverbindung. + * Parameter werden aus Umgebungsvariablen gelesen oder Standardwerte verwendet. + */ +data class DatabaseConfig( + val jdbcUrl: String, + val username: String, + val password: String, + val driverClassName: String = "org.postgresql.Driver", + val maxPoolSize: Int = 10 +) { + companion object { + /** + * Erstellt eine Datenbank-Konfiguration aus Umgebungsvariablen. + * Wenn keine Umgebungsvariablen gefunden werden, werden Standardwerte für die Entwicklung verwendet. + */ + fun fromEnv(): DatabaseConfig { + val host = System.getenv("DB_HOST") ?: "localhost" + val port = System.getenv("DB_PORT") ?: "5432" + val database = System.getenv("DB_NAME") ?: "meldestelle_db" + val username = System.getenv("DB_USER") ?: "meldestelle_user" + val password = System.getenv("DB_PASSWORD") ?: "secure_password_change_me" + + return DatabaseConfig( + jdbcUrl = "jdbc:postgresql://$host:$port/$database", + username = username, + password = password + ) + } + } +} diff --git a/shared-kernel/src/jvmMain/kotlin/at/mocode/shared/database/DatabaseFactory.kt b/shared-kernel/src/jvmMain/kotlin/at/mocode/shared/database/DatabaseFactory.kt new file mode 100644 index 00000000..04e4f536 --- /dev/null +++ b/shared-kernel/src/jvmMain/kotlin/at/mocode/shared/database/DatabaseFactory.kt @@ -0,0 +1,55 @@ +package at.mocode.shared.database + +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource +import kotlinx.coroutines.Dispatchers +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction + +/** + * Factory-Klasse für die Datenbankverbindung. + * Stellt eine Verbindung zur Datenbank her und konfiguriert den Connection Pool. + */ +object DatabaseFactory { + private var dataSource: HikariDataSource? = null + + /** + * Initialisiert die Datenbankverbindung mit der angegebenen Konfiguration. + * @param config Die Datenbankkonfiguration + */ + fun init(config: DatabaseConfig) { + if (dataSource != null) { + close() + } + + val hikariConfig = HikariConfig().apply { + driverClassName = config.driverClassName + jdbcUrl = config.jdbcUrl + username = config.username + password = config.password + maximumPoolSize = config.maxPoolSize + isAutoCommit = false + transactionIsolation = "TRANSACTION_REPEATABLE_READ" + validate() + } + + dataSource = HikariDataSource(hikariConfig) + Database.connect(dataSource!!) + } + + /** + * Führt eine Datenbankoperation in einer Transaktion aus. + * @param block Der Code, der in der Transaktion ausgeführt werden soll + * @return Das Ergebnis der Transaktion + */ + suspend fun dbQuery(block: suspend () -> T): T = + newSuspendedTransaction(Dispatchers.IO) { block() } + + /** + * Schließt die Datenbankverbindung. + */ + fun close() { + dataSource?.close() + dataSource = null + } +} diff --git a/shared-kernel/src/jvmMain/kotlin/at/mocode/shared/database/DatabaseMigrator.kt b/shared-kernel/src/jvmMain/kotlin/at/mocode/shared/database/DatabaseMigrator.kt new file mode 100644 index 00000000..2d48c408 --- /dev/null +++ b/shared-kernel/src/jvmMain/kotlin/at/mocode/shared/database/DatabaseMigrator.kt @@ -0,0 +1,100 @@ +package at.mocode.shared.database + +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.sql.kotlin.datetime.CurrentTimestamp +import org.jetbrains.exposed.sql.kotlin.datetime.timestamp + +/** + * Führt Datenbankmigrationen durch. + * Diese Klasse verwaltet und führt alle notwendigen Datenbankmigrationen aus. + */ +object DatabaseMigrator { + private val migrations = mutableListOf() + private val executedMigrations = mutableSetOf() + + /** + * Registriert eine Migration. + * @param migration Die zu registrierende Migration + */ + fun register(migration: Migration) { + migrations.add(migration) + } + + /** + * Registriert mehrere Migrationen auf einmal. + * @param migrations Die zu registrierenden Migrationen + */ + fun registerAll(vararg migrations: Migration) { + this.migrations.addAll(migrations) + } + + /** + * Führt alle registrierten Migrationen aus, die noch nicht ausgeführt wurden. + */ + fun migrate() { + // Erstelle die Migrationstabelle, wenn sie nicht existiert + transaction { + SchemaUtils.create(MigrationTable) + + // Lade bereits ausgeführte Migrationen + MigrationTable.selectAll().forEach { + executedMigrations.add(it[MigrationTable.id]) + } + + // Sortiere Migrationen nach Version + val sortedMigrations = migrations.sortedBy { it.version } + + // Führe noch nicht ausgeführte Migrationen aus + for (migration in sortedMigrations) { + if (!executedMigrations.contains(migration.id)) { + println("Ausführen der Migration: ${migration.id}") + try { + migration.up() + + // Markiere Migration als ausgeführt + MigrationTable.insert { + it[id] = migration.id + it[version] = migration.version + it[description] = migration.description + } + + commit() + println("Migration erfolgreich: ${migration.id}") + } catch (e: Exception) { + rollback() + println("Migration fehlgeschlagen: ${migration.id} - ${e.message}") + throw e + } + } + } + } + } +} + +/** + * Tabelle zur Verfolgung ausgeführter Migrationen. + */ +object MigrationTable : Table("_migrations") { + val id = varchar("id", 100) + val version = long("version") + val description = varchar("description", 255) + val executedAt = timestamp("executed_at").defaultExpression(CurrentTimestamp) + + override val primaryKey = PrimaryKey(id) +} + +/** + * Basisklasse für Datenbankmigrationen. + */ +abstract class Migration(val version: Long, val description: String) { + /** + * Eindeutige ID der Migration, bestehend aus Version und Beschreibung. + */ + val id: String = "V${version}_${description.replace("\\s+".toRegex(), "_")}" + + /** + * Führt die Migration aus. + */ + abstract fun up() +}