(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 ## Data Models
### Person ### 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 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.HikariConfig
import com.zaxxer.hikari.HikariDataSource import com.zaxxer.hikari.HikariDataSource
import io.ktor.server.application.* import io.ktor.server.application.*
@@ -22,7 +31,7 @@ import java.util.concurrent.TimeUnit
*/ */
fun Application.configureDatabase() { fun Application.configureDatabase() {
val log = LoggerFactory.getLogger("DatabaseInitialization") val log = LoggerFactory.getLogger("DatabaseInitialization")
var connectionSuccessful = false var connectionSuccessful: Boolean
// Environment detection // Environment detection
val isTestEnvironment = System.getProperty("isTestEnvironment")?.toBoolean() ?: false val isTestEnvironment = System.getProperty("isTestEnvironment")?.toBoolean() ?: false
@@ -32,7 +41,7 @@ fun Application.configureDatabase() {
// Get database configuration from application.yaml if available // Get database configuration from application.yaml if available
val dbConfig = try { val dbConfig = try {
environment.config.config("database") environment.config.config("database")
} catch (e: ApplicationConfigurationException) { } catch (_: ApplicationConfigurationException) {
log.warn("No database configuration found in application.yaml, using environment variables") log.warn("No database configuration found in application.yaml, using environment variables")
null null
} }
@@ -51,7 +60,7 @@ fun Application.configureDatabase() {
} }
} }
// Initialize schema if connection was successful // Initialize schema if the connection was successful
if (connectionSuccessful) { if (connectionSuccessful) {
initializeSchema(log, isTestEnvironment, isIdeaEnvironment) initializeSchema(log, isTestEnvironment, isIdeaEnvironment)
} else { } else {
@@ -174,14 +183,15 @@ private fun initializeSchema(log: Logger, isTestEnvironment: Boolean, isIdeaEnvi
try { try {
// Create all tables if they don't exist // Create all tables if they don't exist
SchemaUtils.create( SchemaUtils.create(
_root_ide_package_.at.mocode.tables.VereineTable, VereineTable,
_root_ide_package_.at.mocode.tables.PersonenTable, PersonenTable,
_root_ide_package_.at.mocode.tables.PferdeTable, PferdeTable,
_root_ide_package_.at.mocode.tables.VeranstaltungenTable, VeranstaltungenTable,
_root_ide_package_.at.mocode.tables.TurniereTable, TurniereTable,
_root_ide_package_.at.mocode.tables.ArtikelTable, ArtikelTable,
_root_ide_package_.at.mocode.tables.PlaetzeTable, PlaetzeTable,
_root_ide_package_.at.mocode.tables.LizenzenTable LizenzenTable,
DomQualifikationTable
// Add more tables here if needed // Add more tables here if needed
) )
log.info("Database schema initialized successfully.") log.info("Database schema initialized successfully.")
@@ -1,20 +1,20 @@
package at.mocode.plugins package at.mocode.plugins
import at.mocode.routes.artikelRoutes import at.mocode.config.AppConfig
import at.mocode.routes.domLizenzRoutes import at.mocode.routes.RouteConfiguration.configureApiRoutes
import at.mocode.routes.personRoutes
import at.mocode.routes.vereinRoutes
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.response.respondText import io.ktor.server.response.respondText
import io.ktor.server.routing.application
import io.ktor.server.routing.get import io.ktor.server.routing.get
import io.ktor.server.routing.routing 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() { fun Application.configureRouting() {
// Load application configuration
val appConfig = AppConfig.loadConfig(this)
routing { routing {
// Health check endpoint // Health check endpoint
get("/health") { get("/health") {
@@ -26,18 +26,10 @@ fun Application.configureRouting() {
// Root endpoint with basic information (API info endpoint) // Root endpoint with basic information (API info endpoint)
get("/api") { get("/api") {
// Read application info from config if available call.respondText(appConfig.getAppInfoString())
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")
} }
// API routes // Configure all API routes using the centralized configuration
personRoutes() configureApiRoutes()
vereinRoutes()
artikelRoutes()
domLizenzRoutes()
} }
} }
@@ -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 import com.benasher44.uuid.Uuid
interface ArtikelRepository { 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 at.mocode.model.domaene.DomLizenz
import com.benasher44.uuid.Uuid import com.benasher44.uuid.Uuid
@@ -1,4 +1,4 @@
package at.mocode.model package at.mocode.repositories
import at.mocode.model.domaene.DomPferd import at.mocode.model.domaene.DomPferd
import com.benasher44.uuid.Uuid 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 { interface EventRepository {
} }
@@ -1,4 +1,4 @@
package at.mocode.model package at.mocode.repositories
import at.mocode.stammdaten.Person import at.mocode.stammdaten.Person
import com.benasher44.uuid.Uuid 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 at.mocode.tables.ArtikelTable
import com.benasher44.uuid.Uuid import com.benasher44.uuid.Uuid
import com.ionspin.kotlin.bignum.decimal.BigDecimal 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.model.domaene.DomLizenz
import at.mocode.tables.DomLizenzTable import at.mocode.tables.domaene.DomLizenzTable
import com.benasher44.uuid.Uuid import com.benasher44.uuid.Uuid
import com.benasher44.uuid.uuidFrom
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq 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.model.domaene.DomPferd
import at.mocode.tables.DomPferdTable import at.mocode.tables.domaene.DomPferdTable
import com.benasher44.uuid.Uuid import com.benasher44.uuid.Uuid
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.like
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
class PostgresDomPferdRepository : DomPferdRepository { 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 { class PostgresEventRepository {
} }
@@ -1,8 +1,8 @@
package at.mocode.model package at.mocode.repositories
import at.mocode.enums.FunktionaerRolle import at.mocode.enums.FunktionaerRolle
import at.mocode.stammdaten.Person import at.mocode.stammdaten.Person
import at.mocode.tables.PersonenTable import at.mocode.tables.stammdaten.PersonenTable
import com.benasher44.uuid.Uuid import com.benasher44.uuid.Uuid
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import org.jetbrains.exposed.sql.* 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.stammdaten.Verein
import at.mocode.tables.VereineTable import at.mocode.tables.stammdaten.VereineTable
import com.benasher44.uuid.Uuid import com.benasher44.uuid.Uuid
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import org.jetbrains.exposed.sql.* 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 at.mocode.stammdaten.Verein
import com.benasher44.uuid.Uuid 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 package at.mocode.routes
import at.mocode.model.Artikel import at.mocode.model.Artikel
import at.mocode.model.ArtikelRepository import at.mocode.repositories.ArtikelRepository
import at.mocode.model.PostgresArtikelRepository import at.mocode.services.ServiceLocator
import com.benasher44.uuid.uuidFrom import com.benasher44.uuid.uuidFrom
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.request.* import io.ktor.server.request.*
@@ -11,9 +11,9 @@ import io.ktor.server.routing.*
import kotlin.collections.mapOf import kotlin.collections.mapOf
fun Route.artikelRoutes() { fun Route.artikelRoutes() {
val artikelRepository: ArtikelRepository = PostgresArtikelRepository() val artikelRepository: ArtikelRepository = ServiceLocator.artikelRepository
route("/api/artikel") { route("/artikel") {
// GET /api/artikel - Get all articles // GET /api/artikel - Get all articles
get { get {
try { 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 package at.mocode.routes
import at.mocode.model.DomLizenzRepository import at.mocode.repositories.DomLizenzRepository
import at.mocode.model.PostgresDomLizenzRepository import at.mocode.repositories.PostgresDomLizenzRepository
import at.mocode.model.domaene.DomLizenz import at.mocode.model.domaene.DomLizenz
import com.benasher44.uuid.uuidFrom import com.benasher44.uuid.uuidFrom
import io.ktor.http.* 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 package at.mocode.routes
import at.mocode.model.PersonRepository import at.mocode.repositories.PersonRepository
import at.mocode.model.PostgresPersonRepository 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.*
@@ -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 package at.mocode.routes
import at.mocode.model.PostgresVereinRepository import at.mocode.repositories.PostgresVereinRepository
import at.mocode.model.VereinRepository import at.mocode.repositories.VereinRepository
import at.mocode.stammdaten.Verein import at.mocode.stammdaten.Verein
import com.benasher44.uuid.uuidFrom import com.benasher44.uuid.uuidFrom
import io.ktor.http.* 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() val geldpreisVorlageId = uuid("geldpreis_vorlage_id").nullable()
// Ort/Zeit (Default-Werte) // 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 standardDatum = date("standard_datum").nullable()
val standardBeginnzeitTypE = varchar("standard_beginnzeit_typ", 50).default("ANSCHLIESSEND") val standardBeginnzeitTypE = varchar("standard_beginnzeit_typ", 50).default("ANSCHLIESSEND")
val standardBeginnzeitFix = time("standard_beginnzeit_fix").nullable() val standardBeginnzeitFix = time("standard_beginnzeit_fix").nullable()
@@ -1,5 +1,6 @@
package at.mocode.tables package at.mocode.tables
import at.mocode.tables.stammdaten.PersonenTable
import org.jetbrains.exposed.sql.Table 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.date // Für kotlinx-datetime LocalDate
import org.jetbrains.exposed.sql.kotlin.datetime.datetime // Für kotlinx-datetime LocalDateTime import org.jetbrains.exposed.sql.kotlin.datetime.datetime // Für kotlinx-datetime LocalDateTime
@@ -1,6 +1,6 @@
package at.mocode.tables.domaene 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.Table
import org.jetbrains.exposed.sql.kotlin.datetime.date import org.jetbrains.exposed.sql.kotlin.datetime.date
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp 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.DatenQuelleE
import at.mocode.enums.PferdeGeschlechtE import at.mocode.enums.PferdeGeschlechtE
import at.mocode.tables.PersonenTable import at.mocode.tables.stammdaten.PersonenTable
import at.mocode.tables.VereineTable import at.mocode.tables.stammdaten.VereineTable
import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
@@ -6,7 +6,6 @@ import at.mocode.tables.TurniereTable
import at.mocode.tables.VeranstaltungenTable import at.mocode.tables.VeranstaltungenTable
import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.kotlin.datetime.date import org.jetbrains.exposed.sql.kotlin.datetime.date
import org.jetbrains.exposed.sql.kotlin.datetime.time
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
// Event models tables // 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.KotlinInstantSerializer
import at.mocode.serializers.KotlinLocalDateSerializer import at.mocode.serializers.KotlinLocalDateSerializer
import at.mocode.serializers.UuidSerializer import at.mocode.serializers.UuidSerializer