├── .gitignore ├── Dockerfile ├── README.md ├── build.gradle.kts ├── cert ├── keys.sh ├── server_keystore.jks └── server_keystore.p12 ├── clean-docker.sh ├── docker-compose.yml ├── docs ├── .swagger-codegen-ignore ├── .swagger-codegen │ └── VERSION └── index.html ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── bcrypt.png ├── cache.jpg ├── cors.png ├── docker.jpg ├── expla.png ├── koin.png ├── ktor.png ├── ktor_logo.svg ├── layers.png ├── observer.png ├── postman.png ├── reactive.gif ├── swagger.png ├── testing.png ├── tokens.png └── tsl.jpg ├── postman ├── Ktor-Tenistas-Rest.postman_collection.json ├── imagen.png └── usuario.json ├── settings.gradle.kts └── src ├── main ├── kotlin │ └── joseluisgs │ │ └── es │ │ ├── Application.kt │ │ ├── config │ │ ├── DataBaseConfig.kt │ │ ├── StorageConfig.kt │ │ └── TokenConfig.kt │ │ ├── db │ │ └── Data.kt │ │ ├── dto │ │ ├── Raqueta.kt │ │ ├── Representantes.kt │ │ ├── Tenista.kt │ │ ├── Test.kt │ │ └── Users.kt │ │ ├── entities │ │ ├── Raqueta.kt │ │ ├── Representante.kt │ │ ├── Tenista.kt │ │ └── User.kt │ │ ├── exceptions │ │ ├── DataBaseException.kt │ │ ├── RaquetaException.kt │ │ ├── RepresentanteException.kt │ │ ├── StorageException.kt │ │ ├── TenistaException.kt │ │ └── UserException.kt │ │ ├── mappers │ │ ├── Raqueta.kt │ │ ├── Representante.kt │ │ ├── Tenista.kt │ │ └── Users.kt │ │ ├── models │ │ ├── Notifacion.kt │ │ ├── Raqueta.kt │ │ ├── Representante.kt │ │ ├── Tenista.kt │ │ └── User.kt │ │ ├── plugins │ │ ├── CachingHeaders.kt │ │ ├── Compression.kt │ │ ├── Cors.kt │ │ ├── DataBase.kt │ │ ├── Koin.kt │ │ ├── Routing.kt │ │ ├── Security.kt │ │ ├── Serialization.kt │ │ ├── Storage.kt │ │ ├── Swagger.kt │ │ ├── Validation.kt │ │ └── WebSockets.kt │ │ ├── repositories │ │ ├── CrudRepository.kt │ │ ├── raquetas │ │ │ ├── RaquetasCachedRepositoryImpl.kt │ │ │ ├── RaquetasRepository.kt │ │ │ └── RaquetasRepositoryImpl.kt │ │ ├── representantes │ │ │ ├── RepresentantesCachedRepositoryImpl.kt │ │ │ ├── RepresentantesRepository.kt │ │ │ └── RepresentantesRepositoryImpl.kt │ │ ├── tenistas │ │ │ ├── TenistasCachedRepositoryImpl.kt │ │ │ ├── TenistasRepository.kt │ │ │ └── TenistasRepositoryImpl.kt │ │ └── users │ │ │ ├── UsersRepository.kt │ │ │ └── UsersRepositoryImpl.kt │ │ ├── routes │ │ ├── RaquetasRoutes.kt │ │ ├── RepresentantesRoutes.kt │ │ ├── StorageRoutes.kt │ │ ├── TenistasRoutes.kt │ │ ├── TestRoutes.kt │ │ ├── UsersRoutes.kt │ │ └── WebRoutes.kt │ │ ├── serializers │ │ ├── LocalDateSerializer.kt │ │ ├── LocalDateTimeSerializer.kt │ │ └── UUIDSerializer.kt │ │ ├── services │ │ ├── cache │ │ │ ├── ICache.kt │ │ │ ├── raquetas │ │ │ │ ├── RaquetasCache.kt │ │ │ │ └── RaquetasCacheImpl.kt │ │ │ ├── representantes │ │ │ │ ├── RepresentantesCache.kt │ │ │ │ └── raquetasCacheImpl.kt │ │ │ └── tenistas │ │ │ │ ├── TenistasCache.kt │ │ │ │ └── TenistasCacheImpl.kt │ │ ├── database │ │ │ └── DataBaseService.kt │ │ ├── raquetas │ │ │ ├── RaquetasService.kt │ │ │ └── RaquetasServiceImpl.kt │ │ ├── representantes │ │ │ ├── RepresentantesService.kt │ │ │ └── RepresentantesServiceImpl.kt │ │ ├── storage │ │ │ ├── StorageService.kt │ │ │ └── StorageServiceImpl.kt │ │ ├── tenistas │ │ │ ├── TenistasService.kt │ │ │ └── TenistasServiceImpl.kt │ │ ├── tokens │ │ │ └── TokensService.kt │ │ └── users │ │ │ ├── UsersService.kt │ │ │ └── UsersServiceImpl.kt │ │ ├── utils │ │ └── UuidUtils.kt │ │ └── validators │ │ ├── RaquetasValidator.kt │ │ ├── RepresentantesValidator.kt │ │ ├── TenistasValidator.kt │ │ └── UsersValidator.kt └── resources │ ├── application.conf │ ├── logback.xml │ └── web │ ├── index.html │ └── ktor.png └── test └── kotlin └── joseluisgs └── es ├── ApplicationTest.kt ├── mappers └── RepresentantesKtTest.kt ├── repositories ├── raquetas │ ├── RaquetasCachedRepositoryImplKtTest.kt │ └── RaquetasRepositoryImplTest.kt ├── representantes │ ├── RepresentantesCachedRepositoryImplKtTest.kt │ └── RepresentantesRepositoryImplTest.kt ├── tenistas │ ├── TenistasCachedRepositoryImplKtTest.kt │ └── TenistasRepositoryImplTest.kt ├── users │ └── UsersRepositoryImplTest.kt └── utils │ └── SetupDataBaseService.kt ├── routes ├── RaquetasRoutesKtTest.kt ├── RepresentantesRoutesKtTest.kt ├── TenistasRoutesKtTest.kt └── UsersRoutesKtTest.kt ├── services ├── raquetas │ └── RaquetasServiceImplTest.kt ├── representantes │ └── RepresentantesServiceImplTest.kt ├── tenistas │ └── TenistasServiceImplTest.kt └── users │ └── UsersServiceImplTest.kt └── utils └── UuidUtilsKtTest.kt /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | bin/ 16 | !**/src/main/**/bin/ 17 | !**/src/test/**/bin/ 18 | 19 | ### IntelliJ IDEA ### 20 | .idea 21 | *.iws 22 | *.iml 23 | *.ipr 24 | out/ 25 | !**/src/main/**/out/ 26 | !**/src/test/**/out/ 27 | 28 | ### NetBeans ### 29 | /nbproject/private/ 30 | /nbbuild/ 31 | /dist/ 32 | /nbdist/ 33 | /.nb-gradle/ 34 | 35 | ### VS Code ### 36 | .vscode/uploads/ 37 | 38 | uploads/ 39 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Con este Dockerfile se crea una imagen de Docker que 2 | # compila la aplicación gracias a Gradle 3 | FROM gradle:7-jdk17 AS build 4 | # Copiamos el codigo fuente de la aplicación, es decir, 5 | # lo que hay en el directorio actual 6 | COPY --chown=gradle:gradle . /home/gradle/src 7 | WORKDIR /home/gradle/src 8 | RUN gradle buildFatJar --no-daemon 9 | 10 | # Con esto hacemos una imagen de Docker que ejecuta la aplicación 11 | FROM openjdk:17-jdk-slim-buster 12 | EXPOSE 6969:6969 13 | EXPOSE 6963:6963 14 | # Directorio donde se guarda la aplicación 15 | RUN mkdir /app 16 | # Copiamos los certificados y los recursos de la aplicación en los directorios que necesita 17 | RUN mkdir /cert 18 | COPY --from=build /home/gradle/src/cert/* /cert/ 19 | # Copiamos el JAR de la aplicación 20 | COPY --from=build /home/gradle/src/build/libs/tenistas-rest-ktor-all.jar /app/tenistas-rest-ktor.jar 21 | # Ejecutamos la aplicación, y le pasamos los argumentos si tiene 22 | ENTRYPOINT ["java","-jar","/app/tenistas-rest-ktor.jar"] -------------------------------------------------------------------------------- /cert/keys.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## Llavero Servidor: Par de claves del servidor (privada y pública) formato PEM 4 | keytool -genkeypair -alias serverKeyPair -keyalg RSA -keysize 4096 -validity 365 -storetype PKCS12 -keystore server_keystore.p12 -storepass 1234567 5 | 6 | ## Llavero Cliente: Par de claves del cliente (privada y pública) formato JKS 7 | keytool -genkeypair -alias serverKeyPair -keyalg RSA -keysize 4096 -validity 365 -storetype JKS -keystore server_keystore.jks -storepass 1234567 -------------------------------------------------------------------------------- /cert/server_keystore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArisGuimera/tenistas-rest-ktor-2022-2023/702f23afb6f394988e5b8f816a022439a775508f/cert/server_keystore.jks -------------------------------------------------------------------------------- /cert/server_keystore.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArisGuimera/tenistas-rest-ktor-2022-2023/702f23afb6f394988e5b8f816a022439a775508f/cert/server_keystore.p12 -------------------------------------------------------------------------------- /clean-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker-compose down 3 | docker system prune -f -a --volumes 4 | # docker system prune -f --volumes -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | ktor-api-rest: 4 | build: . 5 | container_name: ktor-api-rest 6 | ports: 7 | - "6969:6969" 8 | - "6963:6963" 9 | 10 | 11 | -------------------------------------------------------------------------------- /docs/.swagger-codegen-ignore: -------------------------------------------------------------------------------- 1 | # Swagger Codegen Ignore 2 | # Generated by swagger-codegen https://github.com/swagger-api/swagger-codegen 3 | 4 | # Use this file to prevent files from being overwritten by the generator. 5 | # The patterns follow closely to .gitignore or .dockerignore. 6 | 7 | # As an example, the C# client generator defines ApiClient.cs. 8 | # You can make changes and tell Swagger Codgen to ignore just this file by uncommenting the following line: 9 | #ApiClient.cs 10 | 11 | # You can match any string of characters against a directory, file or extension with a single asterisk (*): 12 | #foo/*/qux 13 | # The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux 14 | 15 | # You can recursively match patterns against a directory, file or extension with a double asterisk (**): 16 | #foo/**/qux 17 | # This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux 18 | 19 | # You can also negate patterns with an exclamation (!). 20 | # For example, you can ignore all files in a docs folder with the file extension .md: 21 | #docs/*.md 22 | # Then explicitly reverse the ignore rule for a single file: 23 | #!docs/README.md 24 | -------------------------------------------------------------------------------- /docs/.swagger-codegen/VERSION: -------------------------------------------------------------------------------- 1 | 3.0.36 -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Basicos: Kotlin y Ktor 2 | ktor_version=2.2.2 3 | kotlin_version=1.8.0 4 | kotlin.code.style=official 5 | # Logger 6 | # logback_version=1.2.11 7 | micrologging_version=3.0.4 8 | logbackclassic_version=1.4.5 9 | # Cache 10 | cache_version=0.9.0 11 | # Testing 12 | junit_version=5.9.2 13 | mockk_version=1.13.2 14 | # Koin 15 | koin_version=3.3.2 16 | koin_ktor_version=3.3.0 17 | koin_ksp_version=1.1.0 18 | ksp_version=1.8.0-1.0.8" 19 | # Utilidades 20 | bcrypt_version=0.4 21 | coroutines_version=1.6.4 22 | # Database 23 | kotysa_version=2.3.3 24 | h2_r2dbc_version=0.9.1.RELEASE 25 | //=Swagger UI 26 | ktor_swagger_ui_version=1.0.2 27 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArisGuimera/tenistas-rest-ktor-2022-2023/702f23afb6f394988e5b8f816a022439a775508f/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /images/bcrypt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArisGuimera/tenistas-rest-ktor-2022-2023/702f23afb6f394988e5b8f816a022439a775508f/images/bcrypt.png -------------------------------------------------------------------------------- /images/cache.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArisGuimera/tenistas-rest-ktor-2022-2023/702f23afb6f394988e5b8f816a022439a775508f/images/cache.jpg -------------------------------------------------------------------------------- /images/cors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArisGuimera/tenistas-rest-ktor-2022-2023/702f23afb6f394988e5b8f816a022439a775508f/images/cors.png -------------------------------------------------------------------------------- /images/docker.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArisGuimera/tenistas-rest-ktor-2022-2023/702f23afb6f394988e5b8f816a022439a775508f/images/docker.jpg -------------------------------------------------------------------------------- /images/expla.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArisGuimera/tenistas-rest-ktor-2022-2023/702f23afb6f394988e5b8f816a022439a775508f/images/expla.png -------------------------------------------------------------------------------- /images/koin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArisGuimera/tenistas-rest-ktor-2022-2023/702f23afb6f394988e5b8f816a022439a775508f/images/koin.png -------------------------------------------------------------------------------- /images/ktor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArisGuimera/tenistas-rest-ktor-2022-2023/702f23afb6f394988e5b8f816a022439a775508f/images/ktor.png -------------------------------------------------------------------------------- /images/ktor_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /images/layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArisGuimera/tenistas-rest-ktor-2022-2023/702f23afb6f394988e5b8f816a022439a775508f/images/layers.png -------------------------------------------------------------------------------- /images/observer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArisGuimera/tenistas-rest-ktor-2022-2023/702f23afb6f394988e5b8f816a022439a775508f/images/observer.png -------------------------------------------------------------------------------- /images/postman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArisGuimera/tenistas-rest-ktor-2022-2023/702f23afb6f394988e5b8f816a022439a775508f/images/postman.png -------------------------------------------------------------------------------- /images/reactive.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArisGuimera/tenistas-rest-ktor-2022-2023/702f23afb6f394988e5b8f816a022439a775508f/images/reactive.gif -------------------------------------------------------------------------------- /images/swagger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArisGuimera/tenistas-rest-ktor-2022-2023/702f23afb6f394988e5b8f816a022439a775508f/images/swagger.png -------------------------------------------------------------------------------- /images/testing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArisGuimera/tenistas-rest-ktor-2022-2023/702f23afb6f394988e5b8f816a022439a775508f/images/testing.png -------------------------------------------------------------------------------- /images/tokens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArisGuimera/tenistas-rest-ktor-2022-2023/702f23afb6f394988e5b8f816a022439a775508f/images/tokens.png -------------------------------------------------------------------------------- /images/tsl.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArisGuimera/tenistas-rest-ktor-2022-2023/702f23afb6f394988e5b8f816a022439a775508f/images/tsl.jpg -------------------------------------------------------------------------------- /postman/imagen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArisGuimera/tenistas-rest-ktor-2022-2023/702f23afb6f394988e5b8f816a022439a775508f/postman/imagen.png -------------------------------------------------------------------------------- /postman/usuario.json: -------------------------------------------------------------------------------- 1 | { 2 | "nombre": "Usuario Actualizado", 3 | "email": "actualizado@actualizado.com" 4 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "tenistas-rest-ktor" -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/Application.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es 2 | 3 | import io.ktor.server.application.* 4 | import io.ktor.server.netty.* 5 | import joseluisgs.es.plugins.* 6 | import mu.KotlinLogging 7 | 8 | private val logger = KotlinLogging.logger {} 9 | fun main(args: Array): Unit = EngineMain.main(args) 10 | 11 | @Suppress("unused") // application.conf references the main function. This annotation prevents the IDE from marking it as unused. 12 | fun Application.module() { 13 | 14 | // Configuramos e iniciamos cada elemento o Plugin que necesitamos 15 | // OJO con el orden, si no se hace en el orden correcto, no funcionará o dará excepciones 16 | // El pensamiento es lógico, porque unos influyen en el resto 17 | 18 | // El primero es Koin, para que tenga poder inyectar dependencias del resto de cosas que necesitamos 19 | configureKoin() 20 | 21 | // Luego la base de datos, porque sin ella no podemos hacer nada 22 | configureDataBase() 23 | 24 | // Configuramos el almacenamiento 25 | configureStorage() 26 | 27 | // Configuramos Middleware la seguridad con JWT, debe ir antes que el resto de plugins que trabajen con rutas 28 | configureSecurity() 29 | 30 | // // Configuramos WebSockets, ideal para chat o notificaciones en tiempo real. Debe ir antes que las rutas http. 31 | configureWebSockets() 32 | 33 | // Principales que debería tener tu api rest!!! 34 | 35 | // Configuramos la serialización 36 | configureSerialization() 37 | 38 | // Configuramos las rutas 39 | configureRouting() 40 | 41 | // Configuramos la validación de body en requests, puedes hacerlo a mano, pero es más cómodo con este plugin 42 | configureValidation() 43 | 44 | // Opcionales segun el problema interesantes para el desarrollo 45 | 46 | // Configuramos el CORS, fudamentales si tenemos origenes cruzados 47 | configureCors() 48 | 49 | // Configuramos los headers de cacheo, 50 | // configureCachingHeaders() 51 | 52 | // Configuramos el compreso de gzip y otros 53 | // configureCompression() 54 | 55 | // Configuración de Swagger UI 56 | configureSwagger() 57 | 58 | // Otros... 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/config/DataBaseConfig.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.config 2 | 3 | import mu.KotlinLogging 4 | import org.koin.core.annotation.InjectedParam 5 | import org.koin.core.annotation.Single 6 | 7 | private val logger = KotlinLogging.logger {} 8 | 9 | @Single 10 | data class DataBaseConfig( 11 | @InjectedParam private val config: Map 12 | ) { 13 | val driver = config["driver"].toString() 14 | val protocol = config["protocol"].toString() 15 | val user = config["user"].toString() 16 | val password = config["password"].toString() 17 | val database = config["database"].toString() 18 | val initDatabaseData = config["initDatabaseData"]?.toBooleanStrictOrNull() ?: true 19 | 20 | 21 | init { 22 | logger.debug { "Iniciando la configuración de Base de Datos" } 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/config/StorageConfig.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.config 2 | 3 | import mu.KotlinLogging 4 | import org.koin.core.annotation.InjectedParam 5 | import org.koin.core.annotation.Single 6 | 7 | private val logger = KotlinLogging.logger {} 8 | 9 | @Single 10 | data class StorageConfig( 11 | @InjectedParam private val config: Map 12 | ) { 13 | val baseUrl = config["baseUrl"].toString() 14 | val secureUrl = config["secureUrl"].toString() 15 | val environment = config["environment"].toString() 16 | val uploadDir = config["uploadDir"].toString() 17 | val endpoint = config["endpoint"].toString() 18 | 19 | init { 20 | logger.debug { "Iniciando la configuración de Storage" } 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/config/TokenConfig.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.config 2 | 3 | import mu.KotlinLogging 4 | import org.koin.core.annotation.InjectedParam 5 | import org.koin.core.annotation.Single 6 | 7 | private val logger = KotlinLogging.logger {} 8 | 9 | @Single 10 | data class TokenConfig( 11 | @InjectedParam private val config: Map 12 | ) { 13 | val audience = config["audience"].toString() 14 | val secret = config["secret"].toString() 15 | val issuer = config["issuer"].toString() 16 | val realm = config["realm"].toString() 17 | val expiration = config["expiration"].toString().toLongOrNull() ?: 3600 18 | 19 | init { 20 | logger.debug { "Iniciando la configuración de Token" } 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/db/Data.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.db 2 | 3 | import joseluisgs.es.models.Raqueta 4 | import joseluisgs.es.models.Representante 5 | import joseluisgs.es.models.Tenista 6 | import joseluisgs.es.models.User 7 | import org.mindrot.jbcrypt.BCrypt 8 | import java.time.LocalDate 9 | import java.util.* 10 | 11 | // Datos de prueba 12 | 13 | // Representantes 14 | fun getRepresentantesInit() = listOf( 15 | Representante( 16 | id = UUID.fromString("b39a2fd2-f7d7-405d-b73c-b68a8dedbcdf"), 17 | nombre = "Pepe Perez", 18 | email = "pepe@perez.com" 19 | ), 20 | Representante( 21 | id = UUID.fromString("c53062e4-31ea-4f5e-a99d-36c228ed01a3"), 22 | nombre = "Juan Lopez", 23 | email = "juan@lopez.com" 24 | ), 25 | Representante( 26 | id = UUID.fromString("a33cd6a6-e767-48c3-b07b-ab7e015a73cd"), 27 | nombre = "Maria Garcia", 28 | email = "maria@garcia.com" 29 | ), 30 | ) 31 | 32 | // Raquetas 33 | fun getRaquetasInit() = listOf( 34 | Raqueta( 35 | id = UUID.fromString("86084458-4733-4d71-a3db-34b50cd8d68f"), 36 | marca = "Babolat", 37 | precio = 200.0, 38 | representanteId = UUID.fromString("b39a2fd2-f7d7-405d-b73c-b68a8dedbcdf") 39 | ), 40 | Raqueta( 41 | id = UUID.fromString("b0b5b2a1-5b1f-4b0f-8b1f-1b2c2b3c4d5e"), 42 | marca = "Wilson", 43 | precio = 250.0, 44 | representanteId = UUID.fromString("c53062e4-31ea-4f5e-a99d-36c228ed01a3") 45 | ), 46 | Raqueta( 47 | id = UUID.fromString("e4a7b78e-f9ca-43df-b186-3811554eeeb2"), 48 | marca = "Head", 49 | precio = 225.0, 50 | representanteId = UUID.fromString("a33cd6a6-e767-48c3-b07b-ab7e015a73cd") 51 | ), 52 | ) 53 | 54 | // Tenistas 55 | fun getTenistasInit() = listOf( 56 | Tenista( 57 | id = UUID.fromString("ea2962c6-2142-41b8-8dfb-0ecfe67e27df"), 58 | nombre = "Rafael Nadal", 59 | ranking = 2, 60 | fechaNacimiento = LocalDate.parse("1985-06-04"), 61 | añoProfesional = 2005, 62 | altura = 185, 63 | peso = 81, 64 | manoDominante = Tenista.ManoDominante.IZQUIERDA, 65 | tipoReves = Tenista.TipoReves.DOS_MANOS, 66 | puntos = 6789, 67 | pais = "España", 68 | raquetaId = UUID.fromString("86084458-4733-4d71-a3db-34b50cd8d68f") 69 | ), 70 | Tenista( 71 | id = UUID.fromString("f629e649-c6b7-4514-94a8-36bbcd4e7e1b"), 72 | nombre = "Roger Federer", 73 | ranking = 3, 74 | fechaNacimiento = LocalDate.parse("1981-01-01"), 75 | añoProfesional = 2000, 76 | altura = 188, 77 | peso = 83, 78 | manoDominante = Tenista.ManoDominante.DERECHA, 79 | tipoReves = Tenista.TipoReves.UNA_MANO, 80 | puntos = 3789, 81 | pais = "Suiza", 82 | raquetaId = UUID.fromString("b0b5b2a1-5b1f-4b0f-8b1f-1b2c2b3c4d5e") 83 | ), 84 | Tenista( 85 | id = UUID.fromString("24242ae7-1c81-434f-9b33-849a640d68a0"), 86 | nombre = "Novak Djokovic", 87 | ranking = 4, 88 | fechaNacimiento = LocalDate.parse("1986-05-05"), 89 | añoProfesional = 2004, 90 | altura = 189, 91 | peso = 81, 92 | manoDominante = Tenista.ManoDominante.DERECHA, 93 | tipoReves = Tenista.TipoReves.DOS_MANOS, 94 | puntos = 1970, 95 | pais = "Serbia", 96 | raquetaId = UUID.fromString("e4a7b78e-f9ca-43df-b186-3811554eeeb2") 97 | ), 98 | Tenista( 99 | id = UUID.fromString("af04e495-bacc-4bde-8d61-d52f78b52a86"), 100 | nombre = "Dominic Thiem", 101 | ranking = 5, 102 | fechaNacimiento = LocalDate.parse("1985-06-04"), 103 | añoProfesional = 2015, 104 | altura = 188, 105 | peso = 82, 106 | manoDominante = Tenista.ManoDominante.DERECHA, 107 | tipoReves = Tenista.TipoReves.UNA_MANO, 108 | puntos = 1234, 109 | pais = "Austria", 110 | raquetaId = UUID.fromString("86084458-4733-4d71-a3db-34b50cd8d68f") 111 | ), 112 | Tenista( 113 | id = UUID.fromString("a711040a-fb0d-4fe4-b726-75883ca8d907"), 114 | nombre = "Carlos Alcaraz", 115 | ranking = 1, 116 | fechaNacimiento = LocalDate.parse("2003-05-05"), 117 | añoProfesional = 2019, 118 | altura = 185, 119 | peso = 80, 120 | manoDominante = Tenista.ManoDominante.DERECHA, 121 | tipoReves = Tenista.TipoReves.DOS_MANOS, 122 | puntos = 6880, 123 | pais = "España", 124 | raquetaId = UUID.fromString("86084458-4733-4d71-a3db-34b50cd8d68f") 125 | ), 126 | ) 127 | 128 | // Usuarios 129 | fun getUsuariosInit() = listOf( 130 | User( 131 | id = UUID.fromString("b39a2fd2-f7d7-405d-b73c-b68a8dedbcdf"), 132 | nombre = "Pepe Perez", 133 | username = "pepe", 134 | email = "pepe@perez.com", 135 | password = BCrypt.hashpw("pepe1234", BCrypt.gensalt(12)), 136 | avatar = "https://upload.wikimedia.org/wikipedia/commons/f/f4/User_Avatar_2.png", 137 | role = User.Role.ADMIN 138 | ), 139 | User( 140 | id = UUID.fromString("c53062e4-31ea-4f5e-a99d-36c228ed01a3"), 141 | nombre = "Ana Lopez", 142 | username = "ana", 143 | email = "ana@lopez.com", 144 | password = BCrypt.hashpw("ana1234", BCrypt.gensalt(12)), 145 | avatar = "https://upload.wikimedia.org/wikipedia/commons/f/f4/User_Avatar_2.png", 146 | role = User.Role.USER 147 | ) 148 | ) 149 | 150 | 151 | -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/dto/Raqueta.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.dto 2 | 3 | import joseluisgs.es.serializers.LocalDateTimeSerializer 4 | import joseluisgs.es.serializers.UUIDSerializer 5 | import kotlinx.serialization.Serializable 6 | import java.time.LocalDateTime 7 | import java.util.* 8 | 9 | @Serializable 10 | data class RaquetasPageDto( 11 | val page: Int, 12 | val perPage: Int, 13 | val data: List, 14 | @Serializable(with = LocalDateTimeSerializer::class) 15 | val createdAt: LocalDateTime? = LocalDateTime.now() 16 | ) 17 | 18 | @Serializable 19 | data class RaquetaCreateDto( 20 | val marca: String, 21 | val precio: Double, 22 | @Serializable(with = UUIDSerializer::class) 23 | val representanteId: UUID, 24 | ) 25 | 26 | @Serializable 27 | data class RaquetaDto( 28 | @Serializable(with = UUIDSerializer::class) 29 | val id: UUID? = null, 30 | val marca: String, 31 | val precio: Double, 32 | val representante: RepresentanteDto, 33 | val metadata: MetaData? = null, 34 | ) { 35 | @Serializable 36 | data class MetaData( 37 | @Serializable(with = LocalDateTimeSerializer::class) 38 | val createdAt: LocalDateTime? = LocalDateTime.now(), 39 | @Serializable(with = LocalDateTimeSerializer::class) 40 | val updatedAt: LocalDateTime? = LocalDateTime.now(), 41 | val deleted: Boolean = false 42 | ) 43 | } 44 | 45 | @Serializable 46 | data class RaquetaTenistaDto( 47 | @Serializable(with = UUIDSerializer::class) 48 | val id: UUID? = null, 49 | val marca: String, 50 | val precio: Double, 51 | @Serializable(with = UUIDSerializer::class) 52 | val representanteId: UUID? = null, 53 | val metadata: MetaData? = null, 54 | ) { 55 | @Serializable 56 | data class MetaData( 57 | @Serializable(with = LocalDateTimeSerializer::class) 58 | val createdAt: LocalDateTime? = LocalDateTime.now(), 59 | @Serializable(with = LocalDateTimeSerializer::class) 60 | val updatedAt: LocalDateTime? = LocalDateTime.now(), 61 | val deleted: Boolean = false 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/dto/Representantes.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.dto 2 | 3 | import joseluisgs.es.serializers.LocalDateTimeSerializer 4 | import joseluisgs.es.serializers.UUIDSerializer 5 | import kotlinx.serialization.Serializable 6 | import java.time.LocalDateTime 7 | import java.util.* 8 | 9 | /** 10 | * Representante DTO para paginas de datos 11 | */ 12 | @Serializable 13 | data class RepresentantesPageDto( 14 | val page: Int, 15 | val perPage: Int, 16 | val data: List, 17 | @Serializable(with = LocalDateTimeSerializer::class) 18 | val createdAt: LocalDateTime? = LocalDateTime.now() 19 | ) 20 | 21 | /** 22 | * Representante DTO 23 | */ 24 | @Serializable 25 | data class RepresentanteDto( 26 | @Serializable(with = UUIDSerializer::class) 27 | val id: UUID? = null, 28 | val nombre: String, 29 | val email: String, 30 | val metadata: MetaData? = null, 31 | ) { 32 | @Serializable 33 | data class MetaData( 34 | @Serializable(with = LocalDateTimeSerializer::class) 35 | val createdAt: LocalDateTime? = LocalDateTime.now(), 36 | @Serializable(with = LocalDateTimeSerializer::class) 37 | val updatedAt: LocalDateTime? = LocalDateTime.now(), 38 | val deleted: Boolean = false 39 | ) 40 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/dto/Tenista.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.dto 2 | 3 | import joseluisgs.es.models.Tenista 4 | import joseluisgs.es.serializers.LocalDateSerializer 5 | import joseluisgs.es.serializers.LocalDateTimeSerializer 6 | import joseluisgs.es.serializers.UUIDSerializer 7 | import kotlinx.serialization.Serializable 8 | import java.time.LocalDate 9 | import java.time.LocalDateTime 10 | import java.util.* 11 | 12 | @Serializable 13 | data class TenistasPageDto( 14 | val page: Int, 15 | val perPage: Int, 16 | val data: List, 17 | @Serializable(with = LocalDateTimeSerializer::class) 18 | val createdAt: LocalDateTime? = LocalDateTime.now() 19 | ) 20 | 21 | @Serializable 22 | data class TenistaCreateDto( 23 | val nombre: String, 24 | val ranking: Int, 25 | @Serializable(with = LocalDateSerializer::class) 26 | val fechaNacimiento: LocalDate, 27 | val añoProfesional: Int, 28 | val altura: Int, 29 | val peso: Int, 30 | val manoDominante: Tenista.ManoDominante? = Tenista.ManoDominante.DERECHA, 31 | val tipoReves: Tenista.TipoReves? = Tenista.TipoReves.DOS_MANOS, 32 | val puntos: Int, 33 | val pais: String, 34 | @Serializable(with = UUIDSerializer::class) 35 | val raquetaId: UUID? = null, 36 | ) 37 | 38 | @Serializable 39 | data class TenistaDto( 40 | @Serializable(with = UUIDSerializer::class) 41 | val id: UUID? = null, 42 | val nombre: String, 43 | val ranking: Int, 44 | @Serializable(with = LocalDateSerializer::class) 45 | val fechaNacimiento: LocalDate, 46 | val añoProfesional: Int, 47 | val altura: Int, 48 | val peso: Int, 49 | val manoDominante: Tenista.ManoDominante, 50 | val tipoReves: Tenista.TipoReves, 51 | val puntos: Int, 52 | val pais: String, 53 | val raqueta: RaquetaTenistaDto?, 54 | val metadata: MetaData? = null, 55 | ) { 56 | @Serializable 57 | data class MetaData( 58 | @Serializable(with = LocalDateTimeSerializer::class) 59 | val createdAt: LocalDateTime? = LocalDateTime.now(), 60 | @Serializable(with = LocalDateTimeSerializer::class) 61 | val updatedAt: LocalDateTime? = LocalDateTime.now(), 62 | val deleted: Boolean = false 63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/dto/Test.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.dto 2 | 3 | import kotlinx.serialization.Serializable 4 | import java.time.LocalDateTime 5 | 6 | @Serializable 7 | data class TestDto( 8 | val message: String, 9 | val createdAt: String = LocalDateTime.now().toString() 10 | ) -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/dto/Users.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.dto 2 | 3 | import joseluisgs.es.models.User 4 | import joseluisgs.es.serializers.LocalDateTimeSerializer 5 | import joseluisgs.es.serializers.UUIDSerializer 6 | import kotlinx.serialization.Serializable 7 | import java.time.LocalDateTime 8 | import java.util.* 9 | 10 | @Serializable 11 | data class UserDto( 12 | @Serializable(with = UUIDSerializer::class) 13 | val id: UUID? = null, 14 | val nombre: String, 15 | val email: String, 16 | val username: String, 17 | val avatar: String, 18 | val role: User.Role, 19 | val metadata: MetaData 20 | ) { 21 | 22 | @Serializable 23 | data class MetaData( 24 | @Serializable(with = LocalDateTimeSerializer::class) 25 | val createdAt: LocalDateTime? = LocalDateTime.now(), 26 | @Serializable(with = LocalDateTimeSerializer::class) 27 | val updatedAt: LocalDateTime? = LocalDateTime.now(), 28 | val deleted: Boolean = false 29 | ) 30 | } 31 | 32 | @Serializable 33 | data class UserCreateDto( 34 | val nombre: String, 35 | val email: String, 36 | val username: String, 37 | val password: String, 38 | val avatar: String? = null, 39 | val role: User.Role? = User.Role.USER, 40 | ) 41 | 42 | @Serializable 43 | data class UserUpdateDto( 44 | val nombre: String, 45 | val email: String, 46 | val username: String, 47 | ) 48 | 49 | @Serializable 50 | data class UserLoginDto( 51 | val username: String, 52 | val password: String 53 | ) 54 | 55 | @Serializable 56 | data class UserWithTokenDto( 57 | val user: UserDto, 58 | val token: String 59 | ) -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/entities/Raqueta.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.entities 2 | 3 | import org.ufoss.kotysa.h2.H2Table 4 | import java.time.LocalDateTime 5 | import java.util.* 6 | 7 | object RaquetasTable : H2Table("raquetas") { 8 | // Identificador 9 | val id = uuid(RaquetaEntity::id).primaryKey() 10 | 11 | // Datos 12 | val marca = varchar(RaquetaEntity::marca, size = 100) 13 | val precio = doublePrecision(RaquetaEntity::precio) 14 | 15 | // Realcion con el representante 16 | val representanteId = uuid(RaquetaEntity::representanteId, "representante_id").foreignKey(RepresentantesTable.id) 17 | 18 | // Historicos y metadata 19 | val createdAt = timestamp(RaquetaEntity::createdAt, "created_at") 20 | val updatedAt = timestamp(RaquetaEntity::updatedAt, "updated_at") 21 | val deleted = boolean(RaquetaEntity::deleted) 22 | } 23 | 24 | // El DTO de la base de datos 25 | data class RaquetaEntity( 26 | // Identificador 27 | val id: UUID = UUID.randomUUID(), 28 | 29 | // Datos 30 | val marca: String, 31 | val precio: Double, 32 | 33 | // Realcion con el representante 34 | val representanteId: UUID, // No permitimos nulos 35 | 36 | // Historicos y metadata 37 | val createdAt: LocalDateTime = LocalDateTime.now(), 38 | val updatedAt: LocalDateTime = LocalDateTime.now(), 39 | val deleted: Boolean = false // Para el borrado lógico si es necesario 40 | ) -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/entities/Representante.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.entities 2 | 3 | import org.ufoss.kotysa.h2.H2Table 4 | import java.time.LocalDateTime 5 | import java.util.* 6 | 7 | /** 8 | * Objeto que representa la estructura relacional de [Representante] 9 | */ 10 | object RepresentantesTable : H2Table("representantes") { 11 | // Identificador 12 | val id = uuid(RepresentanteEntity::id).primaryKey() 13 | 14 | // Datos 15 | val nombre = varchar(RepresentanteEntity::nombre, size = 100) 16 | val email = varchar(RepresentanteEntity::email, size = 100) 17 | 18 | // Historicos y metadata 19 | val createdAt = timestamp(RepresentanteEntity::createdAt, "created_at") 20 | val updatedAt = timestamp(RepresentanteEntity::updatedAt, "updated_at") 21 | val deleted = boolean(RepresentanteEntity::deleted) 22 | } 23 | 24 | /** 25 | * Entidad que representa una fila de [Representante] 26 | * @see RepresentantesTable 27 | */ 28 | data class RepresentanteEntity( 29 | // Identificador 30 | val id: UUID = UUID.randomUUID(), 31 | 32 | // Datos 33 | val nombre: String, 34 | val email: String, 35 | 36 | // Historicos y metadata 37 | val createdAt: LocalDateTime = LocalDateTime.now(), 38 | val updatedAt: LocalDateTime = LocalDateTime.now(), 39 | val deleted: Boolean = false // Para el borrado lógico si es necesario 40 | ) -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/entities/Tenista.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.entities 2 | 3 | import org.ufoss.kotysa.h2.H2Table 4 | import java.time.LocalDate 5 | import java.time.LocalDateTime 6 | import java.util.* 7 | 8 | object TenistasTable : H2Table("tenistas") { 9 | // Identificador 10 | val id = uuid(TenistaEntity::id).primaryKey() 11 | 12 | // Datos 13 | val nombre = varchar(TenistaEntity::nombre, size = 100) 14 | val ranking = integer(TenistaEntity::ranking) 15 | val fechaNacimiento = date(TenistaEntity::fechaNacimiento, "fecha_nacimiento") 16 | val añoProfesional = integer(TenistaEntity::añoProfesional, "año_profesional") 17 | val altura = integer(TenistaEntity::altura) 18 | val peso = integer(TenistaEntity::peso) 19 | val manoDominante = varchar(TenistaEntity::manoDominante, "mano_dominante", size = 15) 20 | val tipoReves = varchar(TenistaEntity::tipoReves, "tipo_reves", size = 15) 21 | val puntos = integer(TenistaEntity::puntos) 22 | val pais = varchar(TenistaEntity::pais, size = 100) 23 | val raquetaId = uuid(TenistaEntity::raquetaId, "raqueta_id", null).foreignKey(RaquetasTable.id) 24 | 25 | // Historicos y metadata 26 | val createdAt = timestamp(TenistaEntity::createdAt, "created_at") 27 | val updatedAt = timestamp(TenistaEntity::updatedAt, "updated_at") 28 | val deleted = boolean(TenistaEntity::deleted) 29 | } 30 | 31 | // El DTO de la base de datos 32 | data class TenistaEntity( 33 | // Identificador 34 | val id: UUID, 35 | 36 | // Datos 37 | var nombre: String, 38 | var ranking: Int, 39 | var fechaNacimiento: LocalDate, 40 | var añoProfesional: Int, 41 | var altura: Int, 42 | var peso: Int, 43 | var manoDominante: String, 44 | var tipoReves: String, 45 | var puntos: Int, 46 | var pais: String, 47 | var raquetaId: UUID? = null, // No tiene por que tener raqueta 48 | 49 | // Historicos y metadata 50 | val createdAt: LocalDateTime = LocalDateTime.now(), 51 | val updatedAt: LocalDateTime = LocalDateTime.now(), 52 | val deleted: Boolean = false // Para el borrado lógico si es necesario 53 | ) 54 | -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/entities/User.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.entities 2 | 3 | import org.ufoss.kotysa.h2.H2Table 4 | import java.time.LocalDateTime 5 | import java.util.* 6 | 7 | /** 8 | * Tabla de roles de usuario 9 | */ 10 | object UsersTable : H2Table("users") { 11 | // Identificador 12 | val id = uuid(UserEntity::id).primaryKey() 13 | 14 | // Datos 15 | val nombre = varchar(UserEntity::nombre, size = 100) 16 | val email = varchar(UserEntity::email, size = 100) 17 | val username = varchar(UserEntity::username, size = 50) 18 | val password = varchar(UserEntity::password, size = 100) 19 | val avatar = varchar(UserEntity::avatar, size = 100) 20 | val role = varchar(UserEntity::role, size = 100) 21 | 22 | // Historicos y metadata 23 | val createdAt = timestamp(UserEntity::createdAt, "created_at") 24 | val updatedAt = timestamp(UserEntity::updatedAt, "updated_at") 25 | val deleted = boolean(UserEntity::deleted) 26 | } 27 | 28 | // El DTO de la base de datos 29 | data class UserEntity( 30 | // Identificador 31 | val id: UUID = UUID.randomUUID(), 32 | 33 | // Datos 34 | val nombre: String, 35 | val email: String, 36 | val username: String, 37 | val password: String, 38 | val avatar: String, 39 | val role: String, 40 | 41 | // Historicos y metadata 42 | val createdAt: LocalDateTime = LocalDateTime.now(), 43 | val updatedAt: LocalDateTime = LocalDateTime.now(), 44 | val deleted: Boolean = false // Para el borrado lógico si es necesario 45 | ) -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/exceptions/DataBaseException.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.exceptions 2 | 3 | sealed class DataBaseException(message: String?) : RuntimeException(message) 4 | class DataBaseIntegrityViolationException(message: String? = null) : DataBaseException(message) -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/exceptions/RaquetaException.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.exceptions 2 | 3 | sealed class RaquetaException(message: String) : RuntimeException(message) 4 | class RaquetaNotFoundException(message: String) : RaquetaException(message) 5 | class RaquetaBadRequestException(message: String) : RaquetaException(message) 6 | class RaquetaConflictIntegrityException(message: String) : RaquetaException(message) -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/exceptions/RepresentanteException.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.exceptions 2 | 3 | // Vamos a tipificar las excepciones y a crear una jerarquía de excepciones 4 | /** 5 | * RepresentanteException 6 | * @param message: String Mensaje de la excepción 7 | */ 8 | sealed class RepresentanteException(message: String) : RuntimeException(message) 9 | class RepresentanteNotFoundException(message: String) : RepresentanteException(message) 10 | class RepresentanteBadRequestException(message: String) : RepresentanteException(message) 11 | class RepresentanteConflictIntegrityException(message: String) : RepresentanteException(message) -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/exceptions/StorageException.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.exceptions 2 | 3 | // Vamos a tipificar las excepciones y a crear una jerarquía de excepciones 4 | sealed class StorageException(message: String) : RuntimeException(message) 5 | class StorageFileNotFoundException(message: String) : StorageException(message) 6 | class StorageFileNotSaveException(message: String) : StorageException(message) 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/exceptions/TenistaException.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.exceptions 2 | 3 | sealed class TenistaException(message: String) : RuntimeException(message) 4 | class TenistaNotFoundException(message: String) : TenistaException(message) 5 | class TenistaBadRequestException(message: String) : TenistaException(message) -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/exceptions/UserException.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.exceptions 2 | 3 | // Vamos a tipificar las excepciones y a crear una jerarquía de excepciones 4 | sealed class UserException(message: String) : RuntimeException(message) 5 | class UserNotFoundException(message: String) : UserException(message) 6 | class UserBadRequestException(message: String) : UserException(message) 7 | class UserUnauthorizedException(message: String) : UserException(message) 8 | class UserForbiddenException(message: String) : UserException(message) 9 | 10 | -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/mappers/Raqueta.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.mappers 2 | 3 | import joseluisgs.es.dto.RaquetaCreateDto 4 | import joseluisgs.es.dto.RaquetaDto 5 | import joseluisgs.es.dto.RaquetaTenistaDto 6 | import joseluisgs.es.entities.RaquetaEntity 7 | import joseluisgs.es.models.Raqueta 8 | import joseluisgs.es.models.Representante 9 | 10 | fun Raqueta.toDto(representante: Representante) = RaquetaDto( 11 | id = this.id, 12 | marca = this.marca, 13 | precio = this.precio, 14 | representante = representante.toDto(), 15 | metadata = RaquetaDto.MetaData( 16 | createdAt = this.createdAt, 17 | updatedAt = this.updatedAt, 18 | deleted = this.deleted // Solo se verá en el Json si es true 19 | ) 20 | ) 21 | 22 | fun Raqueta.toTenistaDto() = RaquetaTenistaDto( 23 | id = this.id, 24 | marca = this.marca, 25 | precio = this.precio, 26 | representanteId = this.representanteId, 27 | metadata = RaquetaTenistaDto.MetaData( 28 | createdAt = this.createdAt, 29 | updatedAt = this.updatedAt, 30 | deleted = this.deleted // Solo se verá en el Json si es true 31 | ) 32 | ) 33 | 34 | fun RaquetaCreateDto.toModel() = Raqueta( 35 | marca = this.marca, 36 | precio = this.precio, 37 | representanteId = this.representanteId, 38 | ) 39 | 40 | fun Raqueta.toEntity() = RaquetaEntity( 41 | id = this.id, 42 | marca = this.marca, 43 | precio = this.precio, 44 | representanteId = this.representanteId, 45 | createdAt = this.createdAt, 46 | updatedAt = this.updatedAt, 47 | deleted = this.deleted 48 | ) 49 | 50 | fun RaquetaEntity.toModel() = Raqueta( 51 | id = this.id, 52 | marca = this.marca, 53 | precio = this.precio, 54 | representanteId = this.representanteId, 55 | createdAt = this.createdAt, 56 | updatedAt = this.updatedAt, 57 | deleted = this.deleted 58 | ) 59 | 60 | -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/mappers/Representante.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.mappers 2 | 3 | import joseluisgs.es.dto.RepresentanteDto 4 | import joseluisgs.es.entities.RepresentanteEntity 5 | import joseluisgs.es.models.Representante 6 | 7 | /** 8 | * Transformamos un Representante en un RepresentanteDto 9 | */ 10 | fun Representante.toDto() = RepresentanteDto( 11 | id = this.id, 12 | nombre = this.nombre, 13 | email = this.email, 14 | metadata = RepresentanteDto.MetaData( 15 | createdAt = this.createdAt, 16 | updatedAt = this.updatedAt, 17 | deleted = this.deleted // Solo se verá en el Json si es true 18 | ) 19 | ) 20 | 21 | /** 22 | * Transformamos un RepresentanteDto en un Representante 23 | */ 24 | fun RepresentanteDto.toModel() = Representante( 25 | nombre = this.nombre, 26 | email = this.email 27 | ) 28 | 29 | /** 30 | * Transformamos un Representante en un Representante Entity 31 | */ 32 | fun Representante.toEntity() = RepresentanteEntity( 33 | id = this.id, 34 | nombre = this.nombre, 35 | email = this.email, 36 | createdAt = this.createdAt, 37 | updatedAt = this.updatedAt, 38 | deleted = this.deleted 39 | ) 40 | 41 | /** 42 | * Transformamos un Representante Entity en un Representante 43 | */ 44 | fun RepresentanteEntity.toModel() = Representante( 45 | id = this.id, 46 | nombre = this.nombre, 47 | email = this.email, 48 | createdAt = this.createdAt, 49 | updatedAt = this.updatedAt, 50 | deleted = this.deleted 51 | ) 52 | -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/mappers/Tenista.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.mappers 2 | 3 | import joseluisgs.es.dto.TenistaCreateDto 4 | import joseluisgs.es.dto.TenistaDto 5 | import joseluisgs.es.entities.TenistaEntity 6 | import joseluisgs.es.models.Raqueta 7 | import joseluisgs.es.models.Tenista 8 | 9 | fun Tenista.toDto(raqueta: Raqueta?) = TenistaDto( 10 | id = this.id, 11 | nombre = this.nombre, 12 | ranking = this.ranking, 13 | fechaNacimiento = this.fechaNacimiento, 14 | añoProfesional = this.añoProfesional, 15 | altura = this.altura, 16 | peso = this.peso, 17 | manoDominante = this.manoDominante, 18 | tipoReves = this.tipoReves, 19 | puntos = this.puntos, 20 | pais = this.pais, 21 | raqueta = raqueta?.toTenistaDto(), 22 | metadata = TenistaDto.MetaData( 23 | createdAt = this.createdAt, 24 | updatedAt = this.updatedAt, 25 | deleted = this.deleted // Solo se verá en el Json si es true 26 | ) 27 | ) 28 | 29 | fun TenistaCreateDto.toModel() = Tenista( 30 | nombre = this.nombre, 31 | ranking = this.ranking, 32 | fechaNacimiento = this.fechaNacimiento, 33 | añoProfesional = this.añoProfesional, 34 | altura = this.altura, 35 | peso = this.peso, 36 | manoDominante = this.manoDominante ?: Tenista.ManoDominante.DERECHA, 37 | tipoReves = this.tipoReves ?: Tenista.TipoReves.DOS_MANOS, 38 | puntos = this.puntos, 39 | pais = this.pais, 40 | raquetaId = this.raquetaId, 41 | ) 42 | 43 | fun Tenista.toEntity() = TenistaEntity( 44 | id = this.id, 45 | nombre = this.nombre, 46 | ranking = this.ranking, 47 | fechaNacimiento = this.fechaNacimiento, 48 | añoProfesional = this.añoProfesional, 49 | altura = this.altura, 50 | peso = this.peso, 51 | manoDominante = this.manoDominante.name, 52 | tipoReves = this.tipoReves.name, 53 | puntos = this.puntos, 54 | pais = this.pais, 55 | raquetaId = this.raquetaId, 56 | createdAt = this.createdAt, 57 | updatedAt = this.updatedAt, 58 | deleted = this.deleted 59 | ) 60 | 61 | fun TenistaEntity.toModel() = Tenista( 62 | id = this.id, 63 | nombre = this.nombre, 64 | ranking = this.ranking, 65 | fechaNacimiento = this.fechaNacimiento, 66 | añoProfesional = this.añoProfesional, 67 | altura = this.altura, 68 | peso = this.peso, 69 | manoDominante = Tenista.ManoDominante.valueOf(this.manoDominante), 70 | tipoReves = Tenista.TipoReves.valueOf(this.tipoReves), 71 | puntos = this.puntos, 72 | pais = this.pais, 73 | raquetaId = this.raquetaId, 74 | createdAt = this.createdAt, 75 | updatedAt = this.updatedAt, 76 | deleted = this.deleted 77 | ) 78 | 79 | -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/mappers/Users.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.mappers 2 | 3 | import joseluisgs.es.dto.UserCreateDto 4 | import joseluisgs.es.dto.UserDto 5 | import joseluisgs.es.entities.UserEntity 6 | import joseluisgs.es.models.User 7 | 8 | fun User.toDto(): UserDto { 9 | return UserDto( 10 | id = this.id, 11 | nombre = this.nombre, 12 | email = this.email, 13 | username = this.username, 14 | avatar = this.avatar, 15 | role = this.role, 16 | metadata = UserDto.MetaData( 17 | createdAt = this.createdAt, 18 | updatedAt = this.updatedAt, 19 | deleted = this.deleted 20 | ) 21 | ) 22 | } 23 | 24 | fun UserCreateDto.toModel(): User { 25 | return User( 26 | nombre = this.nombre, 27 | email = this.email, 28 | username = this.username, 29 | password = this.password, 30 | avatar = this.avatar ?: "https://upload.wikimedia.org/wikipedia/commons/f/f4/User_Avatar_2.png", 31 | role = this.role ?: User.Role.USER 32 | ) 33 | } 34 | 35 | fun UserEntity.toModel(): User { 36 | return User( 37 | id = this.id, 38 | nombre = this.nombre, 39 | email = this.email, 40 | username = this.username, 41 | password = this.password, 42 | avatar = this.avatar, 43 | role = User.Role.valueOf(this.role), 44 | createdAt = this.createdAt, 45 | updatedAt = this.updatedAt, 46 | deleted = this.deleted 47 | ) 48 | } 49 | 50 | fun User.toEntity(): UserEntity { 51 | return UserEntity( 52 | id = this.id, 53 | nombre = this.nombre, 54 | email = this.email, 55 | username = this.username, 56 | password = this.password, 57 | avatar = this.avatar, 58 | role = this.role.name, 59 | createdAt = this.createdAt, 60 | updatedAt = this.updatedAt, 61 | deleted = this.deleted 62 | ) 63 | } 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/models/Notifacion.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.models 2 | 3 | import joseluisgs.es.dto.RaquetaDto 4 | import joseluisgs.es.dto.RepresentanteDto 5 | import joseluisgs.es.dto.TenistaDto 6 | import joseluisgs.es.serializers.LocalDateTimeSerializer 7 | import joseluisgs.es.serializers.UUIDSerializer 8 | import kotlinx.serialization.Serializable 9 | import java.time.LocalDateTime 10 | import java.util.* 11 | 12 | // Las notificaciones son un modelo de datos que se usan para enviar mensajes a los usuarios 13 | // Los tipos de cambios que permito son 14 | @Serializable 15 | data class Notificacion( 16 | val entity: String, 17 | val tipo: Tipo, 18 | @Serializable(with = UUIDSerializer::class) 19 | val id: UUID, 20 | val data: T, 21 | @Serializable(with = LocalDateTimeSerializer::class) 22 | val createdAt: LocalDateTime? = LocalDateTime.now() 23 | ) { 24 | enum class Tipo { CREATE, UPDATE, DELETE } 25 | } 26 | 27 | // Mis alias, para no estar con los genéricos, mando el DTO por que es lo que quiero que se envíe con sus datos 28 | // visibles en el DTO igual que se ven en las llamadas REST 29 | typealias RepresentantesNotification = Notificacion // RepresentanteDto? 30 | typealias RaquetasNotification = Notificacion // RaquetaDto? 31 | typealias TenistasNotification = Notificacion // TenistaDto? 32 | -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/models/Raqueta.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.models 2 | 3 | import java.time.LocalDateTime 4 | import java.util.* 5 | 6 | data class Raqueta( 7 | // Identificador 8 | val id: UUID = UUID.randomUUID(), 9 | 10 | // Datos 11 | val marca: String, 12 | val precio: Double, 13 | 14 | // Relaciones 15 | val representanteId: UUID, 16 | 17 | // Historicos y metadata 18 | val createdAt: LocalDateTime = LocalDateTime.now(), 19 | val updatedAt: LocalDateTime = LocalDateTime.now(), 20 | val deleted: Boolean = false // Para el borrado lógico si es necesario 21 | 22 | ) -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/models/Representante.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.models 2 | 3 | import java.time.LocalDateTime 4 | import java.util.* 5 | 6 | /** 7 | * Representante Model 8 | */ 9 | data class Representante( 10 | // Identificador 11 | val id: UUID = UUID.randomUUID(), 12 | 13 | // Datos 14 | val nombre: String, 15 | val email: String, 16 | 17 | // Historicos y metadata 18 | val createdAt: LocalDateTime = LocalDateTime.now(), 19 | val updatedAt: LocalDateTime = LocalDateTime.now(), 20 | val deleted: Boolean = false // Para el borrado lógico si es necesario 21 | ) -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/models/Tenista.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.models 2 | 3 | import java.time.LocalDate 4 | import java.time.LocalDateTime 5 | import java.util.* 6 | 7 | data class Tenista( 8 | // Identificador 9 | val id: UUID = UUID.randomUUID(), 10 | 11 | // Datos 12 | var nombre: String, 13 | var ranking: Int, 14 | var fechaNacimiento: LocalDate, 15 | var añoProfesional: Int, 16 | var altura: Int, 17 | var peso: Int, 18 | var manoDominante: ManoDominante, 19 | var tipoReves: TipoReves, 20 | var puntos: Int, 21 | var pais: String, 22 | var raquetaId: UUID? = null, // No tiene por que tener raqueta 23 | 24 | // Historicos y metadata 25 | val createdAt: LocalDateTime = LocalDateTime.now(), 26 | val updatedAt: LocalDateTime = LocalDateTime.now(), 27 | val deleted: Boolean = false // Para el borrado lógico si es necesario 28 | ) { 29 | 30 | // ENUMS de la propia clase 31 | enum class ManoDominante { 32 | DERECHA, IZQUIERDA 33 | } 34 | 35 | enum class TipoReves { 36 | UNA_MANO, DOS_MANOS 37 | } 38 | 39 | 40 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/models/User.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.models 2 | 3 | import java.time.LocalDateTime 4 | import java.util.* 5 | 6 | data class User( 7 | // Identificador 8 | val id: UUID = UUID.randomUUID(), 9 | val nombre: String, 10 | val email: String, 11 | val username: String, 12 | val password: String, 13 | val avatar: String, 14 | val role: Role = Role.USER, 15 | 16 | // Historicos y metadata 17 | val createdAt: LocalDateTime = LocalDateTime.now(), 18 | val updatedAt: LocalDateTime = LocalDateTime.now(), 19 | val deleted: Boolean = false // Para el borrado lógico si es necesario 20 | ) { 21 | enum class Role { 22 | USER, ADMIN 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/plugins/CachingHeaders.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.plugins 2 | 3 | import io.ktor.http.* 4 | import io.ktor.http.content.* 5 | import io.ktor.server.application.* 6 | import io.ktor.server.plugins.cachingheaders.* 7 | import io.ktor.server.routing.* 8 | 9 | fun Application.configureCachingHeaders() { 10 | routing { 11 | // Definimos una estrategia de cacheo, en este caso global pero puede ser por ruta o llamada 12 | install(CachingHeaders) { 13 | options { call, content -> 14 | when (content.contentType?.withoutParameters()) { 15 | ContentType.Text.Plain -> CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 60)) 16 | ContentType.Text.Html -> CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 60)) 17 | // Json y otros 18 | ContentType.Any -> CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 10)) 19 | else -> null 20 | } 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/plugins/Compression.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.plugins 2 | 3 | import io.ktor.server.application.* 4 | import io.ktor.server.plugins.compression.* 5 | 6 | 7 | fun Application.configureCompression() { 8 | // Definimos una estrategia de compresión de contenido 9 | install(Compression) { 10 | gzip { 11 | // El tamaño mínimo para empezar a comprimir, podemos fijar un tamaño en bytes 12 | // y otras opciones 13 | minimumSize(1024) 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/plugins/Cors.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.plugins 2 | 3 | import io.ktor.http.* 4 | import io.ktor.server.application.* 5 | import io.ktor.server.plugins.cors.routing.* 6 | 7 | fun Application.configureCors() { 8 | install(CORS) { 9 | anyHost() // Permite cualquier host 10 | allowHeader(HttpHeaders.ContentType) // Permite el header Content-Type 11 | allowHeader(HttpHeaders.Authorization) 12 | // allowHost("client-host") // Allow requests from client-host 13 | 14 | // Podemos indicar qué host queremos permitir 15 | /*allowHost("client-host") // Allow requests from client-host 16 | allowHost("client-host:8081") // Allow requests from client-host on port 8081 17 | allowHost( 18 | "client-host", 19 | subDomains = listOf("en", "de", "es") 20 | ) // Allow requests from client-host on subdomains en, de and es 21 | allowHost("client-host", schemes = listOf("http", "https")) // Allow requests from client-host on http and https 22 | 23 | // o sobre qué métodos queremos permitir 24 | allowMethod(HttpMethod.Put) // Allow PUT method 25 | allowMethod(HttpMethod.Delete) // Allow DELETE method*/ 26 | } 27 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/plugins/DataBase.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.plugins 2 | 3 | import io.ktor.server.application.* 4 | import joseluisgs.es.config.DataBaseConfig 5 | import joseluisgs.es.services.database.DataBaseService 6 | import org.koin.core.parameter.parametersOf 7 | import org.koin.ktor.ext.get 8 | import org.koin.ktor.ext.inject 9 | 10 | fun Application.configureDataBase() { 11 | // Leemos la configuración de storage de nuestro fichero de configuración 12 | val dataBaseConfigParams = mapOf( 13 | "driver" to environment.config.property("database.driver").getString(), 14 | "protocol" to environment.config.property("database.protocol").getString(), 15 | "user" to environment.config.property("database.user").getString(), 16 | "password" to environment.config.property("database.password").getString(), 17 | "database" to environment.config.property("database.database").getString(), 18 | "initDatabaseData" to environment.config.property("database.initDatabaseData").getString(), 19 | ) 20 | 21 | // Inyectamos la configuración de DataBase 22 | val dataBaseConfig: DataBaseConfig = get { parametersOf(dataBaseConfigParams) } 23 | // Inyectamos el servicio de bases de datos 24 | val dataBaseService: DataBaseService by inject() 25 | // Inicializamos el servicio de storage 26 | dataBaseService.initDataBaseService() 27 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/plugins/Koin.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.plugins 2 | 3 | import io.ktor.server.application.* 4 | import org.koin.ksp.generated.defaultModule 5 | import org.koin.ktor.plugin.Koin 6 | import org.koin.logger.slf4jLogger 7 | 8 | fun Application.configureKoin() { 9 | install(Koin) { 10 | // Si quiero ver los logs de Koin 11 | slf4jLogger() 12 | // Modulos con las dependencias, usamos el default, si no crear modulos 13 | defaultModule() 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/plugins/Routing.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.plugins 2 | 3 | import io.github.smiley4.ktorswaggerui.dsl.get 4 | import io.ktor.http.* 5 | import io.ktor.server.application.* 6 | import io.ktor.server.response.* 7 | import io.ktor.server.routing.* 8 | import joseluisgs.es.routes.* 9 | 10 | // Configuramos las rutas con esta función de extensión 11 | // Podemos definirlas en un fichero aparte o dentro 12 | fun Application.configureRouting() { 13 | 14 | routing { 15 | // Defínelas por orden de prioridad y sin que se solapen 16 | 17 | // Ruta raíz 18 | get("/", { 19 | description = "Hola Tenistas Ktor" 20 | response { 21 | default { 22 | description = "Default Response" 23 | } 24 | HttpStatusCode.OK to { 25 | description = "Respuesta por defecto" 26 | body { description = "el saludo" } 27 | } 28 | } 29 | }) { 30 | call.respondText("Tenistas API REST Ktor. 2º DAM") 31 | } 32 | 33 | // podriamos añadir el resto de rutas aqui de la misma forma 34 | // get("/tenistas") { 35 | 36 | // Pero vamos a crear un fichero de rutas para ello 37 | } 38 | 39 | // Definidas dentro del paquete de rutas: routes 40 | webRoutes() // Rutas web /web 41 | // Intenta ponerlas por orden de importancia y acceso 42 | tenistasRoutes() // Rutas de api /rest/tenistas 43 | raquetasRoutes() // Rutas de api /rest/raquetas 44 | representantesRoutes() // Rutas de api /rest/representantes 45 | usersRoutes() // Rutas de api /rest/users 46 | storageRoutes() // Rutas de api /rest/storage 47 | testRoutes() // Rutas de api /rest/test 48 | } 49 | -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/plugins/Security.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.plugins 2 | 3 | import io.ktor.http.* 4 | import io.ktor.server.application.* 5 | import io.ktor.server.auth.* 6 | import io.ktor.server.auth.jwt.* 7 | import io.ktor.server.response.* 8 | import joseluisgs.es.config.TokenConfig 9 | import joseluisgs.es.services.tokens.TokensService 10 | import org.koin.core.parameter.parametersOf 11 | import org.koin.ktor.ext.get 12 | import org.koin.ktor.ext.inject 13 | 14 | // Seguridad en base a JWT 15 | fun Application.configureSecurity() { 16 | 17 | // Leemos la configuración de tokens de nuestro fichero de configuración 18 | val tokenConfigParams = mapOf( 19 | "audience" to environment.config.property("jwt.audience").getString(), 20 | "secret" to environment.config.property("jwt.secret").getString(), 21 | "issuer" to environment.config.property("jwt.issuer").getString(), 22 | "realm" to environment.config.property("jwt.realm").getString(), 23 | "expiration" to environment.config.property("jwt.expiration").getString() 24 | ) 25 | 26 | // Inyectamos la configuración de Tokens 27 | val tokenConfig: TokenConfig = get { parametersOf(tokenConfigParams) } 28 | 29 | // Inyectamos el servicio de tokens 30 | val jwtService: TokensService by inject() 31 | 32 | authentication { 33 | jwt { 34 | // Cargamos el verificador con los datos de la configuracion 35 | verifier(jwtService.verifyJWT()) 36 | // con realm aseguramos la ruta que estamos protegiendo 37 | realm = tokenConfig.realm 38 | validate { credential -> 39 | // Si el token es valido, ademas tiene la udiencia indicada, 40 | // y tiene el campo del usuario para compararlo con el que nosotros queremos 41 | // devolvemos el JWTPrincipal, si no devolvemos null 42 | if (credential.payload.audience.contains(tokenConfig.audience) && 43 | credential.payload.getClaim("username").asString().isNotEmpty() 44 | ) 45 | JWTPrincipal(credential.payload) 46 | else null 47 | } 48 | 49 | challenge { defaultScheme, realm -> 50 | call.respond(HttpStatusCode.Unauthorized, "Token invalido o expirado") 51 | } 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/plugins/Serialization.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.plugins 2 | 3 | import io.ktor.serialization.kotlinx.json.* 4 | import io.ktor.server.application.* 5 | import io.ktor.server.plugins.contentnegotiation.* 6 | import kotlinx.serialization.json.Json 7 | 8 | fun Application.configureSerialization() { 9 | install(ContentNegotiation) { 10 | // Lo ponemos bonito :) 11 | json(Json { 12 | prettyPrint = true 13 | isLenient = true 14 | }) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/plugins/Storage.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.plugins 2 | 3 | import io.ktor.server.application.* 4 | import joseluisgs.es.config.StorageConfig 5 | import joseluisgs.es.services.storage.StorageService 6 | import org.koin.core.parameter.parametersOf 7 | import org.koin.ktor.ext.get 8 | import org.koin.ktor.ext.inject 9 | 10 | fun Application.configureStorage() { 11 | // Leemos la configuración de storage de nuestro fichero de configuración 12 | val storageConfigParams = mapOf( 13 | "baseUrl" to environment.config.property("server.baseUrl").getString(), 14 | "secureUrl" to environment.config.property("server.baseSecureUrl").getString(), 15 | "environment" to environment.config.property("ktor.environment").getString(), 16 | "uploadDir" to environment.config.property("storage.uploadDir").getString(), 17 | "endpoint" to environment.config.property("storage.endpoint").getString() 18 | ) 19 | 20 | // Inyectamos la configuración de Storage 21 | val storageConfig: StorageConfig = get { parametersOf(storageConfigParams) } 22 | // Inyectamos el servicio de storage 23 | val storageService: StorageService by inject() 24 | // Inicializamos el servicio de storage 25 | storageService.initStorageDirectory() 26 | 27 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/plugins/Swagger.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.plugins 2 | 3 | import io.github.smiley4.ktorswaggerui.SwaggerUI 4 | import io.github.smiley4.ktorswaggerui.dsl.AuthScheme 5 | import io.github.smiley4.ktorswaggerui.dsl.AuthType 6 | import io.ktor.server.application.* 7 | 8 | fun Application.configureSwagger() { 9 | // Metodos oficiales de Ktor Team 10 | // Solo OpenAPI 11 | /*routing { 12 | openAPI(path = "openapi", swaggerFile = "openapi/documentation.yaml") 13 | }*/ 14 | // OpenAPI y SwaggerUI 15 | /*routing { 16 | swaggerUI(path = "swagger", swaggerFile = "openapi/documentation.yaml") 17 | }*/ 18 | 19 | 20 | // https://github.com/SMILEY4/ktor-swagger-ui/wiki/Configuration 21 | install(SwaggerUI) { 22 | swagger { 23 | swaggerUrl = "swagger" 24 | forwardRoot = false 25 | } 26 | info { 27 | title = "Ktor Tenistas API REST" 28 | version = "latest" 29 | description = "Ejemplo de una API Rest usando Ktor y tecnologías Kotlin." 30 | contact { 31 | name = "Jose Luis González Sánchez" 32 | url = "https://github.com/joseluisgs" 33 | } 34 | license { 35 | name = "Creative Commons Attribution-ShareAlike 4.0 International License" 36 | url = "https://joseluisgs.dev/docs/license/" 37 | } 38 | } 39 | server { 40 | url = environment.config.property("server.baseUrl").getString() 41 | description = "Servidor de la API Rest usando Ktor y tecnologías Kotlin." 42 | } 43 | 44 | schemasInComponentSection = true 45 | examplesInComponentSection = true 46 | automaticTagGenerator = { url -> url.firstOrNull() } 47 | // Filtramos las rutas que queremos documentar 48 | // Y los métodos 49 | // Si lo queremos todo, no hace falta filtrar nada 50 | pathFilter = { method, url -> 51 | url.contains("test") 52 | // Habiltamos el GET para todas y completo para test 53 | //(method == HttpMethod.Get && url.firstOrNull() == "api") 54 | // || url.contains("test") 55 | } 56 | 57 | securityScheme("JWT-Auth") { 58 | type = AuthType.HTTP 59 | scheme = AuthScheme.BEARER 60 | bearerFormat = "jwt" 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/plugins/Validation.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.plugins 2 | 3 | import io.ktor.server.application.* 4 | import io.ktor.server.plugins.requestvalidation.* 5 | import joseluisgs.es.validators.raquetasValidation 6 | import joseluisgs.es.validators.representantesValidation 7 | import joseluisgs.es.validators.tenistasValidation 8 | import joseluisgs.es.validators.usersValidation 9 | 10 | fun Application.configureValidation() { 11 | install(RequestValidation) { 12 | usersValidation() 13 | representantesValidation() 14 | raquetasValidation() 15 | tenistasValidation() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/plugins/WebSockets.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.plugins 2 | 3 | import io.ktor.serialization.kotlinx.* 4 | import io.ktor.server.application.* 5 | import io.ktor.server.websocket.* 6 | import kotlinx.serialization.json.Json 7 | 8 | fun Application.configureWebSockets() { 9 | install(WebSockets) { 10 | // Si queremos configurar algo más o personalizar el websocket 11 | // En este caso su serializacion 12 | contentConverter = KotlinxWebsocketSerializationConverter(Json { 13 | prettyPrint = true 14 | isLenient = true 15 | }) 16 | } 17 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/repositories/CrudRepository.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.repositories 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | 5 | // Importante voy a usar los nulos para evitar usar muchos errores y execepciones 6 | // Al poder usar la nulabilidad como algo expecional puedo usar el operador ? 7 | interface CrudRepository { 8 | suspend fun findAll(): Flow 9 | suspend fun findById(id: ID): T? 10 | suspend fun save(entity: T): T 11 | suspend fun update(id: ID, entity: T): T? 12 | suspend fun delete(entity: T): T? 13 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/repositories/raquetas/RaquetasCachedRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.repositories.raquetas 2 | 3 | import joseluisgs.es.exceptions.DataBaseIntegrityViolationException 4 | import joseluisgs.es.models.Raqueta 5 | import joseluisgs.es.services.cache.raquetas.RaquetasCache 6 | import kotlinx.coroutines.* 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.asFlow 9 | import mu.KotlinLogging 10 | import org.koin.core.annotation.Named 11 | import org.koin.core.annotation.Single 12 | import java.time.LocalDateTime 13 | import java.util.* 14 | 15 | private val logger = KotlinLogging.logger {} 16 | 17 | @Single 18 | @Named("RaquetasCachedRepository") 19 | class RaquetasCachedRepositoryImpl( 20 | @Named("RaquetasRepository") // Repositorio de datos originales 21 | private val repository: RaquetasRepository, 22 | private val cache: RaquetasCache // Desacoplamos la cache 23 | ) : RaquetasRepository { 24 | 25 | private var refreshJob: Job? = null // Job para cancelar la ejecución 26 | 27 | 28 | init { 29 | logger.debug { "Inicializando el repositorio cache raquetas. AutoRefreshAll: ${cache.hasRefreshAllCacheJob}" } 30 | // Iniciamos el proceso de refresco de datos 31 | // No es obligatorio hacerlo, pero si queremos que se refresque 32 | if (cache.hasRefreshAllCacheJob) 33 | refreshCacheJob() 34 | } 35 | 36 | private fun refreshCacheJob() { 37 | // Background job para refrescar el cache 38 | // Si tenemos muchos datos, solo se mete en el cache los que se van a usar: 39 | // create, findById, update, delete 40 | // Creamos un Scope propio para que no se enlazado con el actual. 41 | if (refreshJob != null) 42 | refreshJob?.cancel() 43 | 44 | refreshJob = CoroutineScope(Dispatchers.IO).launch { 45 | // refreshJob?.cancel() // Cancelamos el job si existe 46 | do { 47 | logger.debug { "refreshCache: Refrescando cache de Raquetas" } 48 | repository.findAll().collect { representante -> 49 | cache.cache.put(representante.id, representante) 50 | } 51 | logger.debug { "refreshCache: Cache actualizada: ${cache.cache.asMap().values.size}" } 52 | delay(cache.refreshTime) 53 | } while (true) 54 | } 55 | } 56 | 57 | override suspend fun findAll(): Flow { 58 | logger.debug { "findAll: Buscando todos las raquetas en cache" } 59 | 60 | // Si por alguna razón no tenemos datos en el cache, los buscamos en el repositorio 61 | // Ojo si le hemos puesto tamaño máximo a la caché, puede que no estén todos los datos 62 | // si no en los findAll, siempre devolver los datos del repositorio y no hacer refresco 63 | 64 | return if (!cache.hasRefreshAllCacheJob || cache.cache.asMap().isEmpty()) { 65 | logger.debug { "findAll: Devolviendo datos de repositorio" } 66 | repository.findAll() 67 | } else { 68 | logger.debug { "findAll: Devolviendo datos de cache" } 69 | cache.cache.asMap().values.asFlow() 70 | } 71 | } 72 | 73 | 74 | override suspend fun findAllPageable(page: Int, perPage: Int): Flow { 75 | logger.debug { "findAllPageable: Buscando todos las raquetas en cache con página: $page y cantidad: $perPage" } 76 | 77 | // Aquí no se puede cachear, ya que no se puede saber si hay más páginas 78 | // idem al findAll 79 | return repository.findAllPageable(page, perPage) 80 | } 81 | 82 | override suspend fun findByMarca(marca: String): Flow { 83 | logger.debug { "findByNombre: Buscando raquetas en cache con marca: $marca" } 84 | 85 | return repository.findByMarca(marca) 86 | } 87 | 88 | override suspend fun findById(id: UUID): Raqueta? { 89 | logger.debug { "findById: Buscando raqueta en cache con id: $id" } 90 | 91 | // Buscamos en la cache y si no está, lo buscamos en el repositorio y lo añadimos a la cache 92 | return cache.cache.get(id) ?: repository.findById(id) 93 | ?.also { cache.cache.put(id, it) } 94 | } 95 | 96 | override suspend fun save(entity: Raqueta): Raqueta { 97 | logger.debug { "save: Guardando raqueta en cache" } 98 | 99 | // Guardamos en el repositorio y en la cache en paralelo, creando antes el id 100 | val raqueta = 101 | entity.copy(id = UUID.randomUUID(), createdAt = LocalDateTime.now(), updatedAt = LocalDateTime.now()) 102 | // Creamos scope 103 | val scope = CoroutineScope(Dispatchers.IO) 104 | scope.launch { 105 | cache.cache.put(raqueta.id, raqueta) 106 | } 107 | scope.launch { 108 | repository.save(raqueta) 109 | } 110 | return raqueta 111 | } 112 | 113 | override suspend fun update(id: UUID, entity: Raqueta): Raqueta? { 114 | logger.debug { "update: Actualizando raqueta en cache" } 115 | 116 | // Debemos ver si existe en la cache, pero... 117 | // si no existe puede que esté en el repositorio, pero no en la cache 118 | // todo depende de si hemos limitado el tamaño de la cache y su tiempo de vida 119 | // o si nos hemos traído todos los datos en el findAll 120 | val existe = findById(id) // hace todo lo anterior 121 | return existe?.let { 122 | // Actualizamos en el repositorio y en la cache en paralelo creando antes el id, tomamos el created de quien ya estaba 123 | val raqueta = entity.copy(id = id, createdAt = existe.createdAt, updatedAt = LocalDateTime.now()) 124 | // Creamos scope 125 | val scope = CoroutineScope(Dispatchers.IO) 126 | scope.launch { 127 | cache.cache.put(raqueta.id, raqueta) 128 | } 129 | scope.launch { 130 | repository.update(id, raqueta) 131 | } 132 | return raqueta 133 | } 134 | } 135 | 136 | override suspend fun delete(entity: Raqueta): Raqueta? { 137 | logger.debug { "delete: Eliminando raqueta en cache" } 138 | 139 | // existe? 140 | val existe = findById(entity.id) 141 | return existe?.let { 142 | 143 | // Eliminamos en el repositorio y en la cache en paralelo 144 | // Creamos scope y un handler para el error 145 | val scope = CoroutineScope(Dispatchers.IO) 146 | scope.launch { 147 | cache.cache.invalidate(entity.id) 148 | } 149 | 150 | val delete = scope.async { 151 | repository.delete(entity) 152 | } 153 | // igual que try catch 154 | runCatching { 155 | delete.await() 156 | }.onFailure { 157 | throw DataBaseIntegrityViolationException() 158 | } 159 | 160 | return existe 161 | } 162 | } 163 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/repositories/raquetas/RaquetasRepository.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.repositories.raquetas 2 | 3 | import joseluisgs.es.models.Raqueta 4 | import joseluisgs.es.repositories.CrudRepository 5 | import kotlinx.coroutines.flow.Flow 6 | import java.util.* 7 | 8 | interface RaquetasRepository : CrudRepository { 9 | suspend fun findAllPageable(page: Int = 0, perPage: Int = 10): Flow 10 | suspend fun findByMarca(marca: String): Flow 11 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/repositories/raquetas/RaquetasRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.repositories.raquetas 2 | 3 | import joseluisgs.es.entities.RaquetasTable 4 | import joseluisgs.es.exceptions.DataBaseIntegrityViolationException 5 | import joseluisgs.es.mappers.toEntity 6 | import joseluisgs.es.mappers.toModel 7 | import joseluisgs.es.models.Raqueta 8 | import joseluisgs.es.services.database.DataBaseService 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.flow.Flow 11 | import kotlinx.coroutines.flow.filter 12 | import kotlinx.coroutines.flow.map 13 | import kotlinx.coroutines.withContext 14 | import mu.KotlinLogging 15 | import org.koin.core.annotation.Named 16 | import org.koin.core.annotation.Single 17 | import java.util.* 18 | 19 | private val logger = KotlinLogging.logger {} 20 | 21 | 22 | @Single 23 | @Named("RaquetasRepository") 24 | class RaquetasRepositoryImpl( 25 | private val dataBaseService: DataBaseService 26 | ) : RaquetasRepository { 27 | 28 | init { 29 | logger.debug { "Iniciando Repositorio de Raquetas" } 30 | } 31 | 32 | override suspend fun findAll(): Flow = withContext(Dispatchers.IO) { 33 | logger.debug { "findAll: Buscando todas las raquetas" } 34 | 35 | return@withContext (dataBaseService.client selectFrom RaquetasTable) 36 | .fetchAll() 37 | .map { it.toModel() } 38 | } 39 | 40 | override suspend fun findAllPageable(page: Int, perPage: Int): Flow = withContext(Dispatchers.IO) { 41 | logger.debug { "findAllPageable: Buscando todas las raquetas con página: $page y cantidad: $perPage" } 42 | 43 | val myLimit = if (perPage > 100) 100L else perPage.toLong() 44 | val myOffset = (page * perPage).toLong() 45 | 46 | return@withContext (dataBaseService.client selectFrom RaquetasTable limit myLimit offset myOffset) 47 | .fetchAll() 48 | .map { it.toModel() } 49 | 50 | } 51 | 52 | override suspend fun findById(id: UUID): Raqueta? = withContext(Dispatchers.IO) { 53 | logger.debug { "findById: Buscando raqueta con id: $id" } 54 | 55 | // Buscamos 56 | return@withContext (dataBaseService.client selectFrom RaquetasTable 57 | where RaquetasTable.id eq id 58 | ).fetchFirstOrNull()?.toModel() 59 | } 60 | 61 | override suspend fun findByMarca(marca: String): Flow = withContext(Dispatchers.IO) { 62 | logger.debug { "findByMarca: Buscando raqueta con marca: $marca" } 63 | 64 | return@withContext (dataBaseService.client selectFrom RaquetasTable).fetchAll() 65 | .filter { it.marca.lowercase().contains(marca.lowercase()) } 66 | .map { it.toModel() } 67 | } 68 | 69 | override suspend fun save(entity: Raqueta): Raqueta = withContext(Dispatchers.IO) { 70 | logger.debug { "save: Guardando raqueta: $entity" } 71 | 72 | return@withContext (dataBaseService.client insertAndReturn entity.toEntity()) 73 | .toModel() 74 | 75 | } 76 | 77 | override suspend fun update(id: UUID, entity: Raqueta): Raqueta? = withContext(Dispatchers.IO) { 78 | logger.debug { "update: Actualizando raqueta: $entity" } 79 | 80 | entity.let { 81 | val updateEntity = entity.toEntity() 82 | 83 | val res = (dataBaseService.client update RaquetasTable 84 | set RaquetasTable.marca eq updateEntity.marca 85 | set RaquetasTable.precio eq updateEntity.precio 86 | set RaquetasTable.representanteId eq updateEntity.representanteId 87 | where RaquetasTable.id eq id) 88 | .execute() 89 | 90 | if (res > 0) { 91 | return@withContext entity 92 | } else { 93 | return@withContext null 94 | } 95 | } 96 | 97 | } 98 | 99 | override suspend fun delete(entity: Raqueta): Raqueta? = withContext(Dispatchers.IO) { 100 | logger.debug { "delete: Guardando raqueta: ${entity.id}" } 101 | 102 | // Buscamos 103 | entity.let { 104 | // meto el try catch para que no una raqueta puede tener tenistas y no se pueda borrar 105 | try { 106 | val res = (dataBaseService.client deleteFrom RaquetasTable 107 | where RaquetasTable.id eq it.id) 108 | .execute() 109 | 110 | if (res > 0) { 111 | return@withContext entity 112 | } else { 113 | return@withContext null 114 | } 115 | } catch (e: Exception) { 116 | throw DataBaseIntegrityViolationException() 117 | } 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/repositories/representantes/RepresentantesRepository.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.repositories.representantes 2 | 3 | import joseluisgs.es.models.Representante 4 | import joseluisgs.es.repositories.CrudRepository 5 | import kotlinx.coroutines.flow.Flow 6 | import java.util.* 7 | 8 | interface RepresentantesRepository : CrudRepository { 9 | suspend fun findAllPageable(page: Int = 0, perPage: Int = 10): Flow 10 | suspend fun findByNombre(nombre: String): Flow 11 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/repositories/representantes/RepresentantesRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.repositories.representantes 2 | 3 | import joseluisgs.es.entities.RepresentantesTable 4 | import joseluisgs.es.exceptions.DataBaseIntegrityViolationException 5 | import joseluisgs.es.mappers.toEntity 6 | import joseluisgs.es.mappers.toModel 7 | import joseluisgs.es.models.Representante 8 | import joseluisgs.es.services.database.DataBaseService 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.flow.Flow 11 | import kotlinx.coroutines.flow.filter 12 | import kotlinx.coroutines.flow.map 13 | import kotlinx.coroutines.withContext 14 | import mu.KotlinLogging 15 | import org.koin.core.annotation.Named 16 | import org.koin.core.annotation.Single 17 | import java.util.* 18 | 19 | private val logger = KotlinLogging.logger {} 20 | 21 | @Single 22 | @Named("PersonasRepository") 23 | /** 24 | * Repositorio de [Representante] 25 | * @param dataBaseService Servicio de base de datos 26 | * @constructor Crea un repositorio de Representantes 27 | * @see RepresentantesRepository 28 | * @see Representante 29 | */ 30 | class RepresentantesRepositoryImpl( 31 | private val dataBaseService: DataBaseService 32 | ) : RepresentantesRepository { 33 | 34 | /** 35 | * Inicializamos el repositorio 36 | */ 37 | init { 38 | logger.debug { "Iniciando Repositorio de Representantes" } 39 | } 40 | 41 | /** 42 | * Buscamos todos los representantes 43 | * @return Flow de Representantes 44 | */ 45 | override suspend fun findAll(): Flow = withContext(Dispatchers.IO) { 46 | logger.debug { "findAll: Buscando todos los representantes" } 47 | 48 | return@withContext (dataBaseService.client selectFrom RepresentantesTable) 49 | .fetchAll() 50 | .map { it.toModel() } 51 | } 52 | 53 | /** 54 | * Buscamos todos los representantes paginados 55 | * @param page Página 56 | * @param perPage Cantidad por página 57 | * @return Flow de Representantes 58 | */ 59 | override suspend fun findAllPageable(page: Int, perPage: Int): Flow = withContext(Dispatchers.IO) { 60 | logger.debug { "findAllPageable: Buscando todos los representantes con página: $page y cantidad: $perPage" } 61 | 62 | val myLimit = if (perPage > 100) 100L else perPage.toLong() 63 | val myOffset = (page * perPage).toLong() 64 | 65 | return@withContext (dataBaseService.client selectFrom RepresentantesTable limit myLimit offset myOffset) 66 | .fetchAll() 67 | .map { it.toModel() } 68 | } 69 | 70 | /** 71 | * Buscamos un representante por su id 72 | * @param id Id del representante 73 | * @return Representante? Representante si existe o null si no existe 74 | */ 75 | override suspend fun findById(id: UUID): Representante? = withContext(Dispatchers.IO) { 76 | logger.debug { "findById: Buscando representante con id: $id" } 77 | 78 | // Buscamos 79 | return@withContext (dataBaseService.client selectFrom RepresentantesTable 80 | where RepresentantesTable.id eq id 81 | ).fetchFirstOrNull()?.toModel() 82 | } 83 | 84 | /** 85 | * Buscamos un representante por su nombre 86 | * @param nombre Nombre del representante 87 | * @return Flow de Representantes 88 | */ 89 | override suspend fun findByNombre(nombre: String): Flow = withContext(Dispatchers.IO) { 90 | logger.debug { "findByNombre: Buscando representante con nombre: $nombre" } 91 | 92 | return@withContext (dataBaseService.client selectFrom RepresentantesTable) 93 | .fetchAll() 94 | .filter { it.nombre.lowercase().contains(nombre.lowercase()) } 95 | .map { it.toModel() } 96 | } 97 | 98 | /** 99 | * Salvamos un representante 100 | * @param entity Representante a salvar 101 | * @return Representante salvado 102 | */ 103 | override suspend fun save(entity: Representante): Representante = withContext(Dispatchers.IO) { 104 | logger.debug { "save: Guardando representante: $entity" } 105 | 106 | return@withContext (dataBaseService.client insertAndReturn entity.toEntity()) 107 | .toModel() 108 | } 109 | 110 | /** 111 | * Actualizamos un representante 112 | * @param id Id del representante 113 | * @param entity Representante a actualizar 114 | * @return Representante? actualizado o null si no se ha podido actualizar 115 | */ 116 | override suspend fun update(id: UUID, entity: Representante): Representante? = withContext(Dispatchers.IO) { 117 | logger.debug { "update: Actualizando representante: $entity" } 118 | 119 | // Buscamos 120 | // val representante = findById(id) // no va a ser null por que lo filtro en la cache 121 | // Actualizamos los datos 122 | entity.let { 123 | val updateEntity = entity.toEntity() 124 | 125 | val res = (dataBaseService.client update RepresentantesTable 126 | set RepresentantesTable.nombre eq updateEntity.nombre 127 | set RepresentantesTable.email eq updateEntity.email 128 | where RepresentantesTable.id eq id) 129 | .execute() 130 | 131 | if (res > 0) { 132 | return@withContext entity 133 | } else { 134 | return@withContext null 135 | } 136 | } 137 | } 138 | 139 | /** 140 | * Borramos un representante 141 | * @param entity Representante a borrar 142 | * @return Representante? borrado o null si no se ha podido borrar 143 | */ 144 | override suspend fun delete(entity: Representante): Representante? = withContext(Dispatchers.IO) { 145 | logger.debug { "delete: Borrando representante con id: ${entity.id}" } 146 | 147 | // Buscamos 148 | // val representante = findById(id) // no va a ser null por que lo filtro en la cache 149 | // Borramos 150 | entity.let { 151 | // meto el try catch para que no se caiga la aplicación si no se puede borrar por tener raquetas asociadas 152 | try { 153 | val res = (dataBaseService.client deleteFrom RepresentantesTable 154 | where RepresentantesTable.id eq it.id) 155 | .execute() 156 | 157 | if (res > 0) { 158 | return@withContext entity 159 | } else { 160 | return@withContext null 161 | } 162 | } catch (e: Exception) { 163 | throw DataBaseIntegrityViolationException() 164 | } 165 | } 166 | } 167 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/repositories/tenistas/TenistasCachedRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.repositories.tenistas 2 | 3 | import joseluisgs.es.models.Tenista 4 | import joseluisgs.es.services.cache.tenistas.TenistasCache 5 | import kotlinx.coroutines.* 6 | import kotlinx.coroutines.flow.Flow 7 | import kotlinx.coroutines.flow.asFlow 8 | import mu.KotlinLogging 9 | import org.koin.core.annotation.Named 10 | import org.koin.core.annotation.Single 11 | import java.time.LocalDateTime 12 | import java.util.* 13 | 14 | private val logger = KotlinLogging.logger {} 15 | 16 | @Single 17 | @Named("TenistasCachedRepository") 18 | class TenistasCachedRepositoryImpl( 19 | @Named("TenistasRepository") // Repositorio de datos originales 20 | private val repository: TenistasRepository, 21 | private val cache: TenistasCache // Desacoplamos la cache 22 | ) : TenistasRepository { 23 | 24 | private var refreshJob: Job? = null // Job para cancelar la ejecución 25 | 26 | 27 | init { 28 | logger.debug { "Inicializando el repositorio cache tenistas. AutoRefreshAll: ${cache.hasRefreshAllCacheJob}" } 29 | // Iniciamos el proceso de refresco de datos 30 | // No es obligatorio hacerlo, pero si queremos que se refresque 31 | if (cache.hasRefreshAllCacheJob) 32 | refreshCacheJob() 33 | } 34 | 35 | private fun refreshCacheJob() { 36 | // Background job para refrescar el cache 37 | // Si tenemos muchos datos, solo se mete en el cache los que se van a usar: 38 | // create, findById, update, delete 39 | // Creamos un Scope propio para que no se enlazado con el actual. 40 | if (refreshJob != null) 41 | refreshJob?.cancel() 42 | 43 | refreshJob = CoroutineScope(Dispatchers.IO).launch { 44 | // refreshJob?.cancel() // Cancelamos el job si existe 45 | do { 46 | logger.debug { "refreshCache: Refrescando cache de Representantes" } 47 | repository.findAll().collect { representante -> 48 | cache.cache.put(representante.id, representante) 49 | } 50 | logger.debug { "refreshCache: Cache actualizada: ${cache.cache.asMap().values.size}" } 51 | delay(cache.refreshTime) 52 | } while (true) 53 | } 54 | } 55 | 56 | override suspend fun findAll(): Flow { 57 | logger.debug { "findAll: Buscando todos los tenistas en cache" } 58 | 59 | // Si por alguna razón no tenemos datos en el cache, los buscamos en el repositorio 60 | // Ojo si le hemos puesto tamaño máximo a la caché, puede que no estén todos los datos 61 | // si no en los findAll, siempre devolver los datos del repositorio y no hacer refresco 62 | 63 | return if (!cache.hasRefreshAllCacheJob || cache.cache.asMap().isEmpty()) { 64 | logger.debug { "findAll: Devolviendo datos de repositorio" } 65 | repository.findAll() 66 | } else { 67 | logger.debug { "findAll: Devolviendo datos de cache" } 68 | cache.cache.asMap().values.asFlow() 69 | } 70 | } 71 | 72 | 73 | override suspend fun findAllPageable(page: Int, perPage: Int): Flow { 74 | logger.debug { "findAllPageable: Buscando todos los tenistas con página: $page y cantidad: $perPage" } 75 | 76 | // Aquí no se puede cachear, ya que no se puede saber si hay más páginas 77 | // idem al findAll 78 | return repository.findAllPageable(page, perPage) 79 | } 80 | 81 | override suspend fun findByRanking(ranking: Int): Tenista? { 82 | logger.debug { "findByRanking: Buscando tenista con ranking: $ranking" } 83 | 84 | // Buscamos en la cache y si no está, lo buscamos en el repositorio y lo añadimos a la cache 85 | return cache.cache.asMap().values.find { it.ranking == ranking } 86 | ?: repository.findByRanking(ranking) 87 | ?.also { cache.cache.put(it.id, it) } 88 | } 89 | 90 | override suspend fun findByNombre(nombre: String): Flow { 91 | logger.debug { "findByNombre: Buscando tenista con nombre: $nombre" } 92 | 93 | return repository.findByNombre(nombre) 94 | } 95 | 96 | override suspend fun findById(id: UUID): Tenista? { 97 | logger.debug { "findById: Buscando tenista en cache con id: $id" } 98 | 99 | // Buscamos en la cache y si no está, lo buscamos en el repositorio y lo añadimos a la cache 100 | return cache.cache.get(id) ?: repository.findById(id) 101 | ?.also { cache.cache.put(id, it) } 102 | } 103 | 104 | 105 | override suspend fun save(entity: Tenista): Tenista { 106 | logger.debug { "save: Guardando tenista en cache" } 107 | 108 | // Guardamos en el repositorio y en la cache en paralelo, creando antes el id 109 | val tenista = 110 | entity.copy(id = UUID.randomUUID(), createdAt = LocalDateTime.now(), updatedAt = LocalDateTime.now()) 111 | // Creamos scope 112 | val scope = CoroutineScope(Dispatchers.IO) 113 | scope.launch { 114 | cache.cache.put(tenista.id, tenista) 115 | } 116 | scope.launch { 117 | repository.save(tenista) 118 | } 119 | return tenista 120 | } 121 | 122 | override suspend fun update(id: UUID, entity: Tenista): Tenista? { 123 | logger.debug { "update: Actualizando tenista en cache" } 124 | 125 | // Debemos ver si existe en la cache, pero... 126 | // si no existe puede que esté en el repositorio, pero no en la cache 127 | // todo depende de si hemos limitado el tamaño de la cache y su tiempo de vida 128 | // o si nos hemos traído todos los datos en el findAll 129 | val existe = findById(id) // hace todo lo anterior 130 | return existe?.let { 131 | // Actualizamos en el repositorio y en la cache en paralelo creando antes el id, tomamos el created de quien ya estaba 132 | val tenista = entity.copy(id = id, createdAt = existe.createdAt, updatedAt = LocalDateTime.now()) 133 | // Creamos scope 134 | val scope = CoroutineScope(Dispatchers.IO) 135 | scope.launch { 136 | cache.cache.put(tenista.id, tenista) 137 | } 138 | scope.launch { 139 | repository.update(id, tenista) 140 | } 141 | return tenista 142 | } 143 | } 144 | 145 | override suspend fun delete(entity: Tenista): Tenista? { 146 | logger.debug { "delete: Eliminando tenista en cache" } 147 | 148 | // existe? 149 | val existe = findById(entity.id) 150 | return existe?.let { 151 | // Eliminamos en el repositorio y en la cache en paralelo 152 | // Creamos scope y un handler para el error 153 | val scope = CoroutineScope(Dispatchers.IO) 154 | scope.launch { 155 | cache.cache.invalidate(entity.id) 156 | } 157 | scope.launch { 158 | repository.delete(entity) 159 | } 160 | 161 | return existe 162 | } 163 | } 164 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/repositories/tenistas/TenistasRepository.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.repositories.tenistas 2 | 3 | import joseluisgs.es.models.Tenista 4 | import joseluisgs.es.repositories.CrudRepository 5 | import kotlinx.coroutines.flow.Flow 6 | import java.util.* 7 | 8 | interface TenistasRepository : CrudRepository { 9 | suspend fun findAllPageable(page: Int = 0, perPage: Int = 10): Flow 10 | 11 | suspend fun findByRanking(ranking: Int): Tenista? 12 | suspend fun findByNombre(nombre: String): Flow 13 | 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/repositories/tenistas/TenistasRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.repositories.tenistas 2 | 3 | import joseluisgs.es.entities.TenistasTable 4 | import joseluisgs.es.mappers.toEntity 5 | import joseluisgs.es.mappers.toModel 6 | import joseluisgs.es.models.Tenista 7 | import joseluisgs.es.services.database.DataBaseService 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.flow.Flow 10 | import kotlinx.coroutines.flow.filter 11 | import kotlinx.coroutines.flow.map 12 | import kotlinx.coroutines.withContext 13 | import mu.KotlinLogging 14 | import org.koin.core.annotation.Named 15 | import org.koin.core.annotation.Single 16 | import java.util.* 17 | 18 | private val logger = KotlinLogging.logger {} 19 | 20 | @Single 21 | @Named("TenistasRepository") 22 | class TenistasRepositoryImpl( 23 | private val dataBaseService: DataBaseService 24 | ) : TenistasRepository { 25 | 26 | init { 27 | logger.debug { "Iniciando Repositorio de Tenistas" } 28 | } 29 | 30 | override suspend fun findAll(): Flow = withContext(Dispatchers.IO) { 31 | logger.debug { "findAll: Buscando los tenistas" } 32 | 33 | return@withContext (dataBaseService.client selectFrom TenistasTable 34 | orderByAsc TenistasTable.ranking) 35 | .fetchAll() 36 | .map { it.toModel() } 37 | } 38 | 39 | override suspend fun findAllPageable(page: Int, perPage: Int): Flow = withContext(Dispatchers.IO) { 40 | logger.debug { "findAllPageable: Buscando todos los tenistas con página: $page y cantidad: $perPage" } 41 | 42 | val myLimit = if (perPage > 100) 100L else perPage.toLong() 43 | val myOffset = (page * perPage).toLong() 44 | 45 | // Ojo que están ordenados por ranking 46 | return@withContext (dataBaseService.client selectFrom TenistasTable 47 | orderByAsc TenistasTable.ranking 48 | limit myLimit offset myOffset) 49 | .fetchAll() 50 | .map { it.toModel() } 51 | 52 | } 53 | 54 | override suspend fun findById(id: UUID): Tenista? = withContext(Dispatchers.IO) { 55 | logger.debug { "findById: Buscando tenista con id: $id" } 56 | 57 | // Buscamos 58 | return@withContext (dataBaseService.client selectFrom TenistasTable 59 | where TenistasTable.id eq id 60 | ).fetchFirstOrNull()?.toModel() 61 | } 62 | 63 | override suspend fun findByNombre(nombre: String): Flow = withContext(Dispatchers.IO) { 64 | logger.debug { "findByMarca: Buscando tenista con nombre: $nombre" } 65 | 66 | return@withContext (dataBaseService.client selectFrom TenistasTable) 67 | .fetchAll() 68 | .filter { it.nombre.lowercase().contains(nombre.lowercase()) } 69 | .map { it.toModel() } 70 | } 71 | 72 | override suspend fun findByRanking(ranking: Int): Tenista? = withContext(Dispatchers.IO) { 73 | logger.debug { "findByRanking: Buscando tenista con ranking: $ranking" } 74 | 75 | // Buscamos 76 | return@withContext (dataBaseService.client selectFrom TenistasTable 77 | where TenistasTable.ranking eq ranking 78 | ).fetchFirstOrNull()?.toModel() 79 | } 80 | 81 | override suspend fun save(entity: Tenista): Tenista = withContext(Dispatchers.IO) { 82 | logger.debug { "save: Guardando tenista: $entity" } 83 | 84 | return@withContext (dataBaseService.client insertAndReturn entity.toEntity()) 85 | .toModel() 86 | 87 | } 88 | 89 | override suspend fun update(id: UUID, entity: Tenista): Tenista? = withContext(Dispatchers.IO) { 90 | logger.debug { "update: Actualizando tenista: $entity" } 91 | 92 | entity.let { 93 | val updateEntity = entity.toEntity() 94 | 95 | val res = (dataBaseService.client update TenistasTable 96 | set TenistasTable.nombre eq updateEntity.nombre 97 | set TenistasTable.ranking eq updateEntity.ranking 98 | set TenistasTable.fechaNacimiento eq updateEntity.fechaNacimiento 99 | set TenistasTable.añoProfesional eq updateEntity.añoProfesional 100 | set TenistasTable.altura eq updateEntity.altura 101 | set TenistasTable.peso eq updateEntity.peso 102 | set TenistasTable.manoDominante eq updateEntity.manoDominante 103 | set TenistasTable.tipoReves eq updateEntity.tipoReves 104 | set TenistasTable.puntos eq updateEntity.puntos 105 | set TenistasTable.pais eq updateEntity.pais 106 | where TenistasTable.id eq id) 107 | .execute() 108 | 109 | if (res > 0) { 110 | return@withContext entity 111 | } else { 112 | return@withContext null 113 | } 114 | } 115 | 116 | } 117 | 118 | override suspend fun delete(entity: Tenista): Tenista? = withContext(Dispatchers.IO) { 119 | logger.debug { "delete: Guardando tenista: ${entity.id}" } 120 | 121 | // Buscamos 122 | entity.let { 123 | val res = (dataBaseService.client deleteFrom TenistasTable 124 | where TenistasTable.id eq it.id) 125 | .execute() 126 | 127 | if (res > 0) { 128 | return@withContext entity 129 | } else { 130 | return@withContext null 131 | } 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/repositories/users/UsersRepository.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.repositories.users 2 | 3 | import joseluisgs.es.models.User 4 | import joseluisgs.es.repositories.CrudRepository 5 | import kotlinx.coroutines.flow.Flow 6 | import java.util.* 7 | 8 | interface UsersRepository : CrudRepository { 9 | suspend fun findAll(limit: Int?): Flow 10 | suspend fun findByUsername(username: String): User? 11 | fun hashedPassword(password: String): String 12 | suspend fun checkUserNameAndPassword(username: String, password: String): User? 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/repositories/users/UsersRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.repositories.users 2 | 3 | import joseluisgs.es.entities.UsersTable 4 | import joseluisgs.es.mappers.toEntity 5 | import joseluisgs.es.mappers.toModel 6 | import joseluisgs.es.models.User 7 | import joseluisgs.es.services.database.DataBaseService 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.flow.Flow 10 | import kotlinx.coroutines.flow.map 11 | import kotlinx.coroutines.withContext 12 | import mu.KotlinLogging 13 | import org.koin.core.annotation.Single 14 | import org.mindrot.jbcrypt.BCrypt 15 | import java.util.* 16 | 17 | private val logger = KotlinLogging.logger {} 18 | private const val BCRYPT_SALT = 12 19 | 20 | @Single 21 | class UsersRepositoryImpl( 22 | private val dataBaseService: DataBaseService 23 | ) : UsersRepository { 24 | 25 | init { 26 | logger.debug { "Inicializando el repositorio de Usuarios" } 27 | } 28 | 29 | override suspend fun findAll(limit: Int?): Flow = withContext(Dispatchers.IO) { 30 | logger.debug { "findAll: Buscando todos los usuarios" } 31 | 32 | val myLimit = limit ?: Int.MAX_VALUE 33 | 34 | return@withContext (dataBaseService.client selectFrom UsersTable limit myLimit.toLong()) 35 | .fetchAll() 36 | .map { it.toModel() } 37 | } 38 | 39 | override suspend fun findAll(): Flow = withContext(Dispatchers.IO) { 40 | logger.debug { "findAll: Buscando todos los usuarios" } 41 | 42 | return@withContext (dataBaseService.client selectFrom UsersTable) 43 | .fetchAll() 44 | .map { it.toModel() } 45 | } 46 | 47 | 48 | override fun hashedPassword(password: String) = BCrypt.hashpw(password, BCrypt.gensalt(BCRYPT_SALT)) 49 | 50 | override suspend fun checkUserNameAndPassword(username: String, password: String): User? = 51 | withContext(Dispatchers.IO) { 52 | val user = findByUsername(username) 53 | return@withContext user?.let { 54 | if (BCrypt.checkpw(password, user.password)) { 55 | return@withContext user 56 | } 57 | return@withContext null 58 | } 59 | } 60 | 61 | override suspend fun findById(id: UUID): User? = withContext(Dispatchers.IO) { 62 | logger.debug { "findById: Buscando usuario con id: $id" } 63 | 64 | return@withContext (dataBaseService.client selectFrom UsersTable 65 | where UsersTable.id eq id 66 | ).fetchFirstOrNull()?.toModel() 67 | } 68 | 69 | override suspend fun findByUsername(username: String): User? = withContext(Dispatchers.IO) { 70 | logger.debug { "findByUsername: Buscando usuario con username: $username" } 71 | 72 | return@withContext (dataBaseService.client selectFrom UsersTable 73 | where UsersTable.username eq username 74 | ).fetchFirstOrNull()?.toModel() 75 | } 76 | 77 | 78 | override suspend fun save(entity: User): User = withContext(Dispatchers.IO) { 79 | logger.debug { "save: Guardando usuario: $entity" } 80 | 81 | return@withContext (dataBaseService.client insertAndReturn entity.toEntity()) 82 | .toModel() 83 | 84 | } 85 | 86 | override suspend fun update(id: UUID, entity: User): User? = withContext(Dispatchers.IO) { 87 | logger.debug { "update: Actualizando usuario: $entity" } 88 | 89 | // Buscamos, viene filtrado y si no el update no hace nada 90 | // val usuario = findById(id) 91 | 92 | // Actualizamos los datos 93 | entity.let { 94 | val updateEntity = entity.toEntity() 95 | 96 | val res = (dataBaseService.client update UsersTable 97 | set UsersTable.nombre eq updateEntity.nombre 98 | set UsersTable.email eq updateEntity.email 99 | set UsersTable.password eq updateEntity.password 100 | set UsersTable.avatar eq updateEntity.avatar 101 | set UsersTable.role eq updateEntity.role 102 | set UsersTable.updatedAt eq updateEntity.updatedAt 103 | where UsersTable.id eq id) 104 | .execute() 105 | 106 | if (res > 0) { 107 | return@withContext entity 108 | } else { 109 | return@withContext null 110 | } 111 | } 112 | 113 | } 114 | 115 | override suspend fun delete(entity: User): User? = withContext(Dispatchers.IO) { 116 | logger.debug { "delete: Eliminando usuario con id: ${entity.id}" } 117 | 118 | //val usuario = findById(id) 119 | 120 | entity.let { 121 | val res = (dataBaseService.client deleteFrom UsersTable 122 | where UsersTable.id eq it.id) 123 | .execute() 124 | 125 | if (res > 0) { 126 | return@withContext entity 127 | } else { 128 | return@withContext null 129 | } 130 | } 131 | } 132 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/routes/StorageRoutes.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.routes 2 | 3 | import io.ktor.http.* 4 | import io.ktor.server.application.* 5 | import io.ktor.server.auth.* 6 | import io.ktor.server.auth.jwt.* 7 | import io.ktor.server.request.* 8 | import io.ktor.server.response.* 9 | import io.ktor.server.routing.* 10 | import joseluisgs.es.exceptions.StorageFileNotFoundException 11 | import joseluisgs.es.exceptions.StorageFileNotSaveException 12 | import joseluisgs.es.services.storage.StorageService 13 | import mu.KotlinLogging 14 | import org.koin.ktor.ext.inject 15 | import java.time.LocalDateTime 16 | import java.util.* 17 | 18 | private val logger = KotlinLogging.logger {} 19 | 20 | private const val ENDPOINT = "api/storage" // Ruta de acceso, puede aunar un recurso 21 | 22 | fun Application.storageRoutes() { 23 | 24 | val storageService: StorageService by inject() 25 | 26 | routing { 27 | route("/$ENDPOINT") { 28 | // Get all -> / 29 | get("check") { 30 | logger.debug { "GET ALL /$ENDPOINT/check" } 31 | // respond, a veces no es necesario un dto, si lo tenemos muy claro 32 | // con un mapa de datos es suficiente 33 | call.respond( 34 | HttpStatusCode.OK, 35 | mapOf( 36 | "status" to "OK", 37 | "message" to "Storage API REST Ktor. 2º DAM", 38 | "createdAt" to LocalDateTime.now().toString() 39 | ) 40 | ) 41 | } 42 | post { 43 | // Recibimos el archivo 44 | logger.debug { "POST /$ENDPOINT" } 45 | try { 46 | val readChannel = call.receiveChannel() 47 | // Lo guardamos en disco 48 | val fileName = UUID.randomUUID().toString() 49 | val res = storageService.saveFile(fileName, readChannel) 50 | // Respondemos 51 | call.respond(HttpStatusCode.OK, res) 52 | } catch (e: StorageFileNotSaveException) { 53 | call.respond(HttpStatusCode.InternalServerError, e.message.toString()) 54 | } catch (e: Exception) { 55 | call.respondText("Error: ${e.message}") 56 | } 57 | } 58 | 59 | // GET -> /{fileName} 60 | get("{fileName}") { 61 | logger.debug { "GET /$ENDPOINT/{fileName}" } 62 | try { 63 | // Recuperamos el nombre del fichero 64 | val fileName = call.parameters["fileName"].toString() 65 | // Recuperamos el fichero 66 | val file = storageService.getFile(fileName) 67 | // De esta manera lo podria visiualizar el navegador 68 | // call.respondFile(file) 69 | // si lo hago así me pide descargar 70 | //call.response.header("Content-Disposition", "attachment; filename=\"${file.name}\"") 71 | // Para hacer lo anterior lo mejor es saber si tiene una extensión 72 | // y en función de eso devolver un tipo de contenido 73 | call.respondFile(file) 74 | } catch (e: StorageFileNotFoundException) { 75 | call.respond(HttpStatusCode.NotFound, e.message.toString()) 76 | } 77 | } 78 | 79 | // DELETE /rest/uploads/ 80 | // Si queremos rizar el rizo, podemos decir que solo borre si está autenticado 81 | // o cuando sea admin, etc.... quizas debas importar otro servicio 82 | // Estas rutas están autenticadas --> Protegidas por JWT 83 | authenticate { 84 | delete("{fileName}") { 85 | logger.debug { "DELETE /$ENDPOINT/{fileName}" } 86 | try { 87 | val jwt = call.principal() 88 | // Recuperamos el nombre del fichero 89 | val fileName = call.parameters["fileName"].toString() 90 | // Recuperamos el fichero 91 | storageService.deleteFile(fileName) 92 | // Respondemos 93 | call.respond(HttpStatusCode.NoContent) 94 | } catch (e: StorageFileNotFoundException) { 95 | call.respond(HttpStatusCode.NotFound, e.message.toString()) 96 | } 97 | } 98 | } 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/routes/WebRoutes.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.routes 2 | 3 | import io.ktor.server.application.* 4 | import io.ktor.server.http.content.* 5 | import io.ktor.server.routing.* 6 | 7 | // Vamos a crear una ruta web, para ello usamos una función de extensión de la clase Router 8 | // La llamamos webContent y le decimos el contenido que queremos que se muestre 9 | fun Application.webRoutes() { 10 | 11 | routing { 12 | // Contenido estático, desde la carpeta resources cuando entran a /web 13 | static { 14 | // Si nos preguntan por /web desde la raíz, le mandamos el contenido estático. 15 | // Tambiín aplicamos redireccion 16 | resource("/web", "web/index.html") 17 | resource("*", "web/index.html") 18 | // todo contenido estático con web/, lo busca en la carpeta web 19 | static("web") { 20 | resources("web") 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/serializers/LocalDateSerializer.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.serializers 2 | 3 | import kotlinx.serialization.KSerializer 4 | import kotlinx.serialization.descriptors.PrimitiveKind 5 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 6 | import kotlinx.serialization.encoding.Decoder 7 | import kotlinx.serialization.encoding.Encoder 8 | import java.time.LocalDate 9 | 10 | object LocalDateSerializer : KSerializer { 11 | override val descriptor = PrimitiveSerialDescriptor("LocalDate", PrimitiveKind.STRING) 12 | 13 | override fun deserialize(decoder: Decoder): LocalDate { 14 | return LocalDate.parse(decoder.decodeString()) 15 | } 16 | 17 | override fun serialize(encoder: Encoder, value: LocalDate) { 18 | encoder.encodeString(value.toString()) 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/serializers/LocalDateTimeSerializer.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.serializers 2 | 3 | import kotlinx.serialization.KSerializer 4 | import kotlinx.serialization.descriptors.PrimitiveKind 5 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 6 | import kotlinx.serialization.encoding.Decoder 7 | import kotlinx.serialization.encoding.Encoder 8 | import java.time.LocalDateTime 9 | 10 | object LocalDateTimeSerializer : KSerializer { 11 | override val descriptor = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING) 12 | 13 | override fun deserialize(decoder: Decoder): LocalDateTime { 14 | return LocalDateTime.parse(decoder.decodeString()) 15 | } 16 | 17 | override fun serialize(encoder: Encoder, value: LocalDateTime) { 18 | encoder.encodeString(value.toString()) 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/serializers/UUIDSerializer.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.serializers 2 | 3 | import kotlinx.serialization.KSerializer 4 | import kotlinx.serialization.descriptors.PrimitiveKind 5 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 6 | import kotlinx.serialization.encoding.Decoder 7 | import kotlinx.serialization.encoding.Encoder 8 | import java.util.* 9 | 10 | object UUIDSerializer : KSerializer { 11 | override val descriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING) 12 | 13 | override fun deserialize(decoder: Decoder): UUID { 14 | return UUID.fromString(decoder.decodeString()) 15 | } 16 | 17 | override fun serialize(encoder: Encoder, value: UUID) { 18 | encoder.encodeString(value.toString()) 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/services/cache/ICache.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.services.cache 2 | 3 | import io.github.reactivecircus.cache4k.Cache 4 | 5 | interface ICache { 6 | val hasRefreshAllCacheJob: Boolean // Si queremos que se refresque el cache con todos los datos 7 | val refreshTime: Long // tiempo de refresco de todos los datos 8 | val cache: Cache // La caché 9 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/services/cache/raquetas/RaquetasCache.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.services.cache.raquetas 2 | 3 | import joseluisgs.es.models.Raqueta 4 | import joseluisgs.es.services.cache.ICache 5 | import java.util.* 6 | 7 | interface RaquetasCache : ICache -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/services/cache/raquetas/RaquetasCacheImpl.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.services.cache.raquetas 2 | 3 | import io.github.reactivecircus.cache4k.Cache 4 | import joseluisgs.es.models.Raqueta 5 | import mu.KotlinLogging 6 | import org.koin.core.annotation.Single 7 | import java.util.* 8 | import kotlin.time.Duration.Companion.minutes 9 | 10 | private val logger = KotlinLogging.logger {} 11 | 12 | @Single 13 | class RaquetasCacheImpl : RaquetasCache { 14 | override val hasRefreshAllCacheJob: Boolean = false // Si queremos que se refresque el cache 15 | override val refreshTime = 60 * 60 * 1000L // 1 hora en milisegundos 16 | 17 | // Creamos la caché y configuramos a medida 18 | override val cache = Cache.Builder() 19 | // Si le ponemos opciones de cacheo si no usara las de por defecto 20 | .maximumCacheSize(100) // Tamaño máximo de la caché si queremos limitarla 21 | .expireAfterAccess(60.minutes) // Vamos a cachear durante 22 | .build() 23 | 24 | init { 25 | logger.debug { "Iniciando el sistema de caché de raquetas" } 26 | } 27 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/services/cache/representantes/RepresentantesCache.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.services.cache.representantes 2 | 3 | import joseluisgs.es.models.Representante 4 | import joseluisgs.es.services.cache.ICache 5 | import java.util.* 6 | 7 | interface RepresentantesCache : ICache -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/services/cache/representantes/raquetasCacheImpl.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.services.cache.representantes 2 | 3 | import io.github.reactivecircus.cache4k.Cache 4 | import joseluisgs.es.models.Representante 5 | import mu.KotlinLogging 6 | import org.koin.core.annotation.Single 7 | import java.util.* 8 | import kotlin.time.Duration.Companion.minutes 9 | 10 | private val logger = KotlinLogging.logger {} 11 | 12 | @Single 13 | /** 14 | * Cache de [Representante] 15 | * @property hasRefreshAllCacheJob Si queremos que se refresque el cache 16 | * @property refreshTime Tiempo de refresco de la caché 17 | * @property cache Caché de [Representante] 18 | * @constructor Crea una caché de [Representante] 19 | * @see [Cache4k](https://reactivecircus.github.io/cache4k/) 20 | * @see Representante 21 | */ 22 | class RepresentantesCacheImpl : RepresentantesCache { 23 | override val hasRefreshAllCacheJob: Boolean = false // Si queremos que se refresque el cache 24 | override val refreshTime = 60 * 60 * 1000L // 1 hora en milisegundos 25 | 26 | // Creamos la caché y configuramos a medida 27 | override val cache = Cache.Builder() 28 | // Si le ponemos opciones de cacheo si no usara las de por defecto 29 | .maximumCacheSize(100) // Tamaño máximo de la caché si queremos limitarla 30 | .expireAfterAccess(60.minutes) // Vamos a cachear durante 31 | .build() 32 | 33 | init { 34 | logger.debug { "Iniciando el sistema de caché de representantes" } 35 | } 36 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/services/cache/tenistas/TenistasCache.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.services.cache.tenistas 2 | 3 | import joseluisgs.es.models.Tenista 4 | import joseluisgs.es.services.cache.ICache 5 | import java.util.* 6 | 7 | interface TenistasCache : ICache -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/services/cache/tenistas/TenistasCacheImpl.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.services.cache.tenistas 2 | 3 | import io.github.reactivecircus.cache4k.Cache 4 | import joseluisgs.es.models.Tenista 5 | import mu.KotlinLogging 6 | import org.koin.core.annotation.Single 7 | import java.util.* 8 | import kotlin.time.Duration.Companion.minutes 9 | 10 | private val logger = KotlinLogging.logger {} 11 | 12 | @Single 13 | class TenistasCacheImpl : TenistasCache { 14 | override val hasRefreshAllCacheJob: Boolean = false // Si queremos que se refresque el cache 15 | override val refreshTime = 60 * 60 * 1000L // 1 hora en milisegundos 16 | 17 | // Creamos la caché y configuramos a medida 18 | override val cache = Cache.Builder() 19 | // Si le ponemos opciones de cacheo si no usara las de por defecto 20 | .maximumCacheSize(100) // Tamaño máximo de la caché si queremos limitarla 21 | .expireAfterAccess(60.minutes) // Vamos a cachear durante 22 | .build() 23 | 24 | init { 25 | logger.debug { "Iniciando el sistema de caché de tenistas" } 26 | } 27 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/services/database/DataBaseService.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.services.database 2 | 3 | import io.r2dbc.spi.ConnectionFactories 4 | import io.r2dbc.spi.ConnectionFactoryOptions 5 | import joseluisgs.es.config.DataBaseConfig 6 | import joseluisgs.es.db.getRaquetasInit 7 | import joseluisgs.es.db.getRepresentantesInit 8 | import joseluisgs.es.db.getTenistasInit 9 | import joseluisgs.es.db.getUsuariosInit 10 | import joseluisgs.es.entities.RaquetasTable 11 | import joseluisgs.es.entities.RepresentantesTable 12 | import joseluisgs.es.entities.TenistasTable 13 | import joseluisgs.es.entities.UsersTable 14 | import joseluisgs.es.mappers.toEntity 15 | import kotlinx.coroutines.CoroutineScope 16 | import kotlinx.coroutines.Dispatchers 17 | import kotlinx.coroutines.launch 18 | import kotlinx.coroutines.runBlocking 19 | import mu.KotlinLogging 20 | import org.koin.core.annotation.Single 21 | import org.ufoss.kotysa.H2Tables 22 | import org.ufoss.kotysa.r2dbc.sqlClient 23 | import org.ufoss.kotysa.tables 24 | 25 | private val logger = KotlinLogging.logger {} 26 | 27 | @Single 28 | class DataBaseService( 29 | private val dataBaseConfig: DataBaseConfig 30 | ) { 31 | 32 | 33 | private val connectionOptions = ConnectionFactoryOptions.builder() 34 | .option(ConnectionFactoryOptions.DRIVER, dataBaseConfig.driver) 35 | .option(ConnectionFactoryOptions.PROTOCOL, dataBaseConfig.protocol) // file, mem 36 | .option(ConnectionFactoryOptions.USER, dataBaseConfig.user) 37 | .option(ConnectionFactoryOptions.PASSWORD, dataBaseConfig.password) 38 | .option(ConnectionFactoryOptions.DATABASE, dataBaseConfig.database) 39 | .build() 40 | 41 | val client = ConnectionFactories 42 | .get(connectionOptions) 43 | .sqlClient(getTables()) 44 | 45 | val initData get() = dataBaseConfig.initDatabaseData 46 | 47 | 48 | fun initDataBaseService() { 49 | logger.debug { "Inicializando servicio de Bases de Datos: ${dataBaseConfig.database}" } 50 | 51 | // creamos las tablas 52 | createTables() 53 | 54 | // Inicializamos los datos de la base de datos 55 | if (initData) { 56 | logger.debug { "Inicializando datos de la base de datos" } 57 | clearDataBaseData() 58 | initDataBaseData() 59 | } 60 | } 61 | 62 | private fun getTables(): H2Tables { 63 | // Creamos un objeto H2Tables con las tablas de la base de datos 64 | // Entidades de la base de datos 65 | return tables() 66 | .h2( 67 | UsersTable, 68 | RepresentantesTable, 69 | RaquetasTable, 70 | TenistasTable 71 | ) 72 | } 73 | 74 | private fun createTables() = runBlocking { 75 | val scope = CoroutineScope(Dispatchers.IO) 76 | logger.debug { "Creando tablas de la base de datos" } 77 | // Creamos las tablas 78 | scope.launch { 79 | client createTableIfNotExists UsersTable 80 | client createTableIfNotExists RepresentantesTable 81 | client createTableIfNotExists RaquetasTable 82 | client createTableIfNotExists TenistasTable 83 | } 84 | } 85 | 86 | fun clearDataBaseData() = runBlocking { 87 | // Primero borramos los datos evitando la cascada 88 | logger.debug { "Borrando datos..." } 89 | try { 90 | client deleteAllFrom TenistasTable 91 | client deleteAllFrom RaquetasTable 92 | client deleteAllFrom RepresentantesTable 93 | client deleteAllFrom UsersTable 94 | } catch (_: Exception) { 95 | 96 | } 97 | } 98 | 99 | fun initDataBaseData() = runBlocking { 100 | 101 | // Creamos los datos 102 | logger.debug { "Creando datos..." } 103 | 104 | // Seguimos el orden de las tablas 105 | 106 | logger.debug { "Creando usuarios..." } 107 | getUsuariosInit().forEach { 108 | client insert it.toEntity() 109 | } 110 | 111 | logger.debug { "Creando representantes..." } 112 | getRepresentantesInit().forEach { 113 | client insert it.toEntity() 114 | } 115 | 116 | logger.debug { "Creando raquetas..." } 117 | getRaquetasInit().forEach { 118 | client insert it.toEntity() 119 | } 120 | 121 | logger.debug { "Creando tenistas..." } 122 | getTenistasInit().forEach { 123 | client insert it.toEntity() 124 | } 125 | } 126 | 127 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/services/raquetas/RaquetasService.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.services.raquetas 2 | 3 | import joseluisgs.es.models.Raqueta 4 | import joseluisgs.es.models.RaquetasNotification 5 | import joseluisgs.es.models.Representante 6 | import kotlinx.coroutines.flow.Flow 7 | import java.util.* 8 | 9 | interface RaquetasService { 10 | suspend fun findAll(): Flow 11 | suspend fun findAllPageable(page: Int, perPage: Int): Flow 12 | suspend fun findById(id: UUID): Raqueta 13 | suspend fun findByMarca(marca: String): Flow 14 | suspend fun save(raqueta: Raqueta): Raqueta 15 | suspend fun update(id: UUID, raqueta: Raqueta): Raqueta 16 | suspend fun delete(id: UUID): Raqueta 17 | suspend fun findRepresentante(id: UUID): Representante 18 | 19 | // Suscripción a cambios para notificar tiempo real 20 | fun addSuscriptor(id: Int, suscriptor: suspend (RaquetasNotification) -> Unit) 21 | fun removeSuscriptor(id: Int) 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/services/raquetas/RaquetasServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.services.raquetas 2 | 3 | import joseluisgs.es.exceptions.RaquetaConflictIntegrityException 4 | import joseluisgs.es.exceptions.RaquetaNotFoundException 5 | import joseluisgs.es.exceptions.RepresentanteNotFoundException 6 | import joseluisgs.es.mappers.toDto 7 | import joseluisgs.es.models.* 8 | import joseluisgs.es.repositories.raquetas.RaquetasRepository 9 | import joseluisgs.es.repositories.representantes.RepresentantesRepository 10 | import kotlinx.coroutines.CoroutineScope 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.launch 14 | import kotlinx.coroutines.withContext 15 | import mu.KotlinLogging 16 | import org.koin.core.annotation.Named 17 | import org.koin.core.annotation.Single 18 | import java.util.* 19 | 20 | private val logger = KotlinLogging.logger {} 21 | 22 | @Single 23 | // @Named("RepresentantesService") 24 | class RaquetasServiceImpl( 25 | @Named("RaquetasCachedRepository") // Repositorio de Representantes Cacheado 26 | private val repository: RaquetasRepository, 27 | @Named("RepresentantesCachedRepository") // Repositorio de Representantes Cacheado 28 | private val representantesRepository: RepresentantesRepository 29 | ) : RaquetasService { 30 | 31 | init { 32 | logger.debug { "Inicializando el servicio de raquetas" } 33 | } 34 | 35 | override suspend fun findAll(): Flow { 36 | logger.debug { "findAll: Buscando todas las raquetas en servicio" } 37 | 38 | return repository.findAll() 39 | } 40 | 41 | override suspend fun findAllPageable(page: Int, perPage: Int): Flow = withContext(Dispatchers.IO) { 42 | logger.debug { "findAllPageable: Buscando todas las raquetas en servicio con página: $page y cantidad: $perPage" } 43 | 44 | return@withContext repository.findAllPageable(page, perPage) 45 | } 46 | 47 | override suspend fun findById(id: UUID): Raqueta { 48 | logger.debug { "findById: Buscando raqueta en servicio con id: $id" } 49 | 50 | // return repository.findById(id) ?: throw NoSuchElementException("No se ha encontrado el representante con id: $id") 51 | return repository.findById(id) 52 | ?: throw RaquetaNotFoundException("No se ha encontrado la raqueta con id: $id") 53 | 54 | } 55 | 56 | override suspend fun findByMarca(marca: String): Flow { 57 | logger.debug { "findByNombre: Buscando raqueta en servicio con marca: $marca" } 58 | 59 | return repository.findByMarca(marca) 60 | } 61 | 62 | 63 | override suspend fun save(raqueta: Raqueta): Raqueta { 64 | logger.debug { "create: Creando raqueta en servicio" } 65 | 66 | // Existe el representante! 67 | val representante = findRepresentante(raqueta.representanteId) 68 | 69 | // Insertamos el representante y devolvemos el resultado y avisa a los subscriptores 70 | return repository.save(raqueta) 71 | .also { onChange(Notificacion.Tipo.CREATE, it.id, it) } 72 | } 73 | 74 | override suspend fun update(id: UUID, raqueta: Raqueta): Raqueta { 75 | logger.debug { "update: Actualizando raqueta en servicio" } 76 | 77 | val existe = repository.findById(id) 78 | 79 | // Existe el representante! 80 | val representante = findRepresentante(raqueta.representanteId) 81 | 82 | existe?.let { 83 | return repository.update(id, raqueta) 84 | ?.also { onChange(Notificacion.Tipo.UPDATE, it.id, it) }!! 85 | } ?: throw RaquetaNotFoundException("No se ha encontrado la raqueta con id: $id") 86 | } 87 | 88 | override suspend fun delete(id: UUID): Raqueta { 89 | logger.debug { "delete: Borrando raqueta en servicio" } 90 | 91 | val existe = repository.findById(id) 92 | 93 | existe?.let { 94 | // meto el try catch para que no se caiga la aplicación si no se puede borrar por tener raquetas asociadas 95 | try { 96 | return repository.delete(existe) 97 | .also { onChange(Notificacion.Tipo.DELETE, it!!.id, it) }!! 98 | } catch (e: Exception) { 99 | throw RaquetaConflictIntegrityException("No se puede borrar la raqueta con id: $id porque tiene tenistas asociados") 100 | } 101 | } ?: throw RaquetaNotFoundException("No se ha encontrado la raqueta con id: $id") 102 | } 103 | 104 | override suspend fun findRepresentante(id: UUID): Representante { 105 | logger.debug { "findRepresentante: Buscando representante en servicio" } 106 | 107 | return representantesRepository.findById(id) 108 | ?: throw RepresentanteNotFoundException("No se ha encontrado el representante con id: $id") 109 | } 110 | 111 | /// ---- Tiempo real, patrón observer!!! 112 | 113 | // Mis suscriptores, un mapa de codigo, con la función que se ejecutará 114 | // Si no te gusta usar la función como parámetro, puedes usar el objeto de la sesión (pero para eso Kotlin 115 | // es funcional ;) 116 | private val suscriptores = 117 | mutableMapOf Unit>() 118 | 119 | override fun addSuscriptor(id: Int, suscriptor: suspend (RaquetasNotification) -> Unit) { 120 | logger.debug { "addSuscriptor: Añadiendo suscriptor con id: $id" } 121 | 122 | // Añadimos el suscriptor, que es la función que se ejecutará 123 | suscriptores[id] = suscriptor 124 | } 125 | 126 | override fun removeSuscriptor(id: Int) { 127 | logger.debug { "removeSuscriptor: Desconectando suscriptor con id: $" } 128 | 129 | suscriptores.remove(id) 130 | } 131 | 132 | // Se ejecuta en cada cambio 133 | private suspend fun onChange(tipo: Notificacion.Tipo, id: UUID, data: Raqueta? = null) { 134 | logger.debug { "onChange: Cambio en Raquetas: $tipo, notificando a los suscriptores afectada entidad: $data" } 135 | 136 | val myScope = CoroutineScope(Dispatchers.IO) 137 | // Por cada suscriptor, ejecutamos la función que se ha almacenado 138 | // Si almacenas el objeto de la sesión, puedes usar el método de la sesión, que es sendSerialized 139 | myScope.launch { 140 | suscriptores.values.forEach { 141 | it.invoke( 142 | RaquetasNotification( 143 | "RAQUETA", 144 | tipo, 145 | id, 146 | data?.toDto(findRepresentante(data.representanteId)) 147 | ) 148 | ) 149 | } 150 | } 151 | } 152 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/services/representantes/RepresentantesService.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.services.representantes 2 | 3 | import joseluisgs.es.models.Representante 4 | import joseluisgs.es.models.RepresentantesNotification 5 | import kotlinx.coroutines.flow.Flow 6 | import java.util.* 7 | 8 | interface RepresentantesService { 9 | suspend fun findAll(): Flow 10 | suspend fun findAllPageable(page: Int, perPage: Int): Flow 11 | suspend fun findById(id: UUID): Representante 12 | suspend fun findByNombre(nombre: String): Flow 13 | suspend fun save(representante: Representante): Representante 14 | suspend fun update(id: UUID, representante: Representante): Representante 15 | suspend fun delete(id: UUID): Representante 16 | 17 | // Suscripción a cambios para notificar tiempo real 18 | fun addSuscriptor(id: Int, suscriptor: suspend (RepresentantesNotification) -> Unit) 19 | fun removeSuscriptor(id: Int) 20 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/services/storage/StorageService.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.services.storage 2 | 3 | import io.ktor.utils.io.* 4 | import joseluisgs.es.config.StorageConfig 5 | import java.io.File 6 | 7 | interface StorageService { 8 | fun getConfig(): StorageConfig 9 | fun initStorageDirectory() 10 | suspend fun saveFile(fileName: String, fileBytes: ByteArray): Map 11 | suspend fun saveFile(fileName: String, fileBytes: ByteReadChannel): Map 12 | suspend fun getFile(fileName: String): File 13 | suspend fun deleteFile(fileName: String) 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/services/storage/StorageServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.services.storage 2 | 3 | import io.ktor.util.cio.* 4 | import io.ktor.utils.io.* 5 | import joseluisgs.es.config.StorageConfig 6 | import joseluisgs.es.exceptions.StorageFileNotFoundException 7 | import joseluisgs.es.exceptions.StorageFileNotSaveException 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.withContext 10 | import mu.KotlinLogging 11 | import org.koin.core.annotation.Single 12 | import java.io.File 13 | import java.time.LocalDateTime 14 | 15 | private val logger = KotlinLogging.logger {} 16 | 17 | @Single 18 | class StorageServiceImpl( 19 | private val storageConfig: StorageConfig 20 | ) : StorageService { 21 | 22 | init { 23 | logger.debug { "Iniciando servicio de almacenamiento en: ${storageConfig.uploadDir}" } 24 | } 25 | 26 | override fun getConfig(): StorageConfig { 27 | return storageConfig 28 | } 29 | 30 | override fun initStorageDirectory() { 31 | logger.debug { "Iniciando el directorio de almacenamiento en: ${storageConfig.uploadDir}" } 32 | if (!File(storageConfig.uploadDir).exists()) { 33 | logger.debug { "Creando el directorio de almacenamiento en: ${storageConfig.uploadDir}" } 34 | File(storageConfig.uploadDir).mkdir() 35 | } else { 36 | // Si existe, borramos todos los ficheros // solo en dev 37 | if (storageConfig.environment == "dev") { 38 | logger.debug { "Modo de desarrollo. Borrando el contenido del almacenamiento" } 39 | File(storageConfig.uploadDir).listFiles()?.forEach { it.delete() } 40 | } 41 | } 42 | } 43 | 44 | override suspend fun saveFile(fileName: String, fileBytes: ByteArray): Map = 45 | withContext(Dispatchers.IO) { 46 | try { 47 | val file = File("${storageConfig.uploadDir}/$fileName") 48 | file.writeBytes(fileBytes) // sobreescritura si existe 49 | logger.debug { "Fichero guardado en: ${file.absolutePath}" } 50 | return@withContext mapOf( 51 | "fileName" to fileName, 52 | "createdAt" to LocalDateTime.now().toString(), 53 | "size" to fileBytes.size.toString(), 54 | "baseUrl" to storageConfig.baseUrl + "/" + storageConfig.endpoint + "/" + fileName, 55 | "secureUrl" to storageConfig.secureUrl + "/" + storageConfig.endpoint + "/" + fileName, 56 | ) 57 | } catch (e: Exception) { 58 | throw StorageFileNotSaveException("Error al guardar el fichero: ${e.message}") 59 | } 60 | } 61 | 62 | override suspend fun saveFile(fileName: String, fileBytes: ByteReadChannel): Map = 63 | withContext(Dispatchers.IO) { 64 | try { 65 | logger.debug { "Guardando fichero en: $fileName" } 66 | val file = File("${storageConfig.uploadDir}/$fileName") 67 | val res = fileBytes.copyAndClose(file.writeChannel()) 68 | logger.debug { "Fichero guardado en: $file" } 69 | return@withContext mapOf( 70 | "fileName" to fileName, 71 | "createdAt" to LocalDateTime.now().toString(), 72 | "size" to res.toString(), 73 | "baseUrl" to storageConfig.baseUrl + "/" + storageConfig.endpoint + "/" + fileName, 74 | "secureUrl" to storageConfig.secureUrl + "/" + storageConfig.endpoint + "/" + fileName, 75 | ) 76 | } catch (e: Exception) { 77 | throw StorageFileNotSaveException("Error al guardar el fichero: ${e.message}") 78 | } 79 | } 80 | 81 | override suspend fun getFile(fileName: String): File = withContext(Dispatchers.IO) { 82 | logger.debug { "Buscando fichero en: $fileName" } 83 | val file = File("${storageConfig.uploadDir}/$fileName") 84 | logger.debug { "Fichero path: $file" } 85 | if (!file.exists()) { 86 | throw StorageFileNotFoundException("No se ha encontrado el fichero: $fileName") 87 | } else { 88 | return@withContext file 89 | } 90 | } 91 | 92 | override suspend fun deleteFile(fileName: String): Unit = withContext(Dispatchers.IO) { 93 | logger.debug { "Borrando fichero en: $fileName" } 94 | val file = File("${storageConfig.uploadDir}/$fileName") 95 | logger.debug { "Fichero path: $file" } 96 | if (!file.exists()) { 97 | throw StorageFileNotFoundException("No se ha encontrado el fichero: $fileName") 98 | } else { 99 | file.delete() 100 | } 101 | } 102 | 103 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/services/tenistas/TenistasService.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.services.tenistas 2 | 3 | import joseluisgs.es.models.Raqueta 4 | import joseluisgs.es.models.Tenista 5 | import joseluisgs.es.models.TenistasNotification 6 | import kotlinx.coroutines.flow.Flow 7 | import java.util.* 8 | 9 | interface TenistasService { 10 | suspend fun findAll(): Flow 11 | suspend fun findAllPageable(page: Int, perPage: Int): Flow 12 | suspend fun findById(id: UUID): Tenista 13 | suspend fun findByNombre(nombre: String): Flow 14 | suspend fun findByRanking(ranking: Int): Tenista 15 | suspend fun save(tenista: Tenista): Tenista 16 | suspend fun update(id: UUID, tenista: Tenista): Tenista 17 | suspend fun delete(id: UUID): Tenista 18 | suspend fun findRaqueta(raquetaId: UUID?): Raqueta? 19 | 20 | // Suscripción a cambios para notificar tiempo real 21 | fun addSuscriptor(id: Int, suscriptor: suspend (TenistasNotification) -> Unit) 22 | fun removeSuscriptor(id: Int) 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/services/tenistas/TenistasServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.services.tenistas 2 | 3 | import joseluisgs.es.exceptions.RaquetaNotFoundException 4 | import joseluisgs.es.exceptions.TenistaNotFoundException 5 | import joseluisgs.es.mappers.toDto 6 | import joseluisgs.es.models.* 7 | import joseluisgs.es.repositories.raquetas.RaquetasRepository 8 | import joseluisgs.es.repositories.tenistas.TenistasRepository 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.flow.Flow 11 | import kotlinx.coroutines.withContext 12 | import mu.KotlinLogging 13 | import org.koin.core.annotation.Named 14 | import org.koin.core.annotation.Single 15 | import java.util.* 16 | 17 | private val logger = KotlinLogging.logger {} 18 | 19 | @Single 20 | class TenistasServiceImpl( 21 | @Named("TenistasCachedRepository") // Repositorio de Tenistas Cacheado 22 | private val repository: TenistasRepository, 23 | @Named("RaquetasCachedRepository") // Repositorio de Raquetas Cacheado 24 | private val raquetasRepository: RaquetasRepository 25 | ) : TenistasService { 26 | 27 | init { 28 | logger.debug { "Inicializando el servicio de tenistas" } 29 | } 30 | 31 | override suspend fun findAll(): Flow { 32 | logger.debug { "findAll: Buscando todos los tenistas en servicio" } 33 | 34 | return repository.findAll() 35 | } 36 | 37 | override suspend fun findAllPageable(page: Int, perPage: Int): Flow = withContext(Dispatchers.IO) { 38 | logger.debug { "findAllPageable: Buscando todos los tenistas en servicio con página: $page y cantidad: $perPage" } 39 | 40 | return@withContext repository.findAllPageable(page, perPage) 41 | } 42 | 43 | override suspend fun findById(id: UUID): Tenista { 44 | logger.debug { "findById: Buscando tenista en servicio con id: $id" } 45 | 46 | return repository.findById(id) 47 | ?: throw TenistaNotFoundException("No se ha encontrado el tenista con id: $id") 48 | 49 | } 50 | 51 | override suspend fun findByNombre(nombre: String): Flow { 52 | logger.debug { "findByNombre: Buscando tenistas en servicio con nombre: $nombre" } 53 | 54 | return repository.findByNombre(nombre) 55 | } 56 | 57 | override suspend fun findByRanking(ranking: Int): Tenista { 58 | logger.debug { "findByRanking: Buscando tenistas en servicio con ranking: $ranking" } 59 | 60 | return repository.findByRanking(ranking) 61 | ?: throw TenistaNotFoundException("No se ha encontrado el tenista con ranking: $ranking") 62 | } 63 | 64 | override suspend fun save(tenista: Tenista): Tenista { 65 | logger.debug { "create: Creando tenistas en servicio" } 66 | 67 | // Existe la raqueta!! 68 | val raqueta = findRaqueta(tenista.raquetaId) 69 | 70 | // Insertamos el representante y devolvemos el resultado y avisa a los subscriptores 71 | return repository.save(tenista) 72 | .also { onChange(Notificacion.Tipo.CREATE, it.id, it) } 73 | } 74 | 75 | override suspend fun update(id: UUID, tenista: Tenista): Tenista { 76 | logger.debug { "update: Actualizando tenista en servicio" } 77 | 78 | val existe = repository.findById(id) 79 | 80 | // Existe la raqueta!! 81 | val raqueta = findRaqueta(tenista.raquetaId) 82 | 83 | existe?.let { 84 | return repository.update(id, tenista) 85 | ?.also { onChange(Notificacion.Tipo.UPDATE, it.id, it) }!! 86 | } ?: throw TenistaNotFoundException("No se ha encontrado el tenista con id: $id") 87 | } 88 | 89 | override suspend fun delete(id: UUID): Tenista { 90 | logger.debug { "delete: Borrando tenista en servicio" } 91 | 92 | val existe = repository.findById(id) 93 | 94 | existe?.let { 95 | // meto el try catch para que no se caiga la aplicación si no se puede borrar por tener raquetas asociadas 96 | return repository.delete(existe) 97 | .also { onChange(Notificacion.Tipo.DELETE, it!!.id, it) }!! 98 | 99 | } ?: throw TenistaNotFoundException("No se ha encontrado el tenista con id: $id") 100 | 101 | } 102 | 103 | override suspend fun findRaqueta(raquetaId: UUID?): Raqueta? { 104 | logger.debug { "findRaqueta: Buscando raqueta en servicio" } 105 | 106 | raquetaId?.let { 107 | return raquetasRepository.findById(raquetaId) 108 | ?: throw RaquetaNotFoundException("No se ha encontrado la raqueta con id: $raquetaId") 109 | } ?: return null 110 | } 111 | 112 | /// ---- Tiempo real, patrón observer!!! 113 | 114 | // Mis suscriptores, un mapa de codigo, con la función que se ejecutará 115 | // Si no te gusta usar la función como parámetro, puedes usar el objeto de la sesión (pero para eso Kotlin 116 | // es funcional ;) 117 | private val suscriptores = 118 | mutableMapOf Unit>() 119 | 120 | override fun addSuscriptor(id: Int, suscriptor: suspend (TenistasNotification) -> Unit) { 121 | logger.debug { "addSuscriptor: Añadiendo suscriptor con id: $id" } 122 | 123 | // Añadimos el suscriptor, que es la función que se ejecutará 124 | suscriptores[id] = suscriptor 125 | } 126 | 127 | override fun removeSuscriptor(id: Int) { 128 | logger.debug { "removeSuscriptor: Desconectando suscriptor con id: $" } 129 | 130 | suscriptores.remove(id) 131 | } 132 | 133 | // Se ejecuta en cada cambio 134 | private suspend fun onChange(tipo: Notificacion.Tipo, id: UUID, data: Tenista? = null) { 135 | logger.debug { "onChange: Cambio en Tenistas: $tipo, notificando a los suscriptores afectada entidad: $data" } 136 | 137 | // Por cada suscriptor, ejecutamos la función que se ha almacenado 138 | // Si almacenas el objeto de la sesión, puedes usar el método de la sesión, que es sendSerialized 139 | suscriptores.values.forEach { 140 | it.invoke( 141 | Notificacion( 142 | "TENISTA", 143 | tipo, 144 | id, 145 | data?.toDto(findRaqueta(data.raquetaId)) 146 | ) 147 | ) 148 | } 149 | } 150 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/services/tokens/TokensService.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.services.tokens 2 | 3 | import com.auth0.jwt.JWT 4 | import com.auth0.jwt.JWTVerifier 5 | import com.auth0.jwt.algorithms.Algorithm 6 | import joseluisgs.es.config.TokenConfig 7 | import joseluisgs.es.models.User 8 | import mu.KotlinLogging 9 | import org.koin.core.annotation.Single 10 | import java.util.* 11 | 12 | private val logger = KotlinLogging.logger {} 13 | 14 | @Single 15 | class TokensService( 16 | private val tokenConfig: TokenConfig 17 | ) { 18 | 19 | init { 20 | logger.debug { "Iniciando servicio de tokens con audience: ${tokenConfig.audience}" } 21 | } 22 | 23 | fun generateJWT(user: User): String { 24 | return JWT.create() 25 | .withAudience(tokenConfig.audience) 26 | .withIssuer(tokenConfig.issuer) 27 | .withSubject("Authentication") 28 | // claims de usuario 29 | .withClaim("username", user.username) 30 | .withClaim("usermail", user.email) 31 | .withClaim("userId", user.id.toString()) 32 | // claims de tiempo de expiración milisegundos desde 1970 + (tiempo en segundos) * 1000 (milisegundos) 33 | .withExpiresAt( 34 | Date(System.currentTimeMillis() + tokenConfig.expiration * 1000L) 35 | ) 36 | .sign(Algorithm.HMAC512(tokenConfig.secret)) 37 | } 38 | 39 | fun verifyJWT(): JWTVerifier { 40 | return JWT.require(Algorithm.HMAC512(tokenConfig.secret)) 41 | .withAudience(tokenConfig.audience) 42 | .withIssuer(tokenConfig.issuer) 43 | .build() 44 | } 45 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/services/users/UsersService.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.services.users 2 | 3 | import joseluisgs.es.models.User 4 | import kotlinx.coroutines.flow.Flow 5 | import java.util.* 6 | 7 | interface UsersService { 8 | suspend fun findAll(limit: Int?): Flow 9 | 10 | suspend fun findById(id: UUID): User 11 | suspend fun findByUsername(username: String): User? 12 | fun hashedPassword(password: String): String 13 | suspend fun checkUserNameAndPassword(username: String, password: String): User? 14 | suspend fun save(entity: User): User 15 | suspend fun update(id: UUID, entity: User): User? 16 | suspend fun delete(id: UUID): User? 17 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/services/users/UsersServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.services.users 2 | 3 | import joseluisgs.es.exceptions.UserBadRequestException 4 | import joseluisgs.es.exceptions.UserNotFoundException 5 | import joseluisgs.es.exceptions.UserUnauthorizedException 6 | import joseluisgs.es.models.User 7 | import joseluisgs.es.repositories.users.UsersRepository 8 | import kotlinx.coroutines.flow.Flow 9 | import mu.KotlinLogging 10 | import org.koin.core.annotation.Single 11 | import java.time.LocalDateTime 12 | import java.util.* 13 | 14 | private val logger = KotlinLogging.logger {} 15 | 16 | @Single 17 | class UsersServiceImpl( 18 | private val repository: UsersRepository 19 | ) : UsersService { 20 | 21 | init { 22 | logger.debug { "Inicializando el servicio de Usuarios" } 23 | } 24 | 25 | override suspend fun findAll(limit: Int?): Flow { 26 | logger.debug { "findAll: Buscando todos los usuarios" } 27 | 28 | return repository.findAll(limit) 29 | } 30 | 31 | override suspend fun findById(id: UUID): User { 32 | logger.debug { "findById: Buscando usuario con id: $id" } 33 | 34 | return repository.findById(id) ?: throw UserNotFoundException("No se ha encontrado el usuario con id: $id") 35 | } 36 | 37 | override suspend fun findByUsername(username: String): User { 38 | logger.debug { "findByUsername: Buscando usuario con username: $username" } 39 | 40 | return repository.findByUsername(username) 41 | ?: throw UserNotFoundException("No se ha encontrado el usuario con username: $username") 42 | } 43 | 44 | override fun hashedPassword(password: String): String { 45 | logger.debug { "hashedPassword: Hasheando la contraseña" } 46 | 47 | return repository.hashedPassword(password) 48 | } 49 | 50 | override suspend fun checkUserNameAndPassword(username: String, password: String): User { 51 | logger.debug { "checkUserNameAndPassword: Comprobando el usuario y contraseña" } 52 | 53 | return repository.checkUserNameAndPassword(username, password) 54 | ?: throw UserUnauthorizedException("Nombre de usuario o contraseña incorrectos") 55 | } 56 | 57 | override suspend fun save(entity: User): User { 58 | logger.debug { "save: Creando usuario" } 59 | 60 | // Sus credenciales son validas y su nombre de usuario no existe 61 | val existingUser = repository.findByUsername(entity.username) 62 | if (existingUser != null) { 63 | throw UserBadRequestException("Ya existe un usuario con username: ${entity.username}") 64 | } 65 | 66 | val user = 67 | entity.copy( 68 | id = UUID.randomUUID(), 69 | password = hashedPassword(entity.password), 70 | createdAt = LocalDateTime.now(), 71 | updatedAt = LocalDateTime.now(), 72 | ) 73 | 74 | return repository.save(user) 75 | } 76 | 77 | override suspend fun update(id: UUID, entity: User): User { 78 | logger.debug { "update: Actualizando usuario con id: $id" } 79 | 80 | // No lo necesitamos, pero lo dejamos por si acaso 81 | val existingUser = repository.findByUsername(entity.username) 82 | if (existingUser != null && existingUser.id != id) { 83 | throw UserBadRequestException("Ya existe un usuario con username: ${entity.username}") 84 | } 85 | 86 | val user = 87 | entity.copy( 88 | updatedAt = LocalDateTime.now(), 89 | ) 90 | 91 | return repository.update(id, user)!! 92 | 93 | } 94 | 95 | override suspend fun delete(id: UUID): User? { 96 | logger.debug { "delete: Borrando usuario con id: $id" } 97 | 98 | val user = repository.findById(id) 99 | user?.let { 100 | repository.delete(it) 101 | } 102 | return user 103 | } 104 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/utils/UuidUtils.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.utils 2 | 3 | import java.util.* 4 | 5 | class UUIDException(message: String) : Exception(message) 6 | 7 | fun String.toUUID(): UUID { 8 | return try { 9 | UUID.fromString(this.trim()) 10 | } catch (e: IllegalArgumentException) { 11 | throw UUIDException("El id no es válido o no está en el formato UUID") 12 | } 13 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/validators/RaquetasValidator.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.validators 2 | 3 | import io.ktor.server.plugins.requestvalidation.* 4 | import joseluisgs.es.dto.RaquetaCreateDto 5 | 6 | 7 | // Validadores de entrada de datos 8 | fun RequestValidationConfig.raquetasValidation() { 9 | validate { raqueta -> 10 | if (raqueta.marca.isBlank()) { 11 | ValidationResult.Invalid("El nombre no puede estar vacío") 12 | } else if (raqueta.representanteId.toString().isBlank()) { 13 | ValidationResult.Invalid("El identificador del representante no puede estar vacío") 14 | // validar email con regex 15 | } else if (raqueta.precio < 0) { 16 | ValidationResult.Invalid("El precio no puede ser negativo") 17 | } else { 18 | ValidationResult.Valid 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/validators/RepresentantesValidator.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.validators 2 | 3 | import io.ktor.server.plugins.requestvalidation.* 4 | import joseluisgs.es.dto.RepresentanteDto 5 | 6 | 7 | // Validadores de entrada de datos 8 | fun RequestValidationConfig.representantesValidation() { 9 | validate { representante -> 10 | if (representante.nombre.isBlank()) { 11 | ValidationResult.Invalid("El nombre no puede estar vacío") 12 | } else if (representante.email.isBlank()) { 13 | ValidationResult.Invalid("El email no puede estar vacío") 14 | // validar email con regex 15 | } else if (!representante.email.matches(Regex("^[A-Za-z0-9+_.-]+@(.+)\$"))) { 16 | ValidationResult.Invalid("El email no es válido") 17 | } else { 18 | ValidationResult.Valid 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/validators/TenistasValidator.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.validators 2 | 3 | import io.ktor.server.plugins.requestvalidation.* 4 | import joseluisgs.es.dto.TenistaCreateDto 5 | import java.time.LocalDate 6 | 7 | 8 | // Validadores de entrada de datos 9 | fun RequestValidationConfig.tenistasValidation() { 10 | validate { tenista -> 11 | if (tenista.nombre.isBlank()) { 12 | ValidationResult.Invalid("El nombre no puede estar vacío") 13 | } else if (tenista.ranking <= 0) { 14 | ValidationResult.Invalid("El ranking debe ser mayor que 0") 15 | } else if (tenista.fechaNacimiento.isAfter(LocalDate.now())) { 16 | ValidationResult.Invalid("La fecha de nacimiento no puede ser mayor que la actual") 17 | } else if (tenista.añoProfesional <= 0) { 18 | ValidationResult.Invalid("El año profesional debe ser mayor que 0") 19 | } else if (tenista.altura <= 0) { 20 | ValidationResult.Invalid("La altura debe ser mayor que 0") 21 | } else if (tenista.peso <= 0) { 22 | ValidationResult.Invalid("El peso debe ser mayor que 0") 23 | } else if (tenista.puntos <= 0) { 24 | ValidationResult.Invalid("Los puntos deben ser mayor que 0") 25 | } else if (tenista.pais.isBlank()) { 26 | ValidationResult.Invalid("El país no puede estar vacío") 27 | } else { 28 | ValidationResult.Valid 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/kotlin/joseluisgs/es/validators/UsersValidator.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.validators 2 | 3 | import io.ktor.server.plugins.requestvalidation.* 4 | import joseluisgs.es.dto.UserCreateDto 5 | import joseluisgs.es.dto.UserLoginDto 6 | import joseluisgs.es.dto.UserUpdateDto 7 | 8 | 9 | // Validadores de entrada de datos 10 | fun RequestValidationConfig.usersValidation() { 11 | 12 | validate { user -> 13 | if (user.nombre.isBlank()) { 14 | ValidationResult.Invalid("El nombre no puede estar vacío") 15 | } else if (user.email.isBlank()) { 16 | ValidationResult.Invalid("El email no puede estar vacío") 17 | // validar email con regex 18 | } else if (!user.email.matches(Regex("^[A-Za-z0-9+_.-]+@(.+)\$"))) { 19 | ValidationResult.Invalid("El email no es válido") 20 | } else if (user.username.isBlank() && user.username.length < 3) { 21 | ValidationResult.Invalid("El nombre de usuario no puede estar vacío") 22 | } else if (user.password.isBlank() || user.password.length < 7) { 23 | ValidationResult.Invalid("La contraseña no puede estar vacía o ser menor de 7 caracteres") 24 | } else { 25 | ValidationResult.Valid 26 | } 27 | } 28 | 29 | validate { user -> 30 | if (user.nombre.isBlank()) { 31 | ValidationResult.Invalid("El nombre no puede estar vacío") 32 | } else if (user.email.isBlank()) { 33 | ValidationResult.Invalid("El email no puede estar vacío") 34 | // validar email con regex 35 | } else if (!user.email.contains("@")) { 36 | ValidationResult.Invalid("El email no es válido") 37 | } else if (user.username.isBlank() && user.username.length < 3) { 38 | ValidationResult.Invalid("El nombre de usuario no puede estar vacío") 39 | } else { 40 | ValidationResult.Valid 41 | } 42 | } 43 | 44 | validate { user -> 45 | if (user.username.isBlank()) { 46 | ValidationResult.Invalid("El nombre de usuario no puede estar vacío") 47 | } else if (user.password.isBlank() || user.password.length < 7) { 48 | ValidationResult.Invalid("La contraseña no puede estar vacía o ser menor de 7 caracteres") 49 | } else { 50 | ValidationResult.Valid 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | # Fichero para iniciar el servidor puerto 2 | # Y clase principal para la aplicación 3 | 4 | ktor { 5 | ## Para el puerto 6 | deployment { 7 | ## Si no se especifica el puerto, se usa el 8080, si solo queremos SSL quitar el puerto normal 8 | port = 6969 9 | port = ${?PORT} 10 | ## Para SSL, si es necesario poner el puerto 11 | sslPort = 6963 12 | sslPort = ${?SSL_PORT} 13 | } 14 | 15 | ## Para la clase principal 16 | application { 17 | modules = [ joseluisgs.es.ApplicationKt.module ] 18 | } 19 | 20 | ## Modo de desarrollo, se dispara cuando detecta cambios 21 | ## development = true 22 | deployment { 23 | ## Directorios a vigilar 24 | watch = [ classes, resources ] 25 | } 26 | 27 | ## Modo de ejecución 28 | environment = dev 29 | environment = ${?KTOR_ENV} 30 | 31 | ## Para SSL/TSL configuración del llavero y certificado 32 | security { 33 | ssl { 34 | keyStore = cert/server_keystore.p12 35 | keyAlias = serverKeyPair 36 | keyStorePassword = 1234567 37 | privateKeyPassword = 1234567 38 | } 39 | } 40 | } 41 | 42 | # Configuración de parametros del rest 43 | rest { 44 | version = "v1" 45 | path = "api" 46 | } 47 | 48 | # Fichero para iniciar el servidor (dominio) puerto en despliegue 49 | server { 50 | baseUrl = "http://localhost:6969" 51 | baseUrl = ${?BASE_URL} 52 | baseSecureUrl = "https://localhost:6963" 53 | baseSecureUrl = ${?BASE_SECURE_URL} 54 | } 55 | 56 | # Configuracion de JWT 57 | jwt { 58 | secret = "Señ0r@DeK0tl1nT0keN2023-MeGustanLosPepinosDeLegan€$" 59 | realm = "tenistas-ktor" 60 | ## Tiempo de expiración en segundos del token si no se pone por defecto: 3600s (1 hora) 61 | expiration = "3600" 62 | issuer = "tenistas-ktor" 63 | audience = "tenistas-ktor-auth" 64 | } 65 | 66 | # Configuración del almacen de datos 67 | storage { 68 | uploadDir = "uploads" 69 | endpoint = api/storage 70 | } 71 | 72 | # Configuración de la base de datos 73 | database { 74 | driver = "h2" 75 | protocol ="mem" 76 | user = "sa" 77 | user = ${?DATABASE_USER} 78 | password = "" 79 | password = ${?DATABASE_PASSWORD} 80 | database = "r2dbc:h2:mem:///tenistas;DB_CLOSE_DELAY=-1" 81 | database = ${?DATABASE_NAME} 82 | ## Para inicializar la base de datos 83 | initDatabaseData = true 84 | } 85 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/resources/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Kotlin Ktor API REST 8 | 9 | 10 | ktor logo 11 |

Kotlin Ktor API REST

12 |

👋 ¡Bienvenid@ a mi API REST!

13 |

Toda la web se ha realizado usando Kotlin con Ktor y tecnología de Jetbrains

14 |

Más información en mi repositorio de GitHub joseluisgs. 15 |

16 | 17 | -------------------------------------------------------------------------------- /src/main/resources/web/ktor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArisGuimera/tenistas-rest-ktor-2022-2023/702f23afb6f394988e5b8f816a022439a775508f/src/main/resources/web/ktor.png -------------------------------------------------------------------------------- /src/test/kotlin/joseluisgs/es/ApplicationTest.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es 2 | 3 | import io.ktor.client.request.* 4 | import io.ktor.client.statement.* 5 | import io.ktor.http.* 6 | import io.ktor.server.config.* 7 | import io.ktor.server.testing.* 8 | import org.junit.jupiter.api.Assertions.assertEquals 9 | import org.junit.jupiter.api.Assertions.assertTrue 10 | import org.junit.jupiter.api.Test 11 | 12 | class ApplicationTest { 13 | // Cargamos la configuración del entorno 14 | // La configuración de entorno se encuentra en el fichero application.conf 15 | // del directorio resources (si hay una propia estará en test/resources) 16 | // Podemos sobreescribir la configuración de entorno en el test para hacerla más rápida 17 | private val config = ApplicationConfig("application.conf") 18 | 19 | 20 | @Test 21 | fun trueIsTrue() { 22 | assertTrue(true) 23 | } 24 | 25 | @Test 26 | fun testRoot() = testApplication { 27 | // Configuramos el entorno de test 28 | environment { config } 29 | 30 | // Lanzamos la consulta 31 | val response = client.get("/") 32 | 33 | // Comprobamos que la respuesta y el contenido es correcto 34 | assertEquals(HttpStatusCode.OK, response.status) 35 | assertEquals("Tenistas API REST Ktor. 2º DAM", response.bodyAsText()) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/kotlin/joseluisgs/es/mappers/RepresentantesKtTest.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.mappers 2 | 3 | import joseluisgs.es.dto.RepresentanteDto 4 | import joseluisgs.es.models.Representante 5 | import org.junit.jupiter.api.Assertions.assertEquals 6 | import org.junit.jupiter.api.Test 7 | import org.junit.jupiter.api.assertAll 8 | import java.time.LocalDateTime 9 | import java.util.* 10 | 11 | 12 | class RepresentantesKtTest { 13 | 14 | val representante = Representante( 15 | id = UUID.fromString("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"), 16 | nombre = "Test", 17 | email = "test@example.com", 18 | createdAt = LocalDateTime.now(), 19 | updatedAt = LocalDateTime.now(), 20 | deleted = false 21 | ) 22 | 23 | val representanteDto = RepresentanteDto( 24 | id = UUID.fromString("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"), 25 | nombre = "Test", 26 | email = "test@example.com", 27 | metadata = RepresentanteDto.MetaData( 28 | createdAt = LocalDateTime.now(), 29 | updatedAt = LocalDateTime.now(), 30 | deleted = false 31 | ) 32 | ) 33 | 34 | @Test 35 | fun toDto() { 36 | val dto = representante.toDto() 37 | assertAll( 38 | { assertEquals(representanteDto.id, dto.id) }, 39 | { assertEquals(representanteDto.nombre, dto.nombre) }, 40 | { assertEquals(representanteDto.email, dto.email) } 41 | ) 42 | } 43 | 44 | @Test 45 | fun toModel() { 46 | val model = representanteDto.toModel() 47 | assertAll( 48 | { assertEquals(representante.nombre, model.nombre) }, 49 | { assertEquals(representante.email, model.email) }, 50 | ) 51 | } 52 | } -------------------------------------------------------------------------------- /src/test/kotlin/joseluisgs/es/repositories/raquetas/RaquetasCachedRepositoryImplKtTest.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.repositories.raquetas 2 | 3 | import io.mockk.MockKAnnotations 4 | import io.mockk.coEvery 5 | import io.mockk.coVerify 6 | import io.mockk.impl.annotations.InjectMockKs 7 | import io.mockk.impl.annotations.MockK 8 | import io.mockk.impl.annotations.SpyK 9 | import io.mockk.junit5.MockKExtension 10 | import joseluisgs.es.models.Raqueta 11 | import joseluisgs.es.services.cache.raquetas.RaquetasCacheImpl 12 | import kotlinx.coroutines.flow.flowOf 13 | import kotlinx.coroutines.flow.toList 14 | import kotlinx.coroutines.test.runTest 15 | import org.junit.jupiter.api.Assertions.* 16 | import org.junit.jupiter.api.Test 17 | import org.junit.jupiter.api.TestInstance 18 | import org.junit.jupiter.api.extension.ExtendWith 19 | import java.util.* 20 | 21 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 22 | @ExtendWith(MockKExtension::class) 23 | class RaquetasCachedRepositoryImplKtTest { 24 | 25 | val raqueta = Raqueta( 26 | id = UUID.fromString("044e6ec7-aa6c-46bb-9433-8094ef4ae8bc"), 27 | marca = "Test", 28 | precio = 199.9, 29 | representanteId = UUID.fromString("b39a2fd2-f7d7-405d-b73c-b68a8dedbcdf") 30 | ) 31 | 32 | @MockK 33 | lateinit var repo: RaquetasRepositoryImpl 34 | 35 | @SpyK 36 | var cache = RaquetasCacheImpl() 37 | 38 | @InjectMockKs 39 | lateinit var repository: RaquetasCachedRepositoryImpl 40 | 41 | init { 42 | MockKAnnotations.init(this) 43 | } 44 | 45 | 46 | @Test 47 | fun findAll() = runTest { 48 | // Usamos coEvery para poder usar corutinas 49 | coEvery { repo.findAll() } returns flowOf(raqueta) 50 | 51 | // Llamamos al método 52 | val result = repository.findAll().toList() 53 | 54 | assertAll( 55 | { assertEquals(1, result.size) }, 56 | { assertEquals(raqueta, result[0]) } 57 | ) 58 | 59 | coVerify(exactly = 1) { repo.findAll() } 60 | 61 | } 62 | 63 | @Test 64 | fun findAllPageable() = runTest { 65 | // Usamos coEvery para poder usar corutinas 66 | coEvery { repo.findAllPageable(0, 10) } returns flowOf(raqueta) 67 | 68 | // Llamamos al método 69 | val result = repository.findAllPageable(0, 10).toList() 70 | 71 | assertAll( 72 | { assertEquals(1, result.size) }, 73 | { assertEquals(raqueta, result[0]) } 74 | ) 75 | 76 | coVerify(exactly = 1) { repo.findAllPageable(0, 10) } 77 | } 78 | 79 | 80 | /* @Test 81 | fun findByNombre() = runTest { 82 | // Usamos coEvery para poder usar corutinas 83 | coEvery { repo.findByNombre(any()) } returns flowOf(representante) 84 | 85 | 86 | // Llamamos al método 87 | val result = repository.findByNombre("Test") 88 | val representantes = mutableListOf() 89 | 90 | result.collect { 91 | representantes.add(it) 92 | } 93 | 94 | assertAll( 95 | { assertEquals(1, representantes.size) }, 96 | { assertEquals(representante, representantes[0]) } 97 | ) 98 | 99 | coVerify { repo.findByNombre(any()) } 100 | }*/ 101 | 102 | @Test 103 | fun findById() = runTest { 104 | // Usamos coEvery para poder usar corutinas 105 | coEvery { repo.findById(any()) } returns raqueta 106 | 107 | // Llamamos al método 108 | val result = repository.findById(raqueta.id) 109 | 110 | assertAll( 111 | { assertEquals(raqueta.marca, result!!.marca) }, 112 | { assertEquals(raqueta.precio, result!!.precio) }, 113 | ) 114 | 115 | 116 | coVerify { repo.findById(any()) } 117 | } 118 | 119 | @Test 120 | fun findByIdNotFound() = runTest { 121 | // Usamos coEvery para poder usar corutinas 122 | coEvery { repo.findById(any()) } returns null 123 | 124 | // Llamamos al método 125 | val result = repository.findById(UUID.randomUUID()) 126 | 127 | assertNull(result) 128 | 129 | coVerify { repo.findById(any()) } 130 | } 131 | 132 | @Test 133 | fun save() = runTest { 134 | coEvery { repo.save(any()) } returns raqueta 135 | 136 | val result = repository.save(raqueta) 137 | 138 | assertAll( 139 | { assertEquals(raqueta.marca, result.marca) }, 140 | { assertEquals(raqueta.precio, result.precio) }, 141 | ) 142 | 143 | coVerify(exactly = 1) { repo.save(any()) } 144 | 145 | } 146 | 147 | @Test 148 | fun update() = runTest { 149 | coEvery { repo.findById(any()) } returns raqueta 150 | coEvery { repo.update(any(), any()) } returns raqueta 151 | 152 | val result = repository.update(raqueta.id, raqueta)!! 153 | 154 | assertAll( 155 | { assertEquals(raqueta.marca, result.marca) }, 156 | { assertEquals(raqueta.precio, result.precio) }, 157 | ) 158 | 159 | coVerify { repo.update(any(), any()) } 160 | } 161 | 162 | @Test 163 | fun delete() = runTest { 164 | coEvery { repo.findById(any()) } returns raqueta 165 | coEvery { repo.delete(any()) } returns raqueta 166 | 167 | val result = repository.delete(raqueta)!! 168 | 169 | assertAll( 170 | { assertEquals(raqueta.marca, result.marca) }, 171 | { assertEquals(raqueta.precio, result.precio) }, 172 | ) 173 | 174 | coVerify { repo.delete(any()) } 175 | } 176 | } -------------------------------------------------------------------------------- /src/test/kotlin/joseluisgs/es/repositories/raquetas/RaquetasRepositoryImplTest.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.repositories.raquetas 2 | 3 | import joseluisgs.es.models.Raqueta 4 | import joseluisgs.es.repositories.utils.getDataBaseService 5 | import joseluisgs.es.utils.toUUID 6 | import kotlinx.coroutines.flow.take 7 | import kotlinx.coroutines.flow.toList 8 | import kotlinx.coroutines.test.runTest 9 | import org.junit.jupiter.api.* 10 | import org.junit.jupiter.api.Assertions.* 11 | import java.util.* 12 | 13 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 14 | class RaquetasRepositoryImplKtTest { 15 | 16 | val dataBaseService = getDataBaseService() 17 | 18 | var repository = RaquetasRepositoryImpl(dataBaseService) 19 | 20 | val raqueta = Raqueta( 21 | id = UUID.fromString("044e6ec7-aa6c-46bb-9433-8094ef4ae8bc"), 22 | marca = "Test", 23 | precio = 199.9, 24 | representanteId = UUID.fromString("b39a2fd2-f7d7-405d-b73c-b68a8dedbcdf") 25 | ) 26 | 27 | 28 | // Con run test podemos ejecutar código asíncrono 29 | @BeforeEach 30 | fun setUp() { 31 | dataBaseService.clearDataBaseData() 32 | dataBaseService.initDataBaseData() 33 | } 34 | 35 | @AfterAll 36 | fun tearDown() { 37 | dataBaseService.clearDataBaseData() 38 | } 39 | 40 | @Test 41 | fun findAll() = runTest { 42 | val result = repository.findAll().toList() 43 | 44 | // Comprobamos que el resultado es correcto 45 | assertAll( 46 | { assertNotNull(result) }, 47 | { assertEquals("Babolat", result[0].marca) }, 48 | ) 49 | } 50 | 51 | @Test 52 | fun findAllPageable() = runTest { 53 | val result = repository.findAllPageable(0, 10).toList() 54 | 55 | // Comprobamos que el resultado es correcto 56 | assertAll( 57 | { assertNotNull(result) }, 58 | { assertEquals("Babolat", result[0].marca) }, 59 | ) 60 | 61 | } 62 | 63 | @Test 64 | fun findById() = runTest { 65 | val result = repository.findById("86084458-4733-4d71-a3db-34b50cd8d68f".toUUID()) 66 | 67 | // Comprobamos que el resultado es correcto 68 | Assertions.assertAll( 69 | { assertEquals("Babolat", result?.marca) }, 70 | { assertEquals(200.0, result?.precio) }, 71 | ) 72 | } 73 | 74 | @Test 75 | fun findByIdNotExists() = runTest { 76 | val result = repository.findById(UUID.randomUUID()) 77 | 78 | // Comprobamos que el resultado es correcto 79 | assertNull(result) 80 | 81 | } 82 | 83 | @Test 84 | fun findByNombre() = runTest { 85 | val result = repository.findByMarca("Babolat").take(1).toList() 86 | 87 | // Comprobamos que el resultado es correcto 88 | assertAll( 89 | { assertNotNull(result) }, 90 | { assertEquals(1, result.size) }, 91 | { assertEquals("Babolat", result[0].marca) }, 92 | ) 93 | } 94 | 95 | @Test 96 | fun findByUsernameNotFound() = runTest { 97 | val result = repository.findByMarca("caca").take(1).toList() 98 | 99 | // Comprobamos que el resultado es correcto 100 | assertAll( 101 | { assertNotNull(result) }, 102 | { assertEquals(0, result.size) }, 103 | ) 104 | } 105 | 106 | @Test 107 | fun save() = runTest { 108 | val result = repository.save(raqueta) 109 | 110 | // Comprobamos que el resultado es correcto 111 | assertAll( 112 | { assertEquals(result.marca, raqueta.marca) }, 113 | { assertEquals(result.precio, raqueta.precio) } 114 | ) 115 | } 116 | 117 | @Test 118 | fun update() = runTest { 119 | val res = repository.save(raqueta) 120 | val update = res.copy(marca = "Test2") 121 | val result = repository.update(raqueta.id, update) 122 | 123 | // Comprobamos que el resultado es correcto 124 | assertAll( 125 | { assertEquals(result?.marca, update.marca) }, 126 | { assertEquals(result?.precio, update.precio) } 127 | ) 128 | } 129 | 130 | /* 131 | No hace falta porque filtro por la cache!! 132 | @Test 133 | fun updateNotExists() = runTest { 134 | val update = representante.copy(nombre = "Test2") 135 | val result = repository.update(UUID.randomUUID(), update) 136 | 137 | // Comprobamos que el resultado es correcto 138 | assertNull(result) 139 | }*/ 140 | 141 | @Test 142 | fun delete() = runTest { 143 | val res = repository.save(raqueta) 144 | val result = repository.delete(res) 145 | 146 | // Comprobamos que el resultado es correcto 147 | assertAll( 148 | { assertEquals(result?.marca, res.marca) }, 149 | { assertEquals(result?.precio, res.precio) } 150 | ) 151 | } 152 | 153 | @Test 154 | fun deleteNotExists() = runTest { 155 | val delete = raqueta.copy(id = UUID.randomUUID()) 156 | val result = repository.delete(delete) 157 | 158 | // Comprobamos que el resultado es correcto 159 | assertNull(result) 160 | } 161 | 162 | } -------------------------------------------------------------------------------- /src/test/kotlin/joseluisgs/es/repositories/representantes/RepresentantesCachedRepositoryImplKtTest.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.repositories.representantes 2 | 3 | import io.mockk.MockKAnnotations 4 | import io.mockk.coEvery 5 | import io.mockk.coVerify 6 | import io.mockk.impl.annotations.InjectMockKs 7 | import io.mockk.impl.annotations.MockK 8 | import io.mockk.impl.annotations.SpyK 9 | import io.mockk.junit5.MockKExtension 10 | import joseluisgs.es.models.Representante 11 | import joseluisgs.es.services.cache.representantes.RepresentantesCacheImpl 12 | import kotlinx.coroutines.flow.flowOf 13 | import kotlinx.coroutines.flow.toList 14 | import kotlinx.coroutines.test.runTest 15 | import org.junit.jupiter.api.Assertions.* 16 | import org.junit.jupiter.api.Test 17 | import org.junit.jupiter.api.TestInstance 18 | import org.junit.jupiter.api.extension.ExtendWith 19 | import java.time.LocalDateTime 20 | import java.util.* 21 | 22 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 23 | @ExtendWith(MockKExtension::class) 24 | class RepresentantesCachedRepositoryImplKtTest { 25 | 26 | val representante = Representante( 27 | id = UUID.fromString("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"), 28 | nombre = "Test", 29 | email = "test@example.com", 30 | createdAt = LocalDateTime.now(), 31 | updatedAt = LocalDateTime.now(), 32 | deleted = false 33 | ) 34 | 35 | @MockK 36 | lateinit var repo: RepresentantesRepositoryImpl 37 | 38 | @SpyK 39 | var cache = RepresentantesCacheImpl() 40 | 41 | @InjectMockKs 42 | lateinit var repository: RepresentantesCachedRepositoryImpl 43 | 44 | init { 45 | MockKAnnotations.init(this) 46 | } 47 | 48 | 49 | @Test 50 | fun findAll() = runTest { 51 | // Usamos coEvery para poder usar corutinas 52 | coEvery { repo.findAll() } returns flowOf(representante) 53 | 54 | // Llamamos al método 55 | val result = repository.findAll().toList() 56 | 57 | assertAll( 58 | { assertEquals(representante, result[0]) } 59 | ) 60 | 61 | coVerify(exactly = 1) { repo.findAll() } 62 | 63 | } 64 | 65 | @Test 66 | fun findAllPageable() = runTest { 67 | // Usamos coEvery para poder usar corutinas 68 | coEvery { repo.findAllPageable(0, 10) } returns flowOf(representante) 69 | 70 | // Llamamos al método 71 | val result = repository.findAllPageable(0, 10).toList() 72 | 73 | assertAll( 74 | { assertEquals(representante, result[0]) } 75 | ) 76 | 77 | coVerify(exactly = 1) { repo.findAllPageable(0, 10) } 78 | } 79 | 80 | 81 | /* @Test 82 | fun findByNombre() = runTest { 83 | // Usamos coEvery para poder usar corutinas 84 | coEvery { repo.findByNombre(any()) } returns flowOf(representante) 85 | 86 | 87 | // Llamamos al método 88 | val result = repository.findByNombre("Test") 89 | val representantes = mutableListOf() 90 | 91 | result.collect { 92 | representantes.add(it) 93 | } 94 | 95 | assertAll( 96 | { assertEquals(1, representantes.size) }, 97 | { assertEquals(representante, representantes[0]) } 98 | ) 99 | 100 | coVerify { repo.findByNombre(any()) } 101 | }*/ 102 | 103 | @Test 104 | fun findById() = runTest { 105 | // Usamos coEvery para poder usar corutinas 106 | coEvery { repo.findById(any()) } returns representante 107 | 108 | // Llamamos al método 109 | val result = repository.findById(representante.id) 110 | 111 | assertAll( 112 | { assertEquals(representante.nombre, result!!.nombre) }, 113 | { assertEquals(representante.email, result!!.email) }, 114 | ) 115 | 116 | 117 | coVerify { repo.findById(any()) } 118 | } 119 | 120 | @Test 121 | fun findByIdNotFound() = runTest { 122 | // Usamos coEvery para poder usar corutinas 123 | coEvery { repo.findById(any()) } returns null 124 | 125 | // Llamamos al método 126 | val result = repository.findById(UUID.randomUUID()) 127 | 128 | assertNull(result) 129 | 130 | coVerify { repo.findById(any()) } 131 | } 132 | 133 | @Test 134 | fun save() = runTest { 135 | coEvery { repo.save(any()) } returns representante 136 | 137 | val result = repository.save(representante) 138 | 139 | assertAll( 140 | { assertEquals(representante.nombre, result.nombre) }, 141 | { assertEquals(representante.email, result.email) }, 142 | ) 143 | 144 | coVerify(exactly = 1) { repo.save(any()) } 145 | 146 | } 147 | 148 | @Test 149 | fun update() = runTest { 150 | coEvery { repo.findById(any()) } returns representante 151 | coEvery { repo.update(any(), any()) } returns representante 152 | 153 | val result = repository.update(representante.id, representante)!! 154 | 155 | assertAll( 156 | { assertEquals(representante.nombre, result.nombre) }, 157 | { assertEquals(representante.email, result.email) }, 158 | ) 159 | 160 | coVerify { repo.update(any(), any()) } 161 | } 162 | 163 | @Test 164 | fun delete() = runTest { 165 | coEvery { repo.findById(any()) } returns representante 166 | coEvery { repo.delete(any()) } returns representante 167 | 168 | val result = repository.delete(representante)!! 169 | 170 | assertAll( 171 | { assertEquals(representante.nombre, result.nombre) }, 172 | { assertEquals(representante.email, result.email) }, 173 | ) 174 | 175 | coVerify { repo.delete(any()) } 176 | } 177 | } -------------------------------------------------------------------------------- /src/test/kotlin/joseluisgs/es/repositories/representantes/RepresentantesRepositoryImplTest.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.repositories.representantes 2 | 3 | import joseluisgs.es.models.Representante 4 | import joseluisgs.es.repositories.utils.getDataBaseService 5 | import joseluisgs.es.utils.toUUID 6 | import kotlinx.coroutines.flow.take 7 | import kotlinx.coroutines.flow.toList 8 | import kotlinx.coroutines.test.runTest 9 | import org.junit.jupiter.api.* 10 | import org.junit.jupiter.api.Assertions.* 11 | import java.time.LocalDateTime 12 | import java.util.* 13 | 14 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 15 | class RepresentantesRepositoryImplKtTest { 16 | 17 | val dataBaseService = getDataBaseService() 18 | 19 | var repository = RepresentantesRepositoryImpl(dataBaseService) 20 | 21 | val representante = Representante( 22 | id = UUID.fromString("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"), 23 | nombre = "Test", 24 | email = "test@example.com", 25 | createdAt = LocalDateTime.now(), 26 | updatedAt = LocalDateTime.now(), 27 | deleted = false 28 | ) 29 | 30 | 31 | // Con run test podemos ejecutar código asíncrono 32 | @BeforeEach 33 | fun setUp() { 34 | dataBaseService.clearDataBaseData() 35 | dataBaseService.initDataBaseData() 36 | } 37 | 38 | @AfterAll 39 | fun tearDown() { 40 | dataBaseService.clearDataBaseData() 41 | } 42 | 43 | @Test 44 | fun findAll() = runTest { 45 | val result = repository.findAll().toList() 46 | 47 | // Comprobamos que el resultado es correcto 48 | assertAll( 49 | { assertNotNull(result) }, 50 | { assertEquals("Pepe Perez", result[0].nombre) }, 51 | ) 52 | } 53 | 54 | @Test 55 | fun findAllPageable() = runTest { 56 | val result = repository.findAllPageable(0, 10).toList() 57 | 58 | // Comprobamos que el resultado es correcto 59 | assertAll( 60 | { assertNotNull(result) }, 61 | { assertEquals("Pepe Perez", result[0].nombre) }, 62 | ) 63 | 64 | } 65 | 66 | @Test 67 | fun findById() = runTest { 68 | val result = repository.findById("b39a2fd2-f7d7-405d-b73c-b68a8dedbcdf".toUUID()) 69 | 70 | // Comprobamos que el resultado es correcto 71 | Assertions.assertAll( 72 | { assertEquals("Pepe Perez", result?.nombre) }, 73 | { assertEquals("pepe@perez.com", result?.email) }, 74 | ) 75 | } 76 | 77 | @Test 78 | fun findByIdNotExists() = runTest { 79 | val result = repository.findById(UUID.randomUUID()) 80 | 81 | // Comprobamos que el resultado es correcto 82 | assertNull(result) 83 | 84 | } 85 | 86 | @Test 87 | fun findByNombre() = runTest { 88 | val result = repository.findByNombre("Pepe Perez").take(1).toList() 89 | 90 | // Comprobamos que el resultado es correcto 91 | assertAll( 92 | { assertNotNull(result) }, 93 | { assertEquals(1, result.size) }, 94 | { assertEquals("Pepe Perez", result[0].nombre) }, 95 | ) 96 | } 97 | 98 | @Test 99 | fun findByUsernameNotFound() = runTest { 100 | val result = repository.findByNombre("caca").take(1).toList() 101 | 102 | // Comprobamos que el resultado es correcto 103 | assertAll( 104 | { assertNotNull(result) }, 105 | { assertEquals(0, result.size) }, 106 | ) 107 | } 108 | 109 | @Test 110 | fun save() = runTest { 111 | val result = repository.save(representante) 112 | 113 | // Comprobamos que el resultado es correcto 114 | assertAll( 115 | { assertEquals(result.nombre, representante.nombre) }, 116 | { assertEquals(result.email, representante.email) } 117 | ) 118 | } 119 | 120 | @Test 121 | fun update() = runTest { 122 | val res = repository.save(representante) 123 | val update = res.copy(nombre = "Test2") 124 | val result = repository.update(representante.id, update) 125 | 126 | // Comprobamos que el resultado es correcto 127 | assertAll( 128 | { assertEquals(result?.nombre, update.nombre) }, 129 | { assertEquals(result?.email, update.email) } 130 | ) 131 | } 132 | 133 | /* 134 | No hace falta porque filtro por la cache!! 135 | @Test 136 | fun updateNotExists() = runTest { 137 | val update = representante.copy(nombre = "Test2") 138 | val result = repository.update(UUID.randomUUID(), update) 139 | 140 | // Comprobamos que el resultado es correcto 141 | assertNull(result) 142 | }*/ 143 | 144 | @Test 145 | fun delete() = runTest { 146 | val res = repository.save(representante) 147 | val result = repository.delete(res) 148 | 149 | // Comprobamos que el resultado es correcto 150 | assertAll( 151 | { assertEquals(result?.nombre, res.nombre) }, 152 | { assertEquals(result?.email, res.email) } 153 | ) 154 | } 155 | 156 | @Test 157 | fun deleteNotExists() = runTest { 158 | val delete = representante.copy(id = UUID.randomUUID()) 159 | val result = repository.delete(delete) 160 | 161 | // Comprobamos que el resultado es correcto 162 | assertNull(result) 163 | } 164 | 165 | } -------------------------------------------------------------------------------- /src/test/kotlin/joseluisgs/es/repositories/tenistas/TenistasCachedRepositoryImplKtTest.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.repositories.tenistas 2 | 3 | import io.mockk.MockKAnnotations 4 | import io.mockk.coEvery 5 | import io.mockk.coVerify 6 | import io.mockk.impl.annotations.InjectMockKs 7 | import io.mockk.impl.annotations.MockK 8 | import io.mockk.impl.annotations.SpyK 9 | import io.mockk.junit5.MockKExtension 10 | import joseluisgs.es.models.Tenista 11 | import joseluisgs.es.services.cache.tenistas.TenistasCacheImpl 12 | import kotlinx.coroutines.flow.flowOf 13 | import kotlinx.coroutines.flow.toList 14 | import kotlinx.coroutines.test.runTest 15 | import org.junit.jupiter.api.Assertions.* 16 | import org.junit.jupiter.api.Test 17 | import org.junit.jupiter.api.TestInstance 18 | import org.junit.jupiter.api.extension.ExtendWith 19 | import java.time.LocalDate 20 | import java.util.* 21 | 22 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 23 | @ExtendWith(MockKExtension::class) 24 | class TenistasCachedRepositoryImplKtTest { 25 | 26 | val tenista = Tenista( 27 | id = UUID.fromString("5d1e6fe1-5fa6-4494-a492-ae9725959035"), 28 | nombre = "Test", 29 | ranking = 99, 30 | fechaNacimiento = LocalDate.parse("1981-01-01"), 31 | añoProfesional = 2000, 32 | altura = 188, 33 | peso = 83, 34 | manoDominante = Tenista.ManoDominante.DERECHA, 35 | tipoReves = Tenista.TipoReves.UNA_MANO, 36 | puntos = 3789, 37 | pais = "Suiza", 38 | raquetaId = UUID.fromString("b0b5b2a1-5b1f-4b0f-8b1f-1b2c2b3c4d5e") 39 | ) 40 | 41 | @MockK 42 | lateinit var repo: TenistasRepositoryImpl 43 | 44 | @SpyK 45 | var cache = TenistasCacheImpl() 46 | 47 | @InjectMockKs 48 | lateinit var repository: TenistasCachedRepositoryImpl 49 | 50 | init { 51 | MockKAnnotations.init(this) 52 | } 53 | 54 | 55 | @Test 56 | fun findAll() = runTest { 57 | // Usamos coEvery para poder usar corutinas 58 | coEvery { repo.findAll() } returns flowOf(tenista) 59 | 60 | // Llamamos al método 61 | val result = repository.findAll().toList() 62 | 63 | assertAll( 64 | { assertEquals(1, result.size) }, 65 | { assertEquals(tenista, result[0]) } 66 | ) 67 | 68 | coVerify(exactly = 1) { repo.findAll() } 69 | 70 | } 71 | 72 | @Test 73 | fun findAllPageable() = runTest { 74 | // Usamos coEvery para poder usar corutinas 75 | coEvery { repo.findAllPageable(0, 10) } returns flowOf(tenista) 76 | 77 | // Llamamos al método 78 | val result = repository.findAllPageable(0, 10).toList() 79 | 80 | assertAll( 81 | { assertEquals(1, result.size) }, 82 | { assertEquals(tenista, result[0]) } 83 | ) 84 | 85 | coVerify(exactly = 1) { repo.findAllPageable(0, 10) } 86 | } 87 | 88 | 89 | /* @Test 90 | fun findByNombre() = runTest { 91 | // Usamos coEvery para poder usar corutinas 92 | coEvery { repo.findByNombre(any()) } returns flowOf(representante) 93 | 94 | 95 | // Llamamos al método 96 | val result = repository.findByNombre("Test") 97 | val representantes = mutableListOf() 98 | 99 | result.collect { 100 | representantes.add(it) 101 | } 102 | 103 | assertAll( 104 | { assertEquals(1, representantes.size) }, 105 | { assertEquals(representante, representantes[0]) } 106 | ) 107 | 108 | coVerify { repo.findByNombre(any()) } 109 | }*/ 110 | 111 | @Test 112 | fun findById() = runTest { 113 | // Usamos coEvery para poder usar corutinas 114 | coEvery { repo.findById(any()) } returns tenista 115 | 116 | // Llamamos al método 117 | val result = repository.findById(tenista.id) 118 | 119 | assertAll( 120 | { assertEquals(tenista.nombre, result!!.nombre) }, 121 | { assertEquals(tenista.ranking, result!!.ranking) }, 122 | ) 123 | 124 | 125 | coVerify { repo.findById(any()) } 126 | } 127 | 128 | @Test 129 | fun findByIdNotFound() = runTest { 130 | // Usamos coEvery para poder usar corutinas 131 | coEvery { repo.findById(any()) } returns null 132 | 133 | // Llamamos al método 134 | val result = repository.findById(UUID.randomUUID()) 135 | 136 | assertNull(result) 137 | 138 | coVerify { repo.findById(any()) } 139 | } 140 | 141 | @Test 142 | fun findByRanking() = runTest { 143 | // Usamos coEvery para poder usar corutinas 144 | coEvery { repo.findByRanking(any()) } returns tenista 145 | 146 | // Llamamos al método 147 | val result = repository.findByRanking(99) 148 | 149 | assertAll( 150 | { assertEquals(tenista.nombre, result!!.nombre) }, 151 | { assertEquals(tenista.ranking, result!!.ranking) }, 152 | ) 153 | 154 | coVerify(exactly = 1) { repo.findByRanking(any()) } 155 | } 156 | 157 | @Test 158 | fun findByNombre() = runTest { 159 | // Usamos coEvery para poder usar corutinas 160 | coEvery { repo.findByNombre(any()) } returns flowOf(tenista) 161 | 162 | // Llamamos al método 163 | val result = repository.findByNombre("Test").toList() 164 | 165 | assertAll( 166 | { assertEquals(tenista.nombre, result[0].nombre) }, 167 | { assertEquals(tenista.ranking, result[0].ranking) }, 168 | ) 169 | 170 | coVerify(exactly = 1) { repo.findByNombre(any()) } 171 | } 172 | 173 | @Test 174 | fun save() = runTest { 175 | coEvery { repo.save(any()) } returns tenista 176 | 177 | val result = repository.save(tenista) 178 | 179 | assertAll( 180 | { assertEquals(tenista.nombre, result.nombre) }, 181 | { assertEquals(tenista.ranking, result.ranking) }, 182 | ) 183 | 184 | coVerify(exactly = 1) { repo.save(any()) } 185 | 186 | } 187 | 188 | @Test 189 | fun update() = runTest { 190 | coEvery { repo.findById(any()) } returns tenista 191 | coEvery { repo.update(any(), any()) } returns tenista 192 | 193 | val result = repository.update(tenista.id, tenista)!! 194 | 195 | assertAll( 196 | { assertEquals(tenista.nombre, result.nombre) }, 197 | { assertEquals(tenista.ranking, result.ranking) }, 198 | ) 199 | 200 | coVerify { repo.update(any(), any()) } 201 | } 202 | 203 | @Test 204 | fun delete() = runTest { 205 | coEvery { repo.findById(any()) } returns tenista 206 | coEvery { repo.delete(any()) } returns tenista 207 | 208 | val result = repository.delete(tenista)!! 209 | 210 | assertAll( 211 | { assertEquals(tenista.nombre, result.nombre) }, 212 | { assertEquals(tenista.ranking, result.ranking) }, 213 | ) 214 | 215 | coVerify { repo.delete(any()) } 216 | } 217 | } -------------------------------------------------------------------------------- /src/test/kotlin/joseluisgs/es/repositories/tenistas/TenistasRepositoryImplTest.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.repositories.tenistas 2 | 3 | import joseluisgs.es.models.Tenista 4 | import joseluisgs.es.repositories.utils.getDataBaseService 5 | import joseluisgs.es.utils.toUUID 6 | import kotlinx.coroutines.flow.take 7 | import kotlinx.coroutines.flow.toList 8 | import kotlinx.coroutines.test.runTest 9 | import org.junit.jupiter.api.* 10 | import org.junit.jupiter.api.Assertions.* 11 | import java.time.LocalDate 12 | import java.util.* 13 | 14 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 15 | class RepresentantesRepositoryImplKtTest { 16 | 17 | val dataBaseService = getDataBaseService() 18 | 19 | var repository = TenistasRepositoryImpl(dataBaseService) 20 | 21 | val tenista = Tenista( 22 | id = UUID.fromString("5d1e6fe1-5fa6-4494-a492-ae9725959035"), 23 | nombre = "Test", 24 | ranking = 99, 25 | fechaNacimiento = LocalDate.parse("1981-01-01"), 26 | añoProfesional = 2000, 27 | altura = 188, 28 | peso = 83, 29 | manoDominante = Tenista.ManoDominante.DERECHA, 30 | tipoReves = Tenista.TipoReves.UNA_MANO, 31 | puntos = 3789, 32 | pais = "Suiza", 33 | raquetaId = UUID.fromString("b0b5b2a1-5b1f-4b0f-8b1f-1b2c2b3c4d5e") 34 | ) 35 | 36 | 37 | // Con run test podemos ejecutar código asíncrono 38 | @BeforeEach 39 | fun setUp() { 40 | dataBaseService.clearDataBaseData() 41 | dataBaseService.initDataBaseData() 42 | } 43 | 44 | @AfterAll 45 | fun tearDown() { 46 | dataBaseService.clearDataBaseData() 47 | } 48 | 49 | @Test 50 | fun findAll() = runTest { 51 | val result = repository.findAll().toList() 52 | 53 | // Comprobamos que el resultado es correcto 54 | assertAll( 55 | { assertNotNull(result) }, 56 | { assertEquals("Carlos Alcaraz", result[0].nombre) }, 57 | ) 58 | } 59 | 60 | @Test 61 | fun findAllPageable() = runTest { 62 | val result = repository.findAllPageable(0, 10).toList() 63 | 64 | // Comprobamos que el resultado es correcto 65 | assertAll( 66 | { assertNotNull(result) }, 67 | { assertEquals("Carlos Alcaraz", result[0].nombre) }, 68 | ) 69 | 70 | } 71 | 72 | @Test 73 | fun findById() = runTest { 74 | val result = repository.findById("ea2962c6-2142-41b8-8dfb-0ecfe67e27df".toUUID()) 75 | 76 | // Comprobamos que el resultado es correcto 77 | assertAll( 78 | { assertEquals("Rafael Nadal", result?.nombre) }, 79 | { assertEquals(2, result?.ranking) } 80 | ) 81 | } 82 | 83 | @Test 84 | fun findByIdNotExists() = runTest { 85 | val result = repository.findById(UUID.randomUUID()) 86 | 87 | // Comprobamos que el resultado es correcto 88 | assertNull(result) 89 | 90 | } 91 | 92 | @Test 93 | fun findByNombre() = runTest { 94 | val result = repository.findByNombre("Rafael Nadal").take(1).toList() 95 | 96 | // Comprobamos que el resultado es correcto 97 | assertAll( 98 | { assertNotNull(result) }, 99 | { assertEquals("Rafael Nadal", result[0].nombre) }, 100 | { assertEquals(2, result[0].ranking) } 101 | ) 102 | } 103 | 104 | @Test 105 | fun findByUsernameNotFound() = runTest { 106 | val result = repository.findByNombre("caca").take(1).toList() 107 | 108 | // Comprobamos que el resultado es correcto 109 | assertAll( 110 | { assertNotNull(result) }, 111 | { assertEquals(0, result.size) }, 112 | ) 113 | } 114 | 115 | @Test 116 | fun findByRanking() = runTest { 117 | val result = repository.findByRanking(2) 118 | 119 | // Comprobamos que el resultado es correcto 120 | assertAll( 121 | { assertNotNull(result) }, 122 | { assertEquals("Rafael Nadal", result?.nombre) }, 123 | ) 124 | } 125 | 126 | @Test 127 | fun findByRankingNotFound() = runTest { 128 | val result = repository.findByRanking(999) 129 | 130 | // Comprobamos que el resultado es correcto 131 | assertNull(result) 132 | } 133 | 134 | @Test 135 | fun save() = runTest { 136 | val result = repository.save(tenista) 137 | 138 | // Comprobamos que el resultado es correcto 139 | assertAll( 140 | { assertEquals(result.nombre, tenista.nombre) }, 141 | { assertEquals(result.ranking, tenista.ranking) } 142 | ) 143 | } 144 | 145 | @Test 146 | fun update() = runTest { 147 | val res = repository.save(tenista) 148 | val update = res.copy(nombre = "Test2") 149 | val result = repository.update(tenista.id, update) 150 | 151 | // Comprobamos que el resultado es correcto 152 | assertAll( 153 | { assertEquals(result?.nombre, update.nombre) }, 154 | { assertEquals(result?.ranking, update.ranking) } 155 | ) 156 | } 157 | 158 | /* 159 | No hace falta porque filtro por la cache!! 160 | @Test 161 | fun updateNotExists() = runTest { 162 | val update = representante.copy(nombre = "Test2") 163 | val result = repository.update(UUID.randomUUID(), update) 164 | 165 | // Comprobamos que el resultado es correcto 166 | assertNull(result) 167 | }*/ 168 | 169 | @Test 170 | fun delete() = runTest { 171 | val res = repository.save(tenista) 172 | val result = repository.delete(res) 173 | 174 | // Comprobamos que el resultado es correcto 175 | assertAll( 176 | { assertEquals(result?.nombre, res.nombre) }, 177 | { assertEquals(result?.ranking, res.ranking) } 178 | ) 179 | } 180 | 181 | @Test 182 | fun deleteNotExists() = runTest { 183 | val delete = tenista.copy(id = UUID.randomUUID()) 184 | val result = repository.delete(delete) 185 | 186 | // Comprobamos que el resultado es correcto 187 | assertNull(result) 188 | } 189 | 190 | } -------------------------------------------------------------------------------- /src/test/kotlin/joseluisgs/es/repositories/users/UsersRepositoryImplTest.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.repositories.users 2 | 3 | import io.ktor.http.* 4 | import io.ktor.server.config.* 5 | import joseluisgs.es.models.User 6 | import joseluisgs.es.repositories.utils.getDataBaseService 7 | import joseluisgs.es.utils.toUUID 8 | import kotlinx.coroutines.flow.take 9 | import kotlinx.coroutines.flow.toList 10 | import kotlinx.coroutines.test.runTest 11 | import org.junit.jupiter.api.AfterAll 12 | import org.junit.jupiter.api.Assertions.* 13 | import org.junit.jupiter.api.BeforeEach 14 | import org.junit.jupiter.api.Test 15 | import org.junit.jupiter.api.TestInstance 16 | import org.mindrot.jbcrypt.BCrypt 17 | import java.util.* 18 | 19 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 20 | class UsersRepositoryImplTest { 21 | val dataBaseService = getDataBaseService() 22 | 23 | var repository = UsersRepositoryImpl(dataBaseService) 24 | 25 | val user = User( 26 | id = UUID.fromString("1314a16c-f4f2-47e2-a7e9-21cac686ee55"), 27 | nombre = "Test", 28 | username = "test", 29 | email = "test@test.com", 30 | password = BCrypt.hashpw("test1234", BCrypt.gensalt(12)), 31 | avatar = "https://upload.wikimedia.org/wikipedia/commons/f/f4/User_Avatar_2.png", 32 | role = User.Role.USER 33 | ) 34 | 35 | @BeforeEach 36 | fun setUp() { 37 | dataBaseService.clearDataBaseData() 38 | dataBaseService.initDataBaseData() 39 | } 40 | 41 | @AfterAll 42 | fun tearDown() { 43 | dataBaseService.clearDataBaseData() 44 | } 45 | 46 | 47 | @Test 48 | fun findAll() = runTest { 49 | val result = repository.findAll().take(1).toList() 50 | 51 | assertAll( 52 | { assertEquals(1, result.size) }, 53 | { assertEquals("Pepe Perez", result[0].nombre) }, 54 | ) 55 | } 56 | 57 | @Test 58 | fun testFindAllLimit() = runTest { 59 | val result = repository.findAll(2).toList() 60 | 61 | assertAll( 62 | { assertEquals(2, result.size) }, 63 | { assertEquals("Pepe Perez", result[0].nombre) }, 64 | { assertEquals("Ana Lopez", result[1].nombre) }, 65 | ) 66 | } 67 | 68 | @Test 69 | fun checkUserNameAndPassword() = runTest { 70 | val result = repository.checkUserNameAndPassword("pepe", "pepe1234") 71 | 72 | assertAll( 73 | { assertEquals("Pepe Perez", result?.nombre) }, 74 | { assertEquals("pepe", result?.username) }, 75 | ) 76 | } 77 | 78 | @Test 79 | fun checkUserNameAndPasswordNotFound() = runTest { 80 | val result = repository.checkUserNameAndPassword("caca", "caca1234") 81 | 82 | assertNull(result) 83 | } 84 | 85 | @Test 86 | fun findById() = runTest { 87 | val result = repository.findById("b39a2fd2-f7d7-405d-b73c-b68a8dedbcdf".toUUID()) 88 | 89 | assertAll( 90 | { assertEquals("Pepe Perez", result?.nombre) }, 91 | { assertEquals("pepe", result?.username) }, 92 | ) 93 | } 94 | 95 | @Test 96 | fun findByIdNotFound() = runTest { 97 | val result = repository.findById(UUID.randomUUID()) 98 | 99 | assertNull(result) 100 | } 101 | 102 | @Test 103 | fun findByUsername() = runTest { 104 | val result = repository.findByUsername("pepe") 105 | 106 | assertAll( 107 | { assertEquals("Pepe Perez", result?.nombre) }, 108 | { assertEquals("pepe", result?.username) }, 109 | ) 110 | } 111 | 112 | @Test 113 | fun findByUsernameNotFound() = runTest { 114 | val result = repository.findByUsername("caca") 115 | 116 | assertNull(result) 117 | } 118 | 119 | @Test 120 | fun save() = runTest { 121 | val res = repository.save(user) 122 | 123 | assertAll( 124 | { assertEquals(user.nombre, res.nombre) }, 125 | { assertEquals(user.username, res.username) }, 126 | { assertEquals(user.email, res.email) }, 127 | ) 128 | } 129 | 130 | 131 | @Test 132 | fun update() = runTest { 133 | val res = repository.save(user) 134 | val res2 = repository.update(user.id, res.copy(nombre = "Test2"))!! 135 | 136 | assertAll( 137 | { assertEquals("Test2", res2.nombre) }, 138 | { assertEquals(user.username, res2.username) }, 139 | { assertEquals(user.email, res2.email) }, 140 | ) 141 | } 142 | 143 | @Test 144 | fun updateNotFound() = runTest { 145 | val res = repository.update(UUID.randomUUID(), user) 146 | assertNull(res) 147 | } 148 | 149 | @Test 150 | fun delete() = runTest { 151 | val res = repository.save(user) 152 | val res2 = repository.delete(res)!! 153 | 154 | assertAll( 155 | { assertEquals(user.nombre, res2.nombre) }, 156 | { assertEquals(user.username, res2.username) }, 157 | { assertEquals(user.email, res2.email) }, 158 | ) 159 | } 160 | 161 | @Test 162 | fun deleteNotFound() = runTest { 163 | val res = repository.delete(user) 164 | 165 | assertNull(res) 166 | } 167 | } -------------------------------------------------------------------------------- /src/test/kotlin/joseluisgs/es/repositories/utils/SetupDataBaseService.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.repositories.utils 2 | 3 | import io.ktor.server.config.* 4 | import joseluisgs.es.config.DataBaseConfig 5 | import joseluisgs.es.services.database.DataBaseService 6 | 7 | fun getDataBaseService(): DataBaseService { 8 | val config = ApplicationConfig("application.conf") 9 | 10 | // Leemos la configuración de la base de datos 11 | val dataBaseConfigParams = mapOf( 12 | "driver" to config.property("database.driver").getString(), 13 | "protocol" to config.property("database.protocol").getString(), 14 | "user" to config.property("database.user").getString(), 15 | "password" to config.property("database.password").getString(), 16 | "database" to config.property("database.database").getString(), 17 | "initDatabaseData" to config.property("database.initDatabaseData").getString(), 18 | ) 19 | 20 | val dataBaseConfig = DataBaseConfig(dataBaseConfigParams) 21 | val dataBaseService = DataBaseService(dataBaseConfig) 22 | dataBaseService.initDataBaseService() 23 | 24 | return dataBaseService 25 | } -------------------------------------------------------------------------------- /src/test/kotlin/joseluisgs/es/services/users/UsersServiceImplTest.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.services.users 2 | 3 | import io.mockk.MockKAnnotations 4 | import io.mockk.coEvery 5 | import io.mockk.coVerify 6 | import io.mockk.impl.annotations.InjectMockKs 7 | import io.mockk.impl.annotations.MockK 8 | import io.mockk.junit5.MockKExtension 9 | import joseluisgs.es.exceptions.UserNotFoundException 10 | import joseluisgs.es.exceptions.UserUnauthorizedException 11 | import joseluisgs.es.models.User 12 | import joseluisgs.es.repositories.users.UsersRepositoryImpl 13 | import kotlinx.coroutines.flow.flowOf 14 | import kotlinx.coroutines.flow.take 15 | import kotlinx.coroutines.flow.toList 16 | import kotlinx.coroutines.test.runTest 17 | import org.junit.jupiter.api.Assertions.* 18 | import org.junit.jupiter.api.Test 19 | import org.junit.jupiter.api.TestInstance 20 | import org.junit.jupiter.api.assertThrows 21 | import org.junit.jupiter.api.extension.ExtendWith 22 | import org.mindrot.jbcrypt.BCrypt 23 | import java.util.* 24 | 25 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 26 | @ExtendWith(MockKExtension::class) 27 | class UsersServiceImplTest { 28 | 29 | val user = User( 30 | id = UUID.fromString("1314a16c-f4f2-47e2-a7e9-21cac686ee55"), 31 | nombre = "Test", 32 | username = "test", 33 | email = "test@test.com", 34 | password = BCrypt.hashpw("test1234", BCrypt.gensalt(12)), 35 | avatar = "https://upload.wikimedia.org/wikipedia/commons/f/f4/User_Avatar_2.png", 36 | role = User.Role.USER 37 | ) 38 | 39 | @MockK 40 | lateinit var repository: UsersRepositoryImpl 41 | 42 | @InjectMockKs 43 | lateinit var service: UsersServiceImpl 44 | 45 | init { 46 | MockKAnnotations.init(this) 47 | } 48 | 49 | @Test 50 | fun findAll() = runTest { 51 | coEvery { repository.findAll(any()) } returns flowOf(user) 52 | 53 | val result = service.findAll(null) 54 | val expected = result.take(1).toList() 55 | 56 | assertAll( 57 | { assertEquals(1, expected.size) }, 58 | { assertEquals("Test", expected[0].nombre) }, 59 | ) 60 | 61 | coVerify { repository.findAll(any()) } 62 | 63 | } 64 | 65 | @Test 66 | fun findById() = runTest { 67 | coEvery { repository.findById(any()) } returns user 68 | 69 | val result = service.findById(user.id) 70 | 71 | assertAll( 72 | { assertEquals("Test", result.nombre) }, 73 | { assertEquals("test", result.username) }, 74 | ) 75 | 76 | coVerify { repository.findById(any()) } 77 | } 78 | 79 | @Test 80 | fun findByIdNotFound() = runTest { 81 | coEvery { repository.findById(any()) } returns null 82 | 83 | val res = assertThrows { 84 | service.findById(user.id) 85 | } 86 | 87 | assertEquals("No se ha encontrado el usuario con id: ${user.id}", res.message) 88 | 89 | coVerify { repository.findById(any()) } 90 | } 91 | 92 | @Test 93 | fun findByUsername() = runTest { 94 | coEvery { repository.findByUsername(any()) } returns user 95 | 96 | val result = service.findByUsername(user.username) 97 | 98 | assertAll( 99 | { assertEquals("Test", result.nombre) }, 100 | { assertEquals("test", result.username) }, 101 | ) 102 | 103 | coVerify { repository.findByUsername(any()) } 104 | } 105 | 106 | @Test 107 | fun findByUsernameNotFound() = runTest { 108 | coEvery { repository.findByUsername(any()) } returns null 109 | 110 | val res = assertThrows { 111 | service.findByUsername(user.username) 112 | } 113 | 114 | assertEquals("No se ha encontrado el usuario con username: ${user.username}", res.message) 115 | 116 | coVerify { repository.findByUsername(any()) } 117 | } 118 | 119 | @Test 120 | fun hashedPassword() = runTest { 121 | coEvery { repository.hashedPassword(any()) } returns BCrypt.hashpw("test1234", BCrypt.gensalt(12)) 122 | 123 | val result = service.hashedPassword("test1234") 124 | 125 | assertTrue(BCrypt.checkpw("test1234", result)) 126 | 127 | coVerify { repository.hashedPassword(any()) } 128 | } 129 | 130 | @Test 131 | fun checkUserNameAndPassword() = runTest { 132 | coEvery { repository.checkUserNameAndPassword(any(), any()) } returns user 133 | 134 | val result = service.checkUserNameAndPassword(user.username, "test1234") 135 | 136 | assertAll( 137 | { assertEquals("Test", result.nombre) }, 138 | { assertEquals("test", result.username) }, 139 | ) 140 | 141 | coVerify { repository.checkUserNameAndPassword(any(), any()) } 142 | } 143 | 144 | @Test 145 | fun checkUserNameAndPasswordNotFound() = runTest { 146 | coEvery { repository.checkUserNameAndPassword(any(), any()) } returns null 147 | 148 | val res = assertThrows { 149 | service.checkUserNameAndPassword(user.username, "test1234") 150 | } 151 | 152 | assertEquals("Nombre de usuario o contraseña incorrectos", res.message) 153 | } 154 | 155 | @Test 156 | fun save() = runTest { 157 | coEvery { repository.save(any()) } returns user 158 | 159 | val result = service.save(user) 160 | 161 | assertAll( 162 | { assertEquals("Test", result.nombre) }, 163 | { assertEquals("test", result.username) }, 164 | ) 165 | 166 | coVerify { repository.save(any()) } 167 | } 168 | 169 | @Test 170 | fun update() = runTest { 171 | coEvery { repository.update(any(), any()) } returns user 172 | 173 | val result = service.update(user.id, user) 174 | 175 | assertAll( 176 | { assertEquals("Test", result.nombre) }, 177 | { assertEquals("test", result.username) }, 178 | ) 179 | 180 | coVerify { repository.update(any(), any()) } 181 | } 182 | 183 | @Test 184 | fun delete() = runTest { 185 | coEvery { repository.findById(any()) } returns user 186 | coEvery { repository.delete(any()) } returns user 187 | 188 | service.delete(user.id) 189 | 190 | coVerify { repository.delete(any()) } 191 | } 192 | } -------------------------------------------------------------------------------- /src/test/kotlin/joseluisgs/es/utils/UuidUtilsKtTest.kt: -------------------------------------------------------------------------------- 1 | package joseluisgs.es.utils 2 | 3 | 4 | import org.junit.jupiter.api.Test 5 | import kotlin.test.assertEquals 6 | import kotlin.test.assertFailsWith 7 | 8 | 9 | class UuidUtilsKtTest { 10 | 11 | @Test 12 | fun toUUID() { 13 | val uuid = "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11" 14 | assertEquals(uuid, uuid.toUUID().toString()) 15 | } 16 | 17 | @Test 18 | fun toUUID2Exception() { 19 | val uuid = "a0eebc99" 20 | val exception = assertFailsWith { 21 | uuid.toUUID() 22 | } 23 | assertEquals("El id no es válido o no está en el formato UUID", exception.message) 24 | } 25 | } --------------------------------------------------------------------------------