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:
@@ -9,7 +9,6 @@ application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(platform(libs.spring.boot.dependencies))
|
|
||||||
// Interne Module
|
// Interne Module
|
||||||
implementation(projects.platform.platformDependencies)
|
implementation(projects.platform.platformDependencies)
|
||||||
implementation(projects.backend.services.masterdata.masterdataDomain)
|
implementation(projects.backend.services.masterdata.masterdataDomain)
|
||||||
@@ -17,13 +16,7 @@ dependencies {
|
|||||||
implementation(projects.core.coreDomain)
|
implementation(projects.core.coreDomain)
|
||||||
implementation(projects.core.coreUtils)
|
implementation(projects.core.coreUtils)
|
||||||
|
|
||||||
// KORREKTUR: Alle externen Abhängigkeiten werden jetzt über den Version Catalog bezogen.
|
// Ktor Server (API ist Ktor-basiert, daher keine Spring BOM/Abhängigkeiten hier)
|
||||||
|
|
||||||
// Spring dependencies
|
|
||||||
implementation(libs.spring.web)
|
|
||||||
implementation(libs.springdoc.openapi.starter.common)
|
|
||||||
|
|
||||||
// Ktor Server
|
|
||||||
implementation(libs.ktor.server.core)
|
implementation(libs.ktor.server.core)
|
||||||
implementation(libs.ktor.server.netty)
|
implementation(libs.ktor.server.netty)
|
||||||
implementation(libs.ktor.server.contentNegotiation)
|
implementation(libs.ktor.server.contentNegotiation)
|
||||||
|
|||||||
+128
-10
@@ -2,6 +2,11 @@
|
|||||||
package at.mocode.masterdata.api.rest
|
package at.mocode.masterdata.api.rest
|
||||||
|
|
||||||
import at.mocode.core.domain.model.ApiResponse
|
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.CreateCountryUseCase
|
||||||
import at.mocode.masterdata.application.usecase.GetCountryUseCase
|
import at.mocode.masterdata.application.usecase.GetCountryUseCase
|
||||||
import at.mocode.masterdata.domain.model.LandDefinition
|
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.response.*
|
||||||
import io.ktor.server.routing.*
|
import io.ktor.server.routing.*
|
||||||
import kotlinx.serialization.Serializable
|
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.
|
* REST API controller for country (Land) management.
|
||||||
@@ -33,8 +41,10 @@ class CountryController(
|
|||||||
val istEwrMitglied: Boolean? = null,
|
val istEwrMitglied: Boolean? = null,
|
||||||
val istAktiv: Boolean = true,
|
val istAktiv: Boolean = true,
|
||||||
val sortierReihenfolge: Int? = null,
|
val sortierReihenfolge: Int? = null,
|
||||||
val createdAt: String,
|
@Serializable(with = InstantSerializer::class)
|
||||||
val updatedAt: String
|
val createdAt: Instant,
|
||||||
|
@Serializable(with = InstantSerializer::class)
|
||||||
|
val updatedAt: Instant
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -54,9 +64,117 @@ class CountryController(
|
|||||||
fun Route.registerRoutes() {
|
fun Route.registerRoutes() {
|
||||||
route("/countries") {
|
route("/countries") {
|
||||||
get {
|
get {
|
||||||
val response = getCountryUseCase.getAllActive()
|
// Query-Parameter: search, active (true|false|all), page, size, sort=field,dir (multi)
|
||||||
val dtos = response.countries.map { it.toDto() }
|
val search = call.request.queryParameters["search"]?.trim().takeUnless { it.isNullOrBlank() }
|
||||||
call.respond(ApiResponse.success(dtos))
|
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}") {
|
get("/{id}") {
|
||||||
@@ -70,13 +188,13 @@ class CountryController(
|
|||||||
}
|
}
|
||||||
?: return@get call.respond(
|
?: return@get call.respond(
|
||||||
HttpStatusCode.BadRequest,
|
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)
|
val response = getCountryUseCase.getById(id)
|
||||||
response.country?.let {
|
response.country?.let {
|
||||||
call.respond(ApiResponse.success(it.toDto()))
|
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 {
|
post {
|
||||||
@@ -101,7 +219,7 @@ class CountryController(
|
|||||||
} else {
|
} else {
|
||||||
call.respond(
|
call.respond(
|
||||||
HttpStatusCode.BadRequest,
|
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,
|
istEwrMitglied = istEwrMitglied,
|
||||||
istAktiv = istAktiv,
|
istAktiv = istAktiv,
|
||||||
sortierReihenfolge = sortierReihenfolge,
|
sortierReihenfolge = sortierReihenfolge,
|
||||||
createdAt = createdAt.toString(),
|
createdAt = createdAt,
|
||||||
updatedAt = updatedAt.toString()
|
updatedAt = updatedAt
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm")
|
alias(libs.plugins.kotlinJvm)
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
val commonMain by getting {
|
val commonMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
// KORREKTUR: Diese zwei Zeilen hinzufügen
|
|
||||||
implementation(projects.core.coreDomain)
|
implementation(projects.core.coreDomain)
|
||||||
implementation(projects.core.coreUtils)
|
implementation(projects.core.coreUtils)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm")
|
alias(libs.plugins.kotlinJvm)
|
||||||
alias(libs.plugins.spring.boot) apply false
|
alias(libs.plugins.spring.boot) apply false
|
||||||
alias(libs.plugins.spring.dependencyManagement)
|
alias(libs.plugins.spring.dependencyManagement)
|
||||||
alias(libs.plugins.kotlinSpring)
|
alias(libs.plugins.kotlinSpring)
|
||||||
@@ -21,7 +21,8 @@ dependencies {
|
|||||||
implementation(libs.exposed.jdbc)
|
implementation(libs.exposed.jdbc)
|
||||||
implementation(libs.exposed.kotlin.datetime)
|
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.masterdataDomain)
|
||||||
implementation(projects.backend.services.masterdata.masterdataInfrastructure)
|
implementation(projects.backend.services.masterdata.masterdataInfrastructure)
|
||||||
implementation(projects.backend.services.masterdata.masterdataCommon)
|
implementation(projects.backend.services.masterdata.masterdataCommon)
|
||||||
|
// Benötigt, da der Service die API-Controller (Ktor) als Beans verdrahtet
|
||||||
implementation(projects.backend.services.masterdata.masterdataApi)
|
implementation(projects.backend.services.masterdata.masterdataApi)
|
||||||
|
|
||||||
// Infrastruktur-Clients
|
// Infrastruktur-Clients
|
||||||
|
|||||||
@@ -13,4 +13,15 @@ object ErrorCodes {
|
|||||||
val DATABASE_ERROR = ErrorCode("DATABASE_ERROR")
|
val DATABASE_ERROR = ErrorCode("DATABASE_ERROR")
|
||||||
val TRANSACTION_ERROR = ErrorCode("TRANSACTION_ERROR")
|
val TRANSACTION_ERROR = ErrorCode("TRANSACTION_ERROR")
|
||||||
val VALIDATION_ERROR = ErrorCode("VALIDATION_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
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user