Merge pull request #18
* MP-19 Refactoring: Einführung der "Registry" & "Masterdata" Trennung … * MP-19 Refactoring: Frontend Tabula Rasa * MP-19 Refactoring: Frontend Tabula Rasa * refactoring: * MP-20 fix(docker/clients): include `:domains` module in web/desktop b… * MP-20 fix(web-app build): resolve JS compile error and add dev/prod b… * MP-20 fix(web-app): remove vendor.js reference and harden JS bootstra… * MP-20 fixing: clients * MP-20 fixing: clients
This commit is contained in:
@@ -1,75 +1,75 @@
|
||||
// Dieses Modul stellt gemeinsame technische Hilfsfunktionen bereit,
|
||||
// wie z.B. Konfigurations-Management, Datenbank-Verbindungen und Service Discovery.
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.kotlinSerialization)
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.kotlinSerialization)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(21)
|
||||
jvmToolchain(21)
|
||||
|
||||
// Target platforms
|
||||
jvm {
|
||||
compilerOptions {
|
||||
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
|
||||
}
|
||||
// Target platforms
|
||||
jvm {
|
||||
compilerOptions {
|
||||
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
|
||||
}
|
||||
}
|
||||
|
||||
js(IR) {
|
||||
browser {
|
||||
testTask {
|
||||
enabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
all {
|
||||
languageSettings.optIn("kotlin.uuid.ExperimentalUuidApi")
|
||||
}
|
||||
|
||||
js(IR) {
|
||||
browser {
|
||||
testTask {
|
||||
enabled = false
|
||||
}
|
||||
}
|
||||
commonMain.dependencies {
|
||||
// Domain models and types (core-utils depends on core-domain, not vice versa)
|
||||
api(projects.core.coreDomain)
|
||||
|
||||
api(libs.kotlinx.serialization.json)
|
||||
api(libs.kotlinx.datetime)
|
||||
// Async support (available for all platforms)
|
||||
api(libs.kotlinx.coroutines.core)
|
||||
// Utilities (multiplatform compatible)
|
||||
api(libs.bignum)
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
all {
|
||||
languageSettings.optIn("kotlin.uuid.ExperimentalUuidApi")
|
||||
}
|
||||
|
||||
commonMain.dependencies {
|
||||
// Domain models and types (core-utils depends on core-domain, not vice versa)
|
||||
api(projects.core.coreDomain)
|
||||
|
||||
api(libs.kotlinx.serialization.json)
|
||||
api(libs.kotlinx.datetime)
|
||||
// Async support (available for all platforms)
|
||||
api(libs.kotlinx.coroutines.core)
|
||||
// Utilities (multiplatform compatible)
|
||||
api(libs.bignum)
|
||||
}
|
||||
|
||||
commonTest.dependencies {
|
||||
implementation(libs.kotlin.test)
|
||||
}
|
||||
|
||||
jvmMain.dependencies {
|
||||
// JVM-specific dependencies - access to central catalog
|
||||
api(projects.platform.platformDependencies)
|
||||
// Database Management (JVM-specific)
|
||||
api(libs.bundles.exposed)
|
||||
api(libs.bundles.flyway)
|
||||
api(libs.hikari.cp)
|
||||
// Service Discovery (JVM-specific)
|
||||
api(libs.spring.cloud.starter.consul.discovery)
|
||||
// Logging (JVM-specific)
|
||||
api(libs.kotlin.logging.jvm)
|
||||
// Jakarta Annotation API
|
||||
api(libs.jakarta.annotation.api)
|
||||
// JSON Processing
|
||||
api(libs.jackson.module.kotlin)
|
||||
api(libs.jackson.datatype.jsr310)
|
||||
}
|
||||
jvmTest.dependencies {
|
||||
// Testing (JVM-specific)
|
||||
implementation(projects.platform.platformTesting)
|
||||
implementation(libs.bundles.testing.jvm)
|
||||
runtimeOnly(libs.postgresql.driver)
|
||||
}
|
||||
commonTest.dependencies {
|
||||
implementation(libs.kotlin.test)
|
||||
}
|
||||
|
||||
jvmMain.dependencies {
|
||||
// JVM-specific dependencies - access to central catalog
|
||||
api(projects.platform.platformDependencies)
|
||||
// Database Management (JVM-specific)
|
||||
api(libs.bundles.exposed)
|
||||
api(libs.bundles.flyway)
|
||||
api(libs.hikari.cp)
|
||||
// Service Discovery (JVM-specific)
|
||||
api(libs.spring.cloud.starter.consul.discovery)
|
||||
// Logging (JVM-specific)
|
||||
api(libs.kotlin.logging.jvm)
|
||||
// Jakarta Annotation API
|
||||
api(libs.jakarta.annotation.api)
|
||||
// JSON Processing
|
||||
api(libs.jackson.module.kotlin)
|
||||
api(libs.jackson.datatype.jsr310)
|
||||
}
|
||||
jvmTest.dependencies {
|
||||
// Testing (JVM-specific)
|
||||
implementation(projects.platform.platformTesting)
|
||||
implementation(libs.bundles.testing.jvm)
|
||||
runtimeOnly(libs.postgresql.driver)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named<Test>("jvmTest") {
|
||||
useJUnitPlatform()
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.core.utils
|
||||
|
||||
import at.mocode.core.domain.model.*
|
||||
@@ -54,14 +55,14 @@ fun String.toErrorCode(): ErrorCode = ErrorCode(this)
|
||||
* Prüft ob der String ein gültiger EventType-Name ist.
|
||||
*/
|
||||
fun String.isValidEventType(): Boolean {
|
||||
return isNotBlank() && matches(Regex("^[A-Za-z][A-Za-z0-9]*$"))
|
||||
return isNotBlank() && matches(Regex("^[A-Za-z][A-Za-z0-9]*$"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob der String ein gültiger ErrorCode ist.
|
||||
*/
|
||||
fun String.isValidErrorCode(): Boolean {
|
||||
return isNotBlank() && matches(Regex("^[A-Z][A-Z0-9_]*$"))
|
||||
return isNotBlank() && matches(Regex("^[A-Z][A-Z0-9_]*$"))
|
||||
}
|
||||
|
||||
// === Collection Extensions ===
|
||||
@@ -70,22 +71,22 @@ fun String.isValidErrorCode(): Boolean {
|
||||
* Erstellt eine PagedResponse aus einer Liste mit Standard-Paginierung.
|
||||
*/
|
||||
fun <T> List<T>.toPagedResponse(
|
||||
page: Int = 0,
|
||||
size: Int = 20
|
||||
page: Int = 0,
|
||||
size: Int = 20
|
||||
): PagedResponse<T> {
|
||||
val startIndex = page * size
|
||||
val endIndex = minOf(startIndex + size, this.size)
|
||||
val content = if (startIndex < this.size) this.subList(startIndex, endIndex) else emptyList()
|
||||
val startIndex = page * size
|
||||
val endIndex = minOf(startIndex + size, this.size)
|
||||
val content = if (startIndex < this.size) this.subList(startIndex, endIndex) else emptyList()
|
||||
|
||||
return PagedResponse.create(
|
||||
content = content,
|
||||
page = page,
|
||||
size = size,
|
||||
totalElements = this.size.toLong(),
|
||||
totalPages = (this.size + size - 1) / size,
|
||||
hasNext = endIndex < this.size,
|
||||
hasPrevious = page > 0
|
||||
)
|
||||
return PagedResponse.create(
|
||||
content = content,
|
||||
page = page,
|
||||
size = size,
|
||||
totalElements = this.size.toLong(),
|
||||
totalPages = (this.size + size - 1) / size,
|
||||
hasNext = endIndex < this.size,
|
||||
hasPrevious = page > 0
|
||||
)
|
||||
}
|
||||
|
||||
// === Validation Extensions ===
|
||||
@@ -94,7 +95,7 @@ fun <T> List<T>.toPagedResponse(
|
||||
* Erstellt eine Liste von ValidationError aus einer Map von Fehlern.
|
||||
*/
|
||||
fun Map<String, String>.toValidationErrors(): List<ValidationError> {
|
||||
return this.map { (field, message) -> ValidationError(field, message, "VALIDATION_ERROR") }
|
||||
return this.map { (field, message) -> ValidationError(field, message, "VALIDATION_ERROR") }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,7 +107,7 @@ fun List<ValidationError>.hasErrors(): Boolean = this.isNotEmpty()
|
||||
* Konvertiert eine Liste von ValidationError zu ErrorDto.
|
||||
*/
|
||||
fun List<ValidationError>.toErrorDtos(): List<ErrorDto> {
|
||||
return this.map { ErrorDto(ErrorCode(it.code), it.message, it.field) }
|
||||
return this.map { ErrorDto(ErrorCode(it.code), it.message, it.field) }
|
||||
}
|
||||
|
||||
// === Time Extensions ===
|
||||
|
||||
@@ -9,315 +9,333 @@ import kotlin.jvm.JvmName
|
||||
* Bietet einen funktionalen Ansatz zur Fehlerbehandlung ohne Exceptions.
|
||||
*/
|
||||
sealed class Result<out T> {
|
||||
/**
|
||||
* Represents a successful operation with a value.
|
||||
*/
|
||||
data class Success<T>(val value: T) : Result<T>()
|
||||
/**
|
||||
* Represents a successful operation with a value.
|
||||
*/
|
||||
data class Success<T>(val value: T) : Result<T>()
|
||||
|
||||
/**
|
||||
* Represents a failed operation with error messages.
|
||||
*/
|
||||
data class Failure(val errors: List<ErrorDto>) : Result<Nothing>()
|
||||
/**
|
||||
* Represents a failed operation with error messages.
|
||||
*/
|
||||
data class Failure(val errors: List<ErrorDto>) : Result<Nothing>()
|
||||
|
||||
/**
|
||||
* Checks if the Result is a success.
|
||||
*/
|
||||
val isSuccess: Boolean get() = this is Success
|
||||
/**
|
||||
* Checks if the Result is a success.
|
||||
*/
|
||||
val isSuccess: Boolean get() = this is Success
|
||||
|
||||
/**
|
||||
* Checks if the Result is a failure.
|
||||
*/
|
||||
val isFailure: Boolean get() = this is Failure
|
||||
/**
|
||||
* Checks if the Result is a failure.
|
||||
*/
|
||||
val isFailure: Boolean get() = this is Failure
|
||||
|
||||
/**
|
||||
* Gets the value if it's a success, otherwise null.
|
||||
*
|
||||
* @return the value if this is a Success, or null if this is a Failure
|
||||
*/
|
||||
fun getOrNull(): T? = when (this) {
|
||||
is Success -> value
|
||||
is Failure -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value if it's a success, otherwise the default value.
|
||||
*
|
||||
* @param defaultValue the value to return if this is a Failure
|
||||
* @return the value if this is a Success, or the default value if this is a Failure
|
||||
*/
|
||||
fun getOrDefault(defaultValue: @UnsafeVariance T): T = when (this) {
|
||||
is Success -> value
|
||||
is Failure -> defaultValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the errors if it's a failure, otherwise an empty list.
|
||||
*
|
||||
* @return the list of errors if this is a Failure, or an empty list if this is a Success
|
||||
*/
|
||||
@JvmName("retrieveErrors")
|
||||
fun getErrors(): List<ErrorDto> = when (this) {
|
||||
is Success -> emptyList()
|
||||
is Failure -> errors
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the value if it's a success.
|
||||
*
|
||||
* @param transform function to apply to the success value
|
||||
* @return a new Success with the transformed value if this is a Success, or this unchanged Failure
|
||||
*/
|
||||
inline fun <R> map(transform: (T) -> R): Result<R> = when (this) {
|
||||
is Success -> Success(transform(value))
|
||||
is Failure -> this
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the Result flatly (for nested Results).
|
||||
* Unlike map, which wraps the transformed value in a new Success, flatMap uses the Result returned by the transform function.
|
||||
*
|
||||
* @param transform function that returns a Result
|
||||
* @return the Result returned by the transform function if this is a Success, or this unchanged Failure
|
||||
*/
|
||||
inline fun <R> flatMap(transform: (T) -> Result<R>): Result<R> = when (this) {
|
||||
is Success -> transform(value)
|
||||
is Failure -> this
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an action if it's a success.
|
||||
*
|
||||
* @param action the function to execute with the success value
|
||||
* @return this Result, unchanged, to allow for chaining
|
||||
*/
|
||||
inline fun onSuccess(action: (T) -> Unit): Result<T> {
|
||||
if (this is Success) action(value)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an action if it's a failure.
|
||||
*
|
||||
* @param action the function to execute with the list of errors
|
||||
* @return this Result, unchanged, to allow for chaining
|
||||
*/
|
||||
inline fun onFailure(action: (List<ErrorDto>) -> Unit): Result<T> {
|
||||
if (this is Failure) action(errors)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the Result by applying one of two functions depending on whether it's a success or failure.
|
||||
*
|
||||
* @param onSuccess function to apply if this is a success
|
||||
* @param onFailure function to apply if this is a failure
|
||||
* @return the result of applying the appropriate function
|
||||
*/
|
||||
inline fun <R> fold(
|
||||
onSuccess: (T) -> R,
|
||||
onFailure: (List<ErrorDto>) -> R
|
||||
): R = when (this) {
|
||||
is Success -> onSuccess(value)
|
||||
is Failure -> onFailure(errors)
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to recover from a failure by applying the specified function to the error list.
|
||||
* If this is already a success, it is returned unchanged.
|
||||
*
|
||||
* @param transform function to apply to the error list to recover
|
||||
* @return a new Success if recovery was successful, or this unchanged Result if already a success
|
||||
*/
|
||||
inline fun recover(transform: (List<ErrorDto>) -> @UnsafeVariance T): Result<T> = when (this) {
|
||||
is Success -> this
|
||||
is Failure -> Success(transform(errors))
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to recover from a failure by applying the specified function to the error list.
|
||||
* If this is already a success, it is returned unchanged.
|
||||
* If an exception occurs during recovery, it is converted to a new failure.
|
||||
*
|
||||
* @param transform function to apply to the error list to recover
|
||||
* @return a new Success if recovery was successful, a new Failure if recovery threw an exception,
|
||||
* or this unchanged Result if already a success
|
||||
*/
|
||||
inline fun recoverCatching(transform: (List<ErrorDto>) -> @UnsafeVariance T): Result<T> = when (this) {
|
||||
is Success -> this
|
||||
is Failure -> try {
|
||||
Success(transform(errors))
|
||||
} catch (e: Exception) {
|
||||
Failure(
|
||||
listOf(
|
||||
ErrorDto(
|
||||
code = at.mocode.core.domain.model.ErrorCode("RECOVERY_FAILED"),
|
||||
message = e.message ?: "Recovery failed with an unknown error"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines this Result with another Result, creating a pair of their values if both are successful.
|
||||
* If either Result is a failure, the combined Result will be a failure containing all errors.
|
||||
*
|
||||
* @param other the Result to combine with this one
|
||||
* @return a Result containing a Pair of values if both are successful, or a Failure with all errors
|
||||
*/
|
||||
fun <R> zip(other: Result<R>): Result<Pair<T, R>> = when {
|
||||
this is Success && other is Success -> Success(Pair(this.value, other.value))
|
||||
this is Success && other is Failure -> Failure(other.errors)
|
||||
this is Failure && other is Success -> Failure(this.errors)
|
||||
this is Failure && other is Failure -> {
|
||||
val allErrors = this.errors + other.errors
|
||||
Failure(allErrors)
|
||||
}
|
||||
// This branch should never be reached due to sealed class, but included for completeness
|
||||
else -> throw IllegalStateException("Unreachable code - Result should be either Success or Failure")
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely attempts to get the value, throwing a custom exception if this is a failure.
|
||||
*
|
||||
* @param errorHandler function that converts the list of errors to an exception
|
||||
* @return the value if this is a Success
|
||||
* @throws E if this is a Failure, as created by the errorHandler
|
||||
*/
|
||||
inline fun <E : Throwable> getOrThrow(errorHandler: (List<ErrorDto>) -> E): T = when (this) {
|
||||
is Success -> value
|
||||
is Failure -> throw errorHandler(errors)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value if it's a success, or throws an IllegalStateException with a message constructed from the errors.
|
||||
*
|
||||
* @return the value if this is a Success
|
||||
* @throws IllegalStateException if this is a Failure, with a message containing the error details
|
||||
*/
|
||||
fun getOrThrow(): T = getOrThrow { errors ->
|
||||
IllegalStateException("Result is a Failure with errors: ${errors.joinToString { it.message }}")
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Gets the value if it's a success, otherwise null.
|
||||
* Creates a successful Result.
|
||||
*
|
||||
* @return the value if this is a Success, or null if this is a Failure
|
||||
* @param value the value to wrap in a Success
|
||||
* @return a new Success containing the provided value
|
||||
*/
|
||||
fun getOrNull(): T? = when (this) {
|
||||
is Success -> value
|
||||
is Failure -> null
|
||||
fun <T> success(value: T): Result<T> = Success(value)
|
||||
|
||||
/**
|
||||
* Creates a failure Result with a single error.
|
||||
*
|
||||
* @param error the error to include in the Failure
|
||||
* @return a new Failure containing the provided error
|
||||
*/
|
||||
fun <T> failure(error: ErrorDto): Result<T> = Failure(listOf(error))
|
||||
|
||||
/**
|
||||
* Creates a failure Result with multiple errors.
|
||||
*
|
||||
* @param errors the list of errors to include in the Failure
|
||||
* @return a new Failure containing the provided errors
|
||||
*/
|
||||
fun <T> failure(errors: List<ErrorDto>): Result<T> = Failure(errors)
|
||||
|
||||
/**
|
||||
* Creates a failure Result from ValidationErrors.
|
||||
* Converts the ValidationErrors to ErrorDtos internally.
|
||||
*
|
||||
* @param validationErrors the list of validation errors to convert and include in the Failure
|
||||
* @return a new Failure containing ErrorDtos converted from the provided ValidationErrors
|
||||
*/
|
||||
@JvmName("failureFromValidationErrors")
|
||||
fun <T> failure(validationErrors: List<ValidationError>): Result<T> =
|
||||
Failure(validationErrors.toErrorDtos())
|
||||
|
||||
/**
|
||||
* Executes an operation that returns a Result and catches exceptions.
|
||||
* Provides more specific error codes based on the type of exception caught.
|
||||
*
|
||||
* @param operation the operation to execute
|
||||
* @return a Success with the operation result, or a Failure with error details if an exception occurred
|
||||
*/
|
||||
inline fun <T> runCatching(operation: () -> T): Result<T> = try {
|
||||
success(operation())
|
||||
} catch (e: IllegalArgumentException) {
|
||||
failure(
|
||||
ErrorDto(
|
||||
code = at.mocode.core.domain.model.ErrorCode("INVALID_ARGUMENT"),
|
||||
message = e.message ?: "Invalid argument provided"
|
||||
)
|
||||
)
|
||||
} catch (e: IllegalStateException) {
|
||||
failure(
|
||||
ErrorDto(
|
||||
code = at.mocode.core.domain.model.ErrorCode("INVALID_STATE"),
|
||||
message = e.message ?: "Operation called in invalid state"
|
||||
)
|
||||
)
|
||||
} catch (e: UnsupportedOperationException) {
|
||||
failure(
|
||||
ErrorDto(
|
||||
code = at.mocode.core.domain.model.ErrorCode("UNSUPPORTED_OPERATION"),
|
||||
message = e.message ?: "Operation not supported"
|
||||
)
|
||||
)
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
failure(
|
||||
ErrorDto(
|
||||
code = at.mocode.core.domain.model.ErrorCode("INDEX_OUT_OF_BOUNDS"),
|
||||
message = e.message ?: "Index out of bounds"
|
||||
)
|
||||
)
|
||||
} catch (e: NullPointerException) {
|
||||
failure(
|
||||
ErrorDto(
|
||||
code = at.mocode.core.domain.model.ErrorCode("NULL_REFERENCE"),
|
||||
message = e.message ?: "Unexpected null reference"
|
||||
)
|
||||
)
|
||||
} catch (e: ClassCastException) {
|
||||
failure(
|
||||
ErrorDto(
|
||||
code = at.mocode.core.domain.model.ErrorCode("TYPE_MISMATCH"),
|
||||
message = e.message ?: "Type mismatch occurred"
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
// Fallback for any other exception type
|
||||
failure(
|
||||
ErrorDto(
|
||||
code = at.mocode.core.domain.model.ErrorCode("OPERATION_FAILED"),
|
||||
message = e.message ?: "Unknown error occurred"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value if it's a success, otherwise the default value.
|
||||
* Combines multiple Results into a single Result with a list.
|
||||
* Optimized for performance with large collections.
|
||||
*
|
||||
* @param defaultValue the value to return if this is a Failure
|
||||
* @return the value if this is a Success, or the default value if this is a Failure
|
||||
* @param results a list of Results to combine
|
||||
* @return a Success containing a list of all success values if all Results are successful,
|
||||
* or a Failure containing all error messages if any Results are failures
|
||||
*/
|
||||
fun getOrDefault(defaultValue: @UnsafeVariance T): T = when (this) {
|
||||
is Success -> value
|
||||
is Failure -> defaultValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the errors if it's a failure, otherwise an empty list.
|
||||
*
|
||||
* @return the list of errors if this is a Failure, or an empty list if this is a Success
|
||||
*/
|
||||
@JvmName("retrieveErrors")
|
||||
fun getErrors(): List<ErrorDto> = when (this) {
|
||||
is Success -> emptyList()
|
||||
is Failure -> errors
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the value if it's a success.
|
||||
*
|
||||
* @param transform function to apply to the success value
|
||||
* @return a new Success with the transformed value if this is a Success, or this unchanged Failure
|
||||
*/
|
||||
inline fun <R> map(transform: (T) -> R): Result<R> = when (this) {
|
||||
is Success -> Success(transform(value))
|
||||
is Failure -> this
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the Result flatly (for nested Results).
|
||||
* Unlike map, which wraps the transformed value in a new Success, flatMap uses the Result returned by the transform function.
|
||||
*
|
||||
* @param transform function that returns a Result
|
||||
* @return the Result returned by the transform function if this is a Success, or this unchanged Failure
|
||||
*/
|
||||
inline fun <R> flatMap(transform: (T) -> Result<R>): Result<R> = when (this) {
|
||||
is Success -> transform(value)
|
||||
is Failure -> this
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an action if it's a success.
|
||||
*
|
||||
* @param action the function to execute with the success value
|
||||
* @return this Result, unchanged, to allow for chaining
|
||||
*/
|
||||
inline fun onSuccess(action: (T) -> Unit): Result<T> {
|
||||
if (this is Success) action(value)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an action if it's a failure.
|
||||
*
|
||||
* @param action the function to execute with the list of errors
|
||||
* @return this Result, unchanged, to allow for chaining
|
||||
*/
|
||||
inline fun onFailure(action: (List<ErrorDto>) -> Unit): Result<T> {
|
||||
if (this is Failure) action(errors)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the Result by applying one of two functions depending on whether it's a success or failure.
|
||||
*
|
||||
* @param onSuccess function to apply if this is a success
|
||||
* @param onFailure function to apply if this is a failure
|
||||
* @return the result of applying the appropriate function
|
||||
*/
|
||||
inline fun <R> fold(
|
||||
onSuccess: (T) -> R,
|
||||
onFailure: (List<ErrorDto>) -> R
|
||||
): R = when (this) {
|
||||
is Success -> onSuccess(value)
|
||||
is Failure -> onFailure(errors)
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to recover from a failure by applying the specified function to the error list.
|
||||
* If this is already a success, it is returned unchanged.
|
||||
*
|
||||
* @param transform function to apply to the error list to recover
|
||||
* @return a new Success if recovery was successful, or this unchanged Result if already a success
|
||||
*/
|
||||
inline fun recover(transform: (List<ErrorDto>) -> @UnsafeVariance T): Result<T> = when (this) {
|
||||
is Success -> this
|
||||
is Failure -> Success(transform(errors))
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to recover from a failure by applying the specified function to the error list.
|
||||
* If this is already a success, it is returned unchanged.
|
||||
* If an exception occurs during recovery, it is converted to a new failure.
|
||||
*
|
||||
* @param transform function to apply to the error list to recover
|
||||
* @return a new Success if recovery was successful, a new Failure if recovery threw an exception,
|
||||
* or this unchanged Result if already a success
|
||||
*/
|
||||
inline fun recoverCatching(transform: (List<ErrorDto>) -> @UnsafeVariance T): Result<T> = when (this) {
|
||||
is Success -> this
|
||||
is Failure -> try {
|
||||
Success(transform(errors))
|
||||
} catch (e: Exception) {
|
||||
Failure(listOf(ErrorDto(
|
||||
code = at.mocode.core.domain.model.ErrorCode("RECOVERY_FAILED"),
|
||||
message = e.message ?: "Recovery failed with an unknown error"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines this Result with another Result, creating a pair of their values if both are successful.
|
||||
* If either Result is a failure, the combined Result will be a failure containing all errors.
|
||||
*
|
||||
* @param other the Result to combine with this one
|
||||
* @return a Result containing a Pair of values if both are successful, or a Failure with all errors
|
||||
*/
|
||||
fun <R> zip(other: Result<R>): Result<Pair<T, R>> = when {
|
||||
this is Success && other is Success -> Success(Pair(this.value, other.value))
|
||||
this is Success && other is Failure -> Failure(other.errors)
|
||||
this is Failure && other is Success -> Failure(this.errors)
|
||||
this is Failure && other is Failure -> {
|
||||
val allErrors = this.errors + other.errors
|
||||
Failure(allErrors)
|
||||
}
|
||||
// This branch should never be reached due to sealed class, but included for completeness
|
||||
else -> throw IllegalStateException("Unreachable code - Result should be either Success or Failure")
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely attempts to get the value, throwing a custom exception if this is a failure.
|
||||
*
|
||||
* @param errorHandler function that converts the list of errors to an exception
|
||||
* @return the value if this is a Success
|
||||
* @throws E if this is a Failure, as created by the errorHandler
|
||||
*/
|
||||
inline fun <E : Throwable> getOrThrow(errorHandler: (List<ErrorDto>) -> E): T = when (this) {
|
||||
is Success -> value
|
||||
is Failure -> throw errorHandler(errors)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value if it's a success, or throws an IllegalStateException with a message constructed from the errors.
|
||||
*
|
||||
* @return the value if this is a Success
|
||||
* @throws IllegalStateException if this is a Failure, with a message containing the error details
|
||||
*/
|
||||
fun getOrThrow(): T = getOrThrow { errors ->
|
||||
IllegalStateException("Result is a Failure with errors: ${errors.joinToString { it.message }}")
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Creates a successful Result.
|
||||
*
|
||||
* @param value the value to wrap in a Success
|
||||
* @return a new Success containing the provided value
|
||||
*/
|
||||
fun <T> success(value: T): Result<T> = Success(value)
|
||||
|
||||
/**
|
||||
* Creates a failure Result with a single error.
|
||||
*
|
||||
* @param error the error to include in the Failure
|
||||
* @return a new Failure containing the provided error
|
||||
*/
|
||||
fun <T> failure(error: ErrorDto): Result<T> = Failure(listOf(error))
|
||||
|
||||
/**
|
||||
* Creates a failure Result with multiple errors.
|
||||
*
|
||||
* @param errors the list of errors to include in the Failure
|
||||
* @return a new Failure containing the provided errors
|
||||
*/
|
||||
fun <T> failure(errors: List<ErrorDto>): Result<T> = Failure(errors)
|
||||
|
||||
/**
|
||||
* Creates a failure Result from ValidationErrors.
|
||||
* Converts the ValidationErrors to ErrorDtos internally.
|
||||
*
|
||||
* @param validationErrors the list of validation errors to convert and include in the Failure
|
||||
* @return a new Failure containing ErrorDtos converted from the provided ValidationErrors
|
||||
*/
|
||||
@JvmName("failureFromValidationErrors")
|
||||
fun <T> failure(validationErrors: List<ValidationError>): Result<T> =
|
||||
Failure(validationErrors.toErrorDtos())
|
||||
|
||||
/**
|
||||
* Executes an operation that returns a Result and catches exceptions.
|
||||
* Provides more specific error codes based on the type of exception caught.
|
||||
*
|
||||
* @param operation the operation to execute
|
||||
* @return a Success with the operation result, or a Failure with error details if an exception occurred
|
||||
*/
|
||||
inline fun <T> runCatching(operation: () -> T): Result<T> = try {
|
||||
success(operation())
|
||||
} catch (e: IllegalArgumentException) {
|
||||
failure(ErrorDto(
|
||||
code = at.mocode.core.domain.model.ErrorCode("INVALID_ARGUMENT"),
|
||||
message = e.message ?: "Invalid argument provided"
|
||||
))
|
||||
} catch (e: IllegalStateException) {
|
||||
failure(ErrorDto(
|
||||
code = at.mocode.core.domain.model.ErrorCode("INVALID_STATE"),
|
||||
message = e.message ?: "Operation called in invalid state"
|
||||
))
|
||||
} catch (e: UnsupportedOperationException) {
|
||||
failure(ErrorDto(
|
||||
code = at.mocode.core.domain.model.ErrorCode("UNSUPPORTED_OPERATION"),
|
||||
message = e.message ?: "Operation not supported"
|
||||
))
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
failure(ErrorDto(
|
||||
code = at.mocode.core.domain.model.ErrorCode("INDEX_OUT_OF_BOUNDS"),
|
||||
message = e.message ?: "Index out of bounds"
|
||||
))
|
||||
} catch (e: NullPointerException) {
|
||||
failure(ErrorDto(
|
||||
code = at.mocode.core.domain.model.ErrorCode("NULL_REFERENCE"),
|
||||
message = e.message ?: "Unexpected null reference"
|
||||
))
|
||||
} catch (e: ClassCastException) {
|
||||
failure(ErrorDto(
|
||||
code = at.mocode.core.domain.model.ErrorCode("TYPE_MISMATCH"),
|
||||
message = e.message ?: "Type mismatch occurred"
|
||||
))
|
||||
} catch (e: Exception) {
|
||||
// Fallback for any other exception type
|
||||
failure(ErrorDto(
|
||||
code = at.mocode.core.domain.model.ErrorCode("OPERATION_FAILED"),
|
||||
message = e.message ?: "Unknown error occurred"
|
||||
))
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines multiple Results into a single Result with a list.
|
||||
* Optimized for performance with large collections.
|
||||
*
|
||||
* @param results a list of Results to combine
|
||||
* @return a Success containing a list of all success values if all Results are successful,
|
||||
* or a Failure containing all error messages if any Results are failures
|
||||
*/
|
||||
fun <T> combine(results: List<Result<T>>): Result<List<T>> {
|
||||
// Fast path for empty list
|
||||
if (results.isEmpty()) {
|
||||
return success(emptyList())
|
||||
}
|
||||
|
||||
// Fast path for single result
|
||||
if (results.size == 1) {
|
||||
return results.first().map { listOf(it) }
|
||||
}
|
||||
|
||||
// Check if there are any failures
|
||||
val anyFailure = results.any { it.isFailure }
|
||||
|
||||
// If no failures, we can optimize by directly mapping to values
|
||||
if (!anyFailure) {
|
||||
return success(results.map { (it as Success).value })
|
||||
}
|
||||
|
||||
// If there are failures, collect all errors
|
||||
val errors = results
|
||||
.filterIsInstance<Failure>()
|
||||
.flatMap { it.errors }
|
||||
|
||||
// If empty results list contained no failures, return empty success
|
||||
if (errors.isEmpty()) {
|
||||
return success(emptyList())
|
||||
}
|
||||
|
||||
return failure(errors)
|
||||
}
|
||||
fun <T> combine(results: List<Result<T>>): Result<List<T>> {
|
||||
// Fast path for empty list
|
||||
if (results.isEmpty()) {
|
||||
return success(emptyList())
|
||||
}
|
||||
|
||||
// Fast path for single result
|
||||
if (results.size == 1) {
|
||||
return results.first().map { listOf(it) }
|
||||
}
|
||||
|
||||
// Check if there are any failures
|
||||
val anyFailure = results.any { it.isFailure }
|
||||
|
||||
// If no failures, we can optimize by directly mapping to values
|
||||
if (!anyFailure) {
|
||||
return success(results.map { (it as Success).value })
|
||||
}
|
||||
|
||||
// If there are failures, collect all errors
|
||||
val errors = results
|
||||
.filterIsInstance<Failure>()
|
||||
.flatMap { it.errors }
|
||||
|
||||
// If empty results list contained no failures, return empty success
|
||||
if (errors.isEmpty()) {
|
||||
return success(emptyList())
|
||||
}
|
||||
|
||||
return failure(errors)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -328,11 +346,13 @@ sealed class Result<out T> {
|
||||
* @return a Success containing the non-null value, or a Failure if the value is null
|
||||
*/
|
||||
fun <T> T?.toResult(errorMessage: String = "Value is null"): Result<T> =
|
||||
if (this != null) {
|
||||
Result.success(this)
|
||||
} else {
|
||||
Result.failure(ErrorDto(
|
||||
code = at.mocode.core.domain.model.ErrorCode("NULL_VALUE"),
|
||||
message = errorMessage
|
||||
))
|
||||
}
|
||||
if (this != null) {
|
||||
Result.success(this)
|
||||
} else {
|
||||
Result.failure(
|
||||
ErrorDto(
|
||||
code = at.mocode.core.domain.model.ErrorCode("NULL_VALUE"),
|
||||
message = errorMessage
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,63 +11,63 @@ import at.mocode.core.domain.model.ValidationError
|
||||
* Builder-Klasse für die Erstellung von Validierungsregeln.
|
||||
*/
|
||||
class ValidationBuilder {
|
||||
private val errors = mutableListOf<ValidationError>()
|
||||
private val errors = mutableListOf<ValidationError>()
|
||||
|
||||
/**
|
||||
* Validiert ein Feld gegen mehrere Regeln.
|
||||
*/
|
||||
fun <T> field(name: String, value: T, vararg rules: ValidationRule<T>): ValidationBuilder {
|
||||
rules.forEach { rule ->
|
||||
rule.validate(name, value)?.let { error ->
|
||||
errors.add(error)
|
||||
}
|
||||
}
|
||||
return this
|
||||
/**
|
||||
* Validiert ein Feld gegen mehrere Regeln.
|
||||
*/
|
||||
fun <T> field(name: String, value: T, vararg rules: ValidationRule<T>): ValidationBuilder {
|
||||
rules.forEach { rule ->
|
||||
rule.validate(name, value)?.let { error ->
|
||||
errors.add(error)
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Fügt einen benutzerdefinierten Validierungsfehler hinzu.
|
||||
*/
|
||||
fun addError(field: String, message: String, code: String = "VALIDATION_ERROR"): ValidationBuilder {
|
||||
errors.add(ValidationError(field, message, code))
|
||||
return this
|
||||
/**
|
||||
* Fügt einen benutzerdefinierten Validierungsfehler hinzu.
|
||||
*/
|
||||
fun addError(field: String, message: String, code: String = "VALIDATION_ERROR"): ValidationBuilder {
|
||||
errors.add(ValidationError(field, message, code))
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt eine benutzerdefinierten Validierung aus.
|
||||
*/
|
||||
fun custom(validation: () -> ValidationError?): ValidationBuilder {
|
||||
validation()?.let { error ->
|
||||
errors.add(error)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt eine benutzerdefinierten Validierung aus.
|
||||
*/
|
||||
fun custom(validation: () -> ValidationError?): ValidationBuilder {
|
||||
validation()?.let { error ->
|
||||
errors.add(error)
|
||||
}
|
||||
return this
|
||||
/**
|
||||
* Erstellt das finale Validierungsergebnis.
|
||||
*/
|
||||
fun build(): Result<Unit> {
|
||||
return if (errors.isEmpty()) {
|
||||
Result.success(Unit)
|
||||
} else {
|
||||
Result.failure(errors)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt das finale Validierungsergebnis.
|
||||
*/
|
||||
fun build(): Result<Unit> {
|
||||
return if (errors.isEmpty()) {
|
||||
Result.success(Unit)
|
||||
} else {
|
||||
Result.failure(errors)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die gesammelten Fehler zurück.
|
||||
*/
|
||||
fun getErrors(): List<ValidationError> = errors.toList()
|
||||
/**
|
||||
* Gibt die gesammelten Fehler zurück.
|
||||
*/
|
||||
fun getErrors(): List<ValidationError> = errors.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface für Validierungsregeln.
|
||||
*/
|
||||
fun interface ValidationRule<T> {
|
||||
/**
|
||||
* Validiert einen Wert und gibt einen Fehler zurück, wenn die Validierung fehlschlägt.
|
||||
*/
|
||||
fun validate(fieldName: String, value: T): ValidationError?
|
||||
/**
|
||||
* Validiert einen Wert und gibt einen Fehler zurück, wenn die Validierung fehlschlägt.
|
||||
*/
|
||||
fun validate(fieldName: String, value: T): ValidationError?
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,150 +75,150 @@ fun interface ValidationRule<T> {
|
||||
*/
|
||||
object ValidationRules {
|
||||
|
||||
// === String-Validierungen ===
|
||||
// === String-Validierungen ===
|
||||
|
||||
/**
|
||||
* Prüft ob ein String nicht leer ist.
|
||||
*/
|
||||
fun notBlank(): ValidationRule<String> = ValidationRule { fieldName, value ->
|
||||
if (value.isBlank()) ValidationError.required(fieldName) else null
|
||||
}
|
||||
/**
|
||||
* Prüft ob ein String nicht leer ist.
|
||||
*/
|
||||
fun notBlank(): ValidationRule<String> = ValidationRule { fieldName, value ->
|
||||
if (value.isBlank()) ValidationError.required(fieldName) else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft die Mindestlänge eines Strings.
|
||||
*/
|
||||
fun minLength(min: Int): ValidationRule<String> = ValidationRule { fieldName, value ->
|
||||
if (value.length < min) {
|
||||
ValidationError.invalidLength(fieldName, "$fieldName must be at least $min characters long")
|
||||
} else null
|
||||
}
|
||||
/**
|
||||
* Prüft die Mindestlänge eines Strings.
|
||||
*/
|
||||
fun minLength(min: Int): ValidationRule<String> = ValidationRule { fieldName, value ->
|
||||
if (value.length < min) {
|
||||
ValidationError.invalidLength(fieldName, "$fieldName muss mindestens $min Zeichen lang sein")
|
||||
} else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft die Maximallänge eines Strings.
|
||||
*/
|
||||
fun maxLength(max: Int): ValidationRule<String> = ValidationRule { fieldName, value ->
|
||||
if (value.length > max) {
|
||||
ValidationError.invalidLength(fieldName, "$fieldName must not exceed $max characters")
|
||||
} else null
|
||||
}
|
||||
/**
|
||||
* Prüft die Maximallänge eines Strings.
|
||||
*/
|
||||
fun maxLength(max: Int): ValidationRule<String> = ValidationRule { fieldName, value ->
|
||||
if (value.length > max) {
|
||||
ValidationError.invalidLength(fieldName, "$fieldName darf $max Zeichen nicht überschreiten")
|
||||
} else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob ein String einem RegEx-Pattern entspricht.
|
||||
*/
|
||||
fun matches(pattern: Regex, message: String): ValidationRule<String> = ValidationRule { fieldName, value ->
|
||||
if (!value.matches(pattern)) {
|
||||
ValidationError.invalidFormat(fieldName, message)
|
||||
} else null
|
||||
}
|
||||
/**
|
||||
* Prüft ob ein String einem RegEx-Pattern entspricht.
|
||||
*/
|
||||
fun matches(pattern: Regex, message: String): ValidationRule<String> = ValidationRule { fieldName, value ->
|
||||
if (!value.matches(pattern)) {
|
||||
ValidationError.invalidFormat(fieldName, message)
|
||||
} else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob ein String eine gültige E-Mail-Adresse ist.
|
||||
*/
|
||||
fun email(): ValidationRule<String> = ValidationRule { fieldName, value ->
|
||||
val emailRegex = Regex("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")
|
||||
if (!value.matches(emailRegex)) {
|
||||
ValidationError.invalidFormat(fieldName, "$fieldName must be a valid email address")
|
||||
} else null
|
||||
}
|
||||
/**
|
||||
* Prüft ob ein String eine gültige E-Mail-Adresse ist.
|
||||
*/
|
||||
fun email(): ValidationRule<String> = ValidationRule { fieldName, value ->
|
||||
val emailRegex = Regex("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")
|
||||
if (!value.matches(emailRegex)) {
|
||||
ValidationError.invalidFormat(fieldName, "$fieldName muss eine gültige E-Mail-Adresse sein")
|
||||
} else null
|
||||
}
|
||||
|
||||
// === Numerische Validierungen ===
|
||||
// === Numerische Validierungen ===
|
||||
|
||||
/**
|
||||
* Prüft den Mindestwert einer Zahl.
|
||||
*/
|
||||
fun <T : Comparable<T>> min(minValue: T): ValidationRule<T> = ValidationRule { fieldName, value ->
|
||||
if (value < minValue) {
|
||||
ValidationError.invalidRange(fieldName, "$fieldName must be at least $minValue")
|
||||
} else null
|
||||
}
|
||||
/**
|
||||
* Prüft den Mindestwert einer Zahl.
|
||||
*/
|
||||
fun <T : Comparable<T>> min(minValue: T): ValidationRule<T> = ValidationRule { fieldName, value ->
|
||||
if (value < minValue) {
|
||||
ValidationError.invalidRange(fieldName, "$fieldName muss mindestens $minValue sein")
|
||||
} else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft den Maximalwert einer Zahl.
|
||||
*/
|
||||
fun <T : Comparable<T>> max(maxValue: T): ValidationRule<T> = ValidationRule { fieldName, value ->
|
||||
if (value > maxValue) {
|
||||
ValidationError.invalidRange(fieldName, "$fieldName must not exceed $maxValue")
|
||||
} else null
|
||||
}
|
||||
/**
|
||||
* Prüft den Maximalwert einer Zahl.
|
||||
*/
|
||||
fun <T : Comparable<T>> max(maxValue: T): ValidationRule<T> = ValidationRule { fieldName, value ->
|
||||
if (value > maxValue) {
|
||||
ValidationError.invalidRange(fieldName, "$fieldName darf $maxValue nicht überschreiten")
|
||||
} else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob eine Zahl positiv ist.
|
||||
*/
|
||||
fun positive(): ValidationRule<Number> = ValidationRule { fieldName, value ->
|
||||
if (value.toDouble() <= 0) {
|
||||
ValidationError.invalidRange(fieldName, "$fieldName must be positive")
|
||||
} else null
|
||||
}
|
||||
/**
|
||||
* Prüft ob eine Zahl positiv ist.
|
||||
*/
|
||||
fun positive(): ValidationRule<Number> = ValidationRule { fieldName, value ->
|
||||
if (value.toDouble() <= 0) {
|
||||
ValidationError.invalidRange(fieldName, "$fieldName muss positiv sein")
|
||||
} else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob eine Zahl nicht negativ ist.
|
||||
*/
|
||||
fun nonNegative(): ValidationRule<Number> = ValidationRule { fieldName, value ->
|
||||
if (value.toDouble() < 0) {
|
||||
ValidationError.invalidRange(fieldName, "$fieldName must not be negative")
|
||||
} else null
|
||||
}
|
||||
/**
|
||||
* Prüft ob eine Zahl nicht negativ ist.
|
||||
*/
|
||||
fun nonNegative(): ValidationRule<Number> = ValidationRule { fieldName, value ->
|
||||
if (value.toDouble() < 0) {
|
||||
ValidationError.invalidRange(fieldName, "$fieldName darf nicht negativ sein")
|
||||
} else null
|
||||
}
|
||||
|
||||
// === Collection-Validierungen ===
|
||||
// === Collection-Validierungen ===
|
||||
|
||||
/**
|
||||
* Prüft ob eine Collection nicht leer ist.
|
||||
*/
|
||||
fun <T> notEmpty(): ValidationRule<Collection<T>> = ValidationRule { fieldName, value ->
|
||||
if (value.isEmpty()) {
|
||||
ValidationError.required(fieldName)
|
||||
} else null
|
||||
}
|
||||
/**
|
||||
* Prüft ob eine Collection nicht leer ist.
|
||||
*/
|
||||
fun <T> notEmpty(): ValidationRule<Collection<T>> = ValidationRule { fieldName, value ->
|
||||
if (value.isEmpty()) {
|
||||
ValidationError.required(fieldName)
|
||||
} else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft die Mindestgröße einer Collection.
|
||||
*/
|
||||
fun <T> minSize(min: Int): ValidationRule<Collection<T>> = ValidationRule { fieldName, value ->
|
||||
if (value.size < min) {
|
||||
ValidationError.invalidLength(fieldName, "$fieldName must contain at least $min items")
|
||||
} else null
|
||||
}
|
||||
/**
|
||||
* Prüft die Mindestgröße einer Collection.
|
||||
*/
|
||||
fun <T> minSize(min: Int): ValidationRule<Collection<T>> = ValidationRule { fieldName, value ->
|
||||
if (value.size < min) {
|
||||
ValidationError.invalidLength(fieldName, "$fieldName muss mindestens $min Elemente enthalten")
|
||||
} else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft die Maximalgröße einer Collection.
|
||||
*/
|
||||
fun <T> maxSize(max: Int): ValidationRule<Collection<T>> = ValidationRule { fieldName, value ->
|
||||
if (value.size > max) {
|
||||
ValidationError.invalidLength(fieldName, "$fieldName must not contain more than $max items")
|
||||
} else null
|
||||
}
|
||||
/**
|
||||
* Prüft die Maximalgröße einer Collection.
|
||||
*/
|
||||
fun <T> maxSize(max: Int): ValidationRule<Collection<T>> = ValidationRule { fieldName, value ->
|
||||
if (value.size > max) {
|
||||
ValidationError.invalidLength(fieldName, "$fieldName darf nicht mehr als $max Elemente enthalten")
|
||||
} else null
|
||||
}
|
||||
|
||||
// === Null-Validierungen ===
|
||||
// === Null-Validierungen ===
|
||||
|
||||
/**
|
||||
* Prüft ob ein Wert nicht null ist.
|
||||
*/
|
||||
fun <T> notNull(): ValidationRule<T?> = ValidationRule { fieldName, value ->
|
||||
if (value == null) ValidationError.required(fieldName) else null
|
||||
}
|
||||
/**
|
||||
* Prüft ob ein Wert nicht null ist.
|
||||
*/
|
||||
fun <T> notNull(): ValidationRule<T?> = ValidationRule { fieldName, value ->
|
||||
if (value == null) ValidationError.required(fieldName) else null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DSL-Funktion für die Erstellung von Validierungen.
|
||||
*/
|
||||
inline fun validate(builder: ValidationBuilder.() -> Unit): Result<Unit> {
|
||||
return ValidationBuilder().apply(builder).build()
|
||||
return ValidationBuilder().apply(builder).build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension-Funktion für einfache String-Validierung.
|
||||
*/
|
||||
fun String?.validateNotBlank(fieldName: String): ValidationError? {
|
||||
return if (this.isNullOrBlank()) ValidationError.required(fieldName) else null
|
||||
return if (this.isNullOrBlank()) ValidationError.required(fieldName) else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension-Funktion für einfache E-Mail-Validierung.
|
||||
*/
|
||||
fun String?.validateEmail(fieldName: String): ValidationError? {
|
||||
if (this.isNullOrBlank()) return ValidationError.required(fieldName)
|
||||
val emailRegex = Regex("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")
|
||||
return if (!this.matches(emailRegex)) {
|
||||
ValidationError.invalidFormat(fieldName, "$fieldName must be a valid email address")
|
||||
} else null
|
||||
if (this.isNullOrBlank()) return ValidationError.required(fieldName)
|
||||
val emailRegex = Regex("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")
|
||||
return if (!this.matches(emailRegex)) {
|
||||
ValidationError.invalidFormat(fieldName, "$fieldName muss eine gültige E-Mail-Adresse sein")
|
||||
} else null
|
||||
}
|
||||
|
||||
+21
-21
@@ -9,28 +9,28 @@ import kotlin.test.assertTrue
|
||||
|
||||
class ExtensionsPagedResponseTest {
|
||||
|
||||
@Test
|
||||
fun `toPagedResponse basic pagination`() {
|
||||
val list = (1..50).toList()
|
||||
@Test
|
||||
fun `toPagedResponse basic pagination`() {
|
||||
val list = (1..50).toList()
|
||||
|
||||
val page0 = list.toPagedResponse(page = 0, size = 10)
|
||||
assertEquals(10, page0.content.size)
|
||||
assertEquals(PageNumber(0), page0.page)
|
||||
assertEquals(PageSize(10), page0.size)
|
||||
assertEquals(50L, page0.totalElements)
|
||||
assertEquals(5, page0.totalPages)
|
||||
assertTrue(page0.hasNext)
|
||||
assertFalse(page0.hasPrevious)
|
||||
val page0 = list.toPagedResponse(page = 0, size = 10)
|
||||
assertEquals(10, page0.content.size)
|
||||
assertEquals(PageNumber(0), page0.page)
|
||||
assertEquals(PageSize(10), page0.size)
|
||||
assertEquals(50L, page0.totalElements)
|
||||
assertEquals(5, page0.totalPages)
|
||||
assertTrue(page0.hasNext)
|
||||
assertFalse(page0.hasPrevious)
|
||||
|
||||
val page4 = list.toPagedResponse(page = 4, size = 10)
|
||||
assertEquals((41..50).toList(), page4.content)
|
||||
assertFalse(page4.hasNext)
|
||||
assertTrue(page4.hasPrevious)
|
||||
val page4 = list.toPagedResponse(page = 4, size = 10)
|
||||
assertEquals((41..50).toList(), page4.content)
|
||||
assertFalse(page4.hasNext)
|
||||
assertTrue(page4.hasPrevious)
|
||||
|
||||
val emptyPage = list.toPagedResponse(page = 6, size = 10)
|
||||
assertTrue(emptyPage.content.isEmpty())
|
||||
assertEquals(5, emptyPage.totalPages)
|
||||
assertFalse(emptyPage.hasNext)
|
||||
assertTrue(emptyPage.hasPrevious)
|
||||
}
|
||||
val emptyPage = list.toPagedResponse(page = 6, size = 10)
|
||||
assertTrue(emptyPage.content.isEmpty())
|
||||
assertEquals(5, emptyPage.totalPages)
|
||||
assertFalse(emptyPage.hasNext)
|
||||
assertTrue(emptyPage.hasPrevious)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,101 +7,103 @@ import kotlin.test.*
|
||||
|
||||
class ResultTest {
|
||||
|
||||
@Test
|
||||
fun `success and failure flags`() {
|
||||
val s = Result.success(1)
|
||||
assertTrue(s.isSuccess)
|
||||
assertFalse(s.isFailure)
|
||||
@Test
|
||||
fun `success and failure flags`() {
|
||||
val s = Result.success(1)
|
||||
assertTrue(s.isSuccess)
|
||||
assertFalse(s.isFailure)
|
||||
|
||||
val f: Result<Int> = Result.failure(ErrorDto(ErrorCode("E"), "m"))
|
||||
assertTrue(f.isFailure)
|
||||
assertFalse(f.isSuccess)
|
||||
val f: Result<Int> = Result.failure(ErrorDto(ErrorCode("E"), "m"))
|
||||
assertTrue(f.isFailure)
|
||||
assertFalse(f.isSuccess)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `map flatMap fold`() {
|
||||
val s = Result.success(2).map { it * 2 }
|
||||
assertEquals(4, (s as Result.Success).value)
|
||||
|
||||
val f: Result<Int> = Result.failure(ErrorDto(ErrorCode("E"), "m"))
|
||||
assertTrue(f.map { it + 1 } is Result.Failure)
|
||||
|
||||
val flat = Result.success(2).flatMap { Result.success(it.toString()) }
|
||||
assertEquals("2", (flat as Result.Success).value)
|
||||
|
||||
val folded = flat.fold({ it.length }, { -1 })
|
||||
assertEquals(1, folded)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `zip and combine`() {
|
||||
val a = Result.success(1)
|
||||
val b = Result.success("x")
|
||||
val zipped = a.zip(b)
|
||||
assertTrue(zipped is Result.Success)
|
||||
assertEquals(Pair(1, "x"), (zipped as Result.Success).value)
|
||||
|
||||
val f1: Result<Int> = Result.failure(ErrorDto(ErrorCode("E1"), ""))
|
||||
val f2: Result<String> = Result.failure(ErrorDto(ErrorCode("E2"), ""))
|
||||
val z2 = f1.zip(b)
|
||||
assertTrue(z2 is Result.Failure)
|
||||
|
||||
val combined = Result.combine(listOf(Result.success(1), Result.success(2)))
|
||||
assertTrue(combined is Result.Success)
|
||||
assertEquals(listOf(1, 2), (combined as Result.Success).value)
|
||||
|
||||
val combinedFail =
|
||||
Result.combine(listOf(f1 as Result<Int>, Result.success(3), Result.failure(ErrorDto(ErrorCode("E3"), ""))))
|
||||
assertTrue(combinedFail is Result.Failure)
|
||||
assertEquals(2, (combinedFail as Result.Failure).errors.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `runCatching failure conversion failureFromValidation and recovery`() {
|
||||
val ok = Result.runCatching { "ok" }
|
||||
assertTrue(ok is Result.Success)
|
||||
|
||||
val iae = Result.runCatching<String> { throw IllegalArgumentException("bad") }
|
||||
assertTrue(iae is Result.Failure)
|
||||
assertEquals("INVALID_ARGUMENT", (iae as Result.Failure).errors.first().code.value)
|
||||
|
||||
val generic = Result.runCatching<String> { throw Exception("x") }
|
||||
assertTrue(generic is Result.Failure)
|
||||
|
||||
val verrs = listOf(ValidationError.required("name"), ValidationError.invalidFormat("email"))
|
||||
val fromVal: Result<Unit> = Result.failure(verrs)
|
||||
assertTrue(fromVal is Result.Failure)
|
||||
assertEquals("REQUIRED", (fromVal as Result.Failure).errors.first().code.value)
|
||||
|
||||
val rec = Result.failure<String>(ErrorDto(ErrorCode("E"), "")).recover { _ -> "fallback" }
|
||||
assertTrue(rec is Result.Success)
|
||||
|
||||
val recFail =
|
||||
Result.failure<String>(ErrorDto(ErrorCode("E"), "")).recoverCatching { _ -> throw IllegalStateException("boom") }
|
||||
assertTrue(recFail is Result.Failure)
|
||||
assertEquals("RECOVERY_FAILED", (recFail as Result.Failure).errors.first().code.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getOrNull default throw and toResult`() {
|
||||
val s = Result.success(5)
|
||||
assertEquals(5, s.getOrNull())
|
||||
|
||||
val f: Result<Int> = Result.failure(ErrorDto(ErrorCode("E"), ""))
|
||||
assertNull(f.getOrNull())
|
||||
assertEquals(7, f.getOrDefault(7))
|
||||
|
||||
assertEquals(5, s.getOrThrow())
|
||||
try {
|
||||
f.getOrThrow()
|
||||
fail("should throw")
|
||||
} catch (e: IllegalStateException) {
|
||||
// ok
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `map flatMap fold`() {
|
||||
val s = Result.success(2).map { it * 2 }
|
||||
assertEquals(4, (s as Result.Success).value)
|
||||
val nullable: Int? = null
|
||||
val r = nullable.toResult("ist leer")
|
||||
assertTrue(r is Result.Failure)
|
||||
|
||||
val f: Result<Int> = Result.failure(ErrorDto(ErrorCode("E"), "m"))
|
||||
assertTrue(f.map { it + 1 } is Result.Failure)
|
||||
|
||||
val flat = Result.success(2).flatMap { Result.success(it.toString()) }
|
||||
assertEquals("2", (flat as Result.Success).value)
|
||||
|
||||
val folded = flat.fold({ it.length }, { -1 })
|
||||
assertEquals(1, folded)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `zip and combine`() {
|
||||
val a = Result.success(1)
|
||||
val b = Result.success("x")
|
||||
val zipped = a.zip(b)
|
||||
assertTrue(zipped is Result.Success)
|
||||
assertEquals(Pair(1, "x"), (zipped as Result.Success).value)
|
||||
|
||||
val f1: Result<Int> = Result.failure(ErrorDto(ErrorCode("E1"), ""))
|
||||
val f2: Result<String> = Result.failure(ErrorDto(ErrorCode("E2"), ""))
|
||||
val z2 = f1.zip(b)
|
||||
assertTrue(z2 is Result.Failure)
|
||||
|
||||
val combined = Result.combine(listOf(Result.success(1), Result.success(2)))
|
||||
assertTrue(combined is Result.Success)
|
||||
assertEquals(listOf(1, 2), (combined as Result.Success).value)
|
||||
|
||||
val combinedFail = Result.combine(listOf(f1 as Result<Int>, Result.success(3), Result.failure(ErrorDto(ErrorCode("E3"), ""))))
|
||||
assertTrue(combinedFail is Result.Failure)
|
||||
assertEquals(2, (combinedFail as Result.Failure).errors.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `runCatching failure conversion failureFromValidation and recovery`() {
|
||||
val ok = Result.runCatching { "ok" }
|
||||
assertTrue(ok is Result.Success)
|
||||
|
||||
val iae = Result.runCatching<String> { throw IllegalArgumentException("bad") }
|
||||
assertTrue(iae is Result.Failure)
|
||||
assertEquals("INVALID_ARGUMENT", (iae as Result.Failure).errors.first().code.value)
|
||||
|
||||
val generic = Result.runCatching<String> { throw Exception("x") }
|
||||
assertTrue(generic is Result.Failure)
|
||||
|
||||
val verrs = listOf(ValidationError.required("name"), ValidationError.invalidFormat("email"))
|
||||
val fromVal: Result<Unit> = Result.failure(verrs)
|
||||
assertTrue(fromVal is Result.Failure)
|
||||
assertEquals("REQUIRED", (fromVal as Result.Failure).errors.first().code.value)
|
||||
|
||||
val rec = Result.failure<String>(ErrorDto(ErrorCode("E"), "")).recover { _ -> "fallback" }
|
||||
assertTrue(rec is Result.Success)
|
||||
|
||||
val recFail = Result.failure<String>(ErrorDto(ErrorCode("E"), "")).recoverCatching { _ -> throw IllegalStateException("boom") }
|
||||
assertTrue(recFail is Result.Failure)
|
||||
assertEquals("RECOVERY_FAILED", (recFail as Result.Failure).errors.first().code.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getOrNull default throw and toResult`() {
|
||||
val s = Result.success(5)
|
||||
assertEquals(5, s.getOrNull())
|
||||
|
||||
val f: Result<Int> = Result.failure(ErrorDto(ErrorCode("E"), ""))
|
||||
assertNull(f.getOrNull())
|
||||
assertEquals(7, f.getOrDefault(7))
|
||||
|
||||
assertEquals(5, s.getOrThrow())
|
||||
try {
|
||||
f.getOrThrow()
|
||||
fail("should throw")
|
||||
} catch (e: IllegalStateException) {
|
||||
// ok
|
||||
}
|
||||
|
||||
val nullable: Int? = null
|
||||
val r = nullable.toResult("ist leer")
|
||||
assertTrue(r is Result.Failure)
|
||||
|
||||
val r2 = 3.toResult()
|
||||
assertTrue(r2 is Result.Success)
|
||||
}
|
||||
val r2 = 3.toResult()
|
||||
assertTrue(r2 is Result.Success)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package at.mocode.core.utils
|
||||
|
||||
import at.mocode.core.domain.model.ErrorCode
|
||||
import at.mocode.core.domain.model.ErrorCodes
|
||||
import at.mocode.core.domain.model.ErrorDto
|
||||
import at.mocode.core.domain.model.PagedResponse
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.statements.BatchInsertStatement
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import java.sql.SQLException
|
||||
import java.sql.SQLTimeoutException
|
||||
|
||||
/**
|
||||
* JVM-specific database utilities for the Core module.
|
||||
@@ -22,61 +23,71 @@ import java.sql.SQLException
|
||||
* @return A Result containing either the operation result or error information
|
||||
*/
|
||||
inline fun <T> transactionResult(
|
||||
database: Database? = null,
|
||||
crossinline block: Transaction.() -> T
|
||||
database: Database? = null,
|
||||
crossinline block: Transaction.() -> T
|
||||
): Result<T> {
|
||||
return try {
|
||||
val result = transaction(database) { block() }
|
||||
Result.success(result)
|
||||
} catch (e: SQLException) {
|
||||
// Handle specific SQL exceptions
|
||||
val errorCode = when {
|
||||
e.message?.contains("constraint", ignoreCase = true) == true -> "CONSTRAINT_VIOLATION"
|
||||
e.message?.contains("duplicate", ignoreCase = true) == true -> "DUPLICATE_ENTRY"
|
||||
e.message?.contains("timeout", ignoreCase = true) == true -> "DATABASE_TIMEOUT"
|
||||
else -> "DATABASE_ERROR"
|
||||
}
|
||||
|
||||
Result.failure(
|
||||
ErrorDto(
|
||||
code = ErrorCode(errorCode),
|
||||
message = "Database operation failed: ${e.message}"
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(
|
||||
ErrorDto(
|
||||
code = ErrorCode("TRANSACTION_ERROR"),
|
||||
message = "Transaction failed: ${e.message ?: "Unknown error"}"
|
||||
)
|
||||
)
|
||||
return try {
|
||||
val result = transaction(database) { block() }
|
||||
Result.success(result)
|
||||
} catch (e: SQLTimeoutException) {
|
||||
Result.failure(
|
||||
ErrorDto(
|
||||
code = ErrorCodes.DATABASE_TIMEOUT,
|
||||
message = "Datenbank-Operation wegen Timeout fehlgeschlagen"
|
||||
)
|
||||
)
|
||||
} catch (e: SQLException) {
|
||||
// Robustere Fehlerbehandlung über SQLSTATE (Postgres)
|
||||
val mapped = when (e.sqlState) {
|
||||
// unique_violation
|
||||
"23505" -> ErrorCodes.DUPLICATE_ENTRY
|
||||
// foreign_key_violation
|
||||
"23503" -> ErrorCodes.FOREIGN_KEY_VIOLATION
|
||||
// check_violation
|
||||
"23514" -> ErrorCodes.CHECK_VIOLATION
|
||||
else -> ErrorCodes.DATABASE_ERROR
|
||||
}
|
||||
|
||||
Result.failure(
|
||||
ErrorDto(
|
||||
code = mapped,
|
||||
message = "Datenbank-Operation fehlgeschlagen"
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(
|
||||
ErrorDto(
|
||||
code = ErrorCodes.TRANSACTION_ERROR,
|
||||
message = "Transaktion fehlgeschlagen"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a write database operation.
|
||||
*/
|
||||
inline fun <T> writeTransaction(
|
||||
database: Database? = null,
|
||||
crossinline block: Transaction.() -> T
|
||||
database: Database? = null,
|
||||
crossinline block: Transaction.() -> T
|
||||
): Result<T> = transactionResult(database, block)
|
||||
|
||||
/**
|
||||
* Executes a read database operation.
|
||||
*/
|
||||
inline fun <T> readTransaction(
|
||||
database: Database? = null,
|
||||
crossinline block: Transaction.() -> T
|
||||
database: Database? = null,
|
||||
crossinline block: Transaction.() -> T
|
||||
): Result<T> = transactionResult(database, block)
|
||||
|
||||
/**
|
||||
* Extension function for Query-Builder to add pagination.
|
||||
*/
|
||||
fun Query.paginate(page: Int, size: Int): Query {
|
||||
require(page >= 0) { "Page number must be non-negative" }
|
||||
require(size > 0) { "Page size must be positive" }
|
||||
require(page >= 0) { "Page number must be non-negative" }
|
||||
require(size > 0) { "Page size must be positive" }
|
||||
|
||||
return limit(size).offset(start = (page * size).toLong())
|
||||
return limit(size).offset(start = (page * size).toLong())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,48 +100,48 @@ fun Query.paginate(page: Int, size: Int): Query {
|
||||
* @return A PagedResponse containing the paginated and transformed data
|
||||
*/
|
||||
fun <T> Query.toPagedResponse(
|
||||
page: Int,
|
||||
size: Int,
|
||||
transform: (ResultRow) -> T
|
||||
page: Int,
|
||||
size: Int,
|
||||
transform: (ResultRow) -> T
|
||||
): PagedResponse<T> {
|
||||
// Validate input parameters
|
||||
require(page >= 0) { "Page number must be non-negative" }
|
||||
require(size > 0) { "Page size must be positive" }
|
||||
// Validate input parameters
|
||||
require(page >= 0) { "Page number must be non-negative" }
|
||||
require(size > 0) { "Page size must be positive" }
|
||||
|
||||
// Calculate the total count first (executes a COUNT query)
|
||||
val totalCount = this.count()
|
||||
|
||||
// If there are no results, return an empty page
|
||||
if (totalCount == 0L) {
|
||||
return PagedResponse.create(
|
||||
content = emptyList(),
|
||||
page = page,
|
||||
size = size,
|
||||
totalElements = 0,
|
||||
totalPages = 0,
|
||||
hasNext = false,
|
||||
hasPrevious = page > 0
|
||||
)
|
||||
}
|
||||
|
||||
// Calculate total pages - use ceil division to ensure we round up
|
||||
val totalPages = ((totalCount + size - 1) / size).toInt()
|
||||
|
||||
// Ensure the requested page exists (if page is beyond available pages, return the last page)
|
||||
val adjustedPage = if (page >= totalPages) (totalPages - 1).coerceAtLeast(0) else page
|
||||
|
||||
// Then apply pagination and transform results
|
||||
val content = this.paginate(adjustedPage, size).map(transform)
|
||||
// Calculate the total count first (executes a COUNT query)
|
||||
val totalCount = this.count()
|
||||
|
||||
// If there are no results, return an empty page
|
||||
if (totalCount == 0L) {
|
||||
return PagedResponse.create(
|
||||
content = content,
|
||||
page = adjustedPage,
|
||||
size = size,
|
||||
totalElements = totalCount,
|
||||
totalPages = totalPages,
|
||||
hasNext = adjustedPage < totalPages - 1,
|
||||
hasPrevious = adjustedPage > 0
|
||||
content = emptyList(),
|
||||
page = page,
|
||||
size = size,
|
||||
totalElements = 0,
|
||||
totalPages = 0,
|
||||
hasNext = false,
|
||||
hasPrevious = page > 0
|
||||
)
|
||||
}
|
||||
|
||||
// Calculate total pages - use ceil division to ensure we round up
|
||||
val totalPages = ((totalCount + size - 1) / size).toInt()
|
||||
|
||||
// Ensure the requested page exists (if page is beyond available pages, return the last page)
|
||||
val adjustedPage = if (page >= totalPages) (totalPages - 1).coerceAtLeast(0) else page
|
||||
|
||||
// Then apply pagination and transform results
|
||||
val content = this.paginate(adjustedPage, size).map(transform)
|
||||
|
||||
return PagedResponse.create(
|
||||
content = content,
|
||||
page = adjustedPage,
|
||||
size = size,
|
||||
totalElements = totalCount,
|
||||
totalPages = totalPages,
|
||||
hasNext = adjustedPage < totalPages - 1,
|
||||
hasPrevious = adjustedPage > 0
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,64 +149,93 @@ fun <T> Query.toPagedResponse(
|
||||
*/
|
||||
object DatabaseUtils {
|
||||
|
||||
/**
|
||||
* Checks if a table exists.
|
||||
* Uses a safe query approach to verify table existence.
|
||||
*/
|
||||
fun tableExists(tableName: String, database: Database? = null): Boolean {
|
||||
return try {
|
||||
transaction(database) {
|
||||
// Execute a safer SQL statement to check if table exists
|
||||
val result = exec("SELECT 1 FROM information_schema.tables WHERE table_name = '$tableName' LIMIT 1")
|
||||
// If the query returns a result, the table exists
|
||||
result != null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
/**
|
||||
* Checks if a table exists.
|
||||
* Uses a safe query approach to verify table existence.
|
||||
*/
|
||||
fun tableExists(tableName: String, database: Database? = null): Boolean {
|
||||
return try {
|
||||
transaction(database) {
|
||||
// Postgres-spezifischer, robuster Ansatz über to_regclass
|
||||
val valid = tableName.trim()
|
||||
if (!valid.matches(Regex("^[A-Za-z_][A-Za-z0-9_]*$"))) return@transaction false
|
||||
exec("SELECT to_regclass('$valid')") { rs ->
|
||||
if (rs.next()) rs.getString(1) else null
|
||||
} != null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an index if it doesn't exist.
|
||||
*/
|
||||
fun createIndexIfNotExists(
|
||||
tableName: String,
|
||||
indexName: String,
|
||||
columns: Array<String>,
|
||||
unique: Boolean = false,
|
||||
database: Database? = null
|
||||
): Result<Unit> {
|
||||
return transactionResult(database) {
|
||||
val uniqueStr = if (unique) "UNIQUE" else ""
|
||||
val columnsStr = columns.joinToString(", ")
|
||||
val sql = "CREATE $uniqueStr INDEX IF NOT EXISTS $indexName ON $tableName ($columnsStr)"
|
||||
exec(sql)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Creates an index if it doesn't exist.
|
||||
*/
|
||||
@JvmName("createIndexIfNotExistsArray")
|
||||
fun createIndexIfNotExists(
|
||||
tableName: String,
|
||||
indexName: String,
|
||||
columns: Array<String>,
|
||||
unique: Boolean = false,
|
||||
database: Database? = null
|
||||
): Result<Unit> = createIndexIfNotExists(tableName, indexName, *columns, unique = unique, database = database)
|
||||
|
||||
/**
|
||||
* Executes a raw SQL query and returns the number of affected rows.
|
||||
*/
|
||||
fun executeRawSql(sql: String, database: Database? = null): Result<Int> {
|
||||
return transactionResult(database) {
|
||||
(exec(sql) ?: 0) as Int
|
||||
}
|
||||
}
|
||||
@JvmName("createIndexIfNotExistsVararg")
|
||||
fun createIndexIfNotExists(
|
||||
tableName: String,
|
||||
indexName: String,
|
||||
vararg columns: String,
|
||||
unique: Boolean = false,
|
||||
database: Database? = null
|
||||
): Result<Unit> {
|
||||
return transactionResult(database) {
|
||||
// Einfache Sanitization + Quoting der Identifier
|
||||
fun quoteIdent(name: String): String {
|
||||
require(name.matches(Regex("^[A-Za-z_][A-Za-z0-9_]*$"))) { "Ungültiger Identifier: $name" }
|
||||
return "\"$name\""
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for batch inserts.
|
||||
*/
|
||||
inline fun <T> batchInsert(
|
||||
table: Table,
|
||||
data: Iterable<T>,
|
||||
crossinline body: BatchInsertStatement.(T) -> Unit
|
||||
): Result<List<ResultRow>> {
|
||||
return transactionResult {
|
||||
table.batchInsert(data) { item ->
|
||||
body(item)
|
||||
}
|
||||
}
|
||||
val uniqueStr = if (unique) "UNIQUE" else ""
|
||||
val qTable = quoteIdent(tableName)
|
||||
val qIndex = quoteIdent(indexName)
|
||||
val cols = columns.map { quoteIdent(it) }.joinToString(", ")
|
||||
val sql = "CREATE $uniqueStr INDEX IF NOT EXISTS $qIndex ON $qTable ($cols)"
|
||||
exec(sql)
|
||||
Unit
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt ein beliebiges SQL-Statement aus (DDL/DML). Liefert keinen Update-Count zurück.
|
||||
*/
|
||||
fun executeRawSql(sql: String, database: Database? = null): Result<Unit> = transactionResult(database) {
|
||||
exec(sql)
|
||||
Unit
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a raw SQL update statement and returns affected rows.
|
||||
*/
|
||||
fun executeUpdate(sql: String, database: Database? = null): Result<Int> = transactionResult(database) {
|
||||
// Nutzt Exposed PreparedStatementApi, kein AutoCloseable
|
||||
val ps = this.connection.prepareStatement(sql, false)
|
||||
ps.executeUpdate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for batch inserts.
|
||||
*/
|
||||
inline fun <T> batchInsert(
|
||||
table: Table,
|
||||
data: Iterable<T>,
|
||||
crossinline body: BatchInsertStatement.(T) -> Unit
|
||||
): Result<List<ResultRow>> {
|
||||
return transactionResult {
|
||||
table.batchInsert(data) { item ->
|
||||
body(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -206,11 +246,11 @@ object DatabaseUtils {
|
||||
* Safely gets a value from a ResultRow.
|
||||
*/
|
||||
fun <T> ResultRow.getOrNull(column: Column<T>): T? {
|
||||
return try {
|
||||
this[column]
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
return try {
|
||||
this[column]
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -218,17 +258,17 @@ fun <T> ResultRow.getOrNull(column: Column<T>): T? {
|
||||
* Safely handles any exceptions during the conversion process.
|
||||
*/
|
||||
fun ResultRow.toMap(): Map<String, Any?> {
|
||||
val result = mutableMapOf<String, Any?>()
|
||||
this.fieldIndex.forEach { (expression, _) ->
|
||||
try {
|
||||
when (expression) {
|
||||
is Column<*> -> result[expression.name] = this[expression]
|
||||
else -> result[expression.toString()] = this[expression]
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Ignore columns that can't be read and log the error if needed
|
||||
// You could add logging here in a production environment
|
||||
}
|
||||
val result = mutableMapOf<String, Any?>()
|
||||
this.fieldIndex.forEach { (expression, _) ->
|
||||
try {
|
||||
when (expression) {
|
||||
is Column<*> -> result[expression.name] = this[expression]
|
||||
else -> result[expression.toString()] = this[expression]
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Ignore columns that can't be read and log the error if needed
|
||||
// You could add logging here in a production environment
|
||||
}
|
||||
return result
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user