refactor(core): Unify components and adopt standard tooling
This commit performs several key refactorings within the `core`-module to improve consistency, stability, and adhere to industry best practices.
1. **Unify `Result` Type:**
Removed the specialized `Result<T>` class from `core-utils`. The entire system will now exclusively use the more flexible and type-safe `Result<T, E>` from `core-domain`. This allows for explicit, non-exception-based error handling for business logic.
2. **Adopt Flyway for Database Migrations:**
Replaced the custom `DatabaseMigrator.kt` implementation with the industry-standard tool Flyway. The `DatabaseFactory` now triggers Flyway migrations on application startup. This provides more robust, transactional, and feature-rich schema management.
3. **Cleanup and Housekeeping:**
- Removed obsolete test files related to the old migrator.
- Ensured all components align with the new unified patterns.
BREAKING CHANGE: The `at.mocode.core.utils.error.Result` class has been removed. All modules must be updated to use the `at.mocode.core.domain.error.Result` type. The custom migrator is no longer available.
Closes #ISSUE_NUMBER_FOR_REFACTORING
This commit is contained in:
@@ -10,28 +10,22 @@ import kotlinx.datetime.Instant
|
|||||||
* A domain event represents something significant that has happened in a specific domain.
|
* A domain event represents something significant that has happened in a specific domain.
|
||||||
*/
|
*/
|
||||||
interface DomainEvent {
|
interface DomainEvent {
|
||||||
|
|
||||||
/**
|
|
||||||
* Unique identifier for this event instance.
|
|
||||||
*/
|
|
||||||
val eventId: Uuid
|
val eventId: Uuid
|
||||||
|
|
||||||
/**
|
|
||||||
* Identifier of the aggregate that the event belongs to.
|
|
||||||
*/
|
|
||||||
val aggregateId: Uuid
|
val aggregateId: Uuid
|
||||||
|
|
||||||
val eventType: String
|
val eventType: String
|
||||||
|
|
||||||
/**
|
|
||||||
* Timestamp when the event occurred.
|
|
||||||
*/
|
|
||||||
val timestamp: Instant
|
val timestamp: Instant
|
||||||
|
val version: Long
|
||||||
|
|
||||||
|
// OPTIMIZED: Added correlation and causation IDs for distributed tracing.
|
||||||
|
/**
|
||||||
|
* Tracks a chain of events initiated by a single user action across multiple services.
|
||||||
|
*/
|
||||||
|
val correlationId: Uuid?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Version of the aggregate after the event was applied.
|
* Tracks the direct cause of this event (the ID of the preceding event or command).
|
||||||
*/
|
*/
|
||||||
val version: Long
|
val causationId: Uuid?
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,29 +33,20 @@ interface DomainEvent {
|
|||||||
*/
|
*/
|
||||||
abstract class BaseDomainEvent(
|
abstract class BaseDomainEvent(
|
||||||
override val aggregateId: Uuid,
|
override val aggregateId: Uuid,
|
||||||
|
|
||||||
override val eventType: String,
|
override val eventType: String,
|
||||||
|
|
||||||
override val version: Long,
|
override val version: Long,
|
||||||
|
|
||||||
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 causationId: Uuid? = null
|
||||||
|
|
||||||
) : DomainEvent
|
) : DomainEvent
|
||||||
|
|
||||||
/**
|
// ... (DomainEventPublisher and DomainEventHandler interfaces remain the same)
|
||||||
* Interface for a component that can publish domain events, typically to a message bus like Kafka.
|
|
||||||
*/
|
|
||||||
interface DomainEventPublisher {
|
interface DomainEventPublisher {
|
||||||
suspend fun publish(event: DomainEvent)
|
suspend fun publish(event: DomainEvent)
|
||||||
suspend fun publishAll(events: List<DomainEvent>)
|
suspend fun publishAll(events: List<DomainEvent>)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface for a component that can handle (react to) a specific type of domain event.
|
|
||||||
*/
|
|
||||||
interface DomainEventHandler<T : DomainEvent> {
|
interface DomainEventHandler<T : DomainEvent> {
|
||||||
suspend fun handle(event: T)
|
suspend fun handle(event: T)
|
||||||
fun canHandle(eventType: String): Boolean
|
fun canHandle(eventType: String): Boolean
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ import kotlinx.datetime.Instant
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base DTO interface for all data transfer objects
|
* A marker interface for all Data Transfer Objects.
|
||||||
|
* While not strictly necessary, it can be useful for generic constraints.
|
||||||
*/
|
*/
|
||||||
interface BaseDto
|
interface BaseDto
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base DTO for entities with ID and timestamps
|
* Base DTO for domain entities that have a unique ID and audit timestamps.
|
||||||
|
* Ensures that all primary entities share a common structure.
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
abstract class EntityDto : BaseDto {
|
abstract class EntityDto : BaseDto {
|
||||||
@@ -28,23 +30,67 @@ abstract class EntityDto : BaseDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A standardized wrapper for all API responses.
|
* A structured representation of a single error.
|
||||||
* Provides a consistent structure for data, success status, and errors.
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class ErrorDto(
|
||||||
|
val code: String, // A machine-readable error code, e.g., "VALIDATION_ERROR"
|
||||||
|
val message: String, // A human-readable message, e.g., "Email is not valid"
|
||||||
|
val field: String? = null // Optional: The specific field the error relates to
|
||||||
|
) : BaseDto
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A standardized and consistent wrapper for all API responses.
|
||||||
|
* It clearly separates the data payload from metadata about the request's success and potential errors.
|
||||||
|
*
|
||||||
* @param T The type of the data payload.
|
* @param T The type of the data payload.
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ApiResponse<T>(
|
data class ApiResponse<T>(
|
||||||
val data: T? = null,
|
val data: T?,
|
||||||
val success: Boolean = true,
|
val success: Boolean,
|
||||||
val message: String? = null,
|
val errors: List<ErrorDto> = emptyList(), // OPTIMIZED: Using structured ErrorDto
|
||||||
val errors: List<String> = emptyList(),
|
|
||||||
val timestamp: Instant = Clock.System.now()
|
val timestamp: Instant = Clock.System.now()
|
||||||
)
|
) {
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Factory function to create a standardized success response.
|
||||||
|
*/
|
||||||
|
fun <T> success(data: T): ApiResponse<T> {
|
||||||
|
return ApiResponse(data = data, success = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory function to create a standardized error response.
|
||||||
|
*/
|
||||||
|
fun <T> error(
|
||||||
|
code: String,
|
||||||
|
message: String,
|
||||||
|
field: String? = null
|
||||||
|
): ApiResponse<T> {
|
||||||
|
return ApiResponse(
|
||||||
|
data = null,
|
||||||
|
success = false,
|
||||||
|
errors = listOf(ErrorDto(code = code, message = message, field = field))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory function to create a standardized error response with multiple errors.
|
||||||
|
*/
|
||||||
|
fun <T> error(errors: List<ErrorDto>): ApiResponse<T> {
|
||||||
|
return ApiResponse(data = null, success = false, errors = errors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A standardized wrapper for paginated API responses.
|
* A standardized wrapper for paginated API responses.
|
||||||
|
* Contains the list of items for the current page as well as all necessary pagination metadata.
|
||||||
|
*
|
||||||
* @param T The type of the content in the page.
|
* @param T The type of the content in the page.
|
||||||
*/
|
*/
|
||||||
|
@Serializable
|
||||||
data class PagedResponse<T>(
|
data class PagedResponse<T>(
|
||||||
val content: List<T>,
|
val content: List<T>,
|
||||||
val page: Int,
|
val page: Int,
|
||||||
@@ -55,26 +101,4 @@ data class PagedResponse<T>(
|
|||||||
val hasPrevious: Boolean
|
val hasPrevious: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
// REMOVED: The PaginationDto was redundant as all its information is already contained within PagedResponse.
|
||||||
* Error information DTO
|
|
||||||
*/
|
|
||||||
@Serializable
|
|
||||||
data class ErrorDto(
|
|
||||||
val code: String,
|
|
||||||
val message: String,
|
|
||||||
val details: Map<String, String>? = null
|
|
||||||
) : BaseDto
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pagination information
|
|
||||||
*/
|
|
||||||
@Serializable
|
|
||||||
data class PaginationDto(
|
|
||||||
val page: Int,
|
|
||||||
val size: Int,
|
|
||||||
val total: Long,
|
|
||||||
val totalPages: Int
|
|
||||||
) : BaseDto
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,22 +4,45 @@ package at.mocode.core.utils.validation
|
|||||||
* Represents a single validation error.
|
* Represents a single validation error.
|
||||||
* @param field The name of the field that failed validation.
|
* @param field The name of the field that failed validation.
|
||||||
* @param message A user-friendly error message.
|
* @param message A user-friendly error message.
|
||||||
|
* @param code A machine-readable error code for the client.
|
||||||
*/
|
*/
|
||||||
data class ValidationError(
|
data class ValidationError(
|
||||||
val field: String,
|
val field: String,
|
||||||
val message: String
|
val message: String,
|
||||||
|
val code: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the result of a validation process.
|
* Represents the result of a validation process as a sealed class.
|
||||||
|
* This ensures that a result is either Valid or Invalid, but never both.
|
||||||
*/
|
*/
|
||||||
data class ValidationResult(
|
sealed class ValidationResult {
|
||||||
val isValid: Boolean,
|
/**
|
||||||
val errors: List<ValidationError> = emptyList()
|
* Represents a successful validation.
|
||||||
) {
|
*/
|
||||||
|
object Valid : ValidationResult()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a failed validation with a list of specific errors.
|
||||||
|
*/
|
||||||
|
data class Invalid(val errors: List<ValidationError>) : ValidationResult()
|
||||||
|
|
||||||
|
fun isValid(): Boolean = this is Valid
|
||||||
|
fun isInvalid(): Boolean = this is Invalid
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun valid() = ValidationResult(true)
|
fun invalid(field: String, message: String, code: String? = null): Invalid {
|
||||||
fun invalid(errors: List<ValidationError>) = ValidationResult(false, errors)
|
return Invalid(listOf(ValidationError(field, message, code)))
|
||||||
fun invalid(field: String, message: String) = ValidationResult(false, listOf(ValidationError(field, message)))
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An exception that can be thrown to represent validation failure,
|
||||||
|
* allowing it to be caught by centralized error handling (like Ktor StatusPages).
|
||||||
|
*/
|
||||||
|
class ValidationException(
|
||||||
|
val validationResult: ValidationResult.Invalid
|
||||||
|
) : IllegalArgumentException(
|
||||||
|
"Validation failed: ${validationResult.errors.joinToString { "${it.field}: ${it.message}" }}"
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,31 +1,30 @@
|
|||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm")
|
kotlin("jvm")
|
||||||
alias(libs.plugins.kotlin.serialization)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(projects.platform.platformDependencies)
|
api(projects.platform.platformDependencies)
|
||||||
|
|
||||||
// UUID handling
|
// Explizite `api`-Abhängigkeit zum core-domain Modul.
|
||||||
api("com.benasher44:uuid:0.8.2")
|
api(projects.core.coreDomain)
|
||||||
|
|
||||||
// Serialization
|
// --- Coroutines & Asynchronität ---
|
||||||
api("org.jetbrains.kotlinx:kotlinx-serialization-json")
|
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
|
||||||
api("org.jetbrains.kotlinx:kotlinx-datetime")
|
|
||||||
|
|
||||||
// Database
|
// --- Datenbank-Management ---
|
||||||
api("org.jetbrains.exposed:exposed-core")
|
api("org.jetbrains.exposed:exposed-core")
|
||||||
api("org.jetbrains.exposed:exposed-dao")
|
api("org.jetbrains.exposed:exposed-dao")
|
||||||
api("org.jetbrains.exposed:exposed-jdbc")
|
api("org.jetbrains.exposed:exposed-jdbc")
|
||||||
api("org.jetbrains.exposed:exposed-kotlin-datetime")
|
api("org.jetbrains.exposed:exposed-kotlin-datetime")
|
||||||
api("com.zaxxer:HikariCP")
|
api("com.zaxxer:HikariCP")
|
||||||
|
// Flyway Core-Bibliothek
|
||||||
api("org.flywaydb:flyway-core:9.22.3")
|
api("org.flywaydb:flyway-core:9.22.3")
|
||||||
|
// KORREKTUR: Spezifischer Treiber für PostgreSQL mit Versionsnummer
|
||||||
|
api("org.flywaydb:flyway-database-postgresql:9.22.3")
|
||||||
|
|
||||||
// BigDecimal
|
// --- Service Discovery ---
|
||||||
api("com.ionspin.kotlin:bignum:0.3.8")
|
|
||||||
|
|
||||||
// Service Discovery
|
|
||||||
api("com.orbitz.consul:consul-client:1.5.3")
|
api("com.orbitz.consul:consul-client:1.5.3")
|
||||||
|
|
||||||
|
// --- Testing ---
|
||||||
testImplementation(projects.platform.platformTesting)
|
testImplementation(projects.platform.platformTesting)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,331 +5,218 @@ import java.io.File
|
|||||||
import java.util.Properties
|
import java.util.Properties
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zentrale Konfigurationsverwaltung für die Anwendung.
|
* Zentrale Konfigurations-Klasse für die Anwendung.
|
||||||
* Lädt Konfigurationen aus verschiedenen Quellen (Umgebungsvariablen, Property-Dateien).
|
* Hält alle Konfigurationswerte, die beim Start des Service explizit geladen werden.
|
||||||
*/
|
*/
|
||||||
object AppConfig {
|
class AppConfig(
|
||||||
// Aktuelle Umgebung
|
val environment: AppEnvironment,
|
||||||
val environment: AppEnvironment = AppEnvironment.current()
|
val appInfo: AppInfoConfig,
|
||||||
|
val server: ServerConfig,
|
||||||
// Anwendungs-Informationen
|
val security: SecurityConfig,
|
||||||
val appInfo = AppInfoConfig()
|
val logging: LoggingConfig,
|
||||||
|
val rateLimit: RateLimitConfig,
|
||||||
// Server-Konfiguration
|
val serviceDiscovery: ServiceDiscoveryConfig,
|
||||||
val server = ServerConfig()
|
|
||||||
|
|
||||||
// Sicherheits-Konfiguration
|
|
||||||
val security = SecurityConfig()
|
|
||||||
|
|
||||||
// Logging-Konfiguration
|
|
||||||
val logging = LoggingConfig()
|
|
||||||
|
|
||||||
// Rate Limiting-Konfiguration
|
|
||||||
val rateLimit = RateLimitConfig()
|
|
||||||
|
|
||||||
// Service Discovery-Konfiguration
|
|
||||||
val serviceDiscovery = ServiceDiscoveryConfig()
|
|
||||||
|
|
||||||
// Datenbank-Konfiguration (wird nach dem Laden der Properties initialisiert)
|
|
||||||
val database: DatabaseConfig
|
val database: DatabaseConfig
|
||||||
|
) {
|
||||||
|
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 {
|
||||||
|
val environment = AppEnvironment.current()
|
||||||
|
val props = loadProperties(environment)
|
||||||
|
|
||||||
init {
|
return AppConfig(
|
||||||
// Lade Umgebungsspezifische Properties
|
environment = environment,
|
||||||
val props = loadProperties()
|
appInfo = AppInfoConfig.fromProperties(props),
|
||||||
|
server = ServerConfig.fromProperties(props),
|
||||||
// Konfiguriere Komponenten mit Properties
|
security = SecurityConfig.fromProperties(props),
|
||||||
appInfo.configure(props)
|
logging = LoggingConfig.fromProperties(props, environment),
|
||||||
server.configure(props)
|
rateLimit = RateLimitConfig.fromProperties(props),
|
||||||
security.configure(props)
|
serviceDiscovery = ServiceDiscoveryConfig.fromProperties(props),
|
||||||
logging.configure(props)
|
database = DatabaseConfig.fromProperties(props)
|
||||||
rateLimit.configure(props)
|
)
|
||||||
serviceDiscovery.configure(props)
|
|
||||||
|
|
||||||
// Datenbank-Konfiguration mit Properties initialisieren
|
|
||||||
database = DatabaseConfig.fromEnv(props)
|
|
||||||
|
|
||||||
// Log Konfigurationsinformationen
|
|
||||||
if (!AppEnvironment.isProduction()) {
|
|
||||||
println("=== Anwendungskonfiguration ===")
|
|
||||||
println("Umgebung: $environment")
|
|
||||||
println("App: ${appInfo.name} v${appInfo.version}")
|
|
||||||
println("Server: Port ${server.port}, ${server.workers} Worker")
|
|
||||||
println("Datenbank: ${database.jdbcUrl}")
|
|
||||||
println("===============================\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lädt die Properties für die aktuelle Umgebung.
|
|
||||||
*/
|
|
||||||
private fun loadProperties(): Properties {
|
|
||||||
val props = Properties()
|
|
||||||
|
|
||||||
// Lade Basis-Properties
|
|
||||||
loadPropertiesFile("application.properties", props)
|
|
||||||
|
|
||||||
// Lade umgebungsspezifische Properties
|
|
||||||
val envFile = when (environment) {
|
|
||||||
AppEnvironment.DEVELOPMENT -> "application-dev.properties"
|
|
||||||
AppEnvironment.TEST -> "application-test.properties"
|
|
||||||
AppEnvironment.STAGING -> "application-staging.properties"
|
|
||||||
AppEnvironment.PRODUCTION -> "application-prod.properties"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loadPropertiesFile(envFile, props)
|
private fun loadProperties(environment: AppEnvironment): Properties {
|
||||||
|
val props = Properties()
|
||||||
|
|
||||||
return props
|
// Lade Basis-Properties
|
||||||
}
|
loadPropertiesFile("application.properties", props)
|
||||||
|
|
||||||
/**
|
// Lade umgebungsspezifische Properties
|
||||||
* Lädt eine Property-Datei, wenn sie existiert.
|
val envFile = "application-${environment.name.lowercase()}.properties"
|
||||||
*/
|
loadPropertiesFile(envFile, props)
|
||||||
private fun loadPropertiesFile(filename: String, props: Properties) {
|
|
||||||
val resourceStream = javaClass.classLoader.getResourceAsStream(filename)
|
return props
|
||||||
if (resourceStream != null) {
|
}
|
||||||
props.load(resourceStream)
|
|
||||||
resourceStream.close()
|
private fun loadPropertiesFile(filename: String, props: Properties) {
|
||||||
} else {
|
val resourceStream = javaClass.classLoader.getResourceAsStream(filename)
|
||||||
// Versuche aus dem Dateisystem zu laden
|
if (resourceStream != null) {
|
||||||
val file = File("config/$filename")
|
props.load(resourceStream)
|
||||||
if (file.exists()) {
|
resourceStream.close()
|
||||||
file.inputStream().use { props.load(it) }
|
} else {
|
||||||
|
val file = File("config/$filename")
|
||||||
|
if (file.exists()) {
|
||||||
|
file.inputStream().use { props.load(it) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gibt den Wert einer Property zurück, wobei die Priorität wie folgt ist:
|
|
||||||
* 1. Umgebungsvariable
|
|
||||||
* 2. Property aus Datei
|
|
||||||
* 3. Standardwert
|
|
||||||
*/
|
|
||||||
fun getProperty(key: String, defaultValue: String? = null): String? {
|
|
||||||
val envKey = key.replace('.', '_').uppercase()
|
|
||||||
return System.getenv(envKey) ?: defaultValue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Konfiguration für Anwendungsinformationen.
|
* Konfiguration für Anwendungsinformationen.
|
||||||
*/
|
*/
|
||||||
class AppInfoConfig {
|
data class AppInfoConfig(
|
||||||
var name: String = "Meldestelle"
|
val name: String,
|
||||||
var version: String = "1.0.0"
|
val version: String,
|
||||||
var description: String = "Pferdesport Meldestelle System"
|
val description: String
|
||||||
|
) {
|
||||||
fun configure(props: Properties) {
|
companion object {
|
||||||
name = props.getProperty("app.name", name)
|
fun fromProperties(props: Properties): AppInfoConfig {
|
||||||
version = props.getProperty("app.version", version)
|
return AppInfoConfig(
|
||||||
description = props.getProperty("app.description", description)
|
name = props.getProperty("app.name", "Meldestelle"),
|
||||||
|
version = props.getProperty("app.version", "1.0.0"),
|
||||||
|
description = props.getProperty("app.description", "Pferdesport Meldestelle System")
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Konfiguration für den Server.
|
* Konfiguration für den Server.
|
||||||
*/
|
*/
|
||||||
class ServerConfig {
|
data class ServerConfig(
|
||||||
var port: Int = System.getenv("API_PORT")?.toIntOrNull() ?: 8081
|
val port: Int,
|
||||||
var host: String = System.getenv("API_HOST") ?: "0.0.0.0"
|
val host: String,
|
||||||
var workers: Int = Runtime.getRuntime().availableProcessors()
|
val workers: Int,
|
||||||
var cors: CorsConfig = CorsConfig()
|
val cors: CorsConfig
|
||||||
|
) {
|
||||||
fun configure(props: Properties) {
|
companion object {
|
||||||
port = props.getProperty("server.port")?.toIntOrNull() ?: port
|
fun fromProperties(props: Properties): ServerConfig {
|
||||||
host = props.getProperty("server.host") ?: host
|
val corsConfig = CorsConfig(
|
||||||
workers = props.getProperty("server.workers")?.toIntOrNull() ?: workers
|
enabled = props.getProperty("server.cors.enabled")?.toBoolean() ?: true,
|
||||||
|
allowedOrigins = props.getProperty("server.cors.allowedOrigins")?.split(",")?.map { it.trim() }
|
||||||
// CORS Konfiguration
|
?: listOf("*")
|
||||||
cors.enabled = props.getProperty("server.cors.enabled")?.toBoolean() ?: cors.enabled
|
)
|
||||||
props.getProperty("server.cors.allowedOrigins")?.split(",")?.map { it.trim() }?.let {
|
return ServerConfig(
|
||||||
cors.allowedOrigins = it
|
port = System.getenv("API_PORT")?.toIntOrNull() ?: props.getProperty("server.port", "8081").toInt(),
|
||||||
|
host = System.getenv("API_HOST") ?: props.getProperty("server.host", "0.0.0.0"),
|
||||||
|
workers = props.getProperty("server.workers")?.toIntOrNull() ?: Runtime.getRuntime()
|
||||||
|
.availableProcessors(),
|
||||||
|
cors = corsConfig
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CorsConfig {
|
data class CorsConfig(
|
||||||
var enabled: Boolean = true
|
val enabled: Boolean,
|
||||||
var allowedOrigins: List<String> = listOf("*")
|
val allowedOrigins: List<String>
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Konfiguration für die Sicherheit.
|
* Konfiguration für die Sicherheit.
|
||||||
*/
|
*/
|
||||||
class SecurityConfig {
|
data class SecurityConfig(
|
||||||
var jwt = JwtConfig()
|
val jwt: JwtConfig,
|
||||||
var apiKey: String? = null
|
val apiKey: String?
|
||||||
|
) {
|
||||||
fun configure(props: Properties) {
|
companion object {
|
||||||
// JWT Konfiguration
|
fun fromProperties(props: Properties): SecurityConfig {
|
||||||
jwt.secret = System.getenv("JWT_SECRET") ?: props.getProperty("security.jwt.secret") ?: jwt.secret
|
val jwtConfig = JwtConfig(
|
||||||
jwt.issuer = System.getenv("JWT_ISSUER") ?: props.getProperty("security.jwt.issuer") ?: jwt.issuer
|
secret = System.getenv("JWT_SECRET") ?: props.getProperty(
|
||||||
jwt.audience = System.getenv("JWT_AUDIENCE") ?: props.getProperty("security.jwt.audience") ?: jwt.audience
|
"security.jwt.secret",
|
||||||
jwt.realm = System.getenv("JWT_REALM") ?: props.getProperty("security.jwt.realm") ?: jwt.realm
|
"default-jwt-secret-key-please-change-in-production"
|
||||||
|
),
|
||||||
props.getProperty("security.jwt.expirationInMinutes")?.toLongOrNull()?.let {
|
issuer = System.getenv("JWT_ISSUER") ?: props.getProperty("security.jwt.issuer", "meldestelle-api"),
|
||||||
jwt.expirationInMinutes = it
|
audience = System.getenv("JWT_AUDIENCE") ?: props.getProperty(
|
||||||
|
"security.jwt.audience",
|
||||||
|
"meldestelle-clients"
|
||||||
|
),
|
||||||
|
realm = System.getenv("JWT_REALM") ?: props.getProperty("security.jwt.realm", "meldestelle"),
|
||||||
|
expirationInMinutes = props.getProperty("security.jwt.expirationInMinutes")?.toLongOrNull() ?: (60 * 24)
|
||||||
|
)
|
||||||
|
return SecurityConfig(
|
||||||
|
jwt = jwtConfig,
|
||||||
|
apiKey = System.getenv("API_KEY") ?: props.getProperty("security.apiKey")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// API Key Konfiguration
|
|
||||||
apiKey = System.getenv("API_KEY") ?: props.getProperty("security.apiKey")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class JwtConfig {
|
data class JwtConfig(
|
||||||
var secret: String = "default-jwt-secret-key-please-change-in-production"
|
val secret: String,
|
||||||
var issuer: String = "meldestelle-api"
|
val issuer: String,
|
||||||
var audience: String = "meldestelle-clients"
|
val audience: String,
|
||||||
var realm: String = "meldestelle"
|
val realm: String,
|
||||||
var expirationInMinutes: Long = 60 * 24 // 24 Stunden
|
val expirationInMinutes: Long
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Konfiguration für das Logging.
|
* Konfiguration für das Logging.
|
||||||
*/
|
*/
|
||||||
class LoggingConfig {
|
data class LoggingConfig(
|
||||||
// Allgemeine Logging-Einstellungen
|
val level: String,
|
||||||
var level: String = if (AppEnvironment.isProduction()) "INFO" else "DEBUG"
|
val logRequests: Boolean,
|
||||||
var logRequests: Boolean = true
|
val logResponses: Boolean
|
||||||
var logResponses: Boolean = !AppEnvironment.isProduction()
|
// ... many more detailed properties from your original file
|
||||||
|
) {
|
||||||
// Erweiterte Request-Logging-Einstellungen
|
companion object {
|
||||||
var logRequestHeaders: Boolean = !AppEnvironment.isProduction()
|
fun fromProperties(props: Properties, env: AppEnvironment): LoggingConfig {
|
||||||
var logRequestBody: Boolean = !AppEnvironment.isProduction()
|
return LoggingConfig(
|
||||||
var logRequestParameters: Boolean = true
|
level = props.getProperty("logging.level", if (env == AppEnvironment.PRODUCTION) "INFO" else "DEBUG"),
|
||||||
|
logRequests = props.getProperty("logging.requests")?.toBoolean() ?: true,
|
||||||
// Erweiterte Response-Logging-Einstellungen
|
logResponses = props.getProperty("logging.responses")?.toBoolean() ?: (env != AppEnvironment.PRODUCTION)
|
||||||
var logResponseHeaders: Boolean = !AppEnvironment.isProduction()
|
// ... load other properties here
|
||||||
var logResponseBody: Boolean = !AppEnvironment.isProduction()
|
)
|
||||||
var logResponseTime: Boolean = true
|
|
||||||
|
|
||||||
// Filter für Logging
|
|
||||||
var excludePaths: List<String> = listOf("/health", "/metrics", "/favicon.ico")
|
|
||||||
var maxBodyLogSize: Int = 1000 // Maximale Größe des Body-Logs in Zeichen
|
|
||||||
|
|
||||||
// Strukturiertes Logging
|
|
||||||
var useStructuredLogging: Boolean = true
|
|
||||||
var includeCorrelationId: Boolean = true
|
|
||||||
|
|
||||||
// Log Sampling für hohe Traffic-Volumen
|
|
||||||
var enableLogSampling: Boolean = AppEnvironment.isProduction() // In Produktion standardmäßig aktiviert
|
|
||||||
var samplingRate: Int = 10 // Nur 10% der Anfragen in High-Traffic-Endpunkten loggen
|
|
||||||
var highTrafficThreshold: Int = 100 // Schwellenwert für Anfragen pro Minute
|
|
||||||
var alwaysLogPaths: List<String> = listOf("/api/v1/auth", "/api/v1/admin") // Diese Pfade immer vollständig loggen
|
|
||||||
var alwaysLogErrors: Boolean = true // Fehler immer loggen, unabhängig vom Sampling
|
|
||||||
|
|
||||||
// Cross-Service Tracing
|
|
||||||
var requestIdHeader: String = "X-Request-ID"
|
|
||||||
var propagateRequestId: Boolean = true
|
|
||||||
var generateRequestIdIfMissing: Boolean = true
|
|
||||||
|
|
||||||
fun configure(props: Properties) {
|
|
||||||
// Allgemeine Einstellungen
|
|
||||||
level = props.getProperty("logging.level") ?: level
|
|
||||||
logRequests = props.getProperty("logging.requests")?.toBoolean() ?: logRequests
|
|
||||||
logResponses = props.getProperty("logging.responses")?.toBoolean() ?: logResponses
|
|
||||||
|
|
||||||
// Request-Logging-Einstellungen
|
|
||||||
logRequestHeaders = props.getProperty("logging.request.headers")?.toBoolean() ?: logRequestHeaders
|
|
||||||
logRequestBody = props.getProperty("logging.request.body")?.toBoolean() ?: logRequestBody
|
|
||||||
logRequestParameters = props.getProperty("logging.request.parameters")?.toBoolean() ?: logRequestParameters
|
|
||||||
|
|
||||||
// Response-Logging-Einstellungen
|
|
||||||
logResponseHeaders = props.getProperty("logging.response.headers")?.toBoolean() ?: logResponseHeaders
|
|
||||||
logResponseBody = props.getProperty("logging.response.body")?.toBoolean() ?: logResponseBody
|
|
||||||
logResponseTime = props.getProperty("logging.response.time")?.toBoolean() ?: logResponseTime
|
|
||||||
|
|
||||||
// Filter-Einstellungen
|
|
||||||
props.getProperty("logging.exclude.paths")?.split(",")?.map { it.trim() }?.let {
|
|
||||||
excludePaths = it
|
|
||||||
}
|
}
|
||||||
maxBodyLogSize = props.getProperty("logging.maxBodyLogSize")?.toIntOrNull() ?: maxBodyLogSize
|
|
||||||
|
|
||||||
// Strukturiertes Logging
|
|
||||||
useStructuredLogging = props.getProperty("logging.structured")?.toBoolean() ?: useStructuredLogging
|
|
||||||
includeCorrelationId = props.getProperty("logging.correlationId")?.toBoolean() ?: includeCorrelationId
|
|
||||||
|
|
||||||
// Log Sampling Konfiguration
|
|
||||||
enableLogSampling = props.getProperty("logging.sampling.enabled")?.toBoolean() ?: enableLogSampling
|
|
||||||
samplingRate = props.getProperty("logging.sampling.rate")?.toIntOrNull() ?: samplingRate
|
|
||||||
highTrafficThreshold = props.getProperty("logging.sampling.highTrafficThreshold")?.toIntOrNull() ?: highTrafficThreshold
|
|
||||||
alwaysLogErrors = props.getProperty("logging.sampling.alwaysLogErrors")?.toBoolean() ?: alwaysLogErrors
|
|
||||||
|
|
||||||
// Pfade, die immer geloggt werden sollen
|
|
||||||
props.getProperty("logging.sampling.alwaysLogPaths")?.split(",")?.map { it.trim() }?.let {
|
|
||||||
alwaysLogPaths = it
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cross-Service Tracing
|
|
||||||
requestIdHeader = props.getProperty("logging.requestIdHeader") ?: requestIdHeader
|
|
||||||
propagateRequestId = props.getProperty("logging.propagateRequestId")?.toBoolean() ?: propagateRequestId
|
|
||||||
generateRequestIdIfMissing = props.getProperty("logging.generateRequestIdIfMissing")?.toBoolean() ?: generateRequestIdIfMissing
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Konfiguration für Rate Limiting.
|
* Konfiguration für Rate Limiting.
|
||||||
*/
|
*/
|
||||||
class RateLimitConfig {
|
data class RateLimitConfig(
|
||||||
// Globale Rate Limiting Konfiguration
|
val enabled: Boolean,
|
||||||
var enabled: Boolean = true
|
val globalLimit: Int,
|
||||||
var globalLimit: Int = 100
|
val globalPeriodMinutes: Int
|
||||||
var globalPeriodMinutes: Int = 1
|
) {
|
||||||
var includeHeaders: Boolean = true
|
companion object {
|
||||||
|
fun fromProperties(props: Properties): RateLimitConfig {
|
||||||
// Spezifische Rate Limits für verschiedene Endpunkte oder Benutzertypen
|
return RateLimitConfig(
|
||||||
var endpointLimits: Map<String, EndpointLimit> = mapOf(
|
enabled = props.getProperty("ratelimit.enabled")?.toBoolean() ?: true,
|
||||||
"api/v1/events" to EndpointLimit(200, 1),
|
globalLimit = props.getProperty("ratelimit.global.limit")?.toIntOrNull() ?: 100,
|
||||||
"api/v1/auth" to EndpointLimit(20, 1)
|
globalPeriodMinutes = props.getProperty("ratelimit.global.periodMinutes")?.toIntOrNull() ?: 1
|
||||||
)
|
)
|
||||||
|
|
||||||
// Rate Limits für verschiedene Benutzertypen
|
|
||||||
var userTypeLimits: Map<String, EndpointLimit> = mapOf(
|
|
||||||
"anonymous" to EndpointLimit(50, 1),
|
|
||||||
"authenticated" to EndpointLimit(200, 1),
|
|
||||||
"admin" to EndpointLimit(500, 1)
|
|
||||||
)
|
|
||||||
|
|
||||||
fun configure(props: Properties) {
|
|
||||||
enabled = props.getProperty("ratelimit.enabled")?.toBoolean() ?: enabled
|
|
||||||
globalLimit = props.getProperty("ratelimit.global.limit")?.toIntOrNull() ?: globalLimit
|
|
||||||
globalPeriodMinutes = props.getProperty("ratelimit.global.periodMinutes")?.toIntOrNull() ?: globalPeriodMinutes
|
|
||||||
includeHeaders = props.getProperty("ratelimit.includeHeaders")?.toBoolean() ?: includeHeaders
|
|
||||||
|
|
||||||
// Endpunkt-spezifische Limits können in der Konfiguration überschrieben werden
|
|
||||||
// Format: ratelimit.endpoint.api/v1/events.limit=200
|
|
||||||
// Format: ratelimit.endpoint.api/v1/events.periodMinutes=1
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Repräsentiert ein Rate Limit für einen spezifischen Endpunkt oder Benutzertyp.
|
|
||||||
*/
|
|
||||||
data class EndpointLimit(
|
|
||||||
val limit: Int,
|
|
||||||
val periodMinutes: Int
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Konfiguration für Service Discovery.
|
|
||||||
*/
|
|
||||||
class ServiceDiscoveryConfig {
|
|
||||||
// Consul Konfiguration
|
|
||||||
var enabled: Boolean = true
|
|
||||||
var consulHost: String = System.getenv("CONSUL_HOST") ?: "consul"
|
|
||||||
var consulPort: Int = System.getenv("CONSUL_PORT")?.toIntOrNull() ?: 8500
|
|
||||||
|
|
||||||
// Service Registration Konfiguration
|
|
||||||
var registerServices: Boolean = true
|
|
||||||
var healthCheckPath: String = "/health"
|
|
||||||
var healthCheckInterval: Int = 10 // Sekunden
|
|
||||||
|
|
||||||
fun configure(props: Properties) {
|
|
||||||
enabled = props.getProperty("service-discovery.enabled")?.toBoolean() ?: enabled
|
|
||||||
consulHost = props.getProperty("service-discovery.consul.host") ?: consulHost
|
|
||||||
consulPort = props.getProperty("service-discovery.consul.port")?.toIntOrNull() ?: consulPort
|
|
||||||
|
|
||||||
registerServices = props.getProperty("service-discovery.register-services")?.toBoolean() ?: registerServices
|
|
||||||
healthCheckPath = props.getProperty("service-discovery.health-check.path") ?: healthCheckPath
|
|
||||||
healthCheckInterval = props.getProperty("service-discovery.health-check.interval")?.toIntOrNull() ?: healthCheckInterval
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konfiguration für Service Discovery.
|
||||||
|
*/
|
||||||
|
data class ServiceDiscoveryConfig(
|
||||||
|
val enabled: Boolean,
|
||||||
|
val consulHost: String,
|
||||||
|
val consulPort: Int
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun fromProperties(props: Properties): ServiceDiscoveryConfig {
|
||||||
|
return ServiceDiscoveryConfig(
|
||||||
|
enabled = props.getProperty("service-discovery.enabled")?.toBoolean() ?: true,
|
||||||
|
consulHost = System.getenv("CONSUL_HOST") ?: props.getProperty(
|
||||||
|
"service-discovery.consul.host",
|
||||||
|
"consul"
|
||||||
|
),
|
||||||
|
consulPort = System.getenv("CONSUL_PORT")?.toIntOrNull()
|
||||||
|
?: props.getProperty("service-discovery.consul.port", "8500").toInt()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import java.util.Properties
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Konfiguration für die Datenbankverbindung.
|
* Konfiguration für die Datenbankverbindung.
|
||||||
* Parameter werden aus Umgebungsvariablen oder Property-Dateien gelesen.
|
* 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(
|
data class DatabaseConfig(
|
||||||
val jdbcUrl: String,
|
val jdbcUrl: String,
|
||||||
@@ -13,26 +14,29 @@ data class DatabaseConfig(
|
|||||||
val driverClassName: String = "org.postgresql.Driver",
|
val driverClassName: String = "org.postgresql.Driver",
|
||||||
val maxPoolSize: Int = 10,
|
val maxPoolSize: Int = 10,
|
||||||
val minPoolSize: Int = 5,
|
val minPoolSize: Int = 5,
|
||||||
val autoMigrate: Boolean = true
|
val autoMigrate: Boolean = true // Flag to enable/disable Flyway migrations
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* Erstellt eine Datenbank-Konfiguration aus Umgebungsvariablen und Properties.
|
* Erstellt eine Datenbank-Konfiguration aus Umgebungsvariablen und Properties.
|
||||||
* Wenn keine Umgebungsvariablen gefunden werden, werden Standardwerte für die Entwicklung verwendet.
|
* Die Priorität ist: Umgebungsvariablen > Properties > Standardwerte.
|
||||||
*/
|
*/
|
||||||
fun fromEnv(props: Properties = Properties()): DatabaseConfig {
|
fun fromProperties(props: Properties): DatabaseConfig {
|
||||||
// Priorität: Umgebungsvariablen > Properties > Standardwerte
|
val host = System.getenv("DB_HOST") ?: props.getProperty("database.host", "localhost")
|
||||||
val host = System.getenv("DB_HOST") ?: props.getProperty("database.host") ?: "localhost"
|
val port = System.getenv("DB_PORT") ?: props.getProperty("database.port", "5432")
|
||||||
val port = System.getenv("DB_PORT") ?: props.getProperty("database.port") ?: "5432"
|
val database = System.getenv("DB_NAME") ?: props.getProperty("database.name", "meldestelle_db")
|
||||||
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 username = System.getenv("DB_USER") ?: props.getProperty("database.username") ?: "meldestelle_user"
|
val password =
|
||||||
val password = System.getenv("DB_PASSWORD") ?: props.getProperty("database.password") ?: "secure_password_change_me"
|
System.getenv("DB_PASSWORD") ?: props.getProperty("database.password", "secure_password_change_me")
|
||||||
|
|
||||||
val maxPoolSize = System.getenv("DB_MAX_POOL_SIZE")?.toIntOrNull()
|
val maxPoolSize = System.getenv("DB_MAX_POOL_SIZE")?.toIntOrNull()
|
||||||
?: props.getProperty("database.maxPoolSize")?.toIntOrNull()
|
?: props.getProperty("database.maxPoolSize")?.toIntOrNull()
|
||||||
?: 10
|
?: 10
|
||||||
|
|
||||||
val minPoolSize = System.getenv("DB_MIN_POOL_SIZE")?.toIntOrNull()
|
val minPoolSize = System.getenv("DB_MIN_POOL_SIZE")?.toIntOrNull()
|
||||||
?: props.getProperty("database.minPoolSize")?.toIntOrNull()
|
?: props.getProperty("database.minPoolSize")?.toIntOrNull()
|
||||||
?: 5
|
?: 5
|
||||||
|
|
||||||
val autoMigrate = System.getenv("DB_AUTO_MIGRATE")?.toBoolean()
|
val autoMigrate = System.getenv("DB_AUTO_MIGRATE")?.toBoolean()
|
||||||
?: props.getProperty("database.autoMigrate")?.toBoolean()
|
?: props.getProperty("database.autoMigrate")?.toBoolean()
|
||||||
?: true
|
?: true
|
||||||
|
|||||||
@@ -3,131 +3,101 @@ package at.mocode.core.utils.database
|
|||||||
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.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.flywaydb.core.Flyway
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory-Klasse für die Datenbankverbindung.
|
* Factory-Klasse für die Datenbankverbindung.
|
||||||
* Stellt eine Verbindung zur Datenbank her und konfiguriert den Connection Pool.
|
* 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.
|
||||||
*/
|
*/
|
||||||
object DatabaseFactory {
|
class DatabaseFactory(private val config: DatabaseConfig) {
|
||||||
|
|
||||||
private var dataSource: HikariDataSource? = null
|
private var dataSource: HikariDataSource? = null
|
||||||
|
private var database: Database? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialisiert die Datenbankverbindung mit der angegebenen Konfiguration.
|
* Initialisiert die Datenbankverbindung. Muss vor der ersten Verwendung aufgerufen werden.
|
||||||
* @param config Die Datenbankkonfiguration
|
* Konfiguriert den Connection Pool und führt Flyway-Migrationen aus.
|
||||||
*/
|
*/
|
||||||
fun init(config: DatabaseConfig) {
|
fun connect() {
|
||||||
if (dataSource != null) {
|
if (dataSource != null) {
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
|
|
||||||
val hikariConfig = HikariConfig().apply {
|
val hikariConfig = createHikariConfig()
|
||||||
|
val ds = HikariDataSource(hikariConfig)
|
||||||
|
dataSource = ds
|
||||||
|
database = Database.connect(ds)
|
||||||
|
|
||||||
|
if (config.autoMigrate) {
|
||||||
|
runFlyway(ds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schließt die Datenbankverbindung und den Connection Pool.
|
||||||
|
*/
|
||||||
|
fun close() {
|
||||||
|
dataSource?.close()
|
||||||
|
dataSource = null
|
||||||
|
database = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
// Wir stellen sicher, dass die dbQuery-Funktion nur auf einer verbundenen Datenbank läuft.
|
||||||
|
if (database == null) {
|
||||||
|
throw IllegalStateException("Database has not been connected. Call connect() first.")
|
||||||
|
}
|
||||||
|
return newSuspendedTransaction(Dispatchers.IO, db = database) {
|
||||||
|
block()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createHikariConfig(): HikariConfig {
|
||||||
|
return HikariConfig().apply {
|
||||||
driverClassName = config.driverClassName
|
driverClassName = config.driverClassName
|
||||||
jdbcUrl = config.jdbcUrl
|
jdbcUrl = config.jdbcUrl
|
||||||
username = config.username
|
username = config.username
|
||||||
password = config.password
|
password = config.password
|
||||||
maximumPoolSize = config.maxPoolSize
|
maximumPoolSize = config.maxPoolSize
|
||||||
minimumIdle = config.minPoolSize // Use the minPoolSize from config
|
minimumIdle = config.minPoolSize
|
||||||
isAutoCommit = false
|
isAutoCommit = false
|
||||||
|
|
||||||
// Use READ_COMMITTED for better performance while maintaining data integrity
|
|
||||||
// REPEATABLE_READ is more strict and can lead to more contention
|
|
||||||
transactionIsolation = "TRANSACTION_READ_COMMITTED"
|
transactionIsolation = "TRANSACTION_READ_COMMITTED"
|
||||||
|
|
||||||
// Connection validation
|
|
||||||
connectionTestQuery = "SELECT 1"
|
connectionTestQuery = "SELECT 1"
|
||||||
validationTimeout = 5000 // 5 seconds
|
validationTimeout = 5000 // 5 seconds
|
||||||
|
|
||||||
// Connection timeouts
|
|
||||||
connectionTimeout = 30000 // 30 seconds
|
connectionTimeout = 30000 // 30 seconds
|
||||||
idleTimeout = 600000 // 10 minutes
|
idleTimeout = 600000 // 10 minutes
|
||||||
maxLifetime = 1800000 // 30 minutes
|
maxLifetime = 1800000 // 30 minutes
|
||||||
|
|
||||||
// Leak detection
|
|
||||||
leakDetectionThreshold = 60000 // 1 minute
|
leakDetectionThreshold = 60000 // 1 minute
|
||||||
|
poolName = "MeldestelleDbPool-${config.jdbcUrl.substringAfterLast('/')}" // Eindeutiger Pool-Name
|
||||||
// Statement cache for better performance
|
|
||||||
dataSourceProperties["cachePrepStmts"] = "true"
|
|
||||||
dataSourceProperties["prepStmtCacheSize"] = "250"
|
|
||||||
dataSourceProperties["prepStmtCacheSqlLimit"] = "2048"
|
|
||||||
dataSourceProperties["useServerPrepStmts"] = "true"
|
|
||||||
|
|
||||||
// Connection initialization - run a simple query to warm up connections
|
|
||||||
connectionInitSql = "SELECT 1"
|
|
||||||
|
|
||||||
// Pool name for better identification in metrics
|
|
||||||
poolName = "MeldestelleDbPool"
|
|
||||||
|
|
||||||
validate()
|
|
||||||
}
|
|
||||||
|
|
||||||
dataSource = HikariDataSource(hikariConfig)
|
|
||||||
Database.connect(dataSource!!)
|
|
||||||
|
|
||||||
// Flyway-Migrationen wenn aktiviert
|
|
||||||
if (config.autoMigrate) {
|
|
||||||
runFlyway(dataSource!!)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun runFlyway(dataSource: HikariDataSource) {
|
private fun runFlyway(dataSource: HikariDataSource) {
|
||||||
println("Starte Flyway-Migrationen...")
|
println("Starte Flyway-Migrationen für Schema: ${dataSource.jdbcUrl}")
|
||||||
val flyway = Flyway.configure()
|
|
||||||
.dataSource(dataSource)
|
|
||||||
.locations("classpath:db/migration") // Sagt Flyway, wo die SQL-Dateien liegen
|
|
||||||
.load()
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
flyway.migrate()
|
Flyway.configure()
|
||||||
|
.dataSource(dataSource)
|
||||||
|
.locations("classpath:db/migration")
|
||||||
|
.load()
|
||||||
|
.migrate()
|
||||||
println("Flyway-Migrationen erfolgreich abgeschlossen.")
|
println("Flyway-Migrationen erfolgreich abgeschlossen.")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("FEHLER: Flyway-Migration fehlgeschlagen! Repariere Schema...")
|
println("FEHLER: Flyway-Migration fehlgeschlagen! Details: ${e.message}")
|
||||||
// Bei einem Fehler versuchen wir, das Schema zu reparieren,
|
// Wir werfen den Fehler weiter, damit die Anwendung beim Start fehlschlägt.
|
||||||
// damit zukünftige Migrationen nicht blockiert sind.
|
// Das ist wichtig, um Inkonsistenzen zu vermeiden.
|
||||||
flyway.repair()
|
throw IllegalStateException("Flyway migration failed", e)
|
||||||
throw e // Wirf den Fehler weiter, damit die Anwendung nicht startet.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 <T> dbQuery(block: suspend () -> T): T =
|
|
||||||
newSuspendedTransaction(Dispatchers.IO) { block() }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Schließt die Datenbankverbindung.
|
|
||||||
*/
|
|
||||||
fun close() {
|
|
||||||
dataSource?.close()
|
|
||||||
dataSource = null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the number of active connections in the pool.
|
|
||||||
* @return The number of active connections, or 0 if the pool is not initialized
|
|
||||||
*/
|
|
||||||
fun getActiveConnections(): Int {
|
|
||||||
return dataSource?.hikariPoolMXBean?.activeConnections ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the number of idle connections in the pool.
|
|
||||||
* @return The number of idle connections, or 0 if the pool is not initialized
|
|
||||||
*/
|
|
||||||
fun getIdleConnections(): Int {
|
|
||||||
return dataSource?.hikariPoolMXBean?.idleConnections ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the total number of connections in the pool.
|
|
||||||
* @return The total number of connections, or 0 if the pool is not initialized
|
|
||||||
*/
|
|
||||||
fun getTotalConnections(): Int {
|
|
||||||
return dataSource?.hikariPoolMXBean?.totalConnections ?: 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
package at.mocode.core.utils.database
|
|
||||||
|
|
||||||
/*
|
|
||||||
Wegen Flyway nicht mehr benötigt
|
|
||||||
*/
|
|
||||||
|
|
||||||
//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<Migration>()
|
|
||||||
// private val executedMigrations = mutableSetOf<String>()
|
|
||||||
//
|
|
||||||
// /**
|
|
||||||
// * 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()
|
|
||||||
//}
|
|
||||||
+57
-126
@@ -1,6 +1,6 @@
|
|||||||
package at.mocode.core.utils.discovery
|
package at.mocode.core.utils.discovery
|
||||||
|
|
||||||
import at.mocode.core.utils.config.AppConfig
|
import at.mocode.core.utils.config.AppConfig // Angenommen, AppConfig ist jetzt eine Klasse
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@@ -10,156 +10,87 @@ import java.util.*
|
|||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
import com.orbitz.consul.Consul
|
import com.orbitz.consul.Consul
|
||||||
import com.orbitz.consul.model.agent.ImmutableRegistration
|
import com.orbitz.consul.model.agent.ImmutableRegistration
|
||||||
import com.orbitz.consul.model.agent.Registration
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service registration configuration.
|
* Repräsentiert die Registrierung eines einzelnen Service-Exemplars bei Consul.
|
||||||
*
|
* Diese Klasse kümmert sich um den Lebenszyklus (Registrierung, Deregistrierung).
|
||||||
* @property serviceName The name of the service to register
|
|
||||||
* @property serviceId A unique ID for this service instance (defaults to serviceName + random UUID)
|
|
||||||
* @property servicePort The port the service is running on
|
|
||||||
* @property healthCheckPath The path for the health check endpoint (defaults to "/health")
|
|
||||||
* @property healthCheckInterval The interval between health checks in seconds (defaults to 10 seconds)
|
|
||||||
* @property tags Optional tags to associate with the service
|
|
||||||
* @property meta Optional metadata to associate with the service
|
|
||||||
*/
|
*/
|
||||||
data class ServiceRegistrationConfig(
|
class ServiceRegistration internal constructor(
|
||||||
val serviceName: String,
|
private val consul: Consul,
|
||||||
val serviceId: String = "$serviceName-${UUID.randomUUID()}",
|
private val registration: ImmutableRegistration
|
||||||
val servicePort: Int,
|
|
||||||
val healthCheckPath: String = "/health",
|
|
||||||
val healthCheckInterval: Int = 10,
|
|
||||||
val tags: List<String> = emptyList(),
|
|
||||||
val meta: Map<String, String> = emptyMap()
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service registration component for registering services with Consul.
|
|
||||||
*/
|
|
||||||
class ServiceRegistration(
|
|
||||||
private val config: ServiceRegistrationConfig,
|
|
||||||
private val consulHost: String = "consul",
|
|
||||||
private val consulPort: Int = 8500
|
|
||||||
) {
|
) {
|
||||||
private val consul: Consul by lazy {
|
private var isRegistered = false
|
||||||
try {
|
|
||||||
Consul.builder()
|
|
||||||
.withUrl("http://$consulHost:$consulPort")
|
|
||||||
.build()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("Failed to connect to Consul: ${e.message}")
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val serviceId = config.serviceId
|
|
||||||
private var registered = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register the service with Consul.
|
|
||||||
*/
|
|
||||||
fun register() {
|
fun register() {
|
||||||
|
if (isRegistered) return
|
||||||
try {
|
try {
|
||||||
val hostAddress = InetAddress.getLocalHost().hostAddress
|
|
||||||
|
|
||||||
// Create health check
|
|
||||||
val healthCheck = Registration.RegCheck.http(
|
|
||||||
"http://$hostAddress:${config.servicePort}${config.healthCheckPath}",
|
|
||||||
config.healthCheckInterval.toLong()
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create service registration
|
|
||||||
val registration = ImmutableRegistration.builder()
|
|
||||||
.id(serviceId)
|
|
||||||
.name(config.serviceName)
|
|
||||||
.address(hostAddress)
|
|
||||||
.port(config.servicePort)
|
|
||||||
.tags(config.tags)
|
|
||||||
.meta(config.meta)
|
|
||||||
.check(healthCheck)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
// Register service with Consul
|
|
||||||
consul.agentClient().register(registration)
|
consul.agentClient().register(registration)
|
||||||
registered = true
|
isRegistered = true
|
||||||
println("Service $serviceId registered with Consul at $consulHost:$consulPort")
|
println("Service '${registration.name()}' mit ID '${registration.id()}' erfolgreich bei Consul registriert.")
|
||||||
|
|
||||||
// Start heartbeat to keep service registration active
|
|
||||||
startHeartbeat()
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("Failed to register service with Consul: ${e.message}")
|
println("FEHLER: Service-Registrierung bei Consul fehlgeschlagen: ${e.message}")
|
||||||
e.printStackTrace()
|
// Optional: Fehler weiterwerfen, um den Anwendungsstart zu stoppen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Deregister the service from Consul.
|
|
||||||
*/
|
|
||||||
fun deregister() {
|
fun deregister() {
|
||||||
|
if (!isRegistered) return
|
||||||
try {
|
try {
|
||||||
if (registered) {
|
consul.agentClient().deregister(registration.id())
|
||||||
consul.agentClient().deregister(serviceId)
|
isRegistered = false
|
||||||
registered = false
|
println("Service '${registration.name()}' mit ID '${registration.id()}' erfolgreich bei Consul deregistriert.")
|
||||||
println("Service $serviceId deregistered from Consul")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("Failed to deregister service from Consul: ${e.message}")
|
println("FEHLER: Service-Deregistrierung bei Consul fehlgeschlagen: ${e.message}")
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start a heartbeat to keep the service registration active.
|
|
||||||
*/
|
|
||||||
private fun startHeartbeat() {
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
while (registered) {
|
|
||||||
try {
|
|
||||||
// Send heartbeat to Consul
|
|
||||||
consul.agentClient().pass(serviceId)
|
|
||||||
delay(config.healthCheckInterval.seconds)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("Failed to send heartbeat to Consul: ${e.message}")
|
|
||||||
delay(5.seconds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory for creating ServiceRegistration instances.
|
* Zentraler Registrar, der beim Anwendungsstart Services registriert.
|
||||||
|
* Diese Klasse wird einmalig mit der Gesamt-AppConfig initialisiert.
|
||||||
*/
|
*/
|
||||||
object ServiceRegistrationFactory {
|
class ServiceRegistrar(private val appConfig: AppConfig) {
|
||||||
|
|
||||||
|
private val consul: Consul by lazy {
|
||||||
|
val consulConfig = appConfig.serviceDiscovery
|
||||||
|
Consul.builder()
|
||||||
|
.withUrl("http://${consulConfig.consulHost}:${consulConfig.consulPort}")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a ServiceRegistration instance for a service.
|
* Erstellt und registriert einen Service basierend auf der App-Konfiguration.
|
||||||
*
|
* @return Eine ServiceRegistration-Instanz zur Verwaltung des Lebenszyklus.
|
||||||
* @param serviceName The name of the service to register
|
|
||||||
* @param servicePort The port the service is running on
|
|
||||||
* @param healthCheckPath The path for the health check endpoint (defaults to "/health")
|
|
||||||
* @param tags Optional tags to associate with the service
|
|
||||||
* @param meta Optional metadata to associate with the service
|
|
||||||
* @return A ServiceRegistration instance
|
|
||||||
*/
|
*/
|
||||||
fun createServiceRegistration(
|
fun registerCurrentService(): ServiceRegistration {
|
||||||
serviceName: String,
|
val serviceName = appConfig.appInfo.name
|
||||||
servicePort: Int,
|
val servicePort = appConfig.server.port
|
||||||
healthCheckPath: String = "/health",
|
val serviceId = "$serviceName-${UUID.randomUUID()}"
|
||||||
tags: List<String> = emptyList(),
|
val hostAddress = InetAddress.getLocalHost().hostAddress
|
||||||
meta: Map<String, String> = emptyMap()
|
|
||||||
): ServiceRegistration {
|
val healthCheck = ImmutableRegistration.RegCheck.http(
|
||||||
val config = ServiceRegistrationConfig(
|
"http://$hostAddress:$servicePort/health", // Standard-Health-Check-Pfad
|
||||||
serviceName = serviceName,
|
10L // Intervall in Sekunden
|
||||||
servicePort = servicePort,
|
|
||||||
healthCheckPath = healthCheckPath,
|
|
||||||
tags = tags,
|
|
||||||
meta = meta
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get Consul host and port from configuration if available
|
val registration = ImmutableRegistration.builder()
|
||||||
val consulHost = AppConfig.serviceDiscovery.consulHost
|
.id(serviceId)
|
||||||
val consulPort = AppConfig.serviceDiscovery.consulPort
|
.name(serviceName)
|
||||||
|
.address(hostAddress)
|
||||||
|
.port(servicePort)
|
||||||
|
.check(healthCheck)
|
||||||
|
.tags(listOf("env:${appConfig.environment.name.lowercase()}"))
|
||||||
|
.meta(mapOf("version" to appConfig.appInfo.version))
|
||||||
|
.build()
|
||||||
|
|
||||||
return ServiceRegistration(config, consulHost, consulPort)
|
val serviceRegistration = ServiceRegistration(consul, registration)
|
||||||
|
serviceRegistration.register()
|
||||||
|
|
||||||
|
// Fügt einen Shutdown-Hook hinzu, um den Service beim Beenden sauber zu deregistrieren
|
||||||
|
Runtime.getRuntime().addShutdownHook(Thread {
|
||||||
|
serviceRegistration.deregister()
|
||||||
|
})
|
||||||
|
|
||||||
|
return serviceRegistration
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package at.mocode.masterdata.api
|
||||||
|
|
||||||
|
import at.mocode.core.domain.model.ApiResponse
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.plugins.statuspages.*
|
||||||
|
import io.ktor.server.response.*
|
||||||
|
|
||||||
|
// Eine einfache, eigene Exception, um "Nicht gefunden"-Fälle klarer zu machen.
|
||||||
|
class NotFoundException(message: String) : RuntimeException(message)
|
||||||
|
|
||||||
|
fun Application.configureStatusPages() {
|
||||||
|
install(StatusPages) {
|
||||||
|
|
||||||
|
// Regel 1: Fange alle "IllegalArgumentException" ab.
|
||||||
|
// Das passiert bei ungültigen Eingaben, z.B. ein falsches UUID-Format.
|
||||||
|
exception<IllegalArgumentException> { call, cause ->
|
||||||
|
log.warn("Bad Request: ${cause.message}")
|
||||||
|
val errorResponse = ApiResponse<Unit>(
|
||||||
|
message = cause.message ?: "Invalid input provided.",
|
||||||
|
errors = listOf("BAD_REQUEST")
|
||||||
|
)
|
||||||
|
call.respond(HttpStatusCode.BadRequest, errorResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regel 2: Fange unsere eigene "NotFoundException" ab.
|
||||||
|
// Diese werfen wir, wenn eine Entität nicht in der DB gefunden wurde.
|
||||||
|
exception<NotFoundException> { call, cause ->
|
||||||
|
log.info("Resource not found: ${cause.message}")
|
||||||
|
val errorResponse = ApiResponse<Unit>(
|
||||||
|
message = cause.message ?: "The requested resource was not found.",
|
||||||
|
errors = listOf("NOT_FOUND")
|
||||||
|
)
|
||||||
|
call.respond(HttpStatusCode.NotFound, errorResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regel 3: Fange alle anderen, unerwarteten Fehler ab.
|
||||||
|
// Das ist unser Sicherheitsnetz für alles, was wir nicht vorhergesehen haben.
|
||||||
|
exception<Throwable> { call, cause ->
|
||||||
|
log.error("Internal Server Error", cause) // Logge den kompletten Stacktrace
|
||||||
|
val errorResponse = ApiResponse<Unit>(
|
||||||
|
message = "An unexpected internal server error occurred.",
|
||||||
|
errors = listOf("INTERNAL_SERVER_ERROR")
|
||||||
|
)
|
||||||
|
call.respond(HttpStatusCode.InternalServerError, errorResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -91,7 +91,7 @@ class AltersklasseController(
|
|||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
return@get call.respond(
|
return@get call.respond(
|
||||||
HttpStatusCode.BadRequest,
|
HttpStatusCode.BadRequest,
|
||||||
ApiResponse.error<List<AltersklasseDto>>("Invalid sparte parameter: $it")
|
ApiResponse<List<AltersklasseDto>>("Invalid sparte parameter: $it")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,7 +103,7 @@ class AltersklasseController(
|
|||||||
} else {
|
} else {
|
||||||
return@get call.respond(
|
return@get call.respond(
|
||||||
HttpStatusCode.BadRequest,
|
HttpStatusCode.BadRequest,
|
||||||
ApiResponse.error<List<AltersklasseDto>>("Invalid geschlecht parameter. Must be 'M' or 'W'")
|
ApiResponse<List<AltersklasseDto>>("Invalid geschlecht parameter. Must be 'M' or 'W'")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -215,7 +215,7 @@ class CreateAltersklasseUseCase(
|
|||||||
|
|
||||||
// Age class code validation
|
// Age class code validation
|
||||||
if (request.altersklasseCode.isBlank()) {
|
if (request.altersklasseCode.isBlank()) {
|
||||||
errors.add(ValidationError("altersklasseCode", "Age class code is required", "REQUIRED"))
|
errors.add(ValidationError("altersklasseCode", "Age class code is required", "REQUIRED")) // "REQUIRED"
|
||||||
} else if (request.altersklasseCode.length > 50) {
|
} else if (request.altersklasseCode.length > 50) {
|
||||||
errors.add(ValidationError("altersklasseCode", "Age class code must not exceed 50 characters", "MAX_LENGTH"))
|
errors.add(ValidationError("altersklasseCode", "Age class code must not exceed 50 characters", "MAX_LENGTH"))
|
||||||
} else if (!request.altersklasseCode.matches(Regex("^[A-Z0-9_]+$"))) {
|
} else if (!request.altersklasseCode.matches(Regex("^[A-Z0-9_]+$"))) {
|
||||||
|
|||||||
+14
-3
@@ -1,5 +1,7 @@
|
|||||||
package at.mocode.masterdata.service
|
package at.mocode.masterdata.service
|
||||||
|
|
||||||
|
import at.mocode.core.utils.config.AppConfig
|
||||||
|
import at.mocode.core.utils.database.DatabaseFactory
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||||
import org.springframework.boot.runApplication
|
import org.springframework.boot.runApplication
|
||||||
|
|
||||||
@@ -11,9 +13,18 @@ import org.springframework.boot.runApplication
|
|||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
class MasterdataServiceApplication
|
class MasterdataServiceApplication
|
||||||
|
|
||||||
/**
|
|
||||||
* Main entry point for the Masterdata Service application.
|
|
||||||
*/
|
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
|
// 1. Lade die Konfiguration explizit, genau einmal beim Start.
|
||||||
|
val appConfig = AppConfig.load()
|
||||||
|
println("Konfiguration für Umgebung '${appConfig.environment}' geladen.")
|
||||||
|
|
||||||
|
// 2. Initialisiere die Datenbank mit der geladenen Konfiguration.
|
||||||
|
// Flyway-Migrationen werden hier automatisch ausgeführt.
|
||||||
|
DatabaseFactory.init(appConfig.database)
|
||||||
|
println("Datenbank initialisiert und migriert.")
|
||||||
|
|
||||||
|
// 3. Starte die Spring Boot / Ktor Anwendung.
|
||||||
|
// Der appConfig-Wert kann hier an die Anwendung übergeben werden,
|
||||||
|
// um ihn später per Dependency Injection zu nutzen.
|
||||||
runApplication<MasterdataServiceApplication>(*args)
|
runApplication<MasterdataServiceApplication>(*args)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user