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.
|
||||
*/
|
||||
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}" }}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user