From 418e6920928b0fb779c770f32d1ece462274d6b0 Mon Sep 17 00:00:00 2001 From: StefanMoCoAt Date: Mon, 30 Jun 2025 21:11:09 +0200 Subject: [PATCH] (fix) API Endpoints Creation for All Tables --- README_CODE_ORGANIZATION.md | 224 +++++++++++++++ docs/API_Documentation.md | 146 ++++++++++ .../main/kotlin/at/mocode/config/AppConfig.kt | 124 +++++++++ .../src/main/kotlin/at/mocode/model/Event.kt | 2 - .../main/kotlin/at/mocode/plugins/Database.kt | 32 ++- .../main/kotlin/at/mocode/plugins/Routing.kt | 26 +- .../kotlin/at/mocode/plugins/Serialization.kt | 2 - .../repositories/AbteilungRepository.kt | 15 + .../ArtikelRepository.kt | 3 +- .../mocode/repositories/BewerbRepository.kt | 18 ++ .../DomLizenzRepository.kt | 2 +- .../DomPferdRepository.kt | 2 +- .../DomQualifikationRepository.kt | 18 ++ .../kotlin/at/mocode/repositories/Event.kt | 2 + .../EventRepository.kt | 2 +- .../PersonRepository.kt | 2 +- .../PostgresAbteilungRepository.kt | 156 +++++++++++ .../PostgresArtikelRepository.kt | 3 +- .../repositories/PostgresBewerbRepository.kt | 61 +++++ .../PostgresDomLizenzRepository.kt | 5 +- .../PostgresDomPferdRepository.kt | 5 +- .../PostgresDomQualifikationRepository.kt | 115 ++++++++ .../PostgresEventRepository.kt | 2 +- .../PostgresPersonRepository.kt | 4 +- .../repositories/PostgresTurnierRepository.kt | 46 ++++ .../PostgresVeranstaltungRepository.kt | 46 ++++ .../PostgresVereinRepository.kt | 4 +- .../mocode/repositories/TurnierRepository.kt | 15 + .../repositories/VeranstaltungRepository.kt | 15 + .../VereinRepository.kt | 2 +- .../at/mocode/routes/AbteilungRoutes.kt | 146 ++++++++++ .../kotlin/at/mocode/routes/ArtikelRoutes.kt | 8 +- .../kotlin/at/mocode/routes/BewerbRoutes.kt | 160 +++++++++++ .../at/mocode/routes/DomLizenzRoutes.kt | 4 +- .../kotlin/at/mocode/routes/DomPferdRoutes.kt | 258 ++++++++++++++++++ .../mocode/routes/DomQualifikationRoutes.kt | 183 +++++++++++++ .../kotlin/at/mocode/routes/EventRoutes.kt | 0 .../kotlin/at/mocode/routes/PersonRoutes.kt | 4 +- .../at/mocode/routes/RouteConfiguration.kt | 85 ++++++ .../kotlin/at/mocode/routes/TurnierRoutes.kt | 132 +++++++++ .../at/mocode/routes/VeranstaltungRoutes.kt | 115 ++++++++ .../kotlin/at/mocode/routes/VereinRoutes.kt | 4 +- .../at/mocode/services/ServiceLocator.kt | 39 +++ .../kotlin/at/mocode/tables/BewerbTable.kt | 2 +- .../kotlin/at/mocode/tables/TurniereTable.kt | 1 + .../mocode/tables/domaene/DomLizenzTable.kt | 2 +- .../at/mocode/tables/domaene/DomPferdTable.kt | 4 +- .../veranstaltung/VeranstaltungEventTables.kt | 1 - .../kotlin/at/mocode/utils/ApiResponse.kt | 124 +++++++++ .../main/kotlin/at/mocode/utils/RouteUtils.kt | 114 ++++++++ .../kotlin/at/mocode/DomQualifikationTest.kt | 55 ++++ .../mocode/model/domaene/DomQualifikation.kt | 2 + 52 files changed, 2477 insertions(+), 65 deletions(-) create mode 100644 README_CODE_ORGANIZATION.md create mode 100644 server/src/main/kotlin/at/mocode/config/AppConfig.kt delete mode 100644 server/src/main/kotlin/at/mocode/model/Event.kt delete mode 100644 server/src/main/kotlin/at/mocode/plugins/Serialization.kt create mode 100644 server/src/main/kotlin/at/mocode/repositories/AbteilungRepository.kt rename server/src/main/kotlin/at/mocode/{model => repositories}/ArtikelRepository.kt (87%) create mode 100644 server/src/main/kotlin/at/mocode/repositories/BewerbRepository.kt rename server/src/main/kotlin/at/mocode/{model => repositories}/DomLizenzRepository.kt (95%) rename server/src/main/kotlin/at/mocode/{model => repositories}/DomPferdRepository.kt (96%) create mode 100644 server/src/main/kotlin/at/mocode/repositories/DomQualifikationRepository.kt create mode 100644 server/src/main/kotlin/at/mocode/repositories/Event.kt rename server/src/main/kotlin/at/mocode/{model => repositories}/EventRepository.kt (50%) rename server/src/main/kotlin/at/mocode/{model => repositories}/PersonRepository.kt (94%) create mode 100644 server/src/main/kotlin/at/mocode/repositories/PostgresAbteilungRepository.kt rename server/src/main/kotlin/at/mocode/{model => repositories}/PostgresArtikelRepository.kt (98%) create mode 100644 server/src/main/kotlin/at/mocode/repositories/PostgresBewerbRepository.kt rename server/src/main/kotlin/at/mocode/{model => repositories}/PostgresDomLizenzRepository.kt (97%) rename server/src/main/kotlin/at/mocode/{model => repositories}/PostgresDomPferdRepository.kt (98%) create mode 100644 server/src/main/kotlin/at/mocode/repositories/PostgresDomQualifikationRepository.kt rename server/src/main/kotlin/at/mocode/{model => repositories}/PostgresEventRepository.kt (53%) rename server/src/main/kotlin/at/mocode/{model => repositories}/PostgresPersonRepository.kt (98%) create mode 100644 server/src/main/kotlin/at/mocode/repositories/PostgresTurnierRepository.kt create mode 100644 server/src/main/kotlin/at/mocode/repositories/PostgresVeranstaltungRepository.kt rename server/src/main/kotlin/at/mocode/{model => repositories}/PostgresVereinRepository.kt (98%) create mode 100644 server/src/main/kotlin/at/mocode/repositories/TurnierRepository.kt create mode 100644 server/src/main/kotlin/at/mocode/repositories/VeranstaltungRepository.kt rename server/src/main/kotlin/at/mocode/{model => repositories}/VereinRepository.kt (94%) create mode 100644 server/src/main/kotlin/at/mocode/routes/AbteilungRoutes.kt create mode 100644 server/src/main/kotlin/at/mocode/routes/BewerbRoutes.kt create mode 100644 server/src/main/kotlin/at/mocode/routes/DomPferdRoutes.kt create mode 100644 server/src/main/kotlin/at/mocode/routes/DomQualifikationRoutes.kt create mode 100644 server/src/main/kotlin/at/mocode/routes/EventRoutes.kt create mode 100644 server/src/main/kotlin/at/mocode/routes/RouteConfiguration.kt create mode 100644 server/src/main/kotlin/at/mocode/routes/TurnierRoutes.kt create mode 100644 server/src/main/kotlin/at/mocode/routes/VeranstaltungRoutes.kt create mode 100644 server/src/main/kotlin/at/mocode/services/ServiceLocator.kt create mode 100644 server/src/main/kotlin/at/mocode/utils/ApiResponse.kt create mode 100644 server/src/main/kotlin/at/mocode/utils/RouteUtils.kt create mode 100644 server/src/test/kotlin/at/mocode/DomQualifikationTest.kt diff --git a/README_CODE_ORGANIZATION.md b/README_CODE_ORGANIZATION.md new file mode 100644 index 00000000..9c30a645 --- /dev/null +++ b/README_CODE_ORGANIZATION.md @@ -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() ?: 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. diff --git a/docs/API_Documentation.md b/docs/API_Documentation.md index 9df11229..c7e77109 100644 --- a/docs/API_Documentation.md +++ b/docs/API_Documentation.md @@ -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 diff --git a/server/src/main/kotlin/at/mocode/config/AppConfig.kt b/server/src/main/kotlin/at/mocode/config/AppConfig.kt new file mode 100644 index 00000000..3215e263 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/config/AppConfig.kt @@ -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" + } +} diff --git a/server/src/main/kotlin/at/mocode/model/Event.kt b/server/src/main/kotlin/at/mocode/model/Event.kt deleted file mode 100644 index e1ac1a35..00000000 --- a/server/src/main/kotlin/at/mocode/model/Event.kt +++ /dev/null @@ -1,2 +0,0 @@ -package at.mocode.model - diff --git a/server/src/main/kotlin/at/mocode/plugins/Database.kt b/server/src/main/kotlin/at/mocode/plugins/Database.kt index d4d7e654..d7c795ac 100644 --- a/server/src/main/kotlin/at/mocode/plugins/Database.kt +++ b/server/src/main/kotlin/at/mocode/plugins/Database.kt @@ -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.") diff --git a/server/src/main/kotlin/at/mocode/plugins/Routing.kt b/server/src/main/kotlin/at/mocode/plugins/Routing.kt index 42bc85ba..13bd67b8 100644 --- a/server/src/main/kotlin/at/mocode/plugins/Routing.kt +++ b/server/src/main/kotlin/at/mocode/plugins/Routing.kt @@ -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() } } diff --git a/server/src/main/kotlin/at/mocode/plugins/Serialization.kt b/server/src/main/kotlin/at/mocode/plugins/Serialization.kt deleted file mode 100644 index 65541218..00000000 --- a/server/src/main/kotlin/at/mocode/plugins/Serialization.kt +++ /dev/null @@ -1,2 +0,0 @@ -package at.mocode.plugins - diff --git a/server/src/main/kotlin/at/mocode/repositories/AbteilungRepository.kt b/server/src/main/kotlin/at/mocode/repositories/AbteilungRepository.kt new file mode 100644 index 00000000..fa6cf5cc --- /dev/null +++ b/server/src/main/kotlin/at/mocode/repositories/AbteilungRepository.kt @@ -0,0 +1,15 @@ +package at.mocode.repositories + +import at.mocode.model.Abteilung +import com.benasher44.uuid.Uuid + +interface AbteilungRepository { + suspend fun findAll(): List + suspend fun findById(id: Uuid): Abteilung? + suspend fun findByBewerbId(bewerbId: Uuid): List + 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 + suspend fun findByAktiv(istAktiv: Boolean): List +} diff --git a/server/src/main/kotlin/at/mocode/model/ArtikelRepository.kt b/server/src/main/kotlin/at/mocode/repositories/ArtikelRepository.kt similarity index 87% rename from server/src/main/kotlin/at/mocode/model/ArtikelRepository.kt rename to server/src/main/kotlin/at/mocode/repositories/ArtikelRepository.kt index 6be1a762..27a70013 100644 --- a/server/src/main/kotlin/at/mocode/model/ArtikelRepository.kt +++ b/server/src/main/kotlin/at/mocode/repositories/ArtikelRepository.kt @@ -1,5 +1,6 @@ -package at.mocode.model +package at.mocode.repositories +import at.mocode.model.Artikel import com.benasher44.uuid.Uuid interface ArtikelRepository { diff --git a/server/src/main/kotlin/at/mocode/repositories/BewerbRepository.kt b/server/src/main/kotlin/at/mocode/repositories/BewerbRepository.kt new file mode 100644 index 00000000..2a42e0b9 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/repositories/BewerbRepository.kt @@ -0,0 +1,18 @@ +package at.mocode.repositories + +import at.mocode.model.Bewerb +import com.benasher44.uuid.Uuid + +interface BewerbRepository { + suspend fun findAll(): List + suspend fun findById(id: Uuid): Bewerb? + suspend fun findByTurnierId(turnierId: Uuid): List + suspend fun findBySparte(sparte: String): List + suspend fun findByKlasse(klasse: String): List + 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 + suspend fun findByStartlisteFinal(istFinal: Boolean): List + suspend fun findByErgebnislisteFinal(istFinal: Boolean): List +} diff --git a/server/src/main/kotlin/at/mocode/model/DomLizenzRepository.kt b/server/src/main/kotlin/at/mocode/repositories/DomLizenzRepository.kt similarity index 95% rename from server/src/main/kotlin/at/mocode/model/DomLizenzRepository.kt rename to server/src/main/kotlin/at/mocode/repositories/DomLizenzRepository.kt index 08bd2e78..bc431bdf 100644 --- a/server/src/main/kotlin/at/mocode/model/DomLizenzRepository.kt +++ b/server/src/main/kotlin/at/mocode/repositories/DomLizenzRepository.kt @@ -1,4 +1,4 @@ -package at.mocode.model +package at.mocode.repositories import at.mocode.model.domaene.DomLizenz import com.benasher44.uuid.Uuid diff --git a/server/src/main/kotlin/at/mocode/model/DomPferdRepository.kt b/server/src/main/kotlin/at/mocode/repositories/DomPferdRepository.kt similarity index 96% rename from server/src/main/kotlin/at/mocode/model/DomPferdRepository.kt rename to server/src/main/kotlin/at/mocode/repositories/DomPferdRepository.kt index 767e94fc..297ede1a 100644 --- a/server/src/main/kotlin/at/mocode/model/DomPferdRepository.kt +++ b/server/src/main/kotlin/at/mocode/repositories/DomPferdRepository.kt @@ -1,4 +1,4 @@ -package at.mocode.model +package at.mocode.repositories import at.mocode.model.domaene.DomPferd import com.benasher44.uuid.Uuid diff --git a/server/src/main/kotlin/at/mocode/repositories/DomQualifikationRepository.kt b/server/src/main/kotlin/at/mocode/repositories/DomQualifikationRepository.kt new file mode 100644 index 00000000..11ac6848 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/repositories/DomQualifikationRepository.kt @@ -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 + suspend fun findById(id: Uuid): DomQualifikation? + suspend fun findByPersonId(personId: Uuid): List + suspend fun findByQualTypId(qualTypId: Uuid): List + suspend fun findActiveByPersonId(personId: Uuid): List + suspend fun findByValidityPeriod(fromDate: LocalDate?, toDate: LocalDate?): List + 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 +} diff --git a/server/src/main/kotlin/at/mocode/repositories/Event.kt b/server/src/main/kotlin/at/mocode/repositories/Event.kt new file mode 100644 index 00000000..128f46c2 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/repositories/Event.kt @@ -0,0 +1,2 @@ +package at.mocode.repositories + diff --git a/server/src/main/kotlin/at/mocode/model/EventRepository.kt b/server/src/main/kotlin/at/mocode/repositories/EventRepository.kt similarity index 50% rename from server/src/main/kotlin/at/mocode/model/EventRepository.kt rename to server/src/main/kotlin/at/mocode/repositories/EventRepository.kt index 7b835e1b..137ccdd5 100644 --- a/server/src/main/kotlin/at/mocode/model/EventRepository.kt +++ b/server/src/main/kotlin/at/mocode/repositories/EventRepository.kt @@ -1,4 +1,4 @@ -package at.mocode.model +package at.mocode.repositories interface EventRepository { } diff --git a/server/src/main/kotlin/at/mocode/model/PersonRepository.kt b/server/src/main/kotlin/at/mocode/repositories/PersonRepository.kt similarity index 94% rename from server/src/main/kotlin/at/mocode/model/PersonRepository.kt rename to server/src/main/kotlin/at/mocode/repositories/PersonRepository.kt index 0979be73..82619984 100644 --- a/server/src/main/kotlin/at/mocode/model/PersonRepository.kt +++ b/server/src/main/kotlin/at/mocode/repositories/PersonRepository.kt @@ -1,4 +1,4 @@ -package at.mocode.model +package at.mocode.repositories import at.mocode.stammdaten.Person import com.benasher44.uuid.Uuid diff --git a/server/src/main/kotlin/at/mocode/repositories/PostgresAbteilungRepository.kt b/server/src/main/kotlin/at/mocode/repositories/PostgresAbteilungRepository.kt new file mode 100644 index 00000000..9dedc225 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/repositories/PostgresAbteilungRepository.kt @@ -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 = 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 = 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 = 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 = 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] + ) + } +} diff --git a/server/src/main/kotlin/at/mocode/model/PostgresArtikelRepository.kt b/server/src/main/kotlin/at/mocode/repositories/PostgresArtikelRepository.kt similarity index 98% rename from server/src/main/kotlin/at/mocode/model/PostgresArtikelRepository.kt rename to server/src/main/kotlin/at/mocode/repositories/PostgresArtikelRepository.kt index f6365527..97a3cc0b 100644 --- a/server/src/main/kotlin/at/mocode/model/PostgresArtikelRepository.kt +++ b/server/src/main/kotlin/at/mocode/repositories/PostgresArtikelRepository.kt @@ -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 diff --git a/server/src/main/kotlin/at/mocode/repositories/PostgresBewerbRepository.kt b/server/src/main/kotlin/at/mocode/repositories/PostgresBewerbRepository.kt new file mode 100644 index 00000000..c05b3f72 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/repositories/PostgresBewerbRepository.kt @@ -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 { + // 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 { + // TODO: Implement database operations + return emptyList() + } + + override suspend fun findBySparte(sparte: String): List { + // TODO: Implement database operations + return emptyList() + } + + override suspend fun findByKlasse(klasse: String): List { + // 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 { + // TODO: Implement database operations + return emptyList() + } + + override suspend fun findByStartlisteFinal(istFinal: Boolean): List { + // TODO: Implement database operations + return emptyList() + } + + override suspend fun findByErgebnislisteFinal(istFinal: Boolean): List { + // TODO: Implement database operations + return emptyList() + } +} diff --git a/server/src/main/kotlin/at/mocode/model/PostgresDomLizenzRepository.kt b/server/src/main/kotlin/at/mocode/repositories/PostgresDomLizenzRepository.kt similarity index 97% rename from server/src/main/kotlin/at/mocode/model/PostgresDomLizenzRepository.kt rename to server/src/main/kotlin/at/mocode/repositories/PostgresDomLizenzRepository.kt index 1deaeaf3..ed80e4d4 100644 --- a/server/src/main/kotlin/at/mocode/model/PostgresDomLizenzRepository.kt +++ b/server/src/main/kotlin/at/mocode/repositories/PostgresDomLizenzRepository.kt @@ -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 diff --git a/server/src/main/kotlin/at/mocode/model/PostgresDomPferdRepository.kt b/server/src/main/kotlin/at/mocode/repositories/PostgresDomPferdRepository.kt similarity index 98% rename from server/src/main/kotlin/at/mocode/model/PostgresDomPferdRepository.kt rename to server/src/main/kotlin/at/mocode/repositories/PostgresDomPferdRepository.kt index 92a83278..9c972e09 100644 --- a/server/src/main/kotlin/at/mocode/model/PostgresDomPferdRepository.kt +++ b/server/src/main/kotlin/at/mocode/repositories/PostgresDomPferdRepository.kt @@ -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 { diff --git a/server/src/main/kotlin/at/mocode/repositories/PostgresDomQualifikationRepository.kt b/server/src/main/kotlin/at/mocode/repositories/PostgresDomQualifikationRepository.kt new file mode 100644 index 00000000..4ad7a7c3 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/repositories/PostgresDomQualifikationRepository.kt @@ -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 = 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 = transaction { + DomQualifikationTable.select { DomQualifikationTable.personId eq personId } + .map { rowToDomQualifikation(it) } + } + + override suspend fun findByQualTypId(qualTypId: Uuid): List = transaction { + DomQualifikationTable.select { DomQualifikationTable.qualTypId eq qualTypId } + .map { rowToDomQualifikation(it) } + } + + override suspend fun findActiveByPersonId(personId: Uuid): List = transaction { + DomQualifikationTable.select { + (DomQualifikationTable.personId eq personId) and (DomQualifikationTable.istAktiv eq true) + }.map { rowToDomQualifikation(it) } + } + + override suspend fun findByValidityPeriod(fromDate: LocalDate?, toDate: LocalDate?): List = 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 = 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] + ) + } +} diff --git a/server/src/main/kotlin/at/mocode/model/PostgresEventRepository.kt b/server/src/main/kotlin/at/mocode/repositories/PostgresEventRepository.kt similarity index 53% rename from server/src/main/kotlin/at/mocode/model/PostgresEventRepository.kt rename to server/src/main/kotlin/at/mocode/repositories/PostgresEventRepository.kt index 825c37ea..ccee5233 100644 --- a/server/src/main/kotlin/at/mocode/model/PostgresEventRepository.kt +++ b/server/src/main/kotlin/at/mocode/repositories/PostgresEventRepository.kt @@ -1,4 +1,4 @@ -package at.mocode.model +package at.mocode.repositories class PostgresEventRepository { } diff --git a/server/src/main/kotlin/at/mocode/model/PostgresPersonRepository.kt b/server/src/main/kotlin/at/mocode/repositories/PostgresPersonRepository.kt similarity index 98% rename from server/src/main/kotlin/at/mocode/model/PostgresPersonRepository.kt rename to server/src/main/kotlin/at/mocode/repositories/PostgresPersonRepository.kt index de9fc480..ac2347fb 100644 --- a/server/src/main/kotlin/at/mocode/model/PostgresPersonRepository.kt +++ b/server/src/main/kotlin/at/mocode/repositories/PostgresPersonRepository.kt @@ -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.* diff --git a/server/src/main/kotlin/at/mocode/repositories/PostgresTurnierRepository.kt b/server/src/main/kotlin/at/mocode/repositories/PostgresTurnierRepository.kt new file mode 100644 index 00000000..2d3d66a7 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/repositories/PostgresTurnierRepository.kt @@ -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 { + // 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 { + // 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 { + // TODO: Implement database operations + return emptyList() + } +} diff --git a/server/src/main/kotlin/at/mocode/repositories/PostgresVeranstaltungRepository.kt b/server/src/main/kotlin/at/mocode/repositories/PostgresVeranstaltungRepository.kt new file mode 100644 index 00000000..b9846f51 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/repositories/PostgresVeranstaltungRepository.kt @@ -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 { + // 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 { + // TODO: Implement database operations + return emptyList() + } + + override suspend fun findByVeranstalterOepsNummer(oepsNummer: String): List { + // 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 { + // TODO: Implement database operations + return emptyList() + } +} diff --git a/server/src/main/kotlin/at/mocode/model/PostgresVereinRepository.kt b/server/src/main/kotlin/at/mocode/repositories/PostgresVereinRepository.kt similarity index 98% rename from server/src/main/kotlin/at/mocode/model/PostgresVereinRepository.kt rename to server/src/main/kotlin/at/mocode/repositories/PostgresVereinRepository.kt index 62c20fa8..3018bb12 100644 --- a/server/src/main/kotlin/at/mocode/model/PostgresVereinRepository.kt +++ b/server/src/main/kotlin/at/mocode/repositories/PostgresVereinRepository.kt @@ -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.* diff --git a/server/src/main/kotlin/at/mocode/repositories/TurnierRepository.kt b/server/src/main/kotlin/at/mocode/repositories/TurnierRepository.kt new file mode 100644 index 00000000..322a5c71 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/repositories/TurnierRepository.kt @@ -0,0 +1,15 @@ +package at.mocode.repositories + +import at.mocode.model.Turnier +import com.benasher44.uuid.Uuid + +interface TurnierRepository { + suspend fun findAll(): List + suspend fun findById(id: Uuid): Turnier? + suspend fun findByVeranstaltungId(veranstaltungId: Uuid): List + 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 +} diff --git a/server/src/main/kotlin/at/mocode/repositories/VeranstaltungRepository.kt b/server/src/main/kotlin/at/mocode/repositories/VeranstaltungRepository.kt new file mode 100644 index 00000000..2beea836 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/repositories/VeranstaltungRepository.kt @@ -0,0 +1,15 @@ +package at.mocode.repositories + +import at.mocode.model.Veranstaltung +import com.benasher44.uuid.Uuid + +interface VeranstaltungRepository { + suspend fun findAll(): List + suspend fun findById(id: Uuid): Veranstaltung? + suspend fun findByName(name: String): List + suspend fun findByVeranstalterOepsNummer(oepsNummer: String): List + 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 +} diff --git a/server/src/main/kotlin/at/mocode/model/VereinRepository.kt b/server/src/main/kotlin/at/mocode/repositories/VereinRepository.kt similarity index 94% rename from server/src/main/kotlin/at/mocode/model/VereinRepository.kt rename to server/src/main/kotlin/at/mocode/repositories/VereinRepository.kt index 18a633e2..bc2d24c5 100644 --- a/server/src/main/kotlin/at/mocode/model/VereinRepository.kt +++ b/server/src/main/kotlin/at/mocode/repositories/VereinRepository.kt @@ -1,4 +1,4 @@ -package at.mocode.model +package at.mocode.repositories import at.mocode.stammdaten.Verein import com.benasher44.uuid.Uuid diff --git a/server/src/main/kotlin/at/mocode/routes/AbteilungRoutes.kt b/server/src/main/kotlin/at/mocode/routes/AbteilungRoutes.kt new file mode 100644 index 00000000..d3f2d953 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/routes/AbteilungRoutes.kt @@ -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() + 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() + 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)) + } + } + } +} diff --git a/server/src/main/kotlin/at/mocode/routes/ArtikelRoutes.kt b/server/src/main/kotlin/at/mocode/routes/ArtikelRoutes.kt index 63c45fb9..4d6540fe 100644 --- a/server/src/main/kotlin/at/mocode/routes/ArtikelRoutes.kt +++ b/server/src/main/kotlin/at/mocode/routes/ArtikelRoutes.kt @@ -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 { diff --git a/server/src/main/kotlin/at/mocode/routes/BewerbRoutes.kt b/server/src/main/kotlin/at/mocode/routes/BewerbRoutes.kt new file mode 100644 index 00000000..07df6802 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/routes/BewerbRoutes.kt @@ -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() + 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() + 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)) + } + } + } +} diff --git a/server/src/main/kotlin/at/mocode/routes/DomLizenzRoutes.kt b/server/src/main/kotlin/at/mocode/routes/DomLizenzRoutes.kt index 62c4b84e..093819d0 100644 --- a/server/src/main/kotlin/at/mocode/routes/DomLizenzRoutes.kt +++ b/server/src/main/kotlin/at/mocode/routes/DomLizenzRoutes.kt @@ -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.* diff --git a/server/src/main/kotlin/at/mocode/routes/DomPferdRoutes.kt b/server/src/main/kotlin/at/mocode/routes/DomPferdRoutes.kt new file mode 100644 index 00000000..5a6440d3 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/routes/DomPferdRoutes.kt @@ -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() + 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() + 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)) + } + } + } +} diff --git a/server/src/main/kotlin/at/mocode/routes/DomQualifikationRoutes.kt b/server/src/main/kotlin/at/mocode/routes/DomQualifikationRoutes.kt new file mode 100644 index 00000000..5b116d94 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/routes/DomQualifikationRoutes.kt @@ -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() + 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() + 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)) + } + } + } +} diff --git a/server/src/main/kotlin/at/mocode/routes/EventRoutes.kt b/server/src/main/kotlin/at/mocode/routes/EventRoutes.kt new file mode 100644 index 00000000..e69de29b diff --git a/server/src/main/kotlin/at/mocode/routes/PersonRoutes.kt b/server/src/main/kotlin/at/mocode/routes/PersonRoutes.kt index 3c8d1487..f06d4119 100644 --- a/server/src/main/kotlin/at/mocode/routes/PersonRoutes.kt +++ b/server/src/main/kotlin/at/mocode/routes/PersonRoutes.kt @@ -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.* diff --git a/server/src/main/kotlin/at/mocode/routes/RouteConfiguration.kt b/server/src/main/kotlin/at/mocode/routes/RouteConfiguration.kt new file mode 100644 index 00000000..769f6cb0 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/routes/RouteConfiguration.kt @@ -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() + } + } +} diff --git a/server/src/main/kotlin/at/mocode/routes/TurnierRoutes.kt b/server/src/main/kotlin/at/mocode/routes/TurnierRoutes.kt new file mode 100644 index 00000000..e89c2c01 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/routes/TurnierRoutes.kt @@ -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() + 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() + 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)) + } + } + } +} diff --git a/server/src/main/kotlin/at/mocode/routes/VeranstaltungRoutes.kt b/server/src/main/kotlin/at/mocode/routes/VeranstaltungRoutes.kt new file mode 100644 index 00000000..8dc2190f --- /dev/null +++ b/server/src/main/kotlin/at/mocode/routes/VeranstaltungRoutes.kt @@ -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() + 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() + 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)) + } + } + } +} diff --git a/server/src/main/kotlin/at/mocode/routes/VereinRoutes.kt b/server/src/main/kotlin/at/mocode/routes/VereinRoutes.kt index 90d67991..1bf4aff8 100644 --- a/server/src/main/kotlin/at/mocode/routes/VereinRoutes.kt +++ b/server/src/main/kotlin/at/mocode/routes/VereinRoutes.kt @@ -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.* diff --git a/server/src/main/kotlin/at/mocode/services/ServiceLocator.kt b/server/src/main/kotlin/at/mocode/services/ServiceLocator.kt new file mode 100644 index 00000000..682d0b20 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/services/ServiceLocator.kt @@ -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 + } +} diff --git a/server/src/main/kotlin/at/mocode/tables/BewerbTable.kt b/server/src/main/kotlin/at/mocode/tables/BewerbTable.kt index e9813758..fc7692c5 100644 --- a/server/src/main/kotlin/at/mocode/tables/BewerbTable.kt +++ b/server/src/main/kotlin/at/mocode/tables/BewerbTable.kt @@ -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() diff --git a/server/src/main/kotlin/at/mocode/tables/TurniereTable.kt b/server/src/main/kotlin/at/mocode/tables/TurniereTable.kt index bd8e19b0..03633696 100644 --- a/server/src/main/kotlin/at/mocode/tables/TurniereTable.kt +++ b/server/src/main/kotlin/at/mocode/tables/TurniereTable.kt @@ -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 diff --git a/server/src/main/kotlin/at/mocode/tables/domaene/DomLizenzTable.kt b/server/src/main/kotlin/at/mocode/tables/domaene/DomLizenzTable.kt index ddf32cce..dffc0f3c 100644 --- a/server/src/main/kotlin/at/mocode/tables/domaene/DomLizenzTable.kt +++ b/server/src/main/kotlin/at/mocode/tables/domaene/DomLizenzTable.kt @@ -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 diff --git a/server/src/main/kotlin/at/mocode/tables/domaene/DomPferdTable.kt b/server/src/main/kotlin/at/mocode/tables/domaene/DomPferdTable.kt index 61cfe7c3..a6aaf6aa 100644 --- a/server/src/main/kotlin/at/mocode/tables/domaene/DomPferdTable.kt +++ b/server/src/main/kotlin/at/mocode/tables/domaene/DomPferdTable.kt @@ -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 diff --git a/server/src/main/kotlin/at/mocode/tables/veranstaltung/VeranstaltungEventTables.kt b/server/src/main/kotlin/at/mocode/tables/veranstaltung/VeranstaltungEventTables.kt index 40d0645c..8d574d18 100644 --- a/server/src/main/kotlin/at/mocode/tables/veranstaltung/VeranstaltungEventTables.kt +++ b/server/src/main/kotlin/at/mocode/tables/veranstaltung/VeranstaltungEventTables.kt @@ -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 diff --git a/server/src/main/kotlin/at/mocode/utils/ApiResponse.kt b/server/src/main/kotlin/at/mocode/utils/ApiResponse.kt new file mode 100644 index 00000000..86b402bd --- /dev/null +++ b/server/src/main/kotlin/at/mocode/utils/ApiResponse.kt @@ -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( + 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 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( + 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 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 + ) + } + } +} diff --git a/server/src/main/kotlin/at/mocode/utils/RouteUtils.kt b/server/src/main/kotlin/at/mocode/utils/RouteUtils.kt new file mode 100644 index 00000000..8a9360e8 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/utils/RouteUtils.kt @@ -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 RoutingCall.safeReceive( + resourceName: String = "request body" + ): T? { + return try { + receive() + } catch (e: Exception) { + respondValidationError("Invalid $resourceName format", e.message) + null + } + } + + /** + * Execute repository operation with standardized error handling + */ + suspend inline fun RoutingCall.executeRepositoryOperation( + operation: String, + block: () -> T + ): T? { + return try { + block() + } catch (e: Exception) { + ResponseUtils.run { handleException(e, operation) } + null + } + } +} diff --git a/server/src/test/kotlin/at/mocode/DomQualifikationTest.kt b/server/src/test/kotlin/at/mocode/DomQualifikationTest.kt new file mode 100644 index 00000000..7f1712e0 --- /dev/null +++ b/server/src/test/kotlin/at/mocode/DomQualifikationTest.kt @@ -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) + } +} diff --git a/shared/src/commonMain/kotlin/at/mocode/model/domaene/DomQualifikation.kt b/shared/src/commonMain/kotlin/at/mocode/model/domaene/DomQualifikation.kt index 7a09b514..f7fb730e 100644 --- a/shared/src/commonMain/kotlin/at/mocode/model/domaene/DomQualifikation.kt +++ b/shared/src/commonMain/kotlin/at/mocode/model/domaene/DomQualifikation.kt @@ -1,3 +1,5 @@ +package at.mocode.model.domaene + import at.mocode.serializers.KotlinInstantSerializer import at.mocode.serializers.KotlinLocalDateSerializer import at.mocode.serializers.UuidSerializer