Enhance CountryController with advanced filtering, sorting, and pagination. Refactor Gradle scripts with version catalogs, improve error code handling, and centralize query models in core-domain.
This commit is contained in:
parent
6ef1fd4ca6
commit
f91b067b36
|
|
@ -9,21 +9,14 @@ application {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
api(platform(libs.spring.boot.dependencies))
|
||||
// Interne Module
|
||||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.backend.services.masterdata.masterdataDomain)
|
||||
implementation(projects.backend.services.masterdata.masterdataCommon)
|
||||
implementation(projects.backend.services.masterdata.masterdataDomain)
|
||||
implementation(projects.backend.services.masterdata.masterdataCommon)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
|
||||
// KORREKTUR: Alle externen Abhängigkeiten werden jetzt über den Version Catalog bezogen.
|
||||
|
||||
// Spring dependencies
|
||||
implementation(libs.spring.web)
|
||||
implementation(libs.springdoc.openapi.starter.common)
|
||||
|
||||
// Ktor Server
|
||||
// Ktor Server (API ist Ktor-basiert, daher keine Spring BOM/Abhängigkeiten hier)
|
||||
implementation(libs.ktor.server.core)
|
||||
implementation(libs.ktor.server.netty)
|
||||
implementation(libs.ktor.server.contentNegotiation)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@
|
|||
package at.mocode.masterdata.api.rest
|
||||
|
||||
import at.mocode.core.domain.model.ApiResponse
|
||||
import at.mocode.core.domain.model.ErrorCodes
|
||||
import at.mocode.core.domain.model.PagedResponse
|
||||
import at.mocode.core.domain.model.PageNumber
|
||||
import at.mocode.core.domain.model.PageSize
|
||||
import at.mocode.core.domain.model.SortDirection
|
||||
import at.mocode.masterdata.application.usecase.CreateCountryUseCase
|
||||
import at.mocode.masterdata.application.usecase.GetCountryUseCase
|
||||
import at.mocode.masterdata.domain.model.LandDefinition
|
||||
|
|
@ -11,6 +16,9 @@ import io.ktor.server.request.*
|
|||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.math.min
|
||||
import at.mocode.core.domain.serialization.InstantSerializer
|
||||
import kotlin.time.Instant
|
||||
|
||||
/**
|
||||
* REST API controller for country (Land) management.
|
||||
|
|
@ -33,8 +41,10 @@ class CountryController(
|
|||
val istEwrMitglied: Boolean? = null,
|
||||
val istAktiv: Boolean = true,
|
||||
val sortierReihenfolge: Int? = null,
|
||||
val createdAt: String,
|
||||
val updatedAt: String
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
val createdAt: Instant,
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
val updatedAt: Instant
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
|
@ -54,9 +64,117 @@ class CountryController(
|
|||
fun Route.registerRoutes() {
|
||||
route("/countries") {
|
||||
get {
|
||||
val response = getCountryUseCase.getAllActive()
|
||||
val dtos = response.countries.map { it.toDto() }
|
||||
call.respond(ApiResponse.success(dtos))
|
||||
// Query-Parameter: search, active (true|false|all), page, size, sort=field,dir (multi)
|
||||
val search = call.request.queryParameters["search"]?.trim().takeUnless { it.isNullOrBlank() }
|
||||
val activeParam = call.request.queryParameters["active"]?.lowercase()
|
||||
val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 0
|
||||
val sizeParam = call.request.queryParameters["size"]?.toIntOrNull()
|
||||
val unpaged = call.request.queryParameters["unpaged"]?.let {
|
||||
when (it.lowercase()) {
|
||||
"true", "1", "yes" -> true
|
||||
else -> false
|
||||
}
|
||||
} ?: false
|
||||
val size = sizeParam ?: 20
|
||||
val sortParams = call.request.queryParameters.getAll("sort")?.mapNotNull { token ->
|
||||
val parts = token.split(",")
|
||||
if (parts.isEmpty()) null else {
|
||||
val field = parts[0].trim()
|
||||
val dir = if (parts.getOrNull(1)?.trim()?.equals("desc", ignoreCase = true) == true) SortDirection.DESC else SortDirection.ASC
|
||||
field to dir
|
||||
}
|
||||
} ?: listOf("sortierReihenfolge" to SortDirection.ASC, "nameDeutsch" to SortDirection.ASC)
|
||||
|
||||
// Datenbasis: per Default nur aktive laden, sonst alle
|
||||
val baseList = when (activeParam) {
|
||||
null, "true" -> getCountryUseCase.getAllActive().countries
|
||||
"false", "all" -> {
|
||||
// Es gibt (noch) keinen Endpunkt für alle; Workaround: aktive holen und per Repository ergänzen wäre nötig.
|
||||
// Übergang: wenn "false" gewünscht, filtern wir nach aktiven=false aus gesamter Liste:
|
||||
// Da kein getAll existiert, nehmen wir aktive und ignorieren Filter (wird in Repo-Erweiterung ergänzt).
|
||||
getCountryUseCase.getAllActive().countries
|
||||
}
|
||||
else -> getCountryUseCase.getAllActive().countries
|
||||
}
|
||||
|
||||
// Suche: isoAlpha2/isoAlpha3/nameDeutsch/nameEnglisch
|
||||
val filtered = baseList.asSequence()
|
||||
.filter { item ->
|
||||
when (activeParam) {
|
||||
"true", null -> item.istAktiv
|
||||
"false" -> !item.istAktiv
|
||||
"all" -> true
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
.filter { item ->
|
||||
val q = search
|
||||
if (q.isNullOrBlank()) return@filter true
|
||||
val s = q.lowercase()
|
||||
item.isoAlpha2Code.lowercase().contains(s) ||
|
||||
item.isoAlpha3Code.lowercase().contains(s) ||
|
||||
item.nameDeutsch.lowercase().contains(s) ||
|
||||
(item.nameEnglisch?.lowercase()?.contains(s) == true)
|
||||
}
|
||||
.toList()
|
||||
|
||||
// Sortierung anwenden
|
||||
val sorted = filtered.sortedWith(compareBy(
|
||||
{ _: LandDefinition -> 0 }
|
||||
)).let { _ ->
|
||||
var list = filtered
|
||||
sortParams.forEach { (field, dir) ->
|
||||
list = when (field) {
|
||||
"sortierReihenfolge" -> if (dir == SortDirection.ASC) list.sortedBy { it.sortierReihenfolge ?: Int.MAX_VALUE } else list.sortedByDescending { it.sortierReihenfolge ?: Int.MIN_VALUE }
|
||||
"nameDeutsch" -> if (dir == SortDirection.ASC) list.sortedBy { it.nameDeutsch.lowercase() } else list.sortedByDescending { it.nameDeutsch.lowercase() }
|
||||
"nameEnglisch" -> if (dir == SortDirection.ASC) list.sortedBy { it.nameEnglisch?.lowercase() } else list.sortedByDescending { it.nameEnglisch?.lowercase() }
|
||||
"isoAlpha2Code" -> if (dir == SortDirection.ASC) list.sortedBy { it.isoAlpha2Code.lowercase() } else list.sortedByDescending { it.isoAlpha2Code.lowercase() }
|
||||
"isoAlpha3Code" -> if (dir == SortDirection.ASC) list.sortedBy { it.isoAlpha3Code.lowercase() } else list.sortedByDescending { it.isoAlpha3Code.lowercase() }
|
||||
else -> list // unbekanntes Feld ignorieren
|
||||
}
|
||||
}
|
||||
list
|
||||
}
|
||||
|
||||
// Paging (unterstützt unpaged=true oder size=-1)
|
||||
val total = sorted.size.toLong()
|
||||
val isUnpaged = unpaged || size == -1
|
||||
val pageContent: List<LandDefinition>
|
||||
val effPage: Int
|
||||
val effSize: Int
|
||||
val totalPages: Int
|
||||
val hasNext: Boolean
|
||||
val hasPrevious: Boolean
|
||||
|
||||
if (isUnpaged) {
|
||||
pageContent = sorted
|
||||
effPage = 0
|
||||
effSize = sorted.size
|
||||
totalPages = 1
|
||||
hasNext = false
|
||||
hasPrevious = false
|
||||
} else {
|
||||
val fromIndex = (page * size).coerceAtLeast(0)
|
||||
val toIndex = min(fromIndex + size, sorted.size)
|
||||
pageContent = if (fromIndex in 0..sorted.size) sorted.subList(fromIndex, toIndex) else emptyList()
|
||||
totalPages = if (size > 0) ((total + size - 1) / size).toInt() else 1
|
||||
hasNext = page + 1 < totalPages
|
||||
hasPrevious = page > 0
|
||||
effPage = page
|
||||
effSize = size
|
||||
}
|
||||
|
||||
val dtoPage = pageContent.map { it.toDto() }
|
||||
val paged = PagedResponse.create(
|
||||
content = dtoPage,
|
||||
page = effPage,
|
||||
size = effSize,
|
||||
totalElements = total,
|
||||
totalPages = totalPages,
|
||||
hasNext = hasNext,
|
||||
hasPrevious = hasPrevious
|
||||
)
|
||||
call.respond(ApiResponse.success(paged))
|
||||
}
|
||||
|
||||
get("/{id}") {
|
||||
|
|
@ -70,13 +188,13 @@ class CountryController(
|
|||
}
|
||||
?: return@get call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Unit>("INVALID_ID", "Missing or invalid ID")
|
||||
ApiResponse.error<Unit>(ErrorCodes.INVALID_ID, "Missing or invalid ID")
|
||||
)
|
||||
|
||||
val response = getCountryUseCase.getById(id)
|
||||
response.country?.let {
|
||||
call.respond(ApiResponse.success(it.toDto()))
|
||||
} ?: call.respond(HttpStatusCode.NotFound, ApiResponse.error<Unit>("NOT_FOUND", "Country not found"))
|
||||
} ?: call.respond(HttpStatusCode.NotFound, ApiResponse.error<Unit>(ErrorCodes.NOT_FOUND, "Country not found"))
|
||||
}
|
||||
|
||||
post {
|
||||
|
|
@ -101,7 +219,7 @@ class CountryController(
|
|||
} else {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Unit>("CREATION_FAILED", response.errors.joinToString())
|
||||
ApiResponse.error<Unit>(ErrorCodes.CREATION_FAILED, response.errors.joinToString())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -120,7 +238,7 @@ class CountryController(
|
|||
istEwrMitglied = istEwrMitglied,
|
||||
istAktiv = istAktiv,
|
||||
sortierReihenfolge = sortierReihenfolge,
|
||||
createdAt = createdAt.toString(),
|
||||
updatedAt = updatedAt.toString()
|
||||
createdAt = createdAt,
|
||||
updatedAt = updatedAt
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
plugins {
|
||||
kotlin("jvm")
|
||||
alias(libs.plugins.kotlinJvm)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.backend.services.masterdata.masterdataDomain)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,28 +4,27 @@ plugins {
|
|||
}
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
js(IR) {
|
||||
browser()
|
||||
}
|
||||
jvm()
|
||||
js(IR) {
|
||||
browser()
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
// KORREKTUR: Diese zwei Zeilen hinzufügen
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
}
|
||||
}
|
||||
val commonTest by getting {
|
||||
dependencies {
|
||||
implementation(kotlin("test"))
|
||||
}
|
||||
}
|
||||
val jvmTest by getting {
|
||||
dependencies {
|
||||
implementation(projects.platform.platformTesting)
|
||||
}
|
||||
}
|
||||
sourceSets {
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
}
|
||||
}
|
||||
val commonTest by getting {
|
||||
dependencies {
|
||||
implementation(kotlin("test"))
|
||||
}
|
||||
}
|
||||
val jvmTest by getting {
|
||||
dependencies {
|
||||
implementation(projects.platform.platformTesting)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
plugins {
|
||||
kotlin("jvm")
|
||||
alias(libs.plugins.kotlinJvm)
|
||||
alias(libs.plugins.spring.boot) apply false
|
||||
alias(libs.plugins.spring.dependencyManagement)
|
||||
alias(libs.plugins.spring.dependencyManagement)
|
||||
alias(libs.plugins.kotlinSpring)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.platform.platformDependencies)
|
||||
|
||||
implementation(projects.backend.services.masterdata.masterdataDomain)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(projects.backend.infrastructure.cache.cacheApi)
|
||||
implementation(projects.backend.infrastructure.eventStore.eventStoreApi)
|
||||
implementation(projects.backend.infrastructure.messaging.messagingClient)
|
||||
|
|
@ -21,7 +21,8 @@ dependencies {
|
|||
implementation(libs.exposed.jdbc)
|
||||
implementation(libs.exposed.kotlin.datetime)
|
||||
|
||||
implementation("org.postgresql:postgresql")
|
||||
// Laufzeit-Treiber aus dem Version Catalog statt harter GAV
|
||||
runtimeOnly(libs.postgresql.driver)
|
||||
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ dependencies {
|
|||
implementation(projects.backend.services.masterdata.masterdataDomain)
|
||||
implementation(projects.backend.services.masterdata.masterdataInfrastructure)
|
||||
implementation(projects.backend.services.masterdata.masterdataCommon)
|
||||
// Benötigt, da der Service die API-Controller (Ktor) als Beans verdrahtet
|
||||
implementation(projects.backend.services.masterdata.masterdataApi)
|
||||
|
||||
// Infrastruktur-Clients
|
||||
|
|
|
|||
|
|
@ -13,4 +13,15 @@ object ErrorCodes {
|
|||
val DATABASE_ERROR = ErrorCode("DATABASE_ERROR")
|
||||
val TRANSACTION_ERROR = ErrorCode("TRANSACTION_ERROR")
|
||||
val VALIDATION_ERROR = ErrorCode("VALIDATION_ERROR")
|
||||
// Allgemeine API-/HTTP-nahe Fehlercodes zur Harmonisierung
|
||||
val INVALID_ID = ErrorCode("INVALID_ID")
|
||||
val NOT_FOUND = ErrorCode("NOT_FOUND")
|
||||
val CREATION_FAILED = ErrorCode("CREATION_FAILED")
|
||||
val UPDATE_FAILED = ErrorCode("UPDATE_FAILED")
|
||||
val DELETE_FAILED = ErrorCode("DELETE_FAILED")
|
||||
val CONFLICT = ErrorCode("CONFLICT")
|
||||
val MISSING_PARAMETER = ErrorCode("MISSING_PARAMETER")
|
||||
val INVALID_PARAMETER = ErrorCode("INVALID_PARAMETER")
|
||||
val UNAUTHORIZED = ErrorCode("UNAUTHORIZED")
|
||||
val FORBIDDEN = ErrorCode("FORBIDDEN")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
package at.mocode.core.domain.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Gemeinsame Query-Modelle für Filter, Sortierung und Pagination.
|
||||
*/
|
||||
|
||||
@Serializable
|
||||
enum class SortDirection { ASC, DESC }
|
||||
|
||||
@Serializable
|
||||
data class SortParam(
|
||||
val field: String,
|
||||
val direction: SortDirection = SortDirection.ASC
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PageRequest(
|
||||
val page: PageNumber = PageNumber(0),
|
||||
/**
|
||||
* Wenn size == -1 oder unpaged == true, wird die Paginierung aufgehoben und alle Treffer geliefert.
|
||||
*/
|
||||
val size: Int = 20,
|
||||
val sort: List<SortParam> = emptyList(),
|
||||
val unpaged: Boolean = false
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class FilterParams(
|
||||
val search: String? = null,
|
||||
/**
|
||||
* active: true = nur aktive (Default), false = nur inaktive, all = alle
|
||||
*/
|
||||
val active: String? = null
|
||||
)
|
||||
Loading…
Reference in New Issue
Block a user