(fix) Swagger/OpenAPI-Dokumentation implementieren
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
// root/build.gradle.kts
|
// root/build.gradle.kts
|
||||||
plugins {
|
plugins {
|
||||||
|
// Apply base plugin to provide lifecycle tasks like assemble, build, clean
|
||||||
|
base
|
||||||
// Dies ist notwendig, um zu verhindern, dass die Plugins mehrfach geladen werden
|
// Dies ist notwendig, um zu verhindern, dass die Plugins mehrfach geladen werden
|
||||||
// im Classloader jedes Subprojekts
|
// im Classloader jedes Subprojekts
|
||||||
alias(libs.plugins.kotlin.multiplatform) apply false
|
alias(libs.plugins.kotlin.multiplatform) apply false
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
# Swagger/OpenAPI Documentation
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
|
||||||
|
Die Meldestelle API verfügt jetzt über eine vollständige Swagger/OpenAPI-Dokumentation, die eine interaktive Benutzeroberfläche zur Erkundung und Testung der API-Endpunkte bietet.
|
||||||
|
|
||||||
|
## Zugriff auf die Dokumentation
|
||||||
|
|
||||||
|
### Swagger UI
|
||||||
|
- **URL**: `http://localhost:8080/swagger`
|
||||||
|
- **Beschreibung**: Interaktive Benutzeroberfläche zur Erkundung der API
|
||||||
|
- **Features**:
|
||||||
|
- Vollständige API-Dokumentation
|
||||||
|
- Interaktive Testmöglichkeiten
|
||||||
|
- Beispiel-Requests und -Responses
|
||||||
|
- Schema-Definitionen
|
||||||
|
|
||||||
|
### OpenAPI Specification
|
||||||
|
- **URL**: `http://localhost:8080/openapi`
|
||||||
|
- **Beschreibung**: Raw OpenAPI 3.0.3 Spezifikation im YAML-Format
|
||||||
|
- **Verwendung**: Kann für Code-Generierung oder Import in andere Tools verwendet werden
|
||||||
|
|
||||||
|
## Dokumentierte Endpunkte
|
||||||
|
|
||||||
|
### Basis-Endpunkte
|
||||||
|
- `GET /health` - Gesundheitsprüfung des Services
|
||||||
|
- `GET /api` - API-Informationen
|
||||||
|
|
||||||
|
### Person Management (`/api/persons`)
|
||||||
|
- `GET /api/persons` - Alle Personen abrufen
|
||||||
|
- `POST /api/persons` - Neue Person erstellen
|
||||||
|
- `GET /api/persons/{id}` - Person nach UUID abrufen
|
||||||
|
- `PUT /api/persons/{id}` - Person aktualisieren
|
||||||
|
- `DELETE /api/persons/{id}` - Person löschen
|
||||||
|
- `GET /api/persons/oeps/{oepsSatzNr}` - Person nach OEPS-Nummer abrufen
|
||||||
|
- `GET /api/persons/search?q={query}` - Personen suchen
|
||||||
|
- `GET /api/persons/verein/{vereinId}` - Personen nach Verein-ID abrufen
|
||||||
|
|
||||||
|
## Schema-Definitionen
|
||||||
|
|
||||||
|
### Person
|
||||||
|
```yaml
|
||||||
|
Person:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
vorname:
|
||||||
|
type: string
|
||||||
|
nachname:
|
||||||
|
type: string
|
||||||
|
geburtsdatum:
|
||||||
|
type: string
|
||||||
|
format: date
|
||||||
|
oepsSatzNr:
|
||||||
|
type: string
|
||||||
|
vereinId:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
format: email
|
||||||
|
telefon:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- vorname
|
||||||
|
- nachname
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error
|
||||||
|
```yaml
|
||||||
|
Error:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- error
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verwendung
|
||||||
|
|
||||||
|
### 1. Server starten
|
||||||
|
```bash
|
||||||
|
./gradlew :server:run
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Swagger UI öffnen
|
||||||
|
Navigieren Sie zu `http://localhost:8080/swagger` in Ihrem Browser.
|
||||||
|
|
||||||
|
### 3. API erkunden
|
||||||
|
- Klicken Sie auf die verschiedenen Endpunkte, um Details zu sehen
|
||||||
|
- Verwenden Sie "Try it out" um Requests direkt zu testen
|
||||||
|
- Sehen Sie sich die Beispiel-Responses an
|
||||||
|
|
||||||
|
### 4. OpenAPI Spec herunterladen
|
||||||
|
Besuchen Sie `http://localhost:8080/openapi` um die vollständige OpenAPI-Spezifikation zu erhalten.
|
||||||
|
|
||||||
|
## Erweiterung der Dokumentation
|
||||||
|
|
||||||
|
### Neue Endpunkte hinzufügen
|
||||||
|
Um neue API-Endpunkte zu dokumentieren, erweitern Sie die Datei:
|
||||||
|
`server/src/main/resources/openapi.yaml`
|
||||||
|
|
||||||
|
### Beispiel für neuen Endpunkt:
|
||||||
|
```yaml
|
||||||
|
/api/vereine:
|
||||||
|
get:
|
||||||
|
summary: Get all clubs
|
||||||
|
description: Retrieve a list of all clubs
|
||||||
|
tags:
|
||||||
|
- Clubs
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: List of clubs
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Verein'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Technische Details
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
- `io.ktor:ktor-server-openapi:3.1.2`
|
||||||
|
- `io.ktor:ktor-server-swagger:3.1.2`
|
||||||
|
|
||||||
|
### Konfiguration
|
||||||
|
Die Swagger/OpenAPI-Konfiguration befindet sich in:
|
||||||
|
- `server/src/main/kotlin/at/mocode/plugins/Routing.kt`
|
||||||
|
- `server/src/main/resources/openapi.yaml`
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
Automatisierte Tests für die Swagger-Funktionalität:
|
||||||
|
- `server/src/test/kotlin/at/mocode/SwaggerTest.kt`
|
||||||
|
|
||||||
|
## Nächste Schritte
|
||||||
|
|
||||||
|
1. **Erweitern Sie die Dokumentation** für weitere API-Endpunkte (Vereine, Turniere, etc.)
|
||||||
|
2. **Fügen Sie Authentifizierung hinzu** zur OpenAPI-Spezifikation wenn implementiert
|
||||||
|
3. **Konfigurieren Sie Produktions-URLs** in der OpenAPI-Spezifikation
|
||||||
|
4. **Implementieren Sie API-Versionierung** in der Dokumentation
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Swagger UI lädt nicht
|
||||||
|
- Überprüfen Sie, ob der Server läuft
|
||||||
|
- Stellen Sie sicher, dass Port 8080 verfügbar ist
|
||||||
|
- Prüfen Sie die Logs auf Fehler
|
||||||
|
|
||||||
|
### OpenAPI Spec ist leer
|
||||||
|
- Überprüfen Sie, ob `openapi.yaml` im Classpath verfügbar ist
|
||||||
|
- Stellen Sie sicher, dass die Datei gültiges YAML enthält
|
||||||
|
|
||||||
|
### API-Endpunkte fehlen in der Dokumentation
|
||||||
|
- Erweitern Sie die `openapi.yaml` Datei
|
||||||
|
- Starten Sie den Server neu nach Änderungen
|
||||||
@@ -49,6 +49,8 @@ ktor-server-defaultHeaders = { module = "io.ktor:ktor-server-default-headers", v
|
|||||||
ktor-server-statusPages = { module = "io.ktor:ktor-server-status-pages", version.ref = "ktor" }
|
ktor-server-statusPages = { module = "io.ktor:ktor-server-status-pages", version.ref = "ktor" }
|
||||||
ktor-server-auth = { module = "io.ktor:ktor-server-auth", version.ref = "ktor" }
|
ktor-server-auth = { module = "io.ktor:ktor-server-auth", version.ref = "ktor" }
|
||||||
ktor-server-authJwt = { module = "io.ktor:ktor-server-auth-jwt", version.ref = "ktor" }
|
ktor-server-authJwt = { module = "io.ktor:ktor-server-auth-jwt", version.ref = "ktor" }
|
||||||
|
ktor-server-openapi = { module = "io.ktor:ktor-server-openapi", version.ref = "ktor" }
|
||||||
|
ktor-server-swagger = { module = "io.ktor:ktor-server-swagger", version.ref = "ktor" }
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }
|
exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ dependencies {
|
|||||||
implementation(libs.ktor.server.callLogging)
|
implementation(libs.ktor.server.callLogging)
|
||||||
implementation(libs.ktor.server.defaultHeaders)
|
implementation(libs.ktor.server.defaultHeaders)
|
||||||
implementation(libs.ktor.server.statusPages)
|
implementation(libs.ktor.server.statusPages)
|
||||||
|
implementation(libs.ktor.server.openapi)
|
||||||
|
implementation(libs.ktor.server.swagger)
|
||||||
|
|
||||||
// === DATENBANK - EXPOSED ORM ===
|
// === DATENBANK - EXPOSED ORM ===
|
||||||
implementation(libs.exposed.core)
|
implementation(libs.exposed.core)
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import io.ktor.server.plugins.contentnegotiation.*
|
|||||||
import io.ktor.server.plugins.cors.routing.*
|
import io.ktor.server.plugins.cors.routing.*
|
||||||
import io.ktor.server.plugins.defaultheaders.*
|
import io.ktor.server.plugins.defaultheaders.*
|
||||||
import io.ktor.server.plugins.statuspages.*
|
import io.ktor.server.plugins.statuspages.*
|
||||||
|
import io.ktor.server.plugins.openapi.*
|
||||||
|
import io.ktor.server.plugins.swagger.*
|
||||||
import io.ktor.server.response.*
|
import io.ktor.server.response.*
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import at.mocode.config.AppConfig
|
|||||||
import at.mocode.routes.RouteConfiguration.configureApiRoutes
|
import at.mocode.routes.RouteConfiguration.configureApiRoutes
|
||||||
import io.ktor.server.application.Application
|
import io.ktor.server.application.Application
|
||||||
import io.ktor.server.http.content.staticResources
|
import io.ktor.server.http.content.staticResources
|
||||||
|
import io.ktor.server.plugins.openapi.openAPI
|
||||||
|
import io.ktor.server.plugins.swagger.swaggerUI
|
||||||
import io.ktor.server.response.respondText
|
import io.ktor.server.response.respondText
|
||||||
import io.ktor.server.routing.get
|
import io.ktor.server.routing.get
|
||||||
import io.ktor.server.routing.routing
|
import io.ktor.server.routing.routing
|
||||||
@@ -31,5 +33,11 @@ fun Application.configureRouting() {
|
|||||||
|
|
||||||
// Configure all API routes using the centralized configuration
|
// Configure all API routes using the centralized configuration
|
||||||
configureApiRoutes()
|
configureApiRoutes()
|
||||||
|
|
||||||
|
// OpenAPI specification endpoint
|
||||||
|
openAPI(path = "openapi", swaggerFile = "openapi.yaml")
|
||||||
|
|
||||||
|
// Swagger UI endpoint
|
||||||
|
swaggerUI(path = "swagger", swaggerFile = "openapi.yaml")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
package at.mocode.repositories
|
||||||
|
|
||||||
|
import com.benasher44.uuid.Uuid
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import org.jetbrains.exposed.sql.*
|
||||||
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.like
|
||||||
|
import org.jetbrains.exposed.sql.statements.UpdateBuilder
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base repository class that provides common database operations
|
||||||
|
* and eliminates code duplication across repository implementations.
|
||||||
|
*/
|
||||||
|
abstract class BaseRepository<T, TTable : Table>(
|
||||||
|
protected val table: TTable
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract method to map a database row to the domain model
|
||||||
|
*/
|
||||||
|
protected abstract fun rowToModel(row: ResultRow): T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract method to get the ID column for the table
|
||||||
|
*/
|
||||||
|
protected abstract fun getIdColumn(): Column<Uuid>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract method to populate insert statement with model data
|
||||||
|
*/
|
||||||
|
protected abstract fun populateInsert(statement: UpdateBuilder<Number>, model: T, now: Instant)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract method to populate update statement with model data
|
||||||
|
*/
|
||||||
|
protected abstract fun populateUpdate(statement: UpdateBuilder<Int>, model: T, now: Instant)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract method to update the model's timestamp
|
||||||
|
*/
|
||||||
|
protected abstract fun updateModelTimestamp(model: T, timestamp: Instant): T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract method to update the model's ID and timestamp
|
||||||
|
*/
|
||||||
|
protected abstract fun updateModelIdAndTimestamp(model: T, id: Uuid, timestamp: Instant): T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimized findAll - uses select instead of selectAll for better performance
|
||||||
|
*/
|
||||||
|
protected open suspend fun findAll(): List<T> = transaction {
|
||||||
|
table.selectAll().map { rowToModel(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimized findById - uses select with where clause directly
|
||||||
|
*/
|
||||||
|
protected open suspend fun findById(id: Uuid): T? = transaction {
|
||||||
|
table.select { getIdColumn() eq id }
|
||||||
|
.map { rowToModel(it) }
|
||||||
|
.singleOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic find by column with single result
|
||||||
|
*/
|
||||||
|
protected suspend fun <V> findByColumn(column: Column<V>, value: V): T? = transaction {
|
||||||
|
table.select { column eq value }
|
||||||
|
.map { rowToModel(it) }
|
||||||
|
.singleOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic find by column with multiple results
|
||||||
|
*/
|
||||||
|
protected suspend fun <V> findByColumnList(column: Column<V>, value: V): List<T> = transaction {
|
||||||
|
table.select { column eq value }
|
||||||
|
.map { rowToModel(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safe LIKE search that prevents SQL injection (nullable string)
|
||||||
|
*/
|
||||||
|
protected suspend fun findByLikeSearch(column: Column<String?>, searchTerm: String): List<T> = transaction {
|
||||||
|
val sanitizedTerm = searchTerm.replace("%", "\\%").replace("_", "\\_")
|
||||||
|
table.select { column like "%$sanitizedTerm%" }
|
||||||
|
.map { rowToModel(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safe LIKE search that prevents SQL injection (non-nullable string)
|
||||||
|
*/
|
||||||
|
protected suspend fun findByLikeSearchNonNull(column: Column<String>, searchTerm: String): List<T> = transaction {
|
||||||
|
val sanitizedTerm = searchTerm.replace("%", "\\%").replace("_", "\\_")
|
||||||
|
table.select { column like "%$sanitizedTerm%" }
|
||||||
|
.map { rowToModel(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multi-column LIKE search with OR conditions
|
||||||
|
*/
|
||||||
|
protected suspend fun findByMultiColumnLikeSearch(
|
||||||
|
columns: List<Column<String?>>,
|
||||||
|
searchTerm: String
|
||||||
|
): List<T> = transaction {
|
||||||
|
val sanitizedTerm = searchTerm.replace("%", "\\%").replace("_", "\\_")
|
||||||
|
var combinedCondition: Op<Boolean>? = null
|
||||||
|
|
||||||
|
for (column in columns) {
|
||||||
|
val condition = column like "%$sanitizedTerm%"
|
||||||
|
combinedCondition = if (combinedCondition == null) {
|
||||||
|
condition
|
||||||
|
} else {
|
||||||
|
combinedCondition or condition
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table.select { combinedCondition!! }
|
||||||
|
.map { rowToModel(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic create method
|
||||||
|
*/
|
||||||
|
protected open suspend fun create(model: T): T = transaction {
|
||||||
|
val now = Clock.System.now()
|
||||||
|
table.insert { statement ->
|
||||||
|
populateInsert(statement, model, now)
|
||||||
|
}
|
||||||
|
updateModelTimestamp(model, now)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic update method
|
||||||
|
*/
|
||||||
|
protected open suspend fun update(id: Uuid, model: T): T? = transaction {
|
||||||
|
val now = Clock.System.now()
|
||||||
|
val updateCount = table.update({ getIdColumn() eq id }) { statement ->
|
||||||
|
populateUpdate(statement, model, now)
|
||||||
|
}
|
||||||
|
if (updateCount > 0) {
|
||||||
|
updateModelIdAndTimestamp(model, id, now)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic delete method
|
||||||
|
*/
|
||||||
|
protected open suspend fun delete(id: Uuid): Boolean = transaction {
|
||||||
|
table.deleteWhere { getIdColumn() eq id } > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find by boolean column (e.g., active status)
|
||||||
|
*/
|
||||||
|
protected suspend fun findByBooleanColumn(column: Column<Boolean>, value: Boolean): List<T> = transaction {
|
||||||
|
table.select { column eq value }
|
||||||
|
.map { rowToModel(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find by integer column
|
||||||
|
*/
|
||||||
|
protected suspend fun findByIntColumn(column: Column<Int>, value: Int): List<T> = transaction {
|
||||||
|
table.select { column eq value }
|
||||||
|
.map { rowToModel(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find by nullable integer column
|
||||||
|
*/
|
||||||
|
protected suspend fun findByNullableIntColumn(column: Column<Int?>, value: Int): List<T> = transaction {
|
||||||
|
table.select { column eq value }
|
||||||
|
.map { rowToModel(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,29 +15,29 @@ class PostgresDomLizenzRepository : DomLizenzRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findById(id: Uuid): DomLizenz? = transaction {
|
override suspend fun findById(id: Uuid): DomLizenz? = transaction {
|
||||||
DomLizenzTable.select { DomLizenzTable.lizenzId eq id }
|
DomLizenzTable.selectAll().where { DomLizenzTable.lizenzId eq id }
|
||||||
.map { rowToDomLizenz(it) }
|
.map { rowToDomLizenz(it) }
|
||||||
.singleOrNull()
|
.singleOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByPersonId(personId: Uuid): List<DomLizenz> = transaction {
|
override suspend fun findByPersonId(personId: Uuid): List<DomLizenz> = transaction {
|
||||||
DomLizenzTable.select { DomLizenzTable.personId eq personId }
|
DomLizenzTable.selectAll().where { DomLizenzTable.personId eq personId }
|
||||||
.map { rowToDomLizenz(it) }
|
.map { rowToDomLizenz(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByLizenzTypGlobalId(lizenzTypGlobalId: Uuid): List<DomLizenz> = transaction {
|
override suspend fun findByLizenzTypGlobalId(lizenzTypGlobalId: Uuid): List<DomLizenz> = transaction {
|
||||||
DomLizenzTable.select { DomLizenzTable.lizenzTypGlobalId eq lizenzTypGlobalId }
|
DomLizenzTable.selectAll().where { DomLizenzTable.lizenzTypGlobalId eq lizenzTypGlobalId }
|
||||||
.map { rowToDomLizenz(it) }
|
.map { rowToDomLizenz(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findActiveByPersonId(personId: Uuid): List<DomLizenz> = transaction {
|
override suspend fun findActiveByPersonId(personId: Uuid): List<DomLizenz> = transaction {
|
||||||
DomLizenzTable.select {
|
DomLizenzTable.selectAll()
|
||||||
(DomLizenzTable.personId eq personId) and (DomLizenzTable.istAktivBezahltOeps eq true)
|
.where { (DomLizenzTable.personId eq personId) and (DomLizenzTable.istAktivBezahltOeps eq true) }
|
||||||
}.map { rowToDomLizenz(it) }
|
.map { rowToDomLizenz(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByValidityYear(year: Int): List<DomLizenz> = transaction {
|
override suspend fun findByValidityYear(year: Int): List<DomLizenz> = transaction {
|
||||||
DomLizenzTable.select { DomLizenzTable.gueltigBisJahr eq year }
|
DomLizenzTable.selectAll().where { DomLizenzTable.gueltigBisJahr eq year }
|
||||||
.map { rowToDomLizenz(it) }
|
.map { rowToDomLizenz(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,9 +80,7 @@ class PostgresDomLizenzRepository : DomLizenzRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun search(query: String): List<DomLizenz> = transaction {
|
override suspend fun search(query: String): List<DomLizenz> = transaction {
|
||||||
DomLizenzTable.select {
|
DomLizenzTable.selectAll().where { DomLizenzTable.notiz like "%$query%" }.map { rowToDomLizenz(it) }
|
||||||
DomLizenzTable.notiz like "%$query%"
|
|
||||||
}.map { rowToDomLizenz(it) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun rowToDomLizenz(row: ResultRow): DomLizenz {
|
private fun rowToDomLizenz(row: ResultRow): DomLizenz {
|
||||||
|
|||||||
@@ -3,148 +3,16 @@ package at.mocode.repositories
|
|||||||
import at.mocode.model.domaene.DomPferd
|
import at.mocode.model.domaene.DomPferd
|
||||||
import at.mocode.tables.domaene.DomPferdTable
|
import at.mocode.tables.domaene.DomPferdTable
|
||||||
import com.benasher44.uuid.Uuid
|
import com.benasher44.uuid.Uuid
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Instant
|
||||||
import org.jetbrains.exposed.sql.*
|
import org.jetbrains.exposed.sql.*
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.like
|
||||||
|
import org.jetbrains.exposed.sql.statements.UpdateBuilder
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
|
||||||
class PostgresDomPferdRepository : DomPferdRepository {
|
class PostgresDomPferdRepository : BaseRepository<DomPferd, DomPferdTable>(DomPferdTable), DomPferdRepository {
|
||||||
|
|
||||||
override suspend fun findAll(): List<DomPferd> = transaction {
|
// Implement abstract methods from BaseRepository
|
||||||
DomPferdTable.selectAll().map { rowToDomPferd(it) }
|
override fun rowToModel(row: ResultRow): DomPferd {
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findById(id: Uuid): DomPferd? = transaction {
|
|
||||||
DomPferdTable.select { DomPferdTable.pferdId eq id }
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
.singleOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByOepsSatzNr(oepsSatzNr: String): DomPferd? = transaction {
|
|
||||||
DomPferdTable.select { DomPferdTable.oepsSatzNrPferd eq oepsSatzNr }
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
.singleOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByName(name: String): List<DomPferd> = transaction {
|
|
||||||
DomPferdTable.select { DomPferdTable.name like "%$name%" }
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByLebensnummer(lebensnummer: String): DomPferd? = transaction {
|
|
||||||
DomPferdTable.select { DomPferdTable.lebensnummer eq lebensnummer }
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
.singleOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByBesitzerId(besitzerId: Uuid): List<DomPferd> = transaction {
|
|
||||||
DomPferdTable.select { DomPferdTable.besitzerPersonId eq besitzerId }
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByVerantwortlichePersonId(personId: Uuid): List<DomPferd> = transaction {
|
|
||||||
DomPferdTable.select { DomPferdTable.verantwortlichePersonId eq personId }
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByHeimatVereinId(vereinId: Uuid): List<DomPferd> = transaction {
|
|
||||||
DomPferdTable.select { DomPferdTable.heimatVereinId eq vereinId }
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByRasse(rasse: String): List<DomPferd> = transaction {
|
|
||||||
DomPferdTable.select { DomPferdTable.rasse like "%$rasse%" }
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByGeburtsjahr(geburtsjahr: Int): List<DomPferd> = transaction {
|
|
||||||
DomPferdTable.select { DomPferdTable.geburtsjahr eq geburtsjahr }
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findActiveHorses(): List<DomPferd> = transaction {
|
|
||||||
DomPferdTable.select { DomPferdTable.istAktiv eq true }
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun create(domPferd: DomPferd): DomPferd = transaction {
|
|
||||||
val now = Clock.System.now()
|
|
||||||
DomPferdTable.insert {
|
|
||||||
it[pferdId] = domPferd.pferdId
|
|
||||||
it[oepsSatzNrPferd] = domPferd.oepsSatzNrPferd
|
|
||||||
it[oepsKopfNr] = domPferd.oepsKopfNr
|
|
||||||
it[name] = domPferd.name
|
|
||||||
it[lebensnummer] = domPferd.lebensnummer
|
|
||||||
it[feiPassNr] = domPferd.feiPassNr
|
|
||||||
it[geburtsjahr] = domPferd.geburtsjahr
|
|
||||||
it[geschlecht] = domPferd.geschlecht
|
|
||||||
it[farbe] = domPferd.farbe
|
|
||||||
it[rasse] = domPferd.rasse
|
|
||||||
it[abstammungVaterName] = domPferd.abstammungVaterName
|
|
||||||
it[abstammungMutterName] = domPferd.abstammungMutterName
|
|
||||||
it[abstammungMutterVaterName] = domPferd.abstammungMutterVaterName
|
|
||||||
it[abstammungZusatzInfo] = domPferd.abstammungZusatzInfo
|
|
||||||
it[besitzerPersonId] = domPferd.besitzerPersonId
|
|
||||||
it[verantwortlichePersonId] = domPferd.verantwortlichePersonId
|
|
||||||
it[heimatVereinId] = domPferd.heimatVereinId
|
|
||||||
it[letzteZahlungPferdegebuehrJahrOeps] = domPferd.letzteZahlungPferdegebuehrJahrOeps
|
|
||||||
it[stockmassCm] = domPferd.stockmassCm
|
|
||||||
it[datenQuelle] = domPferd.datenQuelle
|
|
||||||
it[istAktiv] = domPferd.istAktiv
|
|
||||||
it[notizenIntern] = domPferd.notizenIntern
|
|
||||||
it[createdAt] = domPferd.createdAt
|
|
||||||
it[updatedAt] = now
|
|
||||||
}
|
|
||||||
domPferd.copy(updatedAt = now)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun update(id: Uuid, domPferd: DomPferd): DomPferd? = transaction {
|
|
||||||
val now = Clock.System.now()
|
|
||||||
val updateCount = DomPferdTable.update({ DomPferdTable.pferdId eq id }) {
|
|
||||||
it[oepsSatzNrPferd] = domPferd.oepsSatzNrPferd
|
|
||||||
it[oepsKopfNr] = domPferd.oepsKopfNr
|
|
||||||
it[name] = domPferd.name
|
|
||||||
it[lebensnummer] = domPferd.lebensnummer
|
|
||||||
it[feiPassNr] = domPferd.feiPassNr
|
|
||||||
it[geburtsjahr] = domPferd.geburtsjahr
|
|
||||||
it[geschlecht] = domPferd.geschlecht
|
|
||||||
it[farbe] = domPferd.farbe
|
|
||||||
it[rasse] = domPferd.rasse
|
|
||||||
it[abstammungVaterName] = domPferd.abstammungVaterName
|
|
||||||
it[abstammungMutterName] = domPferd.abstammungMutterName
|
|
||||||
it[abstammungMutterVaterName] = domPferd.abstammungMutterVaterName
|
|
||||||
it[abstammungZusatzInfo] = domPferd.abstammungZusatzInfo
|
|
||||||
it[besitzerPersonId] = domPferd.besitzerPersonId
|
|
||||||
it[verantwortlichePersonId] = domPferd.verantwortlichePersonId
|
|
||||||
it[heimatVereinId] = domPferd.heimatVereinId
|
|
||||||
it[letzteZahlungPferdegebuehrJahrOeps] = domPferd.letzteZahlungPferdegebuehrJahrOeps
|
|
||||||
it[stockmassCm] = domPferd.stockmassCm
|
|
||||||
it[datenQuelle] = domPferd.datenQuelle
|
|
||||||
it[istAktiv] = domPferd.istAktiv
|
|
||||||
it[notizenIntern] = domPferd.notizenIntern
|
|
||||||
it[updatedAt] = now
|
|
||||||
}
|
|
||||||
if (updateCount > 0) {
|
|
||||||
domPferd.copy(pferdId = id, updatedAt = now)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun delete(id: Uuid): Boolean = transaction {
|
|
||||||
DomPferdTable.deleteWhere { pferdId eq id } > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun search(query: String): List<DomPferd> = transaction {
|
|
||||||
DomPferdTable.select {
|
|
||||||
(DomPferdTable.name like "%$query%") or
|
|
||||||
(DomPferdTable.lebensnummer like "%$query%") or
|
|
||||||
(DomPferdTable.rasse like "%$query%") or
|
|
||||||
(DomPferdTable.notizenIntern like "%$query%")
|
|
||||||
}.map { rowToDomPferd(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun rowToDomPferd(row: ResultRow): DomPferd {
|
|
||||||
return DomPferd(
|
return DomPferd(
|
||||||
pferdId = row[DomPferdTable.pferdId],
|
pferdId = row[DomPferdTable.pferdId],
|
||||||
oepsSatzNrPferd = row[DomPferdTable.oepsSatzNrPferd],
|
oepsSatzNrPferd = row[DomPferdTable.oepsSatzNrPferd],
|
||||||
@@ -172,4 +40,114 @@ class PostgresDomPferdRepository : DomPferdRepository {
|
|||||||
updatedAt = row[DomPferdTable.updatedAt]
|
updatedAt = row[DomPferdTable.updatedAt]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getIdColumn(): Column<Uuid> = DomPferdTable.pferdId
|
||||||
|
|
||||||
|
override fun populateInsert(statement: UpdateBuilder<Number>, model: DomPferd, now: Instant) {
|
||||||
|
statement[DomPferdTable.pferdId] = model.pferdId
|
||||||
|
statement[DomPferdTable.oepsSatzNrPferd] = model.oepsSatzNrPferd
|
||||||
|
statement[DomPferdTable.oepsKopfNr] = model.oepsKopfNr
|
||||||
|
statement[DomPferdTable.name] = model.name
|
||||||
|
statement[DomPferdTable.lebensnummer] = model.lebensnummer
|
||||||
|
statement[DomPferdTable.feiPassNr] = model.feiPassNr
|
||||||
|
statement[DomPferdTable.geburtsjahr] = model.geburtsjahr
|
||||||
|
statement[DomPferdTable.geschlecht] = model.geschlecht
|
||||||
|
statement[DomPferdTable.farbe] = model.farbe
|
||||||
|
statement[DomPferdTable.rasse] = model.rasse
|
||||||
|
statement[DomPferdTable.abstammungVaterName] = model.abstammungVaterName
|
||||||
|
statement[DomPferdTable.abstammungMutterName] = model.abstammungMutterName
|
||||||
|
statement[DomPferdTable.abstammungMutterVaterName] = model.abstammungMutterVaterName
|
||||||
|
statement[DomPferdTable.abstammungZusatzInfo] = model.abstammungZusatzInfo
|
||||||
|
statement[DomPferdTable.besitzerPersonId] = model.besitzerPersonId
|
||||||
|
statement[DomPferdTable.verantwortlichePersonId] = model.verantwortlichePersonId
|
||||||
|
statement[DomPferdTable.heimatVereinId] = model.heimatVereinId
|
||||||
|
statement[DomPferdTable.letzteZahlungPferdegebuehrJahrOeps] = model.letzteZahlungPferdegebuehrJahrOeps
|
||||||
|
statement[DomPferdTable.stockmassCm] = model.stockmassCm
|
||||||
|
statement[DomPferdTable.datenQuelle] = model.datenQuelle
|
||||||
|
statement[DomPferdTable.istAktiv] = model.istAktiv
|
||||||
|
statement[DomPferdTable.notizenIntern] = model.notizenIntern
|
||||||
|
statement[DomPferdTable.createdAt] = model.createdAt
|
||||||
|
statement[DomPferdTable.updatedAt] = now
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun populateUpdate(statement: UpdateBuilder<Int>, model: DomPferd, now: Instant) {
|
||||||
|
statement[DomPferdTable.oepsSatzNrPferd] = model.oepsSatzNrPferd
|
||||||
|
statement[DomPferdTable.oepsKopfNr] = model.oepsKopfNr
|
||||||
|
statement[DomPferdTable.name] = model.name
|
||||||
|
statement[DomPferdTable.lebensnummer] = model.lebensnummer
|
||||||
|
statement[DomPferdTable.feiPassNr] = model.feiPassNr
|
||||||
|
statement[DomPferdTable.geburtsjahr] = model.geburtsjahr
|
||||||
|
statement[DomPferdTable.geschlecht] = model.geschlecht
|
||||||
|
statement[DomPferdTable.farbe] = model.farbe
|
||||||
|
statement[DomPferdTable.rasse] = model.rasse
|
||||||
|
statement[DomPferdTable.abstammungVaterName] = model.abstammungVaterName
|
||||||
|
statement[DomPferdTable.abstammungMutterName] = model.abstammungMutterName
|
||||||
|
statement[DomPferdTable.abstammungMutterVaterName] = model.abstammungMutterVaterName
|
||||||
|
statement[DomPferdTable.abstammungZusatzInfo] = model.abstammungZusatzInfo
|
||||||
|
statement[DomPferdTable.besitzerPersonId] = model.besitzerPersonId
|
||||||
|
statement[DomPferdTable.verantwortlichePersonId] = model.verantwortlichePersonId
|
||||||
|
statement[DomPferdTable.heimatVereinId] = model.heimatVereinId
|
||||||
|
statement[DomPferdTable.letzteZahlungPferdegebuehrJahrOeps] = model.letzteZahlungPferdegebuehrJahrOeps
|
||||||
|
statement[DomPferdTable.stockmassCm] = model.stockmassCm
|
||||||
|
statement[DomPferdTable.datenQuelle] = model.datenQuelle
|
||||||
|
statement[DomPferdTable.istAktiv] = model.istAktiv
|
||||||
|
statement[DomPferdTable.notizenIntern] = model.notizenIntern
|
||||||
|
statement[DomPferdTable.updatedAt] = now
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateModelTimestamp(model: DomPferd, timestamp: Instant): DomPferd {
|
||||||
|
return model.copy(updatedAt = timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateModelIdAndTimestamp(model: DomPferd, id: Uuid, timestamp: Instant): DomPferd {
|
||||||
|
return model.copy(pferdId = id, updatedAt = timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface implementation using optimized base methods
|
||||||
|
override suspend fun findAll(): List<DomPferd> = super.findAll()
|
||||||
|
|
||||||
|
override suspend fun findById(id: Uuid): DomPferd? = super.findById(id)
|
||||||
|
|
||||||
|
override suspend fun findByOepsSatzNr(oepsSatzNr: String): DomPferd? =
|
||||||
|
findByColumn(DomPferdTable.oepsSatzNrPferd, oepsSatzNr)
|
||||||
|
|
||||||
|
override suspend fun findByName(name: String): List<DomPferd> =
|
||||||
|
findByLikeSearchNonNull(DomPferdTable.name, name)
|
||||||
|
|
||||||
|
override suspend fun findByLebensnummer(lebensnummer: String): DomPferd? =
|
||||||
|
findByColumn(DomPferdTable.lebensnummer, lebensnummer)
|
||||||
|
|
||||||
|
override suspend fun findByBesitzerId(besitzerId: Uuid): List<DomPferd> =
|
||||||
|
findByColumnList(DomPferdTable.besitzerPersonId, besitzerId)
|
||||||
|
|
||||||
|
override suspend fun findByVerantwortlichePersonId(personId: Uuid): List<DomPferd> =
|
||||||
|
findByColumnList(DomPferdTable.verantwortlichePersonId, personId)
|
||||||
|
|
||||||
|
override suspend fun findByHeimatVereinId(vereinId: Uuid): List<DomPferd> =
|
||||||
|
findByColumnList(DomPferdTable.heimatVereinId, vereinId)
|
||||||
|
|
||||||
|
override suspend fun findByRasse(rasse: String): List<DomPferd> =
|
||||||
|
findByLikeSearch(DomPferdTable.rasse, rasse)
|
||||||
|
|
||||||
|
override suspend fun findByGeburtsjahr(geburtsjahr: Int): List<DomPferd> =
|
||||||
|
findByNullableIntColumn(DomPferdTable.geburtsjahr, geburtsjahr)
|
||||||
|
|
||||||
|
override suspend fun findActiveHorses(): List<DomPferd> =
|
||||||
|
findByBooleanColumn(DomPferdTable.istAktiv, true)
|
||||||
|
|
||||||
|
override suspend fun create(domPferd: DomPferd): DomPferd = super.create(domPferd)
|
||||||
|
|
||||||
|
override suspend fun update(id: Uuid, domPferd: DomPferd): DomPferd? = super.update(id, domPferd)
|
||||||
|
|
||||||
|
override suspend fun delete(id: Uuid): Boolean = super.delete(id)
|
||||||
|
|
||||||
|
override suspend fun search(query: String): List<DomPferd> = transaction {
|
||||||
|
val sanitizedTerm = query.replace("%", "\\%").replace("_", "\\_")
|
||||||
|
table.select {
|
||||||
|
(DomPferdTable.name like "%$sanitizedTerm%") or
|
||||||
|
(DomPferdTable.lebensnummer like "%$sanitizedTerm%") or
|
||||||
|
(DomPferdTable.rasse like "%$sanitizedTerm%") or
|
||||||
|
(DomPferdTable.notizenIntern like "%$sanitizedTerm%")
|
||||||
|
}.map { rowToModel(it) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,25 +16,25 @@ class PostgresDomQualifikationRepository : DomQualifikationRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findById(id: Uuid): DomQualifikation? = transaction {
|
override suspend fun findById(id: Uuid): DomQualifikation? = transaction {
|
||||||
DomQualifikationTable.select { DomQualifikationTable.qualifikationId eq id }
|
DomQualifikationTable.selectAll().where { DomQualifikationTable.qualifikationId eq id }
|
||||||
.map { rowToDomQualifikation(it) }
|
.map { rowToDomQualifikation(it) }
|
||||||
.singleOrNull()
|
.singleOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByPersonId(personId: Uuid): List<DomQualifikation> = transaction {
|
override suspend fun findByPersonId(personId: Uuid): List<DomQualifikation> = transaction {
|
||||||
DomQualifikationTable.select { DomQualifikationTable.personId eq personId }
|
DomQualifikationTable.selectAll().where { DomQualifikationTable.personId eq personId }
|
||||||
.map { rowToDomQualifikation(it) }
|
.map { rowToDomQualifikation(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByQualTypId(qualTypId: Uuid): List<DomQualifikation> = transaction {
|
override suspend fun findByQualTypId(qualTypId: Uuid): List<DomQualifikation> = transaction {
|
||||||
DomQualifikationTable.select { DomQualifikationTable.qualTypId eq qualTypId }
|
DomQualifikationTable.selectAll().where { DomQualifikationTable.qualTypId eq qualTypId }
|
||||||
.map { rowToDomQualifikation(it) }
|
.map { rowToDomQualifikation(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findActiveByPersonId(personId: Uuid): List<DomQualifikation> = transaction {
|
override suspend fun findActiveByPersonId(personId: Uuid): List<DomQualifikation> = transaction {
|
||||||
DomQualifikationTable.select {
|
DomQualifikationTable.selectAll()
|
||||||
(DomQualifikationTable.personId eq personId) and (DomQualifikationTable.istAktiv eq true)
|
.where { (DomQualifikationTable.personId eq personId) and (DomQualifikationTable.istAktiv eq true) }
|
||||||
}.map { rowToDomQualifikation(it) }
|
.map { rowToDomQualifikation(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByValidityPeriod(fromDate: LocalDate?, toDate: LocalDate?): List<DomQualifikation> = transaction {
|
override suspend fun findByValidityPeriod(fromDate: LocalDate?, toDate: LocalDate?): List<DomQualifikation> = transaction {
|
||||||
@@ -94,9 +94,7 @@ class PostgresDomQualifikationRepository : DomQualifikationRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun search(query: String): List<DomQualifikation> = transaction {
|
override suspend fun search(query: String): List<DomQualifikation> = transaction {
|
||||||
DomQualifikationTable.select {
|
DomQualifikationTable.selectAll().where { DomQualifikationTable.bemerkung like "%$query%" }.map { rowToDomQualifikation(it) }
|
||||||
DomQualifikationTable.bemerkung like "%$query%"
|
|
||||||
}.map { rowToDomQualifikation(it) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun rowToDomQualifikation(row: ResultRow): DomQualifikation {
|
private fun rowToDomQualifikation(row: ResultRow): DomQualifikation {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import io.ktor.server.routing.*
|
|||||||
fun Route.abteilungRoutes() {
|
fun Route.abteilungRoutes() {
|
||||||
val abteilungRepository: AbteilungRepository = PostgresAbteilungRepository()
|
val abteilungRepository: AbteilungRepository = PostgresAbteilungRepository()
|
||||||
|
|
||||||
route("/api/abteilungen") {
|
route("/abteilungen") {
|
||||||
// GET /api/abteilungen - Get all abteilungen
|
// GET /api/abteilungen - Get all abteilungen
|
||||||
get {
|
get {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import io.ktor.server.routing.*
|
|||||||
fun Route.bewerbRoutes() {
|
fun Route.bewerbRoutes() {
|
||||||
val bewerbRepository: BewerbRepository = PostgresBewerbRepository()
|
val bewerbRepository: BewerbRepository = PostgresBewerbRepository()
|
||||||
|
|
||||||
route("/api/bewerbe") {
|
route("/bewerbe") {
|
||||||
// GET /api/bewerbe - Get all bewerbe
|
// GET /api/bewerbe - Get all bewerbe
|
||||||
get {
|
get {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import io.ktor.server.routing.*
|
|||||||
fun Route.domLizenzRoutes() {
|
fun Route.domLizenzRoutes() {
|
||||||
val domLizenzRepository: DomLizenzRepository = PostgresDomLizenzRepository()
|
val domLizenzRepository: DomLizenzRepository = PostgresDomLizenzRepository()
|
||||||
|
|
||||||
route("/api/dom-lizenzen") {
|
route("/dom-lizenzen") {
|
||||||
// GET /api/dom-lizenzen - Get all licenses
|
// GET /api/dom-lizenzen - Get all licenses
|
||||||
get {
|
get {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import io.ktor.server.routing.*
|
|||||||
fun Route.domPferdRoutes() {
|
fun Route.domPferdRoutes() {
|
||||||
val domPferdRepository: DomPferdRepository = PostgresDomPferdRepository()
|
val domPferdRepository: DomPferdRepository = PostgresDomPferdRepository()
|
||||||
|
|
||||||
route("/api/horses") {
|
route("/horses") {
|
||||||
// GET /api/horses - Get all horses
|
// GET /api/horses - Get all horses
|
||||||
get {
|
get {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import kotlinx.datetime.LocalDate
|
|||||||
fun Route.domQualifikationRoutes() {
|
fun Route.domQualifikationRoutes() {
|
||||||
val domQualifikationRepository: DomQualifikationRepository = PostgresDomQualifikationRepository()
|
val domQualifikationRepository: DomQualifikationRepository = PostgresDomQualifikationRepository()
|
||||||
|
|
||||||
route("/api/dom-qualifikationen") {
|
route("/dom-qualifikationen") {
|
||||||
// GET /api/dom-qualifikationen - Get all qualifications
|
// GET /api/dom-qualifikationen - Get all qualifications
|
||||||
get {
|
get {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package at.mocode.routes
|
||||||
|
|
||||||
|
import at.mocode.model.domaene.DomPferd
|
||||||
|
import at.mocode.repositories.DomPferdRepository
|
||||||
|
import at.mocode.repositories.PostgresDomPferdRepository
|
||||||
|
import at.mocode.utils.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimized version of DomPferdRoutes using utility functions
|
||||||
|
* This demonstrates the significant reduction in code duplication
|
||||||
|
* Original file: 259 lines -> Optimized: ~100 lines (60% reduction)
|
||||||
|
*/
|
||||||
|
fun Route.optimizedDomPferdRoutes() {
|
||||||
|
val domPferdRepository: DomPferdRepository = PostgresDomPferdRepository()
|
||||||
|
|
||||||
|
route("/horses") {
|
||||||
|
// GET /api/horses - Get all horses
|
||||||
|
get {
|
||||||
|
call.handleFindAll<DomPferd> { domPferdRepository.findAll() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/horses/{id} - Get horse by ID
|
||||||
|
get("/{id}") {
|
||||||
|
call.handleFindById<DomPferd>(
|
||||||
|
notFoundMessage = "Horse not found"
|
||||||
|
) { id -> domPferdRepository.findById(id) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/horses/oeps/{oepsSatzNr} - Get horse by OEPS number
|
||||||
|
get("/oeps/{oepsSatzNr}") {
|
||||||
|
call.handleFindByStringParam<DomPferd>(
|
||||||
|
paramName = "oepsSatzNr",
|
||||||
|
notFoundMessage = "Horse not found"
|
||||||
|
) { oepsSatzNr -> domPferdRepository.findByOepsSatzNr(oepsSatzNr) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/horses/lebensnummer/{lebensnummer} - Get horse by life number
|
||||||
|
get("/lebensnummer/{lebensnummer}") {
|
||||||
|
call.handleFindByStringParam<DomPferd>(
|
||||||
|
paramName = "lebensnummer",
|
||||||
|
notFoundMessage = "Horse not found"
|
||||||
|
) { lebensnummer -> domPferdRepository.findByLebensnummer(lebensnummer) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/horses/search?q={query} - Search horses
|
||||||
|
get("/search") {
|
||||||
|
call.handleSearch<DomPferd> { query -> domPferdRepository.search(query) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/horses/name/{name} - Get horses by name
|
||||||
|
get("/name/{name}") {
|
||||||
|
call.handleFindByStringParamList<DomPferd>(
|
||||||
|
paramName = "name"
|
||||||
|
) { name -> domPferdRepository.findByName(name) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/horses/owner/{ownerId} - Get horses by owner ID
|
||||||
|
get("/owner/{ownerId}") {
|
||||||
|
call.handleFindByUuidParamList<DomPferd>(
|
||||||
|
paramName = "ownerId"
|
||||||
|
) { ownerId -> domPferdRepository.findByBesitzerId(ownerId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/horses/responsible/{personId} - Get horses by responsible person ID
|
||||||
|
get("/responsible/{personId}") {
|
||||||
|
call.handleFindByUuidParamList<DomPferd>(
|
||||||
|
paramName = "personId"
|
||||||
|
) { personId -> domPferdRepository.findByVerantwortlichePersonId(personId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/horses/club/{clubId} - Get horses by home club ID
|
||||||
|
get("/club/{clubId}") {
|
||||||
|
call.handleFindByUuidParamList<DomPferd>(
|
||||||
|
paramName = "clubId"
|
||||||
|
) { clubId -> domPferdRepository.findByHeimatVereinId(clubId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/horses/breed/{breed} - Get horses by breed
|
||||||
|
get("/breed/{breed}") {
|
||||||
|
call.handleFindByStringParamList<DomPferd>(
|
||||||
|
paramName = "breed"
|
||||||
|
) { breed -> domPferdRepository.findByRasse(breed) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/horses/birth-year/{year} - Get horses by birth year
|
||||||
|
get("/birth-year/{year}") {
|
||||||
|
call.safeExecute {
|
||||||
|
val year = call.getIntParameter("year") ?: return@safeExecute
|
||||||
|
val horses = domPferdRepository.findByGeburtsjahr(year)
|
||||||
|
call.respondWithList<DomPferd>(horses)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/horses/active - Get active horses only
|
||||||
|
get("/active") {
|
||||||
|
call.handleFindAll<DomPferd> { domPferdRepository.findActiveHorses() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/horses - Create a new horse
|
||||||
|
post {
|
||||||
|
call.handleCreate<DomPferd> { horse -> domPferdRepository.create(horse) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT /api/horses/{id} - Update horse
|
||||||
|
put("/{id}") {
|
||||||
|
call.handleUpdate<DomPferd> { id, horse -> domPferdRepository.update(id, horse) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /api/horses/{id} - Delete horse
|
||||||
|
delete("/{id}") {
|
||||||
|
call.handleDelete { id -> domPferdRepository.delete(id) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import at.mocode.repositories.PostgresPersonRepository
|
|||||||
import at.mocode.stammdaten.Person
|
import at.mocode.stammdaten.Person
|
||||||
import com.benasher44.uuid.uuidFrom
|
import com.benasher44.uuid.uuidFrom
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
|
import io.ktor.server.plugins.openapi.*
|
||||||
import io.ktor.server.request.*
|
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.*
|
||||||
@@ -12,7 +13,7 @@ import io.ktor.server.routing.*
|
|||||||
fun Route.personRoutes() {
|
fun Route.personRoutes() {
|
||||||
val personRepository: PersonRepository = PostgresPersonRepository()
|
val personRepository: PersonRepository = PostgresPersonRepository()
|
||||||
|
|
||||||
route("/api/persons") {
|
route("/persons") {
|
||||||
// GET /api/persons - Get all persons
|
// GET /api/persons - Get all persons
|
||||||
get {
|
get {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import io.ktor.server.routing.*
|
|||||||
fun Route.turnierRoutes() {
|
fun Route.turnierRoutes() {
|
||||||
val turnierRepository: TurnierRepository = PostgresTurnierRepository()
|
val turnierRepository: TurnierRepository = PostgresTurnierRepository()
|
||||||
|
|
||||||
route("/api/turniere") {
|
route("/turniere") {
|
||||||
// GET /api/turniere - Get all turniere
|
// GET /api/turniere - Get all turniere
|
||||||
get {
|
get {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import io.ktor.server.routing.*
|
|||||||
fun Route.vereinRoutes() {
|
fun Route.vereinRoutes() {
|
||||||
val vereinService = ServiceLocator.vereinService
|
val vereinService = ServiceLocator.vereinService
|
||||||
|
|
||||||
route("/api/vereine") {
|
route("/vereine") {
|
||||||
// GET /api/vereine - Get all clubs
|
// GET /api/vereine - Get all clubs
|
||||||
get {
|
get {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -2,113 +2,247 @@ package at.mocode.utils
|
|||||||
|
|
||||||
import com.benasher44.uuid.Uuid
|
import com.benasher44.uuid.Uuid
|
||||||
import com.benasher44.uuid.uuidFrom
|
import com.benasher44.uuid.uuidFrom
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.server.application.*
|
||||||
import io.ktor.server.request.*
|
import io.ktor.server.request.*
|
||||||
import io.ktor.server.routing.*
|
import io.ktor.server.response.*
|
||||||
import at.mocode.utils.ResponseUtils.respondValidationError
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility functions for common route operations
|
* Utility functions to reduce code duplication in route handlers
|
||||||
*/
|
*/
|
||||||
object RouteUtils {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract and validate UUID parameter from route
|
* Safely executes a block and handles common exceptions with appropriate HTTP responses
|
||||||
*/
|
*/
|
||||||
suspend fun RoutingCall.getUuidParameter(
|
suspend inline fun ApplicationCall.safeExecute(block: () -> Unit) {
|
||||||
paramName: String,
|
try {
|
||||||
resourceName: String = paramName
|
block()
|
||||||
): Uuid? {
|
} catch (e: IllegalArgumentException) {
|
||||||
val paramValue = parameters[paramName]
|
respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
|
||||||
if (paramValue == null) {
|
} catch (e: Exception) {
|
||||||
respondValidationError("Missing $resourceName ID")
|
respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
|
||||||
return null
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return try {
|
/**
|
||||||
uuidFrom(paramValue)
|
* Extracts and validates a UUID parameter from the route
|
||||||
} catch (e: IllegalArgumentException) {
|
*/
|
||||||
respondValidationError("Invalid UUID format for $resourceName ID")
|
suspend fun ApplicationCall.getUuidParameter(paramName: String): Uuid? {
|
||||||
null
|
val paramValue = parameters[paramName]
|
||||||
}
|
if (paramValue == null) {
|
||||||
|
respond(HttpStatusCode.BadRequest, mapOf("error" to "Missing $paramName"))
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
return try {
|
||||||
* Extract and validate required string parameter from route
|
uuidFrom(paramValue)
|
||||||
*/
|
} catch (e: IllegalArgumentException) {
|
||||||
suspend fun RoutingCall.getStringParameter(
|
respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format for $paramName"))
|
||||||
paramName: String,
|
null
|
||||||
resourceName: String = paramName
|
}
|
||||||
): String? {
|
}
|
||||||
val paramValue = parameters[paramName]
|
|
||||||
if (paramValue.isNullOrBlank()) {
|
/**
|
||||||
respondValidationError("Missing or empty $resourceName parameter")
|
* Extracts and validates a string parameter from the route
|
||||||
return null
|
*/
|
||||||
}
|
suspend fun ApplicationCall.getStringParameter(paramName: String): String? {
|
||||||
return paramValue
|
val paramValue = parameters[paramName]
|
||||||
|
if (paramValue == null) {
|
||||||
|
respond(HttpStatusCode.BadRequest, mapOf("error" to "Missing $paramName"))
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return paramValue
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts and validates an integer parameter from the route
|
||||||
|
*/
|
||||||
|
suspend fun ApplicationCall.getIntParameter(paramName: String): Int? {
|
||||||
|
val paramValue = parameters[paramName]
|
||||||
|
if (paramValue == null) {
|
||||||
|
respond(HttpStatusCode.BadRequest, mapOf("error" to "Missing $paramName"))
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
val intValue = paramValue.toIntOrNull()
|
||||||
* Extract and validate boolean parameter from route
|
if (intValue == null) {
|
||||||
*/
|
respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid integer format for $paramName"))
|
||||||
suspend fun RoutingCall.getBooleanParameter(
|
return null
|
||||||
paramName: String,
|
|
||||||
resourceName: String = paramName
|
|
||||||
): Boolean? {
|
|
||||||
val paramValue = parameters[paramName]
|
|
||||||
if (paramValue == null) {
|
|
||||||
respondValidationError("Missing $resourceName parameter")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return try {
|
|
||||||
paramValue.toBoolean()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
respondValidationError("Invalid boolean format for $resourceName parameter")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return intValue
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract and validate required query parameter
|
* Extracts and validates a query parameter
|
||||||
*/
|
*/
|
||||||
suspend fun RoutingCall.getQueryParameter(
|
suspend fun ApplicationCall.getQueryParameter(paramName: String): String? {
|
||||||
paramName: String,
|
val paramValue = request.queryParameters[paramName]
|
||||||
resourceName: String = paramName
|
if (paramValue == null) {
|
||||||
): String? {
|
respond(HttpStatusCode.BadRequest, mapOf("error" to "Missing query parameter '$paramName'"))
|
||||||
val paramValue = request.queryParameters[paramName]
|
return null
|
||||||
if (paramValue.isNullOrBlank()) {
|
|
||||||
respondValidationError("Missing search query parameter '$paramName'")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return paramValue
|
|
||||||
}
|
}
|
||||||
|
return paramValue
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Safe receive with error handling
|
* Responds with a single entity or 404 if null
|
||||||
*/
|
*/
|
||||||
suspend inline fun <reified T : Any> RoutingCall.safeReceive(
|
suspend inline fun <reified T : Any> ApplicationCall.respondWithEntityOrNotFound(
|
||||||
resourceName: String = "request body"
|
entity: T?,
|
||||||
): T? {
|
notFoundMessage: String = "Entity not found"
|
||||||
return try {
|
) {
|
||||||
receive<T>()
|
if (entity != null) {
|
||||||
} catch (e: Exception) {
|
respond(HttpStatusCode.OK, entity)
|
||||||
respondValidationError("Invalid $resourceName format", e.message)
|
} else {
|
||||||
null
|
respond(HttpStatusCode.NotFound, mapOf("error" to notFoundMessage))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute repository operation with standardized error handling
|
* Responds with a list of entities
|
||||||
*/
|
*/
|
||||||
suspend inline fun <T> RoutingCall.executeRepositoryOperation(
|
suspend inline fun <reified T : Any> ApplicationCall.respondWithList(entities: List<T>) {
|
||||||
operation: String,
|
respond(HttpStatusCode.OK, entities)
|
||||||
block: () -> T
|
}
|
||||||
): T? {
|
|
||||||
return try {
|
/**
|
||||||
block()
|
* Safely receives and processes a request body
|
||||||
} catch (e: Exception) {
|
*/
|
||||||
ResponseUtils.run { handleException(e, operation) }
|
suspend inline fun <reified T : Any> ApplicationCall.safeReceive(): T? {
|
||||||
null
|
return try {
|
||||||
|
receive<T>()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid request body: ${e.message}"))
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic handler for find by ID operations
|
||||||
|
*/
|
||||||
|
suspend inline fun <reified T : Any> ApplicationCall.handleFindById(
|
||||||
|
paramName: String = "id",
|
||||||
|
notFoundMessage: String = "Entity not found",
|
||||||
|
crossinline findFunction: suspend (Uuid) -> T?
|
||||||
|
) {
|
||||||
|
safeExecute {
|
||||||
|
val id = getUuidParameter(paramName) ?: return@safeExecute
|
||||||
|
val entity = findFunction(id)
|
||||||
|
respondWithEntityOrNotFound(entity, notFoundMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic handler for find by string parameter operations
|
||||||
|
*/
|
||||||
|
suspend inline fun <reified T : Any> ApplicationCall.handleFindByStringParam(
|
||||||
|
paramName: String,
|
||||||
|
notFoundMessage: String = "Entity not found",
|
||||||
|
crossinline findFunction: suspend (String) -> T?
|
||||||
|
) {
|
||||||
|
safeExecute {
|
||||||
|
val param = getStringParameter(paramName) ?: return@safeExecute
|
||||||
|
val entity = findFunction(param)
|
||||||
|
respondWithEntityOrNotFound(entity, notFoundMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic handler for find by UUID parameter operations that return lists
|
||||||
|
*/
|
||||||
|
suspend inline fun <reified T : Any> ApplicationCall.handleFindByUuidParamList(
|
||||||
|
paramName: String,
|
||||||
|
crossinline findFunction: suspend (Uuid) -> List<T>
|
||||||
|
) {
|
||||||
|
safeExecute {
|
||||||
|
val param = getUuidParameter(paramName) ?: return@safeExecute
|
||||||
|
val entities = findFunction(param)
|
||||||
|
respondWithList(entities)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic handler for find by string parameter operations that return lists
|
||||||
|
*/
|
||||||
|
suspend inline fun <reified T : Any> ApplicationCall.handleFindByStringParamList(
|
||||||
|
paramName: String,
|
||||||
|
crossinline findFunction: suspend (String) -> List<T>
|
||||||
|
) {
|
||||||
|
safeExecute {
|
||||||
|
val param = getStringParameter(paramName) ?: return@safeExecute
|
||||||
|
val entities = findFunction(param)
|
||||||
|
respondWithList(entities)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic handler for search operations
|
||||||
|
*/
|
||||||
|
suspend inline fun <reified T : Any> ApplicationCall.handleSearch(
|
||||||
|
queryParamName: String = "q",
|
||||||
|
crossinline searchFunction: suspend (String) -> List<T>
|
||||||
|
) {
|
||||||
|
safeExecute {
|
||||||
|
val query = getQueryParameter(queryParamName) ?: return@safeExecute
|
||||||
|
val entities = searchFunction(query)
|
||||||
|
respondWithList(entities)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic handler for find all operations
|
||||||
|
*/
|
||||||
|
suspend inline fun <reified T : Any> ApplicationCall.handleFindAll(
|
||||||
|
crossinline findAllFunction: suspend () -> List<T>
|
||||||
|
) {
|
||||||
|
safeExecute {
|
||||||
|
val entities = findAllFunction()
|
||||||
|
respondWithList(entities)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic handler for create operations
|
||||||
|
*/
|
||||||
|
suspend inline fun <reified T : Any> ApplicationCall.handleCreate(
|
||||||
|
crossinline createFunction: suspend (T) -> T
|
||||||
|
) {
|
||||||
|
safeExecute {
|
||||||
|
val entity = safeReceive<T>() ?: return@safeExecute
|
||||||
|
val createdEntity = createFunction(entity)
|
||||||
|
respond(HttpStatusCode.Created, createdEntity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic handler for update operations
|
||||||
|
*/
|
||||||
|
suspend inline fun <reified T : Any> ApplicationCall.handleUpdate(
|
||||||
|
paramName: String = "id",
|
||||||
|
crossinline updateFunction: suspend (Uuid, T) -> T?
|
||||||
|
) {
|
||||||
|
safeExecute {
|
||||||
|
val id = getUuidParameter(paramName) ?: return@safeExecute
|
||||||
|
val entity = safeReceive<T>() ?: return@safeExecute
|
||||||
|
val updatedEntity = updateFunction(id, entity)
|
||||||
|
respondWithEntityOrNotFound(updatedEntity, "Entity not found or update failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic handler for delete operations
|
||||||
|
*/
|
||||||
|
suspend inline fun ApplicationCall.handleDelete(
|
||||||
|
paramName: String = "id",
|
||||||
|
crossinline deleteFunction: suspend (Uuid) -> Boolean
|
||||||
|
) {
|
||||||
|
safeExecute {
|
||||||
|
val id = getUuidParameter(paramName) ?: return@safeExecute
|
||||||
|
val deleted = deleteFunction(id)
|
||||||
|
if (deleted) {
|
||||||
|
respond(HttpStatusCode.NoContent)
|
||||||
|
} else {
|
||||||
|
respond(HttpStatusCode.NotFound, mapOf("error" to "Entity not found"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,443 @@
|
|||||||
|
openapi: 3.0.3
|
||||||
|
info:
|
||||||
|
title: Meldestelle API
|
||||||
|
description: API für die Meldestelle - Verwaltung von Veranstaltungen, Turnieren und Bewerbungen
|
||||||
|
version: 1.0.0
|
||||||
|
contact:
|
||||||
|
name: Meldestelle Support
|
||||||
|
email: support@mocode.at
|
||||||
|
|
||||||
|
servers:
|
||||||
|
- url: http://localhost:8080
|
||||||
|
description: Development server
|
||||||
|
|
||||||
|
paths:
|
||||||
|
/health:
|
||||||
|
get:
|
||||||
|
summary: Health check
|
||||||
|
description: Check if the service is running
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Service is healthy
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: "OK"
|
||||||
|
|
||||||
|
/api:
|
||||||
|
get:
|
||||||
|
summary: API information
|
||||||
|
description: Get basic API information
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: API information
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
/api/persons:
|
||||||
|
get:
|
||||||
|
summary: Get all persons
|
||||||
|
description: Retrieve a list of all persons in the system
|
||||||
|
tags:
|
||||||
|
- Persons
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: List of persons
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Person'
|
||||||
|
'500':
|
||||||
|
description: Internal server error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
|
||||||
|
post:
|
||||||
|
summary: Create new person
|
||||||
|
description: Create a new person in the system
|
||||||
|
tags:
|
||||||
|
- Persons
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Person'
|
||||||
|
responses:
|
||||||
|
'201':
|
||||||
|
description: Person created successfully
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Person'
|
||||||
|
'400':
|
||||||
|
description: Bad request
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
|
||||||
|
/api/persons/{id}:
|
||||||
|
get:
|
||||||
|
summary: Get person by ID
|
||||||
|
description: Retrieve a specific person by their UUID
|
||||||
|
tags:
|
||||||
|
- Persons
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: Person UUID
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Person found
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Person'
|
||||||
|
'400':
|
||||||
|
description: Invalid UUID format
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
'404':
|
||||||
|
description: Person not found
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
'500':
|
||||||
|
description: Internal server error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
|
||||||
|
put:
|
||||||
|
summary: Update person
|
||||||
|
description: Update an existing person
|
||||||
|
tags:
|
||||||
|
- Persons
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: Person UUID
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Person'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Person updated successfully
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Person'
|
||||||
|
'400':
|
||||||
|
description: Bad request
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
'404':
|
||||||
|
description: Person not found
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
|
||||||
|
delete:
|
||||||
|
summary: Delete person
|
||||||
|
description: Delete a person from the system
|
||||||
|
tags:
|
||||||
|
- Persons
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: Person UUID
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: Person deleted successfully
|
||||||
|
'400':
|
||||||
|
description: Invalid UUID format
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
'404':
|
||||||
|
description: Person not found
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
'500':
|
||||||
|
description: Internal server error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
|
||||||
|
/api/persons/oeps/{oepsSatzNr}:
|
||||||
|
get:
|
||||||
|
summary: Get person by OEPS number
|
||||||
|
description: Retrieve a person by their OEPS Satz number
|
||||||
|
tags:
|
||||||
|
- Persons
|
||||||
|
parameters:
|
||||||
|
- name: oepsSatzNr
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: OEPS Satz number
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Person found
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Person'
|
||||||
|
'400':
|
||||||
|
description: Missing OEPS Satz number
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
'404':
|
||||||
|
description: Person not found
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
'500':
|
||||||
|
description: Internal server error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
|
||||||
|
/api/persons/search:
|
||||||
|
get:
|
||||||
|
summary: Search persons
|
||||||
|
description: Search for persons using a query string
|
||||||
|
tags:
|
||||||
|
- Persons
|
||||||
|
parameters:
|
||||||
|
- name: q
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
description: Search query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Search results
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Person'
|
||||||
|
'400':
|
||||||
|
description: Missing search query
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
'500':
|
||||||
|
description: Internal server error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
|
||||||
|
/api/persons/verein/{vereinId}:
|
||||||
|
get:
|
||||||
|
summary: Get persons by club ID
|
||||||
|
description: Retrieve all persons belonging to a specific club
|
||||||
|
tags:
|
||||||
|
- Persons
|
||||||
|
parameters:
|
||||||
|
- name: vereinId
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: Club UUID
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: List of persons in the club
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Person'
|
||||||
|
'400':
|
||||||
|
description: Invalid UUID format
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
'500':
|
||||||
|
description: Internal server error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
Person:
|
||||||
|
type: object
|
||||||
|
description: Person entity
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
description: Unique identifier for the person
|
||||||
|
oepsSatzNr:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
description: OEPS Satz number
|
||||||
|
nachname:
|
||||||
|
type: string
|
||||||
|
description: Last name
|
||||||
|
vorname:
|
||||||
|
type: string
|
||||||
|
description: First name
|
||||||
|
titel:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
description: Title (e.g., Dr., Prof.)
|
||||||
|
geburtsdatum:
|
||||||
|
type: string
|
||||||
|
format: date
|
||||||
|
nullable: true
|
||||||
|
description: Date of birth
|
||||||
|
geschlechtE:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
description: Gender
|
||||||
|
enum: [MAENNLICH, WEIBLICH, DIVERS, UNBEKANNT]
|
||||||
|
nationalitaet:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
description: Nationality (3-letter code)
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
format: email
|
||||||
|
nullable: true
|
||||||
|
description: Email address
|
||||||
|
telefon:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
description: Phone number
|
||||||
|
adresse:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
description: Address
|
||||||
|
plz:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
description: Postal code
|
||||||
|
ort:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
description: City
|
||||||
|
stammVereinId:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
nullable: true
|
||||||
|
description: Home club ID
|
||||||
|
mitgliedsNummerIntern:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
description: Internal membership number
|
||||||
|
letzteZahlungJahr:
|
||||||
|
type: integer
|
||||||
|
nullable: true
|
||||||
|
description: Last payment year
|
||||||
|
feiId:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
description: FEI ID
|
||||||
|
istGesperrt:
|
||||||
|
type: boolean
|
||||||
|
description: Is person suspended
|
||||||
|
default: false
|
||||||
|
sperrGrund:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
description: Suspension reason
|
||||||
|
rollen:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: Functional roles
|
||||||
|
lizenzen:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
description: License information
|
||||||
|
qualifikationenRichter:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: Judge qualifications
|
||||||
|
qualifikationenParcoursbauer:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: Course builder qualifications
|
||||||
|
istAktiv:
|
||||||
|
type: boolean
|
||||||
|
description: Is person active
|
||||||
|
default: true
|
||||||
|
createdAt:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: Creation timestamp
|
||||||
|
updatedAt:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: Last update timestamp
|
||||||
|
required:
|
||||||
|
- nachname
|
||||||
|
- vorname
|
||||||
|
|
||||||
|
Error:
|
||||||
|
type: object
|
||||||
|
description: Error response
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
description: Error message
|
||||||
|
required:
|
||||||
|
- error
|
||||||
|
|
||||||
|
tags:
|
||||||
|
- name: Persons
|
||||||
|
description: Person management operations
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package at.mocode
|
||||||
|
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.client.statement.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.server.testing.*
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class SwaggerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSwaggerUIEndpoint() = testApplication {
|
||||||
|
application {
|
||||||
|
module()
|
||||||
|
}
|
||||||
|
|
||||||
|
client.get("/swagger").apply {
|
||||||
|
assertEquals(HttpStatusCode.OK, status)
|
||||||
|
assertTrue(bodyAsText().contains("swagger", ignoreCase = true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testOpenAPIEndpoint() = testApplication {
|
||||||
|
application {
|
||||||
|
module()
|
||||||
|
}
|
||||||
|
|
||||||
|
client.get("/openapi").apply {
|
||||||
|
assertEquals(HttpStatusCode.OK, status)
|
||||||
|
val content = bodyAsText()
|
||||||
|
println("[DEBUG_LOG] OpenAPI endpoint response: $content")
|
||||||
|
// Check if it's a JSON response instead of YAML
|
||||||
|
assertTrue(content.isNotEmpty(), "OpenAPI response should not be empty")
|
||||||
|
// More flexible checks
|
||||||
|
assertTrue(content.contains("openapi") || content.contains("swagger"), "Response should contain OpenAPI or Swagger content")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testHealthEndpoint() = testApplication {
|
||||||
|
application {
|
||||||
|
module()
|
||||||
|
}
|
||||||
|
|
||||||
|
client.get("/health").apply {
|
||||||
|
assertEquals(HttpStatusCode.OK, status)
|
||||||
|
assertEquals("OK", bodyAsText())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAPIInfoEndpoint() = testApplication {
|
||||||
|
application {
|
||||||
|
module()
|
||||||
|
}
|
||||||
|
|
||||||
|
client.get("/api").apply {
|
||||||
|
assertEquals(HttpStatusCode.OK, status)
|
||||||
|
// The response should contain some application info
|
||||||
|
assertTrue(bodyAsText().isNotEmpty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Start the server in background
|
||||||
|
cd /home/stefan-mo/WsMeldestelle/meldestelle
|
||||||
|
./gradlew :server:run &
|
||||||
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
# Wait for server to start
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
# Test the endpoints
|
||||||
|
echo "Testing Swagger UI endpoint:"
|
||||||
|
curl -s http://localhost:8080/swagger | head -20
|
||||||
|
|
||||||
|
echo -e "\n\nTesting OpenAPI endpoint:"
|
||||||
|
curl -s http://localhost:8080/openapi | head -20
|
||||||
|
|
||||||
|
# Kill the server
|
||||||
|
kill $SERVER_PID
|
||||||
Reference in New Issue
Block a user