(fix) API Endpoints Creation for All Tables

This commit is contained in:
2025-06-30 21:11:09 +02:00
parent bf8ba45c01
commit 418e692092
52 changed files with 2477 additions and 65 deletions
+224
View File
@@ -0,0 +1,224 @@
# Code Organization Improvements
This document describes the recent reorganization of the codebase to improve maintainability, extensibility, and clarity.
## Overview
The codebase has been restructured to follow better software engineering practices, making it more organized, maintainable, and easier to extend.
## Key Improvements
### 1. Service Locator Pattern (`ServiceLocator.kt`)
**Location**: `server/src/main/kotlin/at/mocode/services/ServiceLocator.kt`
**Purpose**: Centralized dependency management for repository instances.
**Benefits**:
- Single point of access for all repositories
- Easy to switch implementations (e.g., for testing or different databases)
- Lazy initialization for better performance
- Simplified dependency injection
**Usage**:
```kotlin
val artikelRepository = ServiceLocator.artikelRepository
val vereinRepository = ServiceLocator.vereinRepository
```
### 2. Standardized API Responses (`ApiResponse.kt`)
**Location**: `server/src/main/kotlin/at/mocode/utils/ApiResponse.kt`
**Purpose**: Consistent response format across all API endpoints.
**Benefits**:
- Uniform error handling
- Standardized success/error response structure
- Reduced code duplication
- Better client-side error handling
**Usage**:
```kotlin
call.respondSuccess(data)
call.respondError("Error message")
call.respondNotFound("Resource")
```
### 3. Route Utilities (`RouteUtils.kt`)
**Location**: `server/src/main/kotlin/at/mocode/utils/RouteUtils.kt`
**Purpose**: Common route operations and parameter validation.
**Benefits**:
- Consistent parameter validation
- Reduced boilerplate code
- Standardized error responses
- Type-safe parameter extraction
**Usage**:
```kotlin
val uuid = call.getUuidParameter("id", "artikel") ?: return
val query = call.getQueryParameter("q") ?: return
val data = call.safeReceive<Artikel>() ?: return
```
### 4. Centralized Route Configuration (`RouteConfiguration.kt`)
**Location**: `server/src/main/kotlin/at/mocode/routes/RouteConfiguration.kt`
**Purpose**: Organized route registration by domain and functionality.
**Benefits**:
- Clear API structure
- Logical grouping of related endpoints
- Easy to understand and maintain
- Scalable for future additions
**Structure**:
```
/api
├── /artikel (core routes)
├── /personen
├── /vereine
├── /domain
│ ├── /lizenzen
│ ├── /pferde
│ └── /qualifikationen
└── /events
├── /veranstaltungen
├── /turniere
├── /bewerbe
└── /abteilungen
```
### 5. Configuration Management (`AppConfig.kt`)
**Location**: `server/src/main/kotlin/at/mocode/config/AppConfig.kt`
**Purpose**: Centralized application configuration management.
**Benefits**:
- Environment-specific settings
- Type-safe configuration
- Default values for development
- Easy to extend for new settings
**Features**:
- Application info (name, version, environment)
- Database configuration
- API settings
- Security configuration
## Migration Guide
### For Existing Routes
1. **Update Repository Access**:
```kotlin
// Before
val repository = PostgresArtikelRepository()
// After
val repository = ServiceLocator.artikelRepository
```
2. **Update Route Paths**:
```kotlin
// Before
route("/api/artikel") { ... }
// After
route("/artikel") { ... } // /api prefix handled by RouteConfiguration
```
3. **Use Response Utilities**:
```kotlin
// Before
call.respond(HttpStatusCode.OK, data)
// After
call.respondSuccess(data)
```
4. **Use Route Utilities**:
```kotlin
// Before
val id = call.parameters["id"] ?: return@get call.respond(...)
val uuid = uuidFrom(id)
// After
val uuid = call.getUuidParameter("id") ?: return
```
### For New Routes
1. Add repository interface to `ServiceLocator`
2. Create route function using utilities
3. Register in appropriate section of `RouteConfiguration`
4. Update route paths to exclude `/api` prefix
## Best Practices
### Repository Pattern
- Always use interfaces for repositories
- Implement PostgreSQL versions for production
- Use ServiceLocator for dependency injection
### Error Handling
- Use ResponseUtils for consistent error responses
- Handle common exceptions with `handleException()`
- Provide meaningful error messages
### Route Organization
- Group related routes logically
- Use descriptive route names
- Follow RESTful conventions
- Document complex endpoints
### Configuration
- Use AppConfig for all settings
- Provide sensible defaults
- Support environment-specific overrides
- Keep sensitive data in environment variables
## Future Enhancements
### Planned Improvements
1. **Authentication & Authorization**
- JWT token support
- Role-based access control
- Session management
2. **API Documentation**
- OpenAPI/Swagger integration
- Automatic documentation generation
- Interactive API explorer
3. **Monitoring & Logging**
- Structured logging
- Performance metrics
- Health checks
4. **Testing Framework**
- Unit test utilities
- Integration test helpers
- Mock repository implementations
### Extension Points
- Add new repositories to ServiceLocator
- Extend RouteConfiguration for new domains
- Add configuration sections to AppConfig
- Create new utility functions as needed
## Benefits Summary
1. **Maintainability**: Clear separation of concerns and consistent patterns
2. **Extensibility**: Easy to add new features and endpoints
3. **Testability**: Dependency injection and clear interfaces
4. **Consistency**: Standardized responses and error handling
5. **Documentation**: Self-documenting code structure
6. **Performance**: Lazy loading and efficient resource management
This reorganization provides a solid foundation for future development while maintaining backward compatibility and improving code quality.
+146
View File
@@ -300,6 +300,152 @@ Delete an article.
---
## Horses (Pferde) API
### GET /api/horses
Get all horses.
**Response:**
```json
[
{
"pferdId": "uuid",
"oepsSatzNrPferd": "string",
"oepsKopfNr": "string",
"name": "string",
"lebensnummer": "string",
"feiPassNr": "string",
"geburtsjahr": 2015,
"geschlecht": "WALLACH|STUTE|HENGST",
"farbe": "string",
"rasse": "string",
"abstammungVaterName": "string",
"abstammungMutterName": "string",
"abstammungMutterVaterName": "string",
"abstammungZusatzInfo": "string",
"besitzerPersonId": "uuid",
"verantwortlichePersonId": "uuid",
"heimatVereinId": "uuid",
"letzteZahlungPferdegebuehrJahrOeps": 2023,
"stockmassCm": 165,
"datenQuelle": "MANUELL|ZNS_IMPORT",
"istAktiv": true,
"notizenIntern": "string",
"createdAt": "2023-01-01T00:00:00Z",
"updatedAt": "2023-01-01T00:00:00Z"
}
]
```
### GET /api/horses/{id}
Get horse by ID.
**Parameters:**
- `id` (path) - UUID of the horse
### GET /api/horses/oeps/{oepsSatzNr}
Get horse by OEPS registration number.
**Parameters:**
- `oepsSatzNr` (path) - OEPS registration number
### GET /api/horses/lebensnummer/{lebensnummer}
Get horse by life number (UELN).
**Parameters:**
- `lebensnummer` (path) - Horse life number
### GET /api/horses/search?q={query}
Search horses by name or other attributes.
**Parameters:**
- `q` (query) - Search query string
### GET /api/horses/name/{name}
Get horses by name.
**Parameters:**
- `name` (path) - Horse name
### GET /api/horses/owner/{ownerId}
Get horses by owner ID.
**Parameters:**
- `ownerId` (path) - UUID of the owner person
### GET /api/horses/responsible/{personId}
Get horses by responsible person ID.
**Parameters:**
- `personId` (path) - UUID of the responsible person
### GET /api/horses/club/{clubId}
Get horses by home club ID.
**Parameters:**
- `clubId` (path) - UUID of the home club
### GET /api/horses/breed/{breed}
Get horses by breed.
**Parameters:**
- `breed` (path) - Horse breed
### GET /api/horses/birth-year/{year}
Get horses by birth year.
**Parameters:**
- `year` (path) - Birth year (integer)
### GET /api/horses/active
Get only active horses.
### POST /api/horses
Create a new horse.
**Request Body:**
```json
{
"oepsSatzNrPferd": "string",
"oepsKopfNr": "string",
"name": "string",
"lebensnummer": "string",
"feiPassNr": "string",
"geburtsjahr": 2015,
"geschlecht": "WALLACH",
"farbe": "string",
"rasse": "string",
"abstammungVaterName": "string",
"abstammungMutterName": "string",
"abstammungMutterVaterName": "string",
"abstammungZusatzInfo": "string",
"besitzerPersonId": "uuid",
"verantwortlichePersonId": "uuid",
"heimatVereinId": "uuid",
"letzteZahlungPferdegebuehrJahrOeps": 2023,
"stockmassCm": 165,
"datenQuelle": "MANUELL",
"istAktiv": true,
"notizenIntern": "string"
}
```
### PUT /api/horses/{id}
Update an existing horse.
**Parameters:**
- `id` (path) - UUID of the horse
**Request Body:** Same as POST
### DELETE /api/horses/{id}
Delete a horse.
**Parameters:**
- `id` (path) - UUID of the horse
---
## Data Models
### Person
@@ -0,0 +1,124 @@
package at.mocode.config
import io.ktor.server.application.*
/**
* Application configuration management
* Centralizes all configuration settings for better maintainability
*/
object AppConfig {
/**
* Application information
*/
data class AppInfo(
val name: String,
val version: String,
val environment: String,
val description: String
)
/**
* Database configuration
*/
data class DatabaseConfig(
val url: String,
val driver: String,
val user: String,
val password: String,
val maxPoolSize: Int = 10,
val connectionTimeout: Long = 30000
)
/**
* API configuration
*/
data class ApiConfig(
val baseUrl: String,
val version: String,
val enableCors: Boolean = true,
val enableSwagger: Boolean = false,
val rateLimitEnabled: Boolean = false
)
/**
* Security configuration
*/
data class SecurityConfig(
val jwtSecret: String? = null,
val jwtIssuer: String? = null,
val jwtAudience: String? = null,
val sessionTimeout: Long = 3600000, // 1 hour
val enableAuthentication: Boolean = false
)
/**
* Load configuration from application environment
*/
fun loadConfig(application: Application): AppConfiguration {
val config = application.environment.config
val appInfo = AppInfo(
name = config.propertyOrNull("application.name")?.getString() ?: "Meldestelle API Server",
version = config.propertyOrNull("application.version")?.getString() ?: "1.0.0",
environment = config.propertyOrNull("application.environment")?.getString() ?: "development",
description = config.propertyOrNull("application.description")?.getString() ?: "Equestrian Event Management API"
)
val databaseConfig = DatabaseConfig(
url = config.propertyOrNull("database.url")?.getString() ?: "jdbc:postgresql://localhost:5432/meldestelle",
driver = config.propertyOrNull("database.driver")?.getString() ?: "org.postgresql.Driver",
user = config.propertyOrNull("database.user")?.getString() ?: "postgres",
password = config.propertyOrNull("database.password")?.getString() ?: "password",
maxPoolSize = config.propertyOrNull("database.maxPoolSize")?.getString()?.toIntOrNull() ?: 10,
connectionTimeout = config.propertyOrNull("database.connectionTimeout")?.getString()?.toLongOrNull() ?: 30000
)
val apiConfig = ApiConfig(
baseUrl = config.propertyOrNull("api.baseUrl")?.getString() ?: "http://localhost:8080",
version = config.propertyOrNull("api.version")?.getString() ?: "v1",
enableCors = config.propertyOrNull("api.enableCors")?.getString()?.toBoolean() ?: true,
enableSwagger = config.propertyOrNull("api.enableSwagger")?.getString()?.toBoolean() ?: (appInfo.environment == "development"),
rateLimitEnabled = config.propertyOrNull("api.rateLimitEnabled")?.getString()?.toBoolean() ?: false
)
val securityConfig = SecurityConfig(
jwtSecret = config.propertyOrNull("security.jwt.secret")?.getString(),
jwtIssuer = config.propertyOrNull("security.jwt.issuer")?.getString(),
jwtAudience = config.propertyOrNull("security.jwt.audience")?.getString(),
sessionTimeout = config.propertyOrNull("security.sessionTimeout")?.getString()?.toLongOrNull() ?: 3600000,
enableAuthentication = config.propertyOrNull("security.enableAuthentication")?.getString()?.toBoolean() ?: false
)
return AppConfiguration(appInfo, databaseConfig, apiConfig, securityConfig)
}
}
/**
* Complete application configuration
*/
data class AppConfiguration(
val app: AppConfig.AppInfo,
val database: AppConfig.DatabaseConfig,
val api: AppConfig.ApiConfig,
val security: AppConfig.SecurityConfig
) {
/**
* Check if running in development mode
*/
val isDevelopment: Boolean
get() = app.environment.lowercase() == "development"
/**
* Check if running in production mode
*/
val isProduction: Boolean
get() = app.environment.lowercase() == "production"
/**
* Get application info string for API endpoint
*/
fun getAppInfoString(): String {
return "${app.name} v${app.version} - Running in ${app.environment} mode"
}
}
@@ -1,2 +0,0 @@
package at.mocode.model
@@ -1,5 +1,14 @@
package at.mocode.plugins
import at.mocode.tables.ArtikelTable
import at.mocode.tables.PlaetzeTable
import at.mocode.tables.TurniereTable
import at.mocode.tables.VeranstaltungenTable
import at.mocode.tables.domaene.DomQualifikationTable
import at.mocode.tables.stammdaten.LizenzenTable
import at.mocode.tables.stammdaten.PersonenTable
import at.mocode.tables.stammdaten.PferdeTable
import at.mocode.tables.stammdaten.VereineTable
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import io.ktor.server.application.*
@@ -22,7 +31,7 @@ import java.util.concurrent.TimeUnit
*/
fun Application.configureDatabase() {
val log = LoggerFactory.getLogger("DatabaseInitialization")
var connectionSuccessful = false
var connectionSuccessful: Boolean
// Environment detection
val isTestEnvironment = System.getProperty("isTestEnvironment")?.toBoolean() ?: false
@@ -32,7 +41,7 @@ fun Application.configureDatabase() {
// Get database configuration from application.yaml if available
val dbConfig = try {
environment.config.config("database")
} catch (e: ApplicationConfigurationException) {
} catch (_: ApplicationConfigurationException) {
log.warn("No database configuration found in application.yaml, using environment variables")
null
}
@@ -51,7 +60,7 @@ fun Application.configureDatabase() {
}
}
// Initialize schema if connection was successful
// Initialize schema if the connection was successful
if (connectionSuccessful) {
initializeSchema(log, isTestEnvironment, isIdeaEnvironment)
} else {
@@ -174,14 +183,15 @@ private fun initializeSchema(log: Logger, isTestEnvironment: Boolean, isIdeaEnvi
try {
// Create all tables if they don't exist
SchemaUtils.create(
_root_ide_package_.at.mocode.tables.VereineTable,
_root_ide_package_.at.mocode.tables.PersonenTable,
_root_ide_package_.at.mocode.tables.PferdeTable,
_root_ide_package_.at.mocode.tables.VeranstaltungenTable,
_root_ide_package_.at.mocode.tables.TurniereTable,
_root_ide_package_.at.mocode.tables.ArtikelTable,
_root_ide_package_.at.mocode.tables.PlaetzeTable,
_root_ide_package_.at.mocode.tables.LizenzenTable
VereineTable,
PersonenTable,
PferdeTable,
VeranstaltungenTable,
TurniereTable,
ArtikelTable,
PlaetzeTable,
LizenzenTable,
DomQualifikationTable
// Add more tables here if needed
)
log.info("Database schema initialized successfully.")
@@ -1,20 +1,20 @@
package at.mocode.plugins
import at.mocode.routes.artikelRoutes
import at.mocode.routes.domLizenzRoutes
import at.mocode.routes.personRoutes
import at.mocode.routes.vereinRoutes
import at.mocode.config.AppConfig
import at.mocode.routes.RouteConfiguration.configureApiRoutes
import io.ktor.server.application.Application
import io.ktor.server.http.content.staticResources
import io.ktor.server.response.respondText
import io.ktor.server.routing.application
import io.ktor.server.routing.get
import io.ktor.server.routing.routing
/**
* Configures all routes for the application
* Configures all routes for the application using the centralized route configuration
*/
fun Application.configureRouting() {
// Load application configuration
val appConfig = AppConfig.loadConfig(this)
routing {
// Health check endpoint
get("/health") {
@@ -26,18 +26,10 @@ fun Application.configureRouting() {
// Root endpoint with basic information (API info endpoint)
get("/api") {
// Read application info from config if available
val appName = application.environment.config.propertyOrNull("application.name")?.getString() ?: "Meldestelle API Server"
val appVersion = application.environment.config.propertyOrNull("application.version")?.getString() ?: "1.0.0"
val appEnv = application.environment.config.propertyOrNull("application.environment")?.getString() ?: "development"
call.respondText("$appName v$appVersion - Running in $appEnv mode")
call.respondText(appConfig.getAppInfoString())
}
// API routes
personRoutes()
vereinRoutes()
artikelRoutes()
domLizenzRoutes()
// Configure all API routes using the centralized configuration
configureApiRoutes()
}
}
@@ -1,2 +0,0 @@
package at.mocode.plugins
@@ -0,0 +1,15 @@
package at.mocode.repositories
import at.mocode.model.Abteilung
import com.benasher44.uuid.Uuid
interface AbteilungRepository {
suspend fun findAll(): List<Abteilung>
suspend fun findById(id: Uuid): Abteilung?
suspend fun findByBewerbId(bewerbId: Uuid): List<Abteilung>
suspend fun create(abteilung: Abteilung): Abteilung
suspend fun update(id: Uuid, abteilung: Abteilung): Abteilung?
suspend fun delete(id: Uuid): Boolean
suspend fun search(query: String): List<Abteilung>
suspend fun findByAktiv(istAktiv: Boolean): List<Abteilung>
}
@@ -1,5 +1,6 @@
package at.mocode.model
package at.mocode.repositories
import at.mocode.model.Artikel
import com.benasher44.uuid.Uuid
interface ArtikelRepository {
@@ -0,0 +1,18 @@
package at.mocode.repositories
import at.mocode.model.Bewerb
import com.benasher44.uuid.Uuid
interface BewerbRepository {
suspend fun findAll(): List<Bewerb>
suspend fun findById(id: Uuid): Bewerb?
suspend fun findByTurnierId(turnierId: Uuid): List<Bewerb>
suspend fun findBySparte(sparte: String): List<Bewerb>
suspend fun findByKlasse(klasse: String): List<Bewerb>
suspend fun create(bewerb: Bewerb): Bewerb
suspend fun update(id: Uuid, bewerb: Bewerb): Bewerb?
suspend fun delete(id: Uuid): Boolean
suspend fun search(query: String): List<Bewerb>
suspend fun findByStartlisteFinal(istFinal: Boolean): List<Bewerb>
suspend fun findByErgebnislisteFinal(istFinal: Boolean): List<Bewerb>
}
@@ -1,4 +1,4 @@
package at.mocode.model
package at.mocode.repositories
import at.mocode.model.domaene.DomLizenz
import com.benasher44.uuid.Uuid
@@ -1,4 +1,4 @@
package at.mocode.model
package at.mocode.repositories
import at.mocode.model.domaene.DomPferd
import com.benasher44.uuid.Uuid
@@ -0,0 +1,18 @@
package at.mocode.repositories
import at.mocode.model.domaene.DomQualifikation
import com.benasher44.uuid.Uuid
import kotlinx.datetime.LocalDate
interface DomQualifikationRepository {
suspend fun findAll(): List<DomQualifikation>
suspend fun findById(id: Uuid): DomQualifikation?
suspend fun findByPersonId(personId: Uuid): List<DomQualifikation>
suspend fun findByQualTypId(qualTypId: Uuid): List<DomQualifikation>
suspend fun findActiveByPersonId(personId: Uuid): List<DomQualifikation>
suspend fun findByValidityPeriod(fromDate: LocalDate?, toDate: LocalDate?): List<DomQualifikation>
suspend fun create(domQualifikation: DomQualifikation): DomQualifikation
suspend fun update(id: Uuid, domQualifikation: DomQualifikation): DomQualifikation?
suspend fun delete(id: Uuid): Boolean
suspend fun search(query: String): List<DomQualifikation>
}
@@ -0,0 +1,2 @@
package at.mocode.repositories
@@ -1,4 +1,4 @@
package at.mocode.model
package at.mocode.repositories
interface EventRepository {
}
@@ -1,4 +1,4 @@
package at.mocode.model
package at.mocode.repositories
import at.mocode.stammdaten.Person
import com.benasher44.uuid.Uuid
@@ -0,0 +1,156 @@
package at.mocode.repositories
import at.mocode.enums.BeginnzeitTypE
import at.mocode.model.Abteilung
import at.mocode.tables.AbteilungTable
import com.benasher44.uuid.Uuid
import com.ionspin.kotlin.bignum.decimal.BigDecimal
import kotlinx.datetime.Clock
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.transactions.transaction
import java.math.BigDecimal as JavaBigDecimal
class PostgresAbteilungRepository : AbteilungRepository {
override suspend fun findAll(): List<Abteilung> = transaction {
AbteilungTable.selectAll().map { rowToAbteilung(it) }
}
override suspend fun findById(id: Uuid): Abteilung? = transaction {
AbteilungTable.selectAll().where { AbteilungTable.id eq id }
.map { rowToAbteilung(it) }
.singleOrNull()
}
override suspend fun findByBewerbId(bewerbId: Uuid): List<Abteilung> = transaction {
AbteilungTable.selectAll().where { AbteilungTable.bewerbId eq bewerbId }
.map { rowToAbteilung(it) }
}
override suspend fun create(abteilung: Abteilung): Abteilung = transaction {
val now = Clock.System.now()
AbteilungTable.insert {
it[id] = abteilung.id
it[bewerbId] = abteilung.bewerbId
it[abteilungsKennzeichen] = abteilung.abteilungsKennzeichen
it[bezeichnungIntern] = abteilung.bezeichnungIntern
it[bezeichnungAufStartliste] = abteilung.bezeichnungAufStartliste
it[teilungsKriteriumLizenz] = abteilung.teilungsKriteriumLizenz
it[teilungsKriteriumPferdealter] = abteilung.teilungsKriteriumPferdealter
it[teilungsKriteriumAltersklasseReiter] = abteilung.teilungsKriteriumAltersklasseReiter
it[teilungsKriteriumAnzahlMin] = abteilung.teilungsKriteriumAnzahlMin
it[teilungsKriteriumAnzahlMax] = abteilung.teilungsKriteriumAnzahlMax
it[teilungsKriteriumFreiText] = abteilung.teilungsKriteriumFreiText
it[startgeld] = abteilung.startgeld?.let { bg -> JavaBigDecimal(bg.toStringExpanded()) }
it[platzId] = abteilung.platzId
it[datum] = abteilung.datum
it[beginnzeitTypE] = abteilung.beginnzeitTypE.name
it[beginnzeitFix] = abteilung.beginnzeitFix
it[beginnNachAbteilungId] = abteilung.beginnNachAbteilungId
it[beginnzeitCa] = abteilung.beginnzeitCa
it[dauerProStartGeschaetztSek] = abteilung.dauerProStartGeschaetztSek
it[umbauzeitNachAbteilungMin] = abteilung.umbauzeitNachAbteilungMin
it[besichtigungszeitVorAbteilungMin] = abteilung.besichtigungszeitVorAbteilungMin
it[stechzeitZusaetzlichMin] = abteilung.stechzeitZusaetzlichMin
it[anzahlStarter] = abteilung.anzahlStarter
it[istAktiv] = abteilung.istAktiv
it[createdAt] = now
it[updatedAt] = now
}
abteilung.copy(createdAt = now, updatedAt = now)
}
override suspend fun update(id: Uuid, abteilung: Abteilung): Abteilung? = transaction {
val updateCount = AbteilungTable.update({ AbteilungTable.id eq id }) {
it[bewerbId] = abteilung.bewerbId
it[abteilungsKennzeichen] = abteilung.abteilungsKennzeichen
it[bezeichnungIntern] = abteilung.bezeichnungIntern
it[bezeichnungAufStartliste] = abteilung.bezeichnungAufStartliste
it[teilungsKriteriumLizenz] = abteilung.teilungsKriteriumLizenz
it[teilungsKriteriumPferdealter] = abteilung.teilungsKriteriumPferdealter
it[teilungsKriteriumAltersklasseReiter] = abteilung.teilungsKriteriumAltersklasseReiter
it[teilungsKriteriumAnzahlMin] = abteilung.teilungsKriteriumAnzahlMin
it[teilungsKriteriumAnzahlMax] = abteilung.teilungsKriteriumAnzahlMax
it[teilungsKriteriumFreiText] = abteilung.teilungsKriteriumFreiText
it[startgeld] = abteilung.startgeld?.let { bg -> JavaBigDecimal(bg.toStringExpanded()) }
it[platzId] = abteilung.platzId
it[datum] = abteilung.datum
it[beginnzeitTypE] = abteilung.beginnzeitTypE.name
it[beginnzeitFix] = abteilung.beginnzeitFix
it[beginnNachAbteilungId] = abteilung.beginnNachAbteilungId
it[beginnzeitCa] = abteilung.beginnzeitCa
it[dauerProStartGeschaetztSek] = abteilung.dauerProStartGeschaetztSek
it[umbauzeitNachAbteilungMin] = abteilung.umbauzeitNachAbteilungMin
it[besichtigungszeitVorAbteilungMin] = abteilung.besichtigungszeitVorAbteilungMin
it[stechzeitZusaetzlichMin] = abteilung.stechzeitZusaetzlichMin
it[anzahlStarter] = abteilung.anzahlStarter
it[istAktiv] = abteilung.istAktiv
it[updatedAt] = Clock.System.now()
}
if (updateCount > 0) {
AbteilungTable.selectAll().where { AbteilungTable.id eq id }
.map { rowToAbteilung(it) }
.singleOrNull()
} else null
}
override suspend fun delete(id: Uuid): Boolean = transaction {
AbteilungTable.deleteWhere { AbteilungTable.id eq id } > 0
}
override suspend fun search(query: String): List<Abteilung> = transaction {
AbteilungTable.selectAll().where {
(AbteilungTable.abteilungsKennzeichen.lowerCase() like "%${query.lowercase()}%") or
(AbteilungTable.bezeichnungIntern?.lowerCase()?.like("%${query.lowercase()}%") ?: Op.FALSE) or
(AbteilungTable.bezeichnungAufStartliste?.lowerCase()?.like("%${query.lowercase()}%") ?: Op.FALSE)
}.map { rowToAbteilung(it) }
}
override suspend fun findByAktiv(istAktiv: Boolean): List<Abteilung> = transaction {
AbteilungTable.selectAll().where { AbteilungTable.istAktiv eq istAktiv }
.map { rowToAbteilung(it) }
}
private fun rowToAbteilung(row: ResultRow): Abteilung {
return Abteilung(
id = row[AbteilungTable.id],
bewerbId = row[AbteilungTable.bewerbId],
abteilungsKennzeichen = row[AbteilungTable.abteilungsKennzeichen],
bezeichnungIntern = row[AbteilungTable.bezeichnungIntern],
bezeichnungAufStartliste = row[AbteilungTable.bezeichnungAufStartliste],
teilungsKriteriumLizenz = row[AbteilungTable.teilungsKriteriumLizenz],
teilungsKriteriumPferdealter = row[AbteilungTable.teilungsKriteriumPferdealter],
teilungsKriteriumAltersklasseReiter = row[AbteilungTable.teilungsKriteriumAltersklasseReiter],
teilungsKriteriumAnzahlMin = row[AbteilungTable.teilungsKriteriumAnzahlMin],
teilungsKriteriumAnzahlMax = row[AbteilungTable.teilungsKriteriumAnzahlMax],
teilungsKriteriumFreiText = row[AbteilungTable.teilungsKriteriumFreiText],
startgeld = row[AbteilungTable.startgeld]?.let {
try {
BigDecimal.parseString(it.toString())
} catch (_: Exception) {
null
}
},
dotierungen = emptyList(), // TODO: Load from related table when implemented
platzId = row[AbteilungTable.platzId],
datum = row[AbteilungTable.datum],
beginnzeitTypE = try {
BeginnzeitTypE.valueOf(row[AbteilungTable.beginnzeitTypE])
} catch (_: Exception) {
BeginnzeitTypE.ANSCHLIESSEND
},
beginnzeitFix = row[AbteilungTable.beginnzeitFix],
beginnNachAbteilungId = row[AbteilungTable.beginnNachAbteilungId],
beginnzeitCa = row[AbteilungTable.beginnzeitCa],
dauerProStartGeschaetztSek = row[AbteilungTable.dauerProStartGeschaetztSek],
umbauzeitNachAbteilungMin = row[AbteilungTable.umbauzeitNachAbteilungMin],
besichtigungszeitVorAbteilungMin = row[AbteilungTable.besichtigungszeitVorAbteilungMin],
stechzeitZusaetzlichMin = row[AbteilungTable.stechzeitZusaetzlichMin],
anzahlStarter = row[AbteilungTable.anzahlStarter],
istAktiv = row[AbteilungTable.istAktiv],
createdAt = row[AbteilungTable.createdAt],
updatedAt = row[AbteilungTable.updatedAt]
)
}
}
@@ -1,5 +1,6 @@
package at.mocode.model
package at.mocode.repositories
import at.mocode.model.Artikel
import at.mocode.tables.ArtikelTable
import com.benasher44.uuid.Uuid
import com.ionspin.kotlin.bignum.decimal.BigDecimal
@@ -0,0 +1,61 @@
package at.mocode.repositories
import at.mocode.model.Bewerb
import com.benasher44.uuid.Uuid
class PostgresBewerbRepository : BewerbRepository {
override suspend fun findAll(): List<Bewerb> {
// TODO: Implement database operations
return emptyList()
}
override suspend fun findById(id: Uuid): Bewerb? {
// TODO: Implement database operations
return null
}
override suspend fun findByTurnierId(turnierId: Uuid): List<Bewerb> {
// TODO: Implement database operations
return emptyList()
}
override suspend fun findBySparte(sparte: String): List<Bewerb> {
// TODO: Implement database operations
return emptyList()
}
override suspend fun findByKlasse(klasse: String): List<Bewerb> {
// TODO: Implement database operations
return emptyList()
}
override suspend fun create(bewerb: Bewerb): Bewerb {
// TODO: Implement database operations
return bewerb
}
override suspend fun update(id: Uuid, bewerb: Bewerb): Bewerb? {
// TODO: Implement database operations
return null
}
override suspend fun delete(id: Uuid): Boolean {
// TODO: Implement database operations
return false
}
override suspend fun search(query: String): List<Bewerb> {
// TODO: Implement database operations
return emptyList()
}
override suspend fun findByStartlisteFinal(istFinal: Boolean): List<Bewerb> {
// TODO: Implement database operations
return emptyList()
}
override suspend fun findByErgebnislisteFinal(istFinal: Boolean): List<Bewerb> {
// TODO: Implement database operations
return emptyList()
}
}
@@ -1,9 +1,8 @@
package at.mocode.model
package at.mocode.repositories
import at.mocode.model.domaene.DomLizenz
import at.mocode.tables.DomLizenzTable
import at.mocode.tables.domaene.DomLizenzTable
import com.benasher44.uuid.Uuid
import com.benasher44.uuid.uuidFrom
import kotlinx.datetime.Clock
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
@@ -1,12 +1,11 @@
package at.mocode.model
package at.mocode.repositories
import at.mocode.model.domaene.DomPferd
import at.mocode.tables.DomPferdTable
import at.mocode.tables.domaene.DomPferdTable
import com.benasher44.uuid.Uuid
import kotlinx.datetime.Clock
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.like
import org.jetbrains.exposed.sql.transactions.transaction
class PostgresDomPferdRepository : DomPferdRepository {
@@ -0,0 +1,115 @@
package at.mocode.repositories
import at.mocode.model.domaene.DomQualifikation
import at.mocode.tables.domaene.DomQualifikationTable
import com.benasher44.uuid.Uuid
import kotlinx.datetime.Clock
import kotlinx.datetime.LocalDate
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.transactions.transaction
class PostgresDomQualifikationRepository : DomQualifikationRepository {
override suspend fun findAll(): List<DomQualifikation> = transaction {
DomQualifikationTable.selectAll().map { rowToDomQualifikation(it) }
}
override suspend fun findById(id: Uuid): DomQualifikation? = transaction {
DomQualifikationTable.select { DomQualifikationTable.qualifikationId eq id }
.map { rowToDomQualifikation(it) }
.singleOrNull()
}
override suspend fun findByPersonId(personId: Uuid): List<DomQualifikation> = transaction {
DomQualifikationTable.select { DomQualifikationTable.personId eq personId }
.map { rowToDomQualifikation(it) }
}
override suspend fun findByQualTypId(qualTypId: Uuid): List<DomQualifikation> = transaction {
DomQualifikationTable.select { DomQualifikationTable.qualTypId eq qualTypId }
.map { rowToDomQualifikation(it) }
}
override suspend fun findActiveByPersonId(personId: Uuid): List<DomQualifikation> = transaction {
DomQualifikationTable.select {
(DomQualifikationTable.personId eq personId) and (DomQualifikationTable.istAktiv eq true)
}.map { rowToDomQualifikation(it) }
}
override suspend fun findByValidityPeriod(fromDate: LocalDate?, toDate: LocalDate?): List<DomQualifikation> = transaction {
var query = DomQualifikationTable.selectAll()
if (fromDate != null) {
query = query.andWhere {
DomQualifikationTable.gueltigVon.isNull() or (DomQualifikationTable.gueltigVon greaterEq fromDate)
}
}
if (toDate != null) {
query = query.andWhere {
DomQualifikationTable.gueltigBis.isNull() or (DomQualifikationTable.gueltigBis lessEq toDate)
}
}
query.map { rowToDomQualifikation(it) }
}
override suspend fun create(domQualifikation: DomQualifikation): DomQualifikation = transaction {
val now = Clock.System.now()
DomQualifikationTable.insert {
it[qualifikationId] = domQualifikation.qualifikationId
it[personId] = domQualifikation.personId
it[qualTypId] = domQualifikation.qualTypId
it[bemerkung] = domQualifikation.bemerkung
it[gueltigVon] = domQualifikation.gueltigVon
it[gueltigBis] = domQualifikation.gueltigBis
it[istAktiv] = domQualifikation.istAktiv
it[createdAt] = domQualifikation.createdAt
it[updatedAt] = now
}
domQualifikation.copy(updatedAt = now)
}
override suspend fun update(id: Uuid, domQualifikation: DomQualifikation): DomQualifikation? = transaction {
val now = Clock.System.now()
val updateCount = DomQualifikationTable.update({ DomQualifikationTable.qualifikationId eq id }) {
it[personId] = domQualifikation.personId
it[qualTypId] = domQualifikation.qualTypId
it[bemerkung] = domQualifikation.bemerkung
it[gueltigVon] = domQualifikation.gueltigVon
it[gueltigBis] = domQualifikation.gueltigBis
it[istAktiv] = domQualifikation.istAktiv
it[updatedAt] = now
}
if (updateCount > 0) {
domQualifikation.copy(qualifikationId = id, updatedAt = now)
} else {
null
}
}
override suspend fun delete(id: Uuid): Boolean = transaction {
DomQualifikationTable.deleteWhere { qualifikationId eq id } > 0
}
override suspend fun search(query: String): List<DomQualifikation> = transaction {
DomQualifikationTable.select {
DomQualifikationTable.bemerkung like "%$query%"
}.map { rowToDomQualifikation(it) }
}
private fun rowToDomQualifikation(row: ResultRow): DomQualifikation {
return DomQualifikation(
qualifikationId = row[DomQualifikationTable.qualifikationId],
personId = row[DomQualifikationTable.personId],
qualTypId = row[DomQualifikationTable.qualTypId],
bemerkung = row[DomQualifikationTable.bemerkung],
gueltigVon = row[DomQualifikationTable.gueltigVon],
gueltigBis = row[DomQualifikationTable.gueltigBis],
istAktiv = row[DomQualifikationTable.istAktiv],
createdAt = row[DomQualifikationTable.createdAt],
updatedAt = row[DomQualifikationTable.updatedAt]
)
}
}
@@ -1,4 +1,4 @@
package at.mocode.model
package at.mocode.repositories
class PostgresEventRepository {
}
@@ -1,8 +1,8 @@
package at.mocode.model
package at.mocode.repositories
import at.mocode.enums.FunktionaerRolle
import at.mocode.stammdaten.Person
import at.mocode.tables.PersonenTable
import at.mocode.tables.stammdaten.PersonenTable
import com.benasher44.uuid.Uuid
import kotlinx.datetime.Clock
import org.jetbrains.exposed.sql.*
@@ -0,0 +1,46 @@
package at.mocode.repositories
import at.mocode.model.Turnier
import com.benasher44.uuid.Uuid
class PostgresTurnierRepository : TurnierRepository {
override suspend fun findAll(): List<Turnier> {
// TODO: Implement database operations
return emptyList()
}
override suspend fun findById(id: Uuid): Turnier? {
// TODO: Implement database operations
return null
}
override suspend fun findByVeranstaltungId(veranstaltungId: Uuid): List<Turnier> {
// TODO: Implement database operations
return emptyList()
}
override suspend fun findByOepsTurnierNr(oepsTurnierNr: String): Turnier? {
// TODO: Implement database operations
return null
}
override suspend fun create(turnier: Turnier): Turnier {
// TODO: Implement database operations
return turnier
}
override suspend fun update(id: Uuid, turnier: Turnier): Turnier? {
// TODO: Implement database operations
return null
}
override suspend fun delete(id: Uuid): Boolean {
// TODO: Implement database operations
return false
}
override suspend fun search(query: String): List<Turnier> {
// TODO: Implement database operations
return emptyList()
}
}
@@ -0,0 +1,46 @@
package at.mocode.repositories
import at.mocode.model.Veranstaltung
import com.benasher44.uuid.Uuid
class PostgresVeranstaltungRepository : VeranstaltungRepository {
override suspend fun findAll(): List<Veranstaltung> {
// TODO: Implement database operations
return emptyList()
}
override suspend fun findById(id: Uuid): Veranstaltung? {
// TODO: Implement database operations
return null
}
override suspend fun findByName(name: String): List<Veranstaltung> {
// TODO: Implement database operations
return emptyList()
}
override suspend fun findByVeranstalterOepsNummer(oepsNummer: String): List<Veranstaltung> {
// TODO: Implement database operations
return emptyList()
}
override suspend fun create(veranstaltung: Veranstaltung): Veranstaltung {
// TODO: Implement database operations
return veranstaltung
}
override suspend fun update(id: Uuid, veranstaltung: Veranstaltung): Veranstaltung? {
// TODO: Implement database operations
return null
}
override suspend fun delete(id: Uuid): Boolean {
// TODO: Implement database operations
return false
}
override suspend fun search(query: String): List<Veranstaltung> {
// TODO: Implement database operations
return emptyList()
}
}
@@ -1,7 +1,7 @@
package at.mocode.model
package at.mocode.repositories
import at.mocode.stammdaten.Verein
import at.mocode.tables.VereineTable
import at.mocode.tables.stammdaten.VereineTable
import com.benasher44.uuid.Uuid
import kotlinx.datetime.Clock
import org.jetbrains.exposed.sql.*
@@ -0,0 +1,15 @@
package at.mocode.repositories
import at.mocode.model.Turnier
import com.benasher44.uuid.Uuid
interface TurnierRepository {
suspend fun findAll(): List<Turnier>
suspend fun findById(id: Uuid): Turnier?
suspend fun findByVeranstaltungId(veranstaltungId: Uuid): List<Turnier>
suspend fun findByOepsTurnierNr(oepsTurnierNr: String): Turnier?
suspend fun create(turnier: Turnier): Turnier
suspend fun update(id: Uuid, turnier: Turnier): Turnier?
suspend fun delete(id: Uuid): Boolean
suspend fun search(query: String): List<Turnier>
}
@@ -0,0 +1,15 @@
package at.mocode.repositories
import at.mocode.model.Veranstaltung
import com.benasher44.uuid.Uuid
interface VeranstaltungRepository {
suspend fun findAll(): List<Veranstaltung>
suspend fun findById(id: Uuid): Veranstaltung?
suspend fun findByName(name: String): List<Veranstaltung>
suspend fun findByVeranstalterOepsNummer(oepsNummer: String): List<Veranstaltung>
suspend fun create(veranstaltung: Veranstaltung): Veranstaltung
suspend fun update(id: Uuid, veranstaltung: Veranstaltung): Veranstaltung?
suspend fun delete(id: Uuid): Boolean
suspend fun search(query: String): List<Veranstaltung>
}
@@ -1,4 +1,4 @@
package at.mocode.model
package at.mocode.repositories
import at.mocode.stammdaten.Verein
import com.benasher44.uuid.Uuid
@@ -0,0 +1,146 @@
package at.mocode.routes
import at.mocode.model.Abteilung
import at.mocode.repositories.AbteilungRepository
import at.mocode.repositories.PostgresAbteilungRepository
import com.benasher44.uuid.uuidFrom
import io.ktor.http.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Route.abteilungRoutes() {
val abteilungRepository: AbteilungRepository = PostgresAbteilungRepository()
route("/api/abteilungen") {
// GET /api/abteilungen - Get all abteilungen
get {
try {
val abteilungen = abteilungRepository.findAll()
call.respond(HttpStatusCode.OK, abteilungen)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// GET /api/abteilungen/{id} - Get abteilung by ID
get("/{id}") {
try {
val id = call.parameters["id"] ?: return@get call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing abteilung ID")
)
val uuid = uuidFrom(id)
val abteilung = abteilungRepository.findById(uuid)
if (abteilung != null) {
call.respond(HttpStatusCode.OK, abteilung)
} else {
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Abteilung not found"))
}
} catch (_: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// GET /api/abteilungen/search?q={query} - Search abteilungen
get("/search") {
try {
val query = call.request.queryParameters["q"] ?: return@get call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing search query parameter 'q'")
)
val abteilungen = abteilungRepository.search(query)
call.respond(HttpStatusCode.OK, abteilungen)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// GET /api/abteilungen/bewerb/{bewerbId} - Get abteilungen by bewerb ID
get("/bewerb/{bewerbId}") {
try {
val bewerbId = call.parameters["bewerbId"] ?: return@get call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing bewerb ID")
)
val uuid = uuidFrom(bewerbId)
val abteilungen = abteilungRepository.findByBewerbId(uuid)
call.respond(HttpStatusCode.OK, abteilungen)
} catch (_: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// GET /api/abteilungen/aktiv/{istAktiv} - Get abteilungen by active status
get("/aktiv/{istAktiv}") {
try {
val istAktiv = call.parameters["istAktiv"]?.toBoolean() ?: return@get call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing or invalid aktiv parameter")
)
val abteilungen = abteilungRepository.findByAktiv(istAktiv)
call.respond(HttpStatusCode.OK, abteilungen)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// POST /api/abteilungen - Create new abteilung
post {
try {
val abteilung = call.receive<Abteilung>()
val createdAbteilung = abteilungRepository.create(abteilung)
call.respond(HttpStatusCode.Created, createdAbteilung)
} catch (e: Exception) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to e.message))
}
}
// PUT /api/abteilungen/{id} - Update abteilung
put("/{id}") {
try {
val id = call.parameters["id"] ?: return@put call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing abteilung ID")
)
val uuid = uuidFrom(id)
val abteilung = call.receive<Abteilung>()
val updatedAbteilung = abteilungRepository.update(uuid, abteilung)
if (updatedAbteilung != null) {
call.respond(HttpStatusCode.OK, updatedAbteilung)
} else {
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Abteilung not found"))
}
} catch (_: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
} catch (e: Exception) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to e.message))
}
}
// DELETE /api/abteilungen/{id} - Delete abteilung
delete("/{id}") {
try {
val id = call.parameters["id"] ?: return@delete call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing abteilung ID")
)
val uuid = uuidFrom(id)
val deleted = abteilungRepository.delete(uuid)
if (deleted) {
call.respond(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Abteilung not found"))
}
} catch (_: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
}
}
@@ -1,8 +1,8 @@
package at.mocode.routes
import at.mocode.model.Artikel
import at.mocode.model.ArtikelRepository
import at.mocode.model.PostgresArtikelRepository
import at.mocode.repositories.ArtikelRepository
import at.mocode.services.ServiceLocator
import com.benasher44.uuid.uuidFrom
import io.ktor.http.*
import io.ktor.server.request.*
@@ -11,9 +11,9 @@ import io.ktor.server.routing.*
import kotlin.collections.mapOf
fun Route.artikelRoutes() {
val artikelRepository: ArtikelRepository = PostgresArtikelRepository()
val artikelRepository: ArtikelRepository = ServiceLocator.artikelRepository
route("/api/artikel") {
route("/artikel") {
// GET /api/artikel - Get all articles
get {
try {
@@ -0,0 +1,160 @@
package at.mocode.routes
import at.mocode.model.Bewerb
import at.mocode.repositories.BewerbRepository
import at.mocode.repositories.PostgresBewerbRepository
import com.benasher44.uuid.uuidFrom
import io.ktor.http.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Route.bewerbRoutes() {
val bewerbRepository: BewerbRepository = PostgresBewerbRepository()
route("/api/bewerbe") {
// GET /api/bewerbe - Get all bewerbe
get {
try {
val bewerbe = bewerbRepository.findAll()
call.respond(HttpStatusCode.OK, bewerbe)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// GET /api/bewerbe/{id} - Get bewerb by ID
get("/{id}") {
try {
val id = call.parameters["id"] ?: return@get call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing bewerb ID")
)
val uuid = uuidFrom(id)
val bewerb = bewerbRepository.findById(uuid)
if (bewerb != null) {
call.respond(HttpStatusCode.OK, bewerb)
} else {
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Bewerb not found"))
}
} catch (_: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// GET /api/bewerbe/search?q={query} - Search bewerbe
get("/search") {
try {
val query = call.request.queryParameters["q"] ?: return@get call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing search query parameter 'q'")
)
val bewerbe = bewerbRepository.search(query)
call.respond(HttpStatusCode.OK, bewerbe)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// GET /api/bewerbe/turnier/{turnierId} - Get bewerbe by turnier ID
get("/turnier/{turnierId}") {
try {
val turnierId = call.parameters["turnierId"] ?: return@get call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing turnier ID")
)
val uuid = uuidFrom(turnierId)
val bewerbe = bewerbRepository.findByTurnierId(uuid)
call.respond(HttpStatusCode.OK, bewerbe)
} catch (_: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// GET /api/bewerbe/sparte/{sparte} - Get bewerbe by sparte
get("/sparte/{sparte}") {
try {
val sparte = call.parameters["sparte"] ?: return@get call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing sparte parameter")
)
val bewerbe = bewerbRepository.findBySparte(sparte)
call.respond(HttpStatusCode.OK, bewerbe)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// GET /api/bewerbe/klasse/{klasse} - Get bewerbe by klasse
get("/klasse/{klasse}") {
try {
val klasse = call.parameters["klasse"] ?: return@get call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing klasse parameter")
)
val bewerbe = bewerbRepository.findByKlasse(klasse)
call.respond(HttpStatusCode.OK, bewerbe)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// POST /api/bewerbe - Create new bewerb
post {
try {
val bewerb = call.receive<Bewerb>()
val createdBewerb = bewerbRepository.create(bewerb)
call.respond(HttpStatusCode.Created, createdBewerb)
} catch (e: Exception) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to e.message))
}
}
// PUT /api/bewerbe/{id} - Update bewerb
put("/{id}") {
try {
val id = call.parameters["id"] ?: return@put call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing bewerb ID")
)
val uuid = uuidFrom(id)
val bewerb = call.receive<Bewerb>()
val updatedBewerb = bewerbRepository.update(uuid, bewerb)
if (updatedBewerb != null) {
call.respond(HttpStatusCode.OK, updatedBewerb)
} else {
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Bewerb not found"))
}
} catch (_: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
} catch (e: Exception) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to e.message))
}
}
// DELETE /api/bewerbe/{id} - Delete bewerb
delete("/{id}") {
try {
val id = call.parameters["id"] ?: return@delete call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing bewerb ID")
)
val uuid = uuidFrom(id)
val deleted = bewerbRepository.delete(uuid)
if (deleted) {
call.respond(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Bewerb not found"))
}
} catch (_: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
}
}
@@ -1,7 +1,7 @@
package at.mocode.routes
import at.mocode.model.DomLizenzRepository
import at.mocode.model.PostgresDomLizenzRepository
import at.mocode.repositories.DomLizenzRepository
import at.mocode.repositories.PostgresDomLizenzRepository
import at.mocode.model.domaene.DomLizenz
import com.benasher44.uuid.uuidFrom
import io.ktor.http.*
@@ -0,0 +1,258 @@
package at.mocode.routes
import at.mocode.repositories.DomPferdRepository
import at.mocode.repositories.PostgresDomPferdRepository
import at.mocode.model.domaene.DomPferd
import com.benasher44.uuid.uuidFrom
import io.ktor.http.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Route.domPferdRoutes() {
val domPferdRepository: DomPferdRepository = PostgresDomPferdRepository()
route("/api/horses") {
// GET /api/horses - Get all horses
get {
try {
val horses = domPferdRepository.findAll()
call.respond(HttpStatusCode.OK, horses)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// GET /api/horses/{id} - Get horse by ID
get("/{id}") {
try {
val id = call.parameters["id"] ?: return@get call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing horse ID")
)
val uuid = uuidFrom(id)
val horse = domPferdRepository.findById(uuid)
if (horse != null) {
call.respond(HttpStatusCode.OK, horse)
} else {
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Horse not found"))
}
} catch (_: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// GET /api/horses/oeps/{oepsSatzNr} - Get horse by OEPS number
get("/oeps/{oepsSatzNr}") {
try {
val oepsSatzNr = call.parameters["oepsSatzNr"] ?: return@get call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing OEPS Satz number")
)
val horse = domPferdRepository.findByOepsSatzNr(oepsSatzNr)
if (horse != null) {
call.respond(HttpStatusCode.OK, horse)
} else {
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Horse not found"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// GET /api/horses/lebensnummer/{lebensnummer} - Get horse by life number
get("/lebensnummer/{lebensnummer}") {
try {
val lebensnummer = call.parameters["lebensnummer"] ?: return@get call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing Lebensnummer")
)
val horse = domPferdRepository.findByLebensnummer(lebensnummer)
if (horse != null) {
call.respond(HttpStatusCode.OK, horse)
} else {
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Horse not found"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// GET /api/horses/search?q={query} - Search horses
get("/search") {
try {
val query = call.request.queryParameters["q"] ?: return@get call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing search query parameter 'q'")
)
val horses = domPferdRepository.search(query)
call.respond(HttpStatusCode.OK, horses)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// GET /api/horses/name/{name} - Get horses by name
get("/name/{name}") {
try {
val name = call.parameters["name"] ?: return@get call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing horse name")
)
val horses = domPferdRepository.findByName(name)
call.respond(HttpStatusCode.OK, horses)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// GET /api/horses/owner/{ownerId} - Get horses by owner ID
get("/owner/{ownerId}") {
try {
val ownerId = call.parameters["ownerId"] ?: return@get call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing owner ID")
)
val uuid = uuidFrom(ownerId)
val horses = domPferdRepository.findByBesitzerId(uuid)
call.respond(HttpStatusCode.OK, horses)
} catch (_: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// GET /api/horses/responsible/{personId} - Get horses by responsible person ID
get("/responsible/{personId}") {
try {
val personId = call.parameters["personId"] ?: return@get call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing person ID")
)
val uuid = uuidFrom(personId)
val horses = domPferdRepository.findByVerantwortlichePersonId(uuid)
call.respond(HttpStatusCode.OK, horses)
} catch (_: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// GET /api/horses/club/{clubId} - Get horses by home club ID
get("/club/{clubId}") {
try {
val clubId = call.parameters["clubId"] ?: return@get call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing club ID")
)
val uuid = uuidFrom(clubId)
val horses = domPferdRepository.findByHeimatVereinId(uuid)
call.respond(HttpStatusCode.OK, horses)
} catch (_: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// GET /api/horses/breed/{breed} - Get horses by breed
get("/breed/{breed}") {
try {
val breed = call.parameters["breed"] ?: return@get call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing breed")
)
val horses = domPferdRepository.findByRasse(breed)
call.respond(HttpStatusCode.OK, horses)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// GET /api/horses/birth-year/{year} - Get horses by birth year
get("/birth-year/{year}") {
try {
val yearStr = call.parameters["year"] ?: return@get call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing birth year")
)
val year = yearStr.toIntOrNull() ?: return@get call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Invalid birth year format")
)
val horses = domPferdRepository.findByGeburtsjahr(year)
call.respond(HttpStatusCode.OK, horses)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// GET /api/horses/active - Get active horses only
get("/active") {
try {
val horses = domPferdRepository.findActiveHorses()
call.respond(HttpStatusCode.OK, horses)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// POST /api/horses - Create a new horse
post {
try {
val horse = call.receive<DomPferd>()
val createdHorse = domPferdRepository.create(horse)
call.respond(HttpStatusCode.Created, createdHorse)
} catch (e: Exception) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to e.message))
}
}
// PUT /api/horses/{id} - Update horse
put("/{id}") {
try {
val id = call.parameters["id"] ?: return@put call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing horse ID")
)
val uuid = uuidFrom(id)
val horse = call.receive<DomPferd>()
val updatedHorse = domPferdRepository.update(uuid, horse)
if (updatedHorse != null) {
call.respond(HttpStatusCode.OK, updatedHorse)
} else {
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Horse not found"))
}
} catch (_: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
} catch (e: Exception) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to e.message))
}
}
// DELETE /api/horses/{id} - Delete horse
delete("/{id}") {
try {
val id = call.parameters["id"] ?: return@delete call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing horse ID")
)
val uuid = uuidFrom(id)
val deleted = domPferdRepository.delete(uuid)
if (deleted) {
call.respond(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Horse not found"))
}
} catch (_: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
}
}
@@ -0,0 +1,183 @@
package at.mocode.routes
import at.mocode.repositories.DomQualifikationRepository
import at.mocode.repositories.PostgresDomQualifikationRepository
import at.mocode.model.domaene.DomQualifikation
import com.benasher44.uuid.uuidFrom
import io.ktor.http.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.datetime.LocalDate
fun Route.domQualifikationRoutes() {
val domQualifikationRepository: DomQualifikationRepository = PostgresDomQualifikationRepository()
route("/api/dom-qualifikationen") {
// GET /api/dom-qualifikationen - Get all qualifications
get {
try {
val qualifikationen = domQualifikationRepository.findAll()
call.respond(HttpStatusCode.OK, qualifikationen)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// GET /api/dom-qualifikationen/{id} - Get qualification by ID
get("/{id}") {
try {
val id = call.parameters["id"] ?: return@get call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing qualification ID")
)
val uuid = uuidFrom(id)
val qualifikation = domQualifikationRepository.findById(uuid)
if (qualifikation != null) {
call.respond(HttpStatusCode.OK, qualifikation)
} else {
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Qualification not found"))
}
} catch (_: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// GET /api/dom-qualifikationen/person/{personId} - Get qualifications by person ID
get("/person/{personId}") {
try {
val personId = call.parameters["personId"] ?: return@get call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing person ID")
)
val uuid = uuidFrom(personId)
val qualifikationen = domQualifikationRepository.findByPersonId(uuid)
call.respond(HttpStatusCode.OK, qualifikationen)
} catch (_: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// GET /api/dom-qualifikationen/person/{personId}/active - Get active qualifications by person ID
get("/person/{personId}/active") {
try {
val personId = call.parameters["personId"] ?: return@get call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing person ID")
)
val uuid = uuidFrom(personId)
val qualifikationen = domQualifikationRepository.findActiveByPersonId(uuid)
call.respond(HttpStatusCode.OK, qualifikationen)
} catch (_: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// GET /api/dom-qualifikationen/qual-typ/{qualTypId} - Get qualifications by qualification type
get("/qual-typ/{qualTypId}") {
try {
val qualTypId = call.parameters["qualTypId"] ?: return@get call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing qualification type ID")
)
val uuid = uuidFrom(qualTypId)
val qualifikationen = domQualifikationRepository.findByQualTypId(uuid)
call.respond(HttpStatusCode.OK, qualifikationen)
} catch (_: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// GET /api/dom-qualifikationen/validity-period?from={fromDate}&to={toDate} - Get qualifications by validity period
get("/validity-period") {
try {
val fromDateStr = call.request.queryParameters["from"]
val toDateStr = call.request.queryParameters["to"]
val fromDate = fromDateStr?.let { LocalDate.parse(it) }
val toDate = toDateStr?.let { LocalDate.parse(it) }
val qualifikationen = domQualifikationRepository.findByValidityPeriod(fromDate, toDate)
call.respond(HttpStatusCode.OK, qualifikationen)
} catch (e: Exception) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid date format. Use YYYY-MM-DD"))
}
}
// GET /api/dom-qualifikationen/search?q={query} - Search qualifications
get("/search") {
try {
val query = call.request.queryParameters["q"] ?: return@get call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing search query parameter 'q'")
)
val qualifikationen = domQualifikationRepository.search(query)
call.respond(HttpStatusCode.OK, qualifikationen)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// POST /api/dom-qualifikationen - Create new qualification
post {
try {
val qualifikation = call.receive<DomQualifikation>()
val createdQualifikation = domQualifikationRepository.create(qualifikation)
call.respond(HttpStatusCode.Created, createdQualifikation)
} catch (e: Exception) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to e.message))
}
}
// PUT /api/dom-qualifikationen/{id} - Update qualification
put("/{id}") {
try {
val id = call.parameters["id"] ?: return@put call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing qualification ID")
)
val uuid = uuidFrom(id)
val qualifikation = call.receive<DomQualifikation>()
val updatedQualifikation = domQualifikationRepository.update(uuid, qualifikation)
if (updatedQualifikation != null) {
call.respond(HttpStatusCode.OK, updatedQualifikation)
} else {
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Qualification not found"))
}
} catch (_: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
} catch (e: Exception) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to e.message))
}
}
// DELETE /api/dom-qualifikationen/{id} - Delete qualification
delete("/{id}") {
try {
val id = call.parameters["id"] ?: return@delete call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing qualification ID")
)
val uuid = uuidFrom(id)
val deleted = domQualifikationRepository.delete(uuid)
if (deleted) {
call.respond(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Qualification not found"))
}
} catch (_: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
}
}
@@ -1,7 +1,7 @@
package at.mocode.routes
import at.mocode.model.PersonRepository
import at.mocode.model.PostgresPersonRepository
import at.mocode.repositories.PersonRepository
import at.mocode.repositories.PostgresPersonRepository
import at.mocode.stammdaten.Person
import com.benasher44.uuid.uuidFrom
import io.ktor.http.*
@@ -0,0 +1,85 @@
package at.mocode.routes
import io.ktor.server.routing.*
/**
* Centralized route configuration that organizes all API routes
* by domain and functionality for better maintainability
*/
object RouteConfiguration {
/**
* Configure all API routes in a structured manner
*/
fun Route.configureApiRoutes() {
route("/api") {
// Core domain routes
configureCoreRoutes()
// Domain-specific routes
configureDomainRoutes()
// Event/Tournament management routes
configureEventRoutes()
}
}
/**
* Configure core domain routes (Person, Verein, etc.)
*/
private fun Route.configureCoreRoutes() {
// Person and organization management
personRoutes()
vereinRoutes()
// Articles and products
artikelRoutes()
}
/**
* Configure domain-specific routes (licenses, horses, qualifications)
*/
private fun Route.configureDomainRoutes() {
route("/domain") {
domLizenzRoutes()
domPferdRoutes()
domQualifikationRoutes()
}
}
/**
* Configure event and tournament management routes
*/
private fun Route.configureEventRoutes() {
route("/events") {
// Event hierarchy: Veranstaltung -> Turnier -> Bewerb -> Abteilung
veranstaltungRoutes()
turnierRoutes()
bewerbRoutes()
abteilungRoutes()
}
}
/**
* Configure administrative and utility routes
*/
private fun Route.configureAdminRoutes() {
route("/admin") {
// Future: Admin-specific endpoints
// userManagementRoutes()
// systemConfigRoutes()
// auditLogRoutes()
}
}
/**
* Configure public/external API routes
*/
private fun Route.configurePublicRoutes() {
route("/public") {
// Future: Public endpoints that don't require authentication
// publicEventListRoutes()
// publicResultsRoutes()
}
}
}
@@ -0,0 +1,132 @@
package at.mocode.routes
import at.mocode.model.Turnier
import at.mocode.repositories.TurnierRepository
import at.mocode.repositories.PostgresTurnierRepository
import com.benasher44.uuid.uuidFrom
import io.ktor.http.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Route.turnierRoutes() {
val turnierRepository: TurnierRepository = PostgresTurnierRepository()
route("/api/turniere") {
// GET /api/turniere - Get all turniere
get {
try {
val turniere = turnierRepository.findAll()
call.respond(HttpStatusCode.OK, turniere)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// GET /api/turniere/{id} - Get turnier by ID
get("/{id}") {
try {
val id = call.parameters["id"] ?: return@get call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing turnier ID")
)
val uuid = uuidFrom(id)
val turnier = turnierRepository.findById(uuid)
if (turnier != null) {
call.respond(HttpStatusCode.OK, turnier)
} else {
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Turnier not found"))
}
} catch (_: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// GET /api/turniere/search?q={query} - Search turniere
get("/search") {
try {
val query = call.request.queryParameters["q"] ?: return@get call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing search query parameter 'q'")
)
val turniere = turnierRepository.search(query)
call.respond(HttpStatusCode.OK, turniere)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// GET /api/turniere/veranstaltung/{veranstaltungId} - Get turniere by veranstaltung ID
get("/veranstaltung/{veranstaltungId}") {
try {
val veranstaltungId = call.parameters["veranstaltungId"] ?: return@get call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing veranstaltung ID")
)
val uuid = uuidFrom(veranstaltungId)
val turniere = turnierRepository.findByVeranstaltungId(uuid)
call.respond(HttpStatusCode.OK, turniere)
} catch (_: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// POST /api/turniere - Create new turnier
post {
try {
val turnier = call.receive<Turnier>()
val createdTurnier = turnierRepository.create(turnier)
call.respond(HttpStatusCode.Created, createdTurnier)
} catch (e: Exception) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to e.message))
}
}
// PUT /api/turniere/{id} - Update turnier
put("/{id}") {
try {
val id = call.parameters["id"] ?: return@put call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing turnier ID")
)
val uuid = uuidFrom(id)
val turnier = call.receive<Turnier>()
val updatedTurnier = turnierRepository.update(uuid, turnier)
if (updatedTurnier != null) {
call.respond(HttpStatusCode.OK, updatedTurnier)
} else {
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Turnier not found"))
}
} catch (_: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
} catch (e: Exception) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to e.message))
}
}
// DELETE /api/turniere/{id} - Delete turnier
delete("/{id}") {
try {
val id = call.parameters["id"] ?: return@delete call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing turnier ID")
)
val uuid = uuidFrom(id)
val deleted = turnierRepository.delete(uuid)
if (deleted) {
call.respond(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Turnier not found"))
}
} catch (_: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
}
}
@@ -0,0 +1,115 @@
package at.mocode.routes
import at.mocode.model.Veranstaltung
import at.mocode.repositories.VeranstaltungRepository
import at.mocode.services.ServiceLocator
import com.benasher44.uuid.uuidFrom
import io.ktor.http.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Route.veranstaltungRoutes() {
val veranstaltungRepository: VeranstaltungRepository = ServiceLocator.veranstaltungRepository
route("/veranstaltungen") {
// GET /api/veranstaltungen - Get all veranstaltungen
get {
try {
val veranstaltungen = veranstaltungRepository.findAll()
call.respond(HttpStatusCode.OK, veranstaltungen)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// GET /api/veranstaltungen/{id} - Get veranstaltung by ID
get("/{id}") {
try {
val id = call.parameters["id"] ?: return@get call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing veranstaltung ID")
)
val uuid = uuidFrom(id)
val veranstaltung = veranstaltungRepository.findById(uuid)
if (veranstaltung != null) {
call.respond(HttpStatusCode.OK, veranstaltung)
} else {
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Veranstaltung not found"))
}
} catch (_: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// GET /api/veranstaltungen/search?q={query} - Search veranstaltungen
get("/search") {
try {
val query = call.request.queryParameters["q"] ?: return@get call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing search query parameter 'q'")
)
val veranstaltungen = veranstaltungRepository.search(query)
call.respond(HttpStatusCode.OK, veranstaltungen)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
// POST /api/veranstaltungen - Create new veranstaltung
post {
try {
val veranstaltung = call.receive<Veranstaltung>()
val createdVeranstaltung = veranstaltungRepository.create(veranstaltung)
call.respond(HttpStatusCode.Created, createdVeranstaltung)
} catch (e: Exception) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to e.message))
}
}
// PUT /api/veranstaltungen/{id} - Update veranstaltung
put("/{id}") {
try {
val id = call.parameters["id"] ?: return@put call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing veranstaltung ID")
)
val uuid = uuidFrom(id)
val veranstaltung = call.receive<Veranstaltung>()
val updatedVeranstaltung = veranstaltungRepository.update(uuid, veranstaltung)
if (updatedVeranstaltung != null) {
call.respond(HttpStatusCode.OK, updatedVeranstaltung)
} else {
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Veranstaltung not found"))
}
} catch (_: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
} catch (e: Exception) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to e.message))
}
}
// DELETE /api/veranstaltungen/{id} - Delete veranstaltung
delete("/{id}") {
try {
val id = call.parameters["id"] ?: return@delete call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing veranstaltung ID")
)
val uuid = uuidFrom(id)
val deleted = veranstaltungRepository.delete(uuid)
if (deleted) {
call.respond(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Veranstaltung not found"))
}
} catch (_: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
}
}
}
}
@@ -1,7 +1,7 @@
package at.mocode.routes
import at.mocode.model.PostgresVereinRepository
import at.mocode.model.VereinRepository
import at.mocode.repositories.PostgresVereinRepository
import at.mocode.repositories.VereinRepository
import at.mocode.stammdaten.Verein
import com.benasher44.uuid.uuidFrom
import io.ktor.http.*
@@ -0,0 +1,39 @@
package at.mocode.services
import at.mocode.repositories.*
/**
* Service locator pattern for managing repository instances.
* This provides a centralized way to access repository implementations
* and makes it easier to switch implementations or add caching/decorators.
*/
object ServiceLocator {
// Repository instances - lazy initialization
val artikelRepository: ArtikelRepository by lazy { PostgresArtikelRepository() }
val vereinRepository: VereinRepository by lazy { PostgresVereinRepository() }
val personRepository: PersonRepository by lazy { PostgresPersonRepository() }
val domLizenzRepository: DomLizenzRepository by lazy { PostgresDomLizenzRepository() }
val domPferdRepository: DomPferdRepository by lazy { PostgresDomPferdRepository() }
val domQualifikationRepository: DomQualifikationRepository by lazy { PostgresDomQualifikationRepository() }
val abteilungRepository: AbteilungRepository by lazy { PostgresAbteilungRepository() }
val bewerbRepository: BewerbRepository by lazy { PostgresBewerbRepository() }
val turnierRepository: TurnierRepository by lazy { PostgresTurnierRepository() }
val veranstaltungRepository: VeranstaltungRepository by lazy { PostgresVeranstaltungRepository() }
/**
* Initialize all repositories - useful for eager loading or validation
*/
fun initializeAll() {
artikelRepository
vereinRepository
personRepository
domLizenzRepository
domPferdRepository
domQualifikationRepository
abteilungRepository
bewerbRepository
turnierRepository
veranstaltungRepository
}
}
@@ -44,7 +44,7 @@ object BewerbTable : Table("bewerbe") {
val geldpreisVorlageId = uuid("geldpreis_vorlage_id").nullable()
// Ort/Zeit (Default-Werte)
val standardPlatzId = uuid("standard_platz_id").nullable().references(PlaetzeTable.id)
val standardPlatzId = uuid("standard_platz_id").references(PlaetzeTable.id)
val standardDatum = date("standard_datum").nullable()
val standardBeginnzeitTypE = varchar("standard_beginnzeit_typ", 50).default("ANSCHLIESSEND")
val standardBeginnzeitFix = time("standard_beginnzeit_fix").nullable()
@@ -1,5 +1,6 @@
package at.mocode.tables
import at.mocode.tables.stammdaten.PersonenTable
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.kotlin.datetime.date // Für kotlinx-datetime LocalDate
import org.jetbrains.exposed.sql.kotlin.datetime.datetime // Für kotlinx-datetime LocalDateTime
@@ -1,6 +1,6 @@
package at.mocode.tables.domaene
import at.mocode.tables.PersonenTable
import at.mocode.tables.stammdaten.PersonenTable
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.kotlin.datetime.date
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
@@ -2,8 +2,8 @@ package at.mocode.tables.domaene
import at.mocode.enums.DatenQuelleE
import at.mocode.enums.PferdeGeschlechtE
import at.mocode.tables.PersonenTable
import at.mocode.tables.VereineTable
import at.mocode.tables.stammdaten.PersonenTable
import at.mocode.tables.stammdaten.VereineTable
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
@@ -6,7 +6,6 @@ import at.mocode.tables.TurniereTable
import at.mocode.tables.VeranstaltungenTable
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.kotlin.datetime.date
import org.jetbrains.exposed.sql.kotlin.datetime.time
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
// Event models tables
@@ -0,0 +1,124 @@
package at.mocode.utils
import io.ktor.http.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
/**
* Standardized API response wrapper for a consistent response format
*/
@Serializable
data class ApiResponse<T>(
val success: Boolean,
val data: T? = null,
val error: String? = null,
val message: String? = null
)
/**
* Error response data class
*/
@Serializable
data class ErrorResponse(
val code: String,
val message: String,
val details: String? = null
)
/**
* Utility object for common HTTP responses
*/
object ResponseUtils {
/**
* Respond with success and data
*/
suspend inline fun <reified T> RoutingCall.respondSuccess(
data: T,
status: HttpStatusCode = HttpStatusCode.OK,
message: String? = null
) {
respond(status, ApiResponse(success = true, data = data, message = message))
}
/**
* Respond with error
*/
suspend fun RoutingCall.respondError(
error: String,
status: HttpStatusCode = HttpStatusCode.InternalServerError,
details: String? = null
) {
respond(status, ApiResponse<Nothing>(
success = false,
error = error,
message = details
))
}
/**
* Respond with validation error
*/
suspend fun RoutingCall.respondValidationError(
message: String,
details: String? = null
) {
respondError(
error = "VALIDATION_ERROR",
status = HttpStatusCode.BadRequest,
details = "$message${details?.let { " - $it" } ?: ""}"
)
}
/**
* Respond with not found error
*/
suspend fun RoutingCall.respondNotFound(
resource: String = "Resource"
) {
respondError(
error = "NOT_FOUND",
status = HttpStatusCode.NotFound,
details = "$resource not found"
)
}
/**
* Respond with a created resource
*/
suspend inline fun <reified T> RoutingCall.respondCreated(
data: T,
message: String? = null
) {
respondSuccess(data, HttpStatusCode.Created, message)
}
/**
* Respond with no content (for successful deletions)
*/
suspend fun RoutingCall.respondNoContent() {
respond(HttpStatusCode.NoContent)
}
/**
* Handle common exceptions and respond appropriately
*/
suspend fun RoutingCall.handleException(
exception: Exception,
operation: String = "operation"
) {
when (exception) {
is IllegalArgumentException -> respondValidationError(
"Invalid input for $operation",
exception.message
)
is NoSuchElementException -> respondNotFound()
else -> respondError(
"Internal server error during $operation",
HttpStatusCode.InternalServerError,
exception.message
)
}
}
}
@@ -0,0 +1,114 @@
package at.mocode.utils
import com.benasher44.uuid.Uuid
import com.benasher44.uuid.uuidFrom
import io.ktor.server.request.*
import io.ktor.server.routing.*
import at.mocode.utils.ResponseUtils.respondValidationError
/**
* Utility functions for common route operations
*/
object RouteUtils {
/**
* Extract and validate UUID parameter from route
*/
suspend fun RoutingCall.getUuidParameter(
paramName: String,
resourceName: String = paramName
): Uuid? {
val paramValue = parameters[paramName]
if (paramValue == null) {
respondValidationError("Missing $resourceName ID")
return null
}
return try {
uuidFrom(paramValue)
} catch (e: IllegalArgumentException) {
respondValidationError("Invalid UUID format for $resourceName ID")
null
}
}
/**
* Extract and validate required string parameter from route
*/
suspend fun RoutingCall.getStringParameter(
paramName: String,
resourceName: String = paramName
): String? {
val paramValue = parameters[paramName]
if (paramValue.isNullOrBlank()) {
respondValidationError("Missing or empty $resourceName parameter")
return null
}
return paramValue
}
/**
* Extract and validate boolean parameter from route
*/
suspend fun RoutingCall.getBooleanParameter(
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
}
}
/**
* Extract and validate required query parameter
*/
suspend fun RoutingCall.getQueryParameter(
paramName: String,
resourceName: String = paramName
): String? {
val paramValue = request.queryParameters[paramName]
if (paramValue.isNullOrBlank()) {
respondValidationError("Missing search query parameter '$paramName'")
return null
}
return paramValue
}
/**
* Safe receive with error handling
*/
suspend inline fun <reified T : Any> RoutingCall.safeReceive(
resourceName: String = "request body"
): T? {
return try {
receive<T>()
} catch (e: Exception) {
respondValidationError("Invalid $resourceName format", e.message)
null
}
}
/**
* Execute repository operation with standardized error handling
*/
suspend inline fun <T> RoutingCall.executeRepositoryOperation(
operation: String,
block: () -> T
): T? {
return try {
block()
} catch (e: Exception) {
ResponseUtils.run { handleException(e, operation) }
null
}
}
}
@@ -0,0 +1,55 @@
package at.mocode
import at.mocode.model.domaene.DomQualifikation
import com.benasher44.uuid.uuid4
import kotlinx.datetime.LocalDate
import kotlin.test.*
class DomQualifikationTest {
@Test
fun testDomQualifikationCreation() {
val personId = uuid4()
val qualTypId = uuid4()
val qualification = DomQualifikation(
personId = personId,
qualTypId = qualTypId,
bemerkung = "Test qualification",
gueltigVon = LocalDate(2024, 1, 1),
gueltigBis = LocalDate(2024, 12, 31),
istAktiv = true
)
assertEquals(personId, qualification.personId)
assertEquals(qualTypId, qualification.qualTypId)
assertEquals("Test qualification", qualification.bemerkung)
assertEquals(LocalDate(2024, 1, 1), qualification.gueltigVon)
assertEquals(LocalDate(2024, 12, 31), qualification.gueltigBis)
assertTrue(qualification.istAktiv)
assertNotNull(qualification.qualifikationId)
assertNotNull(qualification.createdAt)
assertNotNull(qualification.updatedAt)
}
@Test
fun testDomQualifikationDefaults() {
val personId = uuid4()
val qualTypId = uuid4()
val qualification = DomQualifikation(
personId = personId,
qualTypId = qualTypId
)
assertEquals(personId, qualification.personId)
assertEquals(qualTypId, qualification.qualTypId)
assertNull(qualification.bemerkung)
assertNull(qualification.gueltigVon)
assertNull(qualification.gueltigBis)
assertTrue(qualification.istAktiv) // Default should be true
assertNotNull(qualification.qualifikationId)
assertNotNull(qualification.createdAt)
assertNotNull(qualification.updatedAt)
}
}
@@ -1,3 +1,5 @@
package at.mocode.model.domaene
import at.mocode.serializers.KotlinInstantSerializer
import at.mocode.serializers.KotlinLocalDateSerializer
import at.mocode.serializers.UuidSerializer