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:
2025-07-28 22:43:28 +02:00
parent ca4d476360
commit 260460149a
13 changed files with 477 additions and 699 deletions
@@ -10,28 +10,22 @@ import kotlinx.datetime.Instant
* A domain event represents something significant that has happened in a specific domain.
*/
interface DomainEvent {
/**
* Unique identifier for this event instance.
*/
val eventId: Uuid
/**
* Identifier of the aggregate that the event belongs to.
*/
val aggregateId: Uuid
val eventType: String
/**
* Timestamp when the event occurred.
*/
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(
override val aggregateId: Uuid,
override val eventType: String,
override val version: Long,
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
/**
* Interface for a component that can publish domain events, typically to a message bus like Kafka.
*/
// ... (DomainEventPublisher and DomainEventHandler interfaces remain the same)
interface DomainEventPublisher {
suspend fun publish(event: 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> {
suspend fun handle(event: T)
fun canHandle(eventType: String): Boolean
@@ -8,12 +8,14 @@ import kotlinx.datetime.Instant
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
/**
* 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
abstract class EntityDto : BaseDto {
@@ -28,23 +30,67 @@ abstract class EntityDto : BaseDto {
}
/**
* A standardized wrapper for all API responses.
* Provides a consistent structure for data, success status, and errors.
* A structured representation of a single error.
*/
@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.
*/
@Serializable
data class ApiResponse<T>(
val data: T? = null,
val success: Boolean = true,
val message: String? = null,
val errors: List<String> = emptyList(),
val data: T?,
val success: Boolean,
val errors: List<ErrorDto> = emptyList(), // OPTIMIZED: Using structured ErrorDto
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.
* 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.
*/
@Serializable
data class PagedResponse<T>(
val content: List<T>,
val page: Int,
@@ -55,26 +101,4 @@ data class PagedResponse<T>(
val hasPrevious: Boolean
)
/**
* 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
// REMOVED: The PaginationDto was redundant as all its information is already contained within PagedResponse.
@@ -4,22 +4,45 @@ package at.mocode.core.utils.validation
* Represents a single validation error.
* @param field The name of the field that failed validation.
* @param message A user-friendly error message.
* @param code A machine-readable error code for the client.
*/
data class ValidationError(
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(
val isValid: Boolean,
val errors: List<ValidationError> = emptyList()
) {
sealed class ValidationResult {
/**
* 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 {
fun valid() = ValidationResult(true)
fun invalid(errors: List<ValidationError>) = ValidationResult(false, errors)
fun invalid(field: String, message: String) = ValidationResult(false, listOf(ValidationError(field, message)))
fun invalid(field: String, message: String, code: String? = null): Invalid {
return Invalid(listOf(ValidationError(field, message, code)))
}
}
}
/**
* 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}" }}"
)