fixing gradle build
This commit is contained in:
@@ -5,14 +5,18 @@ plugins {
|
|||||||
alias(libs.plugins.kotlin.serialization)
|
alias(libs.plugins.kotlin.serialization)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
freeCompilerArgs.add("-Xopt-in=kotlin.time.ExperimentalTime")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Stellt sicher, dass dieses Modul Zugriff auf die im zentralen Katalog
|
// Stellt sicher, dass dieses Modul Zugriff auf die im zentralen Katalog
|
||||||
// definierten Bibliotheken hat.
|
// definierten Bibliotheken hat.
|
||||||
api(projects.platform.platformDependencies)
|
api(projects.platform.platformDependencies)
|
||||||
|
|
||||||
// Kern-Abhängigkeiten für das Domänen-Modell.
|
// Kern-Abhängigkeiten für das Domänen-Modul.
|
||||||
// `api` wird verwendet, damit Services, die `core-domain` einbinden,
|
|
||||||
// diese Typen ebenfalls direkt nutzen können.
|
|
||||||
api(libs.uuid)
|
api(libs.uuid)
|
||||||
api(libs.kotlinx.serialization.json)
|
api(libs.kotlinx.serialization.json)
|
||||||
api(libs.kotlinx.datetime)
|
api(libs.kotlinx.datetime)
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ package at.mocode.core.domain.event
|
|||||||
|
|
||||||
import com.benasher44.uuid.Uuid
|
import com.benasher44.uuid.Uuid
|
||||||
import com.benasher44.uuid.uuid4
|
import com.benasher44.uuid.uuid4
|
||||||
import kotlinx.datetime.Clock
|
import java.util.UUID
|
||||||
import kotlinx.datetime.Instant
|
import kotlin.time.Clock
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base interface for all domain events in the system.
|
* Base interface for all domain events in the system.
|
||||||
@@ -12,9 +13,9 @@ import kotlinx.datetime.Instant
|
|||||||
interface DomainEvent {
|
interface DomainEvent {
|
||||||
val eventId: Uuid
|
val eventId: Uuid
|
||||||
val aggregateId: Uuid
|
val aggregateId: Uuid
|
||||||
val eventType: String
|
val eventType: java.time.Instant
|
||||||
val timestamp: Instant
|
val timestamp: Instant
|
||||||
val version: Long
|
val version: Int
|
||||||
|
|
||||||
// OPTIMIZED: Added correlation and causation IDs for distributed tracing.
|
// OPTIMIZED: Added correlation and causation IDs for distributed tracing.
|
||||||
/**
|
/**
|
||||||
@@ -33,8 +34,8 @@ interface DomainEvent {
|
|||||||
*/
|
*/
|
||||||
abstract class BaseDomainEvent(
|
abstract class BaseDomainEvent(
|
||||||
override val aggregateId: Uuid,
|
override val aggregateId: Uuid,
|
||||||
override val eventType: String,
|
override val eventType: java.time.Instant,
|
||||||
override val version: Long,
|
override val version: Int,
|
||||||
override val eventId: Uuid = uuid4(),
|
override val eventId: Uuid = uuid4(),
|
||||||
override val timestamp: Instant = Clock.System.now(),
|
override val timestamp: Instant = Clock.System.now(),
|
||||||
override val correlationId: Uuid? = null,
|
override val correlationId: Uuid? = null,
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package at.mocode.core.domain.model
|
|||||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
||||||
import at.mocode.core.domain.serialization.UuidSerializer
|
import at.mocode.core.domain.serialization.UuidSerializer
|
||||||
import com.benasher44.uuid.Uuid
|
import com.benasher44.uuid.Uuid
|
||||||
import kotlinx.datetime.Clock
|
import kotlin.time.Clock
|
||||||
import kotlinx.datetime.Instant
|
import kotlin.time.Instant
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,7 +49,8 @@ data class ErrorDto(
|
|||||||
data class ApiResponse<T>(
|
data class ApiResponse<T>(
|
||||||
val data: T?,
|
val data: T?,
|
||||||
val success: Boolean,
|
val success: Boolean,
|
||||||
val errors: List<ErrorDto> = emptyList(), // OPTIMIZED: Using structured ErrorDto
|
val errors: List<ErrorDto> = emptyList(),
|
||||||
|
@Serializable(with = KotlinInstantSerializer::class)
|
||||||
val timestamp: Instant = Clock.System.now()
|
val timestamp: Instant = Clock.System.now()
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package at.mocode.core.domain.serialization
|
|||||||
|
|
||||||
import com.benasher44.uuid.Uuid
|
import com.benasher44.uuid.Uuid
|
||||||
import com.benasher44.uuid.uuidFrom
|
import com.benasher44.uuid.uuidFrom
|
||||||
import kotlinx.datetime.Instant
|
import kotlin.time.Instant
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import kotlinx.datetime.LocalDateTime
|
import kotlinx.datetime.LocalDateTime
|
||||||
import kotlinx.datetime.LocalTime
|
import kotlinx.datetime.LocalTime
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ plugins {
|
|||||||
alias(libs.plugins.kotlin.jvm)
|
alias(libs.plugins.kotlin.jvm)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Abhängigkeit zum platform-Modul für zentrale Versionsverwaltung
|
// Abhängigkeit zum platform-Modul für zentrale Versionsverwaltung
|
||||||
api(projects.platform.platformDependencies)
|
api(projects.platform.platformDependencies)
|
||||||
@@ -31,4 +37,5 @@ dependencies {
|
|||||||
// Testing
|
// Testing
|
||||||
testImplementation(projects.platform.platformTesting)
|
testImplementation(projects.platform.platformTesting)
|
||||||
testImplementation(libs.bundles.testing.jvm)
|
testImplementation(libs.bundles.testing.jvm)
|
||||||
|
testImplementation(libs.kotlin.test)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,24 @@
|
|||||||
package at.mocode.core.utils.config
|
package at.mocode.core.utils.config
|
||||||
|
|
||||||
import at.mocode.core.utils.database.DatabaseConfig
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.net.InetAddress
|
||||||
import java.util.Properties
|
import java.util.Properties
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zentrale Konfigurations-Klasse für die Anwendung.
|
* Zentrale, unveränderliche Konfigurations-Klasse für die Anwendung.
|
||||||
* Hält alle Konfigurationswerte, die beim Start des Service explizit geladen werden.
|
* Hält alle Konfigurationswerte, die beim Start eines Service geladen werden.
|
||||||
*/
|
*/
|
||||||
class AppConfig(
|
class AppConfig(
|
||||||
val environment: AppEnvironment,
|
val environment: AppEnvironment,
|
||||||
val appInfo: AppInfoConfig,
|
val appInfo: AppInfoConfig,
|
||||||
val server: ServerConfig,
|
val server: ServerConfig,
|
||||||
|
val database: DatabaseConfig,
|
||||||
|
val serviceDiscovery: ServiceDiscoveryConfig,
|
||||||
val security: SecurityConfig,
|
val security: SecurityConfig,
|
||||||
val logging: LoggingConfig,
|
val logging: LoggingConfig,
|
||||||
val rateLimit: RateLimitConfig,
|
val rateLimit: RateLimitConfig
|
||||||
val serviceDiscovery: ServiceDiscoveryConfig,
|
|
||||||
val database: DatabaseConfig
|
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
|
||||||
* Factory-Methode, die eine AppConfig-Instanz durch das Laden von
|
|
||||||
* .properties-Dateien und Umgebungsvariablen erstellt.
|
|
||||||
* Dies ist der zentrale Einstiegspunkt, um die Konfiguration zu laden.
|
|
||||||
*/
|
|
||||||
fun load(): AppConfig {
|
fun load(): AppConfig {
|
||||||
val environment = AppEnvironment.current()
|
val environment = AppEnvironment.current()
|
||||||
val props = loadProperties(environment)
|
val props = loadProperties(environment)
|
||||||
@@ -32,191 +27,148 @@ class AppConfig(
|
|||||||
environment = environment,
|
environment = environment,
|
||||||
appInfo = AppInfoConfig.fromProperties(props),
|
appInfo = AppInfoConfig.fromProperties(props),
|
||||||
server = ServerConfig.fromProperties(props),
|
server = ServerConfig.fromProperties(props),
|
||||||
|
database = DatabaseConfig.fromProperties(props),
|
||||||
|
serviceDiscovery = ServiceDiscoveryConfig.fromProperties(props),
|
||||||
security = SecurityConfig.fromProperties(props),
|
security = SecurityConfig.fromProperties(props),
|
||||||
logging = LoggingConfig.fromProperties(props, environment),
|
logging = LoggingConfig.fromProperties(props, environment),
|
||||||
rateLimit = RateLimitConfig.fromProperties(props),
|
rateLimit = RateLimitConfig.fromProperties(props)
|
||||||
serviceDiscovery = ServiceDiscoveryConfig.fromProperties(props),
|
|
||||||
database = DatabaseConfig.fromProperties(props)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadProperties(environment: AppEnvironment): Properties {
|
private fun loadProperties(environment: AppEnvironment): Properties {
|
||||||
val props = Properties()
|
val props = Properties()
|
||||||
|
|
||||||
// Lade Basis-Properties
|
|
||||||
loadPropertiesFile("application.properties", props)
|
loadPropertiesFile("application.properties", props)
|
||||||
|
|
||||||
// Lade umgebungsspezifische Properties
|
|
||||||
val envFile = "application-${environment.name.lowercase()}.properties"
|
val envFile = "application-${environment.name.lowercase()}.properties"
|
||||||
loadPropertiesFile(envFile, props)
|
loadPropertiesFile(envFile, props)
|
||||||
|
|
||||||
return props
|
return props
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadPropertiesFile(filename: String, props: Properties) {
|
private fun loadPropertiesFile(filename: String, props: Properties) {
|
||||||
val resourceStream = javaClass.classLoader.getResourceAsStream(filename)
|
val resourceStream = AppConfig::class.java.classLoader.getResourceAsStream(filename)
|
||||||
if (resourceStream != null) {
|
if (resourceStream != null) {
|
||||||
props.load(resourceStream)
|
resourceStream.use { props.load(it) }
|
||||||
resourceStream.close()
|
return
|
||||||
} else {
|
}
|
||||||
val file = File("config/$filename")
|
val file = File("config/$filename")
|
||||||
if (file.exists()) {
|
if (file.exists()) {
|
||||||
file.inputStream().use { props.load(it) }
|
file.inputStream().use { props.load(it) }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
data class AppInfoConfig(val name: String, val version: String, val description: String) {
|
||||||
* Konfiguration für Anwendungsinformationen.
|
|
||||||
*/
|
|
||||||
data class AppInfoConfig(
|
|
||||||
val name: String,
|
|
||||||
val version: String,
|
|
||||||
val description: String
|
|
||||||
) {
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromProperties(props: Properties): AppInfoConfig {
|
fun fromProperties(props: Properties) = AppInfoConfig(
|
||||||
return AppInfoConfig(
|
name = props.getProperty("app.name", "Meldestelle"),
|
||||||
name = props.getProperty("app.name", "Meldestelle"),
|
version = props.getProperty("app.version", "1.0.0"),
|
||||||
version = props.getProperty("app.version", "1.0.0"),
|
description = props.getProperty("app.description", "Pferdesport Meldestelle System")
|
||||||
description = props.getProperty("app.description", "Pferdesport Meldestelle System")
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Konfiguration für den Server.
|
|
||||||
*/
|
|
||||||
data class ServerConfig(
|
data class ServerConfig(
|
||||||
val port: Int,
|
val port: Int,
|
||||||
val host: String,
|
val host: String,
|
||||||
|
val advertisedHost: String,
|
||||||
val workers: Int,
|
val workers: Int,
|
||||||
val cors: CorsConfig
|
val cors: CorsConfig
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun fromProperties(props: Properties): ServerConfig {
|
fun fromProperties(props: Properties): ServerConfig {
|
||||||
val corsConfig = CorsConfig(
|
val defaultHost = try { InetAddress.getLocalHost().hostAddress } catch (_: Exception) { "127.0.0.1" }
|
||||||
enabled = props.getProperty("server.cors.enabled")?.toBoolean() ?: true,
|
|
||||||
allowedOrigins = props.getProperty("server.cors.allowedOrigins")?.split(",")?.map { it.trim() }
|
|
||||||
?: listOf("*")
|
|
||||||
)
|
|
||||||
return ServerConfig(
|
return ServerConfig(
|
||||||
port = System.getenv("API_PORT")?.toIntOrNull() ?: props.getProperty("server.port", "8081").toInt(),
|
port = props.getIntProperty("server.port", "API_PORT", 8081),
|
||||||
host = System.getenv("API_HOST") ?: props.getProperty("server.host", "0.0.0.0"),
|
host = props.getStringProperty("server.host", "API_HOST", "0.0.0.0"),
|
||||||
workers = props.getProperty("server.workers")?.toIntOrNull() ?: Runtime.getRuntime()
|
advertisedHost = props.getStringProperty("server.advertisedHost", "API_HOST_ADVERTISED", defaultHost),
|
||||||
.availableProcessors(),
|
workers = props.getIntProperty("server.workers", "API_WORKERS", Runtime.getRuntime().availableProcessors()),
|
||||||
cors = corsConfig
|
cors = CorsConfig.fromProperties(props)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data class CorsConfig(val enabled: Boolean, val allowedOrigins: List<String>) {
|
||||||
|
companion object {
|
||||||
|
fun fromProperties(props: Properties) = CorsConfig(
|
||||||
|
enabled = props.getBooleanProperty("server.cors.enabled", "API_CORS_ENABLED", true),
|
||||||
|
allowedOrigins = props.getProperty("server.cors.allowedOrigins")?.split(",")?.map { it.trim() } ?: listOf("*")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class CorsConfig(
|
|
||||||
val enabled: Boolean,
|
|
||||||
val allowedOrigins: List<String>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
data class DatabaseConfig(
|
||||||
* Konfiguration für die Sicherheit.
|
val jdbcUrl: String,
|
||||||
*/
|
val username: String,
|
||||||
data class SecurityConfig(
|
val password: String,
|
||||||
val jwt: JwtConfig,
|
val driverClassName: String,
|
||||||
val apiKey: String?
|
val maxPoolSize: Int,
|
||||||
|
val minPoolSize: Int,
|
||||||
|
val autoMigrate: Boolean
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun fromProperties(props: Properties): SecurityConfig {
|
fun fromProperties(props: Properties): DatabaseConfig {
|
||||||
val jwtConfig = JwtConfig(
|
val host = props.getStringProperty("database.host", "DB_HOST", "localhost")
|
||||||
secret = System.getenv("JWT_SECRET") ?: props.getProperty(
|
val port = props.getIntProperty("database.port", "DB_PORT", 5432)
|
||||||
"security.jwt.secret",
|
val name = props.getStringProperty("database.name", "DB_NAME", "meldestelle_db")
|
||||||
"default-jwt-secret-key-please-change-in-production"
|
return DatabaseConfig(
|
||||||
),
|
jdbcUrl = "jdbc:postgresql://$host:$port/$name",
|
||||||
issuer = System.getenv("JWT_ISSUER") ?: props.getProperty("security.jwt.issuer", "meldestelle-api"),
|
username = props.getStringProperty("database.username", "DB_USER", "meldestelle_user"),
|
||||||
audience = System.getenv("JWT_AUDIENCE") ?: props.getProperty(
|
password = props.getStringProperty("database.password", "DB_PASSWORD", "secure_password_change_me"),
|
||||||
"security.jwt.audience",
|
driverClassName = "org.postgresql.Driver",
|
||||||
"meldestelle-clients"
|
maxPoolSize = props.getIntProperty("database.maxPoolSize", "DB_MAX_POOL_SIZE", 10),
|
||||||
),
|
minPoolSize = props.getIntProperty("database.minPoolSize", "DB_MIN_POOL_SIZE", 5),
|
||||||
realm = System.getenv("JWT_REALM") ?: props.getProperty("security.jwt.realm", "meldestelle"),
|
autoMigrate = props.getBooleanProperty("database.autoMigrate", "DB_AUTO_MIGRATE", true)
|
||||||
expirationInMinutes = props.getProperty("security.jwt.expirationInMinutes")?.toLongOrNull() ?: (60 * 24)
|
|
||||||
)
|
|
||||||
return SecurityConfig(
|
|
||||||
jwt = jwtConfig,
|
|
||||||
apiKey = System.getenv("API_KEY") ?: props.getProperty("security.apiKey")
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class JwtConfig(
|
|
||||||
val secret: String,
|
|
||||||
val issuer: String,
|
|
||||||
val audience: String,
|
|
||||||
val realm: String,
|
|
||||||
val expirationInMinutes: Long
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
data class ServiceDiscoveryConfig(val enabled: Boolean, val consulHost: String, val consulPort: Int) {
|
||||||
* Konfiguration für das Logging.
|
|
||||||
*/
|
|
||||||
data class LoggingConfig(
|
|
||||||
val level: String,
|
|
||||||
val logRequests: Boolean,
|
|
||||||
val logResponses: Boolean
|
|
||||||
// ... many more detailed properties from your original file
|
|
||||||
) {
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromProperties(props: Properties, env: AppEnvironment): LoggingConfig {
|
fun fromProperties(props: Properties) = ServiceDiscoveryConfig(
|
||||||
return LoggingConfig(
|
enabled = props.getBooleanProperty("service-discovery.enabled", "CONSUL_ENABLED", true),
|
||||||
level = props.getProperty("logging.level", if (env == AppEnvironment.PRODUCTION) "INFO" else "DEBUG"),
|
consulHost = props.getStringProperty("service-discovery.consul.host", "CONSUL_HOST", "consul"),
|
||||||
logRequests = props.getProperty("logging.requests")?.toBoolean() ?: true,
|
consulPort = props.getIntProperty("service-discovery.consul.port", "CONSUL_PORT", 8500)
|
||||||
logResponses = props.getProperty("logging.responses")?.toBoolean() ?: (env != AppEnvironment.PRODUCTION)
|
)
|
||||||
// ... load other properties here
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
data class SecurityConfig(val jwt: JwtConfig, val apiKey: String?) {
|
||||||
* Konfiguration für Rate Limiting.
|
|
||||||
*/
|
|
||||||
data class RateLimitConfig(
|
|
||||||
val enabled: Boolean,
|
|
||||||
val globalLimit: Int,
|
|
||||||
val globalPeriodMinutes: Int
|
|
||||||
) {
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromProperties(props: Properties): RateLimitConfig {
|
fun fromProperties(props: Properties) = SecurityConfig(
|
||||||
return RateLimitConfig(
|
jwt = JwtConfig.fromProperties(props),
|
||||||
enabled = props.getProperty("ratelimit.enabled")?.toBoolean() ?: true,
|
apiKey = props.getStringProperty("security.apiKey", "API_KEY", "").ifEmpty { null }
|
||||||
globalLimit = props.getProperty("ratelimit.global.limit")?.toIntOrNull() ?: 100,
|
)
|
||||||
globalPeriodMinutes = props.getProperty("ratelimit.global.periodMinutes")?.toIntOrNull() ?: 1
|
}
|
||||||
|
data class JwtConfig(val secret: String, val issuer: String, val audience: String, val realm: String, val expirationInMinutes: Long) {
|
||||||
|
companion object {
|
||||||
|
fun fromProperties(props: Properties) = JwtConfig(
|
||||||
|
secret = props.getStringProperty("security.jwt.secret", "JWT_SECRET", "default-secret-please-change-in-production"),
|
||||||
|
issuer = props.getStringProperty("security.jwt.issuer", "JWT_ISSUER", "meldestelle-api"),
|
||||||
|
audience = props.getStringProperty("security.jwt.audience", "JWT_AUDIENCE", "meldestelle-clients"),
|
||||||
|
realm = props.getStringProperty("security.jwt.realm", "JWT_REALM", "meldestelle"),
|
||||||
|
expirationInMinutes = props.getLongProperty("security.jwt.expirationInMinutes", "JWT_EXPIRATION_MINUTES", 60 * 24)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
data class LoggingConfig(val level: String, val logRequests: Boolean, val logResponses: Boolean) {
|
||||||
* Konfiguration für Service Discovery.
|
|
||||||
*/
|
|
||||||
data class ServiceDiscoveryConfig(
|
|
||||||
val enabled: Boolean,
|
|
||||||
val consulHost: String,
|
|
||||||
val consulPort: Int
|
|
||||||
) {
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromProperties(props: Properties): ServiceDiscoveryConfig {
|
fun fromProperties(props: Properties, env: AppEnvironment) = LoggingConfig(
|
||||||
return ServiceDiscoveryConfig(
|
level = props.getStringProperty("logging.level", "LOG_LEVEL", if (env.isProduction()) "INFO" else "DEBUG"),
|
||||||
enabled = props.getProperty("service-discovery.enabled")?.toBoolean() ?: true,
|
logRequests = props.getBooleanProperty("logging.requests", "LOG_REQUESTS", true),
|
||||||
consulHost = System.getenv("CONSUL_HOST") ?: props.getProperty(
|
logResponses = props.getBooleanProperty("logging.responses", "LOG_RESPONSES", !env.isProduction())
|
||||||
"service-discovery.consul.host",
|
)
|
||||||
"consul"
|
|
||||||
),
|
|
||||||
consulPort = System.getenv("CONSUL_PORT")?.toIntOrNull()
|
|
||||||
?: props.getProperty("service-discovery.consul.port", "8500").toInt()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class RateLimitConfig(val enabled: Boolean, val globalLimit: Int, val globalPeriodMinutes: Int) {
|
||||||
|
companion object {
|
||||||
|
fun fromProperties(props: Properties) = RateLimitConfig(
|
||||||
|
enabled = props.getBooleanProperty("ratelimit.enabled", "RATE_LIMIT_ENABLED", true),
|
||||||
|
globalLimit = props.getIntProperty("ratelimit.global.limit", "RATE_LIMIT_GLOBAL_LIMIT", 100),
|
||||||
|
globalPeriodMinutes = props.getIntProperty("ratelimit.global.periodMinutes", "RATE_LIMIT_GLOBAL_PERIOD", 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,48 +1,26 @@
|
|||||||
package at.mocode.core.utils.config
|
package at.mocode.core.utils.config
|
||||||
|
|
||||||
/**
|
import org.slf4j.LoggerFactory
|
||||||
* Aufzählung der verschiedenen Anwendungsumgebungen.
|
|
||||||
*/
|
|
||||||
enum class AppEnvironment {
|
enum class AppEnvironment {
|
||||||
DEVELOPMENT, // Lokale Entwicklungsumgebung
|
DEVELOPMENT,
|
||||||
TEST, // Testumgebung (CI/CD, Integrationstests)
|
TEST,
|
||||||
STAGING, // Vorabproduktionsumgebung
|
STAGING,
|
||||||
PRODUCTION; // Produktionsumgebung
|
PRODUCTION;
|
||||||
|
|
||||||
|
fun isProduction() = this == PRODUCTION
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
private val logger = LoggerFactory.getLogger(AppEnvironment::class.java)
|
||||||
* Ermittelt die aktuelle Umgebung basierend auf der APP_ENV Umgebungsvariable.
|
|
||||||
*
|
|
||||||
* @return Die aktuelle Umgebung (Standardmäßig DEVELOPMENT wenn nicht definiert)
|
|
||||||
*/
|
|
||||||
fun current(): AppEnvironment {
|
fun current(): AppEnvironment {
|
||||||
val envName = System.getenv("APP_ENV")?.uppercase() ?: "DEVELOPMENT"
|
val envName = System.getenv("APP_ENV")?.uppercase() ?: "DEVELOPMENT"
|
||||||
return try {
|
return try {
|
||||||
valueOf(envName)
|
valueOf(envName)
|
||||||
} catch (_: IllegalArgumentException) {
|
} catch (_: IllegalArgumentException) {
|
||||||
println("Warnung: Unbekannte Umgebung '$envName', verwende DEVELOPMENT")
|
logger.warn("Unknown environment '{}', falling back to DEVELOPMENT.", envName)
|
||||||
DEVELOPMENT
|
DEVELOPMENT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Prüft, ob die aktuelle Umgebung die Entwicklungsumgebung ist.
|
|
||||||
*/
|
|
||||||
fun isDevelopment() = current() == DEVELOPMENT
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prüft, ob die aktuelle Umgebung die Testumgebung ist.
|
|
||||||
*/
|
|
||||||
fun isTest() = current() == TEST
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prüft, ob die aktuelle Umgebung die Staging-Umgebung ist.
|
|
||||||
*/
|
|
||||||
fun isStaging() = current() == STAGING
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prüft, ob die aktuelle Umgebung die Produktionsumgebung ist.
|
|
||||||
*/
|
|
||||||
fun isProduction() = current() == PRODUCTION
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package at.mocode.core.utils.config
|
||||||
|
|
||||||
|
import java.util.Properties
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest eine String-Property, wobei eine Umgebungsvariable Vorrang hat.
|
||||||
|
*
|
||||||
|
* @param key Der Schlüssel in der '.properties-Datei'.
|
||||||
|
* @param envVar Der Name der Umgebungsvariable.
|
||||||
|
* @param default Der Standardwert, falls weder Property noch Env-Var existieren.
|
||||||
|
* @return Der geladene Konfigurationswert.
|
||||||
|
*/
|
||||||
|
fun Properties.getStringProperty(key: String, envVar: String, default: String): String {
|
||||||
|
return System.getenv(envVar) ?: this.getProperty(key, default)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest eine Integer-Property, wobei eine Umgebungsvariable Vorrang hat.
|
||||||
|
*/
|
||||||
|
fun Properties.getIntProperty(key: String, envVar: String, default: Int): Int {
|
||||||
|
val value = System.getenv(envVar) ?: this.getProperty(key)
|
||||||
|
return value?.toIntOrNull() ?: default
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest eine Boolean-Property, wobei eine Umgebungsvariable Vorrang hat.
|
||||||
|
*/
|
||||||
|
fun Properties.getBooleanProperty(key: String, envVar: String, default: Boolean): Boolean {
|
||||||
|
val value = System.getenv(envVar) ?: this.getProperty(key)
|
||||||
|
return value?.toBoolean() ?: default
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest eine Long-Property, wobei eine Umgebungsvariable Vorrang hat.
|
||||||
|
*/
|
||||||
|
fun Properties.getLongProperty(key: String, envVar: String, default: Long): Long {
|
||||||
|
val value = System.getenv(envVar) ?: this.getProperty(key)
|
||||||
|
return value?.toLongOrNull() ?: default
|
||||||
|
}
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package at.mocode.core.utils.database
|
|
||||||
|
|
||||||
import java.util.Properties
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Konfiguration für die Datenbankverbindung.
|
|
||||||
* Diese Klasse ist ein reiner Datenhalter (Value Object). Die Logik zum Laden
|
|
||||||
* der Werte ist in der companion object Factory-Methode gekapselt.
|
|
||||||
*/
|
|
||||||
data class DatabaseConfig(
|
|
||||||
val jdbcUrl: String,
|
|
||||||
val username: String,
|
|
||||||
val password: String,
|
|
||||||
val driverClassName: String = "org.postgresql.Driver",
|
|
||||||
val maxPoolSize: Int = 10,
|
|
||||||
val minPoolSize: Int = 5,
|
|
||||||
val autoMigrate: Boolean = true // Flag to enable/disable Flyway migrations
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
/**
|
|
||||||
* Erstellt eine Datenbank-Konfiguration aus Umgebungsvariablen und Properties.
|
|
||||||
* Die Priorität ist: Umgebungsvariablen > Properties > Standardwerte.
|
|
||||||
*/
|
|
||||||
fun fromProperties(props: Properties): DatabaseConfig {
|
|
||||||
val host = System.getenv("DB_HOST") ?: props.getProperty("database.host", "localhost")
|
|
||||||
val port = System.getenv("DB_PORT") ?: props.getProperty("database.port", "5432")
|
|
||||||
val database = System.getenv("DB_NAME") ?: props.getProperty("database.name", "meldestelle_db")
|
|
||||||
val username = System.getenv("DB_USER") ?: props.getProperty("database.username", "meldestelle_user")
|
|
||||||
val password =
|
|
||||||
System.getenv("DB_PASSWORD") ?: props.getProperty("database.password", "secure_password_change_me")
|
|
||||||
|
|
||||||
val maxPoolSize = System.getenv("DB_MAX_POOL_SIZE")?.toIntOrNull()
|
|
||||||
?: props.getProperty("database.maxPoolSize")?.toIntOrNull()
|
|
||||||
?: 10
|
|
||||||
|
|
||||||
val minPoolSize = System.getenv("DB_MIN_POOL_SIZE")?.toIntOrNull()
|
|
||||||
?: props.getProperty("database.minPoolSize")?.toIntOrNull()
|
|
||||||
?: 5
|
|
||||||
|
|
||||||
val autoMigrate = System.getenv("DB_AUTO_MIGRATE")?.toBoolean()
|
|
||||||
?: props.getProperty("database.autoMigrate")?.toBoolean()
|
|
||||||
?: true
|
|
||||||
|
|
||||||
return DatabaseConfig(
|
|
||||||
jdbcUrl = "jdbc:postgresql://$host:$port/$database",
|
|
||||||
username = username,
|
|
||||||
password = password,
|
|
||||||
driverClassName = "org.postgresql.Driver",
|
|
||||||
maxPoolSize = maxPoolSize,
|
|
||||||
minPoolSize = minPoolSize,
|
|
||||||
autoMigrate = autoMigrate
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +1,29 @@
|
|||||||
package at.mocode.core.utils.database
|
package at.mocode.core.utils.database
|
||||||
|
|
||||||
|
import at.mocode.core.utils.config.DatabaseConfig
|
||||||
import com.zaxxer.hikari.HikariConfig
|
import com.zaxxer.hikari.HikariConfig
|
||||||
import com.zaxxer.hikari.HikariDataSource
|
import com.zaxxer.hikari.HikariDataSource
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import org.flywaydb.core.Flyway
|
import org.flywaydb.core.Flyway
|
||||||
import org.jetbrains.exposed.sql.Database
|
import org.jetbrains.exposed.sql.Database
|
||||||
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
|
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
/**
|
|
||||||
* Factory-Klasse für die Datenbankverbindung.
|
|
||||||
* Erstellt und konfiguriert eine Datenbankverbindung inklusive Connection Pool
|
|
||||||
* und führt bei der Initialisierung die notwendigen Migrationen aus.
|
|
||||||
*
|
|
||||||
* @property config Die Datenbankkonfiguration, die für diese Instanz verwendet werden soll.
|
|
||||||
*/
|
|
||||||
class DatabaseFactory(private val config: DatabaseConfig) {
|
class DatabaseFactory(private val config: DatabaseConfig) {
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
private val logger = LoggerFactory.getLogger(DatabaseFactory::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
private var dataSource: HikariDataSource? = null
|
private var dataSource: HikariDataSource? = null
|
||||||
private var database: Database? = null
|
private var database: Database? = null
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialisiert die Datenbankverbindung. Muss vor der ersten Verwendung aufgerufen werden.
|
|
||||||
* Konfiguriert den Connection Pool und führt Flyway-Migrationen aus.
|
|
||||||
*/
|
|
||||||
fun connect() {
|
fun connect() {
|
||||||
if (dataSource != null) {
|
if (dataSource != null) {
|
||||||
|
logger.warn("Database already connected. Closing existing connection before creating a new one.")
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
|
logger.info("Initializing database connection to ${config.jdbcUrl}")
|
||||||
val hikariConfig = createHikariConfig()
|
val hikariConfig = createHikariConfig()
|
||||||
val ds = HikariDataSource(hikariConfig)
|
val ds = HikariDataSource(hikariConfig)
|
||||||
dataSource = ds
|
dataSource = ds
|
||||||
@@ -38,28 +34,16 @@ class DatabaseFactory(private val config: DatabaseConfig) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Schließt die Datenbankverbindung und den Connection Pool.
|
|
||||||
*/
|
|
||||||
fun close() {
|
fun close() {
|
||||||
dataSource?.close()
|
dataSource?.close()
|
||||||
dataSource = null
|
dataSource = null
|
||||||
database = null
|
database = null
|
||||||
|
logger.info("Database connection closed.")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Führt eine Datenbankoperation in einer neuen, suspendierenden Transaktion aus.
|
|
||||||
* Dies ist die primäre Methode, um mit der Datenbank zu interagieren.
|
|
||||||
*
|
|
||||||
* @param block Der Code, der in der Transaktion ausgeführt werden soll.
|
|
||||||
* @return Das Ergebnis der Transaktion.
|
|
||||||
*/
|
|
||||||
suspend fun <T> dbQuery(block: suspend () -> T): T {
|
suspend fun <T> dbQuery(block: suspend () -> T): T {
|
||||||
// Wir stellen sicher, dass die dbQuery-Funktion nur auf einer verbundenen Datenbank läuft.
|
val db = database ?: throw IllegalStateException("Database has not been connected. Call connect() first.")
|
||||||
if (database == null) {
|
return newSuspendedTransaction(Dispatchers.IO, db = db) {
|
||||||
throw IllegalStateException("Database has not been connected. Call connect() first.")
|
|
||||||
}
|
|
||||||
return newSuspendedTransaction(Dispatchers.IO, db = database) {
|
|
||||||
block()
|
block()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,29 +58,27 @@ class DatabaseFactory(private val config: DatabaseConfig) {
|
|||||||
minimumIdle = config.minPoolSize
|
minimumIdle = config.minPoolSize
|
||||||
isAutoCommit = false
|
isAutoCommit = false
|
||||||
transactionIsolation = "TRANSACTION_READ_COMMITTED"
|
transactionIsolation = "TRANSACTION_READ_COMMITTED"
|
||||||
connectionTestQuery = "SELECT 1"
|
validationTimeout = 5000
|
||||||
validationTimeout = 5000 // 5 seconds
|
connectionTimeout = 30000
|
||||||
connectionTimeout = 30000 // 30 seconds
|
idleTimeout = 600000
|
||||||
idleTimeout = 600000 // 10 minutes
|
maxLifetime = 1800000
|
||||||
maxLifetime = 1800000 // 30 minutes
|
leakDetectionThreshold = 60000
|
||||||
leakDetectionThreshold = 60000 // 1 minute
|
poolName = "MeldestelleDbPool"
|
||||||
poolName = "MeldestelleDbPool-${config.jdbcUrl.substringAfterLast('/')}" // Eindeutiger Pool-Name
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun runFlyway(dataSource: HikariDataSource) {
|
private fun runFlyway(dataSource: HikariDataSource) {
|
||||||
println("Starte Flyway-Migrationen für Schema: ${dataSource.jdbcUrl}")
|
logger.info("Starting Flyway migrations...")
|
||||||
try {
|
try {
|
||||||
Flyway.configure()
|
val count = Flyway.configure()
|
||||||
.dataSource(dataSource)
|
.dataSource(dataSource)
|
||||||
.locations("classpath:db/migration")
|
.locations("classpath:db/migration")
|
||||||
.load()
|
.load()
|
||||||
.migrate()
|
.migrate()
|
||||||
println("Flyway-Migrationen erfolgreich abgeschlossen.")
|
.migrationsExecuted
|
||||||
|
logger.info("Flyway migrations completed successfully. Applied $count migrations.")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("FEHLER: Flyway-Migration fehlgeschlagen! Details: ${e.message}")
|
logger.error("Flyway migration failed!", e)
|
||||||
// Wir werfen den Fehler weiter, damit die Anwendung beim Start fehlschlägt.
|
|
||||||
// Das ist wichtig, um Inkonsistenzen zu vermeiden.
|
|
||||||
throw IllegalStateException("Flyway migration failed", e)
|
throw IllegalStateException("Flyway migration failed", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-31
@@ -3,31 +3,32 @@ package at.mocode.core.utils.discovery
|
|||||||
import at.mocode.core.utils.config.AppConfig
|
import at.mocode.core.utils.config.AppConfig
|
||||||
import com.orbitz.consul.Consul
|
import com.orbitz.consul.Consul
|
||||||
import com.orbitz.consul.model.agent.ImmutableRegistration
|
import com.orbitz.consul.model.agent.ImmutableRegistration
|
||||||
// KORREKTUR: Expliziter Import für die `Registration`-Klasse, die den `RegCheck` enthält.
|
|
||||||
import com.orbitz.consul.model.agent.Registration
|
import com.orbitz.consul.model.agent.Registration
|
||||||
import java.net.InetAddress
|
import org.slf4j.LoggerFactory
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/**
|
|
||||||
* Repräsentiert die Registrierung eines einzelnen Service-Exemplars bei Consul.
|
|
||||||
* Diese Klasse kümmert sich um den Lebenszyklus (Registrierung, Deregistrierung).
|
|
||||||
*/
|
|
||||||
class ServiceRegistration internal constructor(
|
class ServiceRegistration internal constructor(
|
||||||
private val consul: Consul,
|
private val consul: Consul,
|
||||||
private val registration: ImmutableRegistration
|
private val registration: ImmutableRegistration
|
||||||
) {
|
) {
|
||||||
|
private companion object {
|
||||||
|
private val logger = LoggerFactory.getLogger(ServiceRegistration::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
private var isRegistered = false
|
private var isRegistered = false
|
||||||
|
|
||||||
fun register() {
|
fun register() {
|
||||||
if (isRegistered) return
|
if (isRegistered) return
|
||||||
try {
|
try {
|
||||||
// Der `register`-Aufruf ist korrekt, da das `registration`-Objekt
|
|
||||||
// bereits außerhalb vollständig und korrekt gebaut wurde.
|
|
||||||
consul.agentClient().register(registration)
|
consul.agentClient().register(registration)
|
||||||
isRegistered = true
|
isRegistered = true
|
||||||
println("Service '${registration.name()}' mit ID '${registration.id()}' erfolgreich bei Consul registriert.")
|
logger.info(
|
||||||
|
"Service '{}' with ID '{}' successfully registered with Consul.",
|
||||||
|
registration.name(),
|
||||||
|
registration.id()
|
||||||
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("FEHLER: Service-Registrierung bei Consul fehlgeschlagen: ${e.message}")
|
logger.error("Failed to register service '{}' with Consul.", registration.name(), e)
|
||||||
throw IllegalStateException("Could not register service with Consul", e)
|
throw IllegalStateException("Could not register service with Consul", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,63 +36,65 @@ class ServiceRegistration internal constructor(
|
|||||||
fun deregister() {
|
fun deregister() {
|
||||||
if (!isRegistered) return
|
if (!isRegistered) return
|
||||||
try {
|
try {
|
||||||
// Der `deregister`-Aufruf ist korrekt. Er erwartet die Service-ID als einfachen String.
|
|
||||||
consul.agentClient().deregister(registration.id())
|
consul.agentClient().deregister(registration.id())
|
||||||
isRegistered = false
|
isRegistered = false
|
||||||
println("Service '${registration.name()}' mit ID '${registration.id()}' erfolgreich bei Consul deregistriert.")
|
logger.info(
|
||||||
|
"Service '{}' with ID '{}' successfully deregistered from Consul.",
|
||||||
|
registration.name(),
|
||||||
|
registration.id()
|
||||||
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("FEHLER: Service-Deregistrierung bei Consul fehlgeschlagen: ${e.message}")
|
logger.error("Failed to deregister service '{}' from Consul.", registration.id(), e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Zentraler Registrar, der beim Anwendungsstart Services registriert.
|
|
||||||
* Diese Klasse wird einmalig mit der Gesamt-AppConfig initialisiert.
|
|
||||||
*/
|
|
||||||
class ServiceRegistrar(private val appConfig: AppConfig) {
|
class ServiceRegistrar(private val appConfig: AppConfig) {
|
||||||
|
private companion object {
|
||||||
|
private val logger = LoggerFactory.getLogger(ServiceRegistrar::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
private val consul: Consul by lazy {
|
private val consul: Consul by lazy {
|
||||||
val consulConfig = appConfig.serviceDiscovery
|
val consulConfig = appConfig.serviceDiscovery
|
||||||
|
logger.info("Connecting to Consul at {}:{}", consulConfig.consulHost, consulConfig.consulPort)
|
||||||
Consul.builder()
|
Consul.builder()
|
||||||
.withUrl("http://${consulConfig.consulHost}:${consulConfig.consulPort}")
|
.withUrl("http://${consulConfig.consulHost}:${consulConfig.consulPort}")
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Erstellt und registriert einen Service basierend auf der App-Konfiguration.
|
|
||||||
* @return Eine ServiceRegistration-Instanz zur Verwaltung des Lebenszyklus.
|
|
||||||
*/
|
|
||||||
fun registerCurrentService(): ServiceRegistration {
|
fun registerCurrentService(): ServiceRegistration {
|
||||||
val serviceName = appConfig.appInfo.name
|
val serviceName = appConfig.appInfo.name
|
||||||
val servicePort = appConfig.server.port
|
val servicePort = appConfig.server.port
|
||||||
val serviceId = "$serviceName-${UUID.randomUUID()}"
|
val serviceId = "$serviceName-${UUID.randomUUID()}"
|
||||||
val hostAddress = InetAddress.getLocalHost().hostAddress
|
val hostAddress = appConfig.server.advertisedHost
|
||||||
|
|
||||||
// KORREKTUR: Der Health Check MUSS über die statische Factory-Methode `http`
|
|
||||||
// der `Registration.RegCheck`-Klasse erstellt werden. Dies war die Hauptfehlerquelle.
|
|
||||||
val healthCheck = Registration.RegCheck.http(
|
val healthCheck = Registration.RegCheck.http(
|
||||||
"http://$hostAddress:$servicePort/health", // Standard-Health-Check-Pfad
|
"http://$hostAddress:$servicePort/health",
|
||||||
10L, // Intervall in Sekunden
|
10L,
|
||||||
5L // Timeout in Sekunden
|
5L
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ========= FINALE KORREKTUR =========
|
||||||
|
// Wir erstellen die Liste und die Map VORHER mit expliziten Typen,
|
||||||
|
// um dem Compiler bei der Typinferenz zu helfen.
|
||||||
|
val serviceTags: List<String> = listOf("env:${appConfig.environment.name.lowercase()}")
|
||||||
|
val serviceMeta: Map<String, String> = mapOf("version" to appConfig.appInfo.version)
|
||||||
|
|
||||||
val registration = ImmutableRegistration.builder()
|
val registration = ImmutableRegistration.builder()
|
||||||
.id(serviceId)
|
.id(serviceId)
|
||||||
.name(serviceName)
|
.name(serviceName)
|
||||||
.address(hostAddress)
|
.address(hostAddress)
|
||||||
.port(servicePort)
|
.port(servicePort)
|
||||||
.check(healthCheck)
|
.check(healthCheck)
|
||||||
.tags(listOf("env:${appConfig.environment.name.lowercase()}"))
|
.tags(serviceTags) // Verwenden der explizit typisierten Variablen
|
||||||
.meta(mapOf("version" to appConfig.appInfo.version))
|
.meta(serviceMeta) // Verwenden der explizit typisierten Variablen
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val serviceRegistration = ServiceRegistration(consul, registration)
|
val serviceRegistration = ServiceRegistration(consul, registration)
|
||||||
serviceRegistration.register()
|
serviceRegistration.register()
|
||||||
|
|
||||||
// Fügt einen Shutdown-Hook hinzu, um den Service beim Beenden sauber zu deregistrieren
|
|
||||||
Runtime.getRuntime().addShutdownHook(Thread {
|
Runtime.getRuntime().addShutdownHook(Thread {
|
||||||
println("Shutdown-Hook: Deregistriere Service ${serviceId}...")
|
logger.info("Shutdown hook triggered: Deregistering service '{}'...", serviceId)
|
||||||
serviceRegistration.deregister()
|
serviceRegistration.deregister()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import com.benasher44.uuid.Uuid
|
|||||||
import com.benasher44.uuid.uuidFrom
|
import com.benasher44.uuid.uuidFrom
|
||||||
// KORREKTUR: Der Import wurde von java.math.BigDecimal auf die korrekte Bibliothek geändert.
|
// KORREKTUR: Der Import wurde von java.math.BigDecimal auf die korrekte Bibliothek geändert.
|
||||||
import com.ionspin.kotlin.bignum.decimal.BigDecimal
|
import com.ionspin.kotlin.bignum.decimal.BigDecimal
|
||||||
import kotlinx.datetime.Instant
|
import kotlin.time.Instant
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import kotlinx.datetime.LocalDateTime
|
import kotlinx.datetime.LocalDateTime
|
||||||
import kotlinx.datetime.LocalTime
|
import kotlinx.datetime.LocalTime
|
||||||
@@ -14,6 +14,7 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
|||||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
import kotlinx.serialization.encoding.Decoder
|
import kotlinx.serialization.encoding.Decoder
|
||||||
import kotlinx.serialization.encoding.Encoder
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlin.time.ExperimentalTime
|
||||||
|
|
||||||
object BigDecimalSerializer : KSerializer<BigDecimal> {
|
object BigDecimalSerializer : KSerializer<BigDecimal> {
|
||||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("BigDecimal", PrimitiveKind.STRING)
|
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("BigDecimal", PrimitiveKind.STRING)
|
||||||
@@ -27,6 +28,7 @@ object UuidSerializer : KSerializer<Uuid> {
|
|||||||
override fun deserialize(decoder: Decoder): Uuid = uuidFrom(decoder.decodeString())
|
override fun deserialize(decoder: Decoder): Uuid = uuidFrom(decoder.decodeString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTime::class)
|
||||||
object KotlinInstantSerializer : KSerializer<Instant> {
|
object KotlinInstantSerializer : KSerializer<Instant> {
|
||||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING)
|
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING)
|
||||||
override fun serialize(encoder: Encoder, value: Instant) = encoder.encodeString(value.toString())
|
override fun serialize(encoder: Encoder, value: Instant) = encoder.encodeString(value.toString())
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
package at.mocode.core.utils.validation
|
package at.mocode.core.utils.validation
|
||||||
|
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import kotlinx.datetime.Clock
|
import kotlin.time.Clock
|
||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
import kotlinx.datetime.todayIn
|
import kotlinx.datetime.todayIn
|
||||||
|
import kotlin.time.ExperimentalTime
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Common validation utilities
|
* Common validation utilities
|
||||||
@@ -92,6 +93,7 @@ object ValidationUtils {
|
|||||||
/**
|
/**
|
||||||
* Validates birth date
|
* Validates birth date
|
||||||
*/
|
*/
|
||||||
|
@OptIn(ExperimentalTime::class)
|
||||||
fun validateBirthDate(birthDate: LocalDate?, fieldName: String = "geburtsdatum"): ValidationError? {
|
fun validateBirthDate(birthDate: LocalDate?, fieldName: String = "geburtsdatum"): ValidationError? {
|
||||||
if (birthDate == null) return null
|
if (birthDate == null) return null
|
||||||
|
|
||||||
@@ -116,6 +118,7 @@ object ValidationUtils {
|
|||||||
/**
|
/**
|
||||||
* Validates year value
|
* Validates year value
|
||||||
*/
|
*/
|
||||||
|
@OptIn(ExperimentalTime::class)
|
||||||
fun validateYear(year: Int?, fieldName: String, minYear: Int = 1900): ValidationError? {
|
fun validateYear(year: Int?, fieldName: String, minYear: Int = 1900): ValidationError? {
|
||||||
if (year == null) return null
|
if (year == null) return null
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ package at.mocode.core.utils.database
|
|||||||
|
|
||||||
import org.jetbrains.exposed.sql.*
|
import org.jetbrains.exposed.sql.*
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import kotlin.test.Ignore
|
import org.junit.*
|
||||||
import kotlin.test.Test
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import kotlin.test.assertEquals
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Comprehensive database connectivity and operations test.
|
* Comprehensive database connectivity and operations test.
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ lettuce = "6.3.1.RELEASE"
|
|||||||
# --- Service Discovery & Monitoring ---
|
# --- Service Discovery & Monitoring ---
|
||||||
consulClient = "1.5.3"
|
consulClient = "1.5.3"
|
||||||
micrometer = "1.12.2"
|
micrometer = "1.12.2"
|
||||||
micrometerTracing = "1.2.5" # NEU
|
micrometerTracing = "1.2.5"
|
||||||
zipkin = "2.24.4" # NEU (Verwendet eine neuere, kompatible Version)
|
zipkin = "3.0.5"
|
||||||
zipkinReporter = "2.16.4" # NEU
|
zipkinReporter = "2.16.4"
|
||||||
|
|
||||||
# --- Authentication ---
|
# --- Authentication ---
|
||||||
auth0Jwt = "4.4.0"
|
auth0Jwt = "4.4.0"
|
||||||
@@ -134,10 +134,10 @@ lettuce-core = { module = "io.lettuce:lettuce-core", version.ref = "lettuce" }
|
|||||||
consul-client = { module = "com.orbitz.consul:consul-client", version.ref = "consulClient" }
|
consul-client = { module = "com.orbitz.consul:consul-client", version.ref = "consulClient" }
|
||||||
micrometer-prometheus = { module = "io.micrometer:micrometer-registry-prometheus", version.ref = "micrometer" }
|
micrometer-prometheus = { module = "io.micrometer:micrometer-registry-prometheus", version.ref = "micrometer" }
|
||||||
micrometer-tracing-bridge-brave = { module = "io.micrometer:micrometer-tracing-bridge-brave", version.ref = "micrometerTracing" } # NEU
|
micrometer-tracing-bridge-brave = { module = "io.micrometer:micrometer-tracing-bridge-brave", version.ref = "micrometerTracing" } # NEU
|
||||||
zipkin-reporter-brave = { module = "io.zipkin.reporter2:zipkin-reporter-brave", version.ref = "zipkinReporter" } # NEU
|
zipkin-reporter-brave = { module = "io.zipkin.reporter2:zipkin-reporter-brave", version.ref = "zipkinReporter" }
|
||||||
zipkin-sender-okhttp3 = { module = "io.zipkin.reporter2:zipkin-sender-okhttp3", version.ref = "zipkinReporter" } # NEU
|
zipkin-sender-okhttp3 = { module = "io.zipkin.reporter2:zipkin-sender-okhttp3", version.ref = "zipkinReporter" }
|
||||||
zipkin-server = { module = "io.zipkin:zipkin-server", version.ref = "zipkin" } # NEU
|
zipkin-server = { module = "io.zipkin:zipkin-server", version.ref = "zipkin" }
|
||||||
zipkin-autoconfigure-ui = { module = "io.zipkin:zipkin-autoconfigure-ui", version.ref = "zipkin" } # NEU
|
zipkin-autoconfigure-ui = { module = "io.zipkin:zipkin-autoconfigure-ui", version.ref = "zipkin" }
|
||||||
|
|
||||||
# --- Authentication ---
|
# --- Authentication ---
|
||||||
auth0-java-jwt = { module = "com.auth0:java-jwt", version.ref = "auth0Jwt" }
|
auth0-java-jwt = { module = "com.auth0:java-jwt", version.ref = "auth0Jwt" }
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ plugins {
|
|||||||
alias(libs.plugins.spring.dependencyManagement)
|
alias(libs.plugins.spring.dependencyManagement)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deaktiviert die Erstellung eines ausführbaren Jars für dieses Bibliotheks-Modul.
|
||||||
|
tasks.getByName("bootJar") {
|
||||||
|
enabled = false
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Stellt sicher, dass alle Versionen aus der zentralen BOM kommen.
|
// Stellt sicher, dass alle Versionen aus der zentralen BOM kommen.
|
||||||
api(platform(projects.platform.platformBom))
|
api(platform(projects.platform.platformBom))
|
||||||
@@ -20,4 +25,6 @@ dependencies {
|
|||||||
|
|
||||||
// Stellt alle Test-Abhängigkeiten gebündelt bereit.
|
// Stellt alle Test-Abhängigkeiten gebündelt bereit.
|
||||||
testImplementation(projects.platform.platformTesting)
|
testImplementation(projects.platform.platformTesting)
|
||||||
|
testImplementation(libs.bundles.testing.jvm)
|
||||||
|
testImplementation(libs.kotlin.test)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -2,6 +2,8 @@ package at.mocode.infrastructure.eventstore.redis
|
|||||||
|
|
||||||
import at.mocode.core.domain.event.DomainEvent
|
import at.mocode.core.domain.event.DomainEvent
|
||||||
import at.mocode.infrastructure.eventstore.api.EventSerializer
|
import at.mocode.infrastructure.eventstore.api.EventSerializer
|
||||||
|
import jakarta.annotation.PostConstruct
|
||||||
|
import jakarta.annotation.PreDestroy
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.data.domain.Range
|
import org.springframework.data.domain.Range
|
||||||
import org.springframework.data.redis.connection.stream.*
|
import org.springframework.data.redis.connection.stream.*
|
||||||
@@ -9,8 +11,6 @@ import org.springframework.data.redis.core.StringRedisTemplate
|
|||||||
import org.springframework.scheduling.annotation.Scheduled
|
import org.springframework.scheduling.annotation.Scheduled
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
import javax.annotation.PostConstruct
|
|
||||||
import javax.annotation.PreDestroy
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Consumer for Redis Streams that processes events using consumer groups.
|
* Consumer for Redis Streams that processes events using consumer groups.
|
||||||
|
|||||||
+4
-4
@@ -5,6 +5,8 @@ import at.mocode.core.domain.event.DomainEvent
|
|||||||
import at.mocode.infrastructure.eventstore.api.EventSerializer
|
import at.mocode.infrastructure.eventstore.api.EventSerializer
|
||||||
import at.mocode.infrastructure.eventstore.api.EventStore
|
import at.mocode.infrastructure.eventstore.api.EventStore
|
||||||
import org.junit.jupiter.api.AfterEach
|
import org.junit.jupiter.api.AfterEach
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.springframework.data.redis.connection.RedisStandaloneConfiguration
|
import org.springframework.data.redis.connection.RedisStandaloneConfiguration
|
||||||
@@ -18,8 +20,6 @@ import java.time.Instant
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Integration tests for Redis Event Store.
|
* Integration tests for Redis Event Store.
|
||||||
@@ -285,7 +285,7 @@ class RedisEventStoreIntegrationTest {
|
|||||||
override val eventId: UUID = UUID.randomUUID(),
|
override val eventId: UUID = UUID.randomUUID(),
|
||||||
override val timestamp: Instant = Instant.now(),
|
override val timestamp: Instant = Instant.now(),
|
||||||
override val aggregateId: UUID,
|
override val aggregateId: UUID,
|
||||||
override val version: Long,
|
override val version: Int,
|
||||||
val name: String
|
val name: String
|
||||||
) : BaseDomainEvent(eventId, timestamp, aggregateId, version)
|
) : BaseDomainEvent(eventId, timestamp, aggregateId, version)
|
||||||
|
|
||||||
@@ -293,7 +293,7 @@ class RedisEventStoreIntegrationTest {
|
|||||||
override val eventId: UUID = UUID.randomUUID(),
|
override val eventId: UUID = UUID.randomUUID(),
|
||||||
override val timestamp: Instant = Instant.now(),
|
override val timestamp: Instant = Instant.now(),
|
||||||
override val aggregateId: UUID,
|
override val aggregateId: UUID,
|
||||||
override val version: Long,
|
override val version: Int,
|
||||||
val name: String
|
val name: String
|
||||||
) : BaseDomainEvent(eventId, timestamp, aggregateId, version)
|
) : BaseDomainEvent(eventId, timestamp, aggregateId, version)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -519,7 +519,7 @@ class RedisEventStoreTest {
|
|||||||
override val eventId: UUID = UUID.randomUUID(),
|
override val eventId: UUID = UUID.randomUUID(),
|
||||||
override val timestamp: Instant = Instant.now(),
|
override val timestamp: Instant = Instant.now(),
|
||||||
override val aggregateId: UUID,
|
override val aggregateId: UUID,
|
||||||
override val version: Long,
|
override val version: UUID,
|
||||||
val name: String
|
val name: String
|
||||||
) : BaseDomainEvent(eventId, timestamp, aggregateId, version)
|
) : BaseDomainEvent(eventId, timestamp, aggregateId, version)
|
||||||
|
|
||||||
@@ -527,7 +527,7 @@ class RedisEventStoreTest {
|
|||||||
override val eventId: UUID = UUID.randomUUID(),
|
override val eventId: UUID = UUID.randomUUID(),
|
||||||
override val timestamp: Instant = Instant.now(),
|
override val timestamp: Instant = Instant.now(),
|
||||||
override val aggregateId: UUID,
|
override val aggregateId: UUID,
|
||||||
override val version: Long,
|
override val version: UUID,
|
||||||
val name: String
|
val name: String
|
||||||
) : BaseDomainEvent(eventId, timestamp, aggregateId, version)
|
) : BaseDomainEvent(eventId, timestamp, aggregateId, version)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -231,7 +231,7 @@ class RedisIntegrationTest {
|
|||||||
override val eventId: UUID = UUID.randomUUID(),
|
override val eventId: UUID = UUID.randomUUID(),
|
||||||
override val timestamp: Instant = Instant.now(),
|
override val timestamp: Instant = Instant.now(),
|
||||||
override val aggregateId: UUID,
|
override val aggregateId: UUID,
|
||||||
override val version: Long,
|
override val version: UUID,
|
||||||
val name: String
|
val name: String
|
||||||
) : BaseDomainEvent(eventId, timestamp, aggregateId, version)
|
) : BaseDomainEvent(eventId, timestamp, aggregateId, version)
|
||||||
|
|
||||||
@@ -239,7 +239,7 @@ class RedisIntegrationTest {
|
|||||||
override val eventId: UUID = UUID.randomUUID(),
|
override val eventId: UUID = UUID.randomUUID(),
|
||||||
override val timestamp: Instant = Instant.now(),
|
override val timestamp: Instant = Instant.now(),
|
||||||
override val aggregateId: UUID,
|
override val aggregateId: UUID,
|
||||||
override val version: Long,
|
override val version: UUID,
|
||||||
val name: String
|
val name: String
|
||||||
) : BaseDomainEvent(eventId, timestamp, aggregateId, version)
|
) : BaseDomainEvent(eventId, timestamp, aggregateId, version)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ plugins {
|
|||||||
alias(libs.plugins.spring.dependencyManagement)
|
alias(libs.plugins.spring.dependencyManagement)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deaktiviert die Erstellung eines ausführbaren Jars für dieses Bibliotheks-Modul.
|
||||||
|
tasks.getByName("bootJar") {
|
||||||
|
enabled = false
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Stellt sicher, dass alle Versionen aus der zentralen BOM kommen.
|
// Stellt sicher, dass alle Versionen aus der zentralen BOM kommen.
|
||||||
implementation(platform(projects.platform.platformBom))
|
implementation(platform(projects.platform.platformBom))
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ plugins {
|
|||||||
alias(libs.plugins.spring.dependencyManagement)
|
alias(libs.plugins.spring.dependencyManagement)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deaktiviert die Erstellung eines ausführbaren Jars für dieses Bibliotheks-Modul.
|
||||||
|
tasks.getByName("bootJar") {
|
||||||
|
enabled = false
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Stellt sicher, dass alle Versionen aus der zentralen BOM kommen.
|
// Stellt sicher, dass alle Versionen aus der zentralen BOM kommen.
|
||||||
api(platform(projects.platform.platformBom))
|
api(platform(projects.platform.platformBom))
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ plugins {
|
|||||||
alias(libs.plugins.spring.dependencyManagement)
|
alias(libs.plugins.spring.dependencyManagement)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deaktiviert die Erstellung eines ausführbaren Jars für dieses Bibliotheks-Modul.
|
||||||
|
tasks.getByName("bootJar") {
|
||||||
|
enabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Stellt sicher, dass alle Versionen aus der zentralen BOM kommen.
|
// Stellt sicher, dass alle Versionen aus der zentralen BOM kommen.
|
||||||
implementation(platform(projects.platform.platformBom))
|
implementation(platform(projects.platform.platformBom))
|
||||||
|
|||||||
@@ -23,9 +23,7 @@ dependencies {
|
|||||||
implementation(libs.spring.boot.starter.actuator)
|
implementation(libs.spring.boot.starter.actuator)
|
||||||
|
|
||||||
// Abhängigkeiten für den Zipkin-Server und seine UI.
|
// Abhängigkeiten für den Zipkin-Server und seine UI.
|
||||||
// OPTIMIERUNG: Versionen werden jetzt zentral über libs.versions.toml verwaltet.
|
|
||||||
implementation(libs.zipkin.server)
|
implementation(libs.zipkin.server)
|
||||||
implementation(libs.zipkin.autoconfigure.ui)
|
|
||||||
|
|
||||||
// Stellt alle Test-Abhängigkeiten gebündelt bereit.
|
// Stellt alle Test-Abhängigkeiten gebündelt bereit.
|
||||||
testImplementation(projects.platform.platformTesting)
|
testImplementation(projects.platform.platformTesting)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
/*plugins {
|
/*
|
||||||
|
plugins {
|
||||||
alias(libs.plugins.kotlin.multiplatform)
|
alias(libs.plugins.kotlin.multiplatform)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +35,9 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}*/
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
// Dieses Modul bündelt alle für JVM-Tests notwendigen Abhängigkeiten.
|
// Dieses Modul bündelt alle für JVM-Tests notwendigen Abhängigkeiten.
|
||||||
// Jedes Modul, das Tests enthält, sollte dieses Modul mit `testImplementation` einbinden.
|
// Jedes Modul, das Tests enthält, sollte dieses Modul mit `testImplementation` einbinden.
|
||||||
@@ -46,12 +49,11 @@ dependencies {
|
|||||||
// Importiert die zentrale BOM für konsistente Versionen.
|
// Importiert die zentrale BOM für konsistente Versionen.
|
||||||
api(platform(projects.platform.platformBom))
|
api(platform(projects.platform.platformBom))
|
||||||
|
|
||||||
// OPTIMIERUNG: Verwendung von Bundles, um die Konfiguration zu vereinfachen.
|
|
||||||
// Diese Bundles sind in `libs.versions.toml` definiert.
|
// Diese Bundles sind in `libs.versions.toml` definiert.
|
||||||
api(libs.bundles.testing.jvm)
|
api(libs.bundles.testing.jvm)
|
||||||
api(libs.bundles.testcontainers)
|
api(libs.bundles.testcontainers)
|
||||||
|
|
||||||
// Einzelne Test-Abhängigkeiten, die nicht in den Haupt-Bundles enthalten sind.
|
// Stellt Spring Boot Test-Abhängigkeiten und die H2-Datenbank für Tests bereit.
|
||||||
api(libs.spring.boot.starter.test)
|
api(libs.spring.boot.starter.test)
|
||||||
api(libs.h2.driver) // H2 wird oft für In-Memory-Tests benötigt.
|
api(libs.h2.driver)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user