diff --git a/.gitignore b/.gitignore index 2f7dbd9..2f2bb05 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ *.db target .env -Icon? \ No newline at end of file +Icon? +.docker \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 86c75db..2fb68df 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,11 +26,11 @@ FROM eclipse-temurin:21-jre-alpine WORKDIR /app # Seguridad: Usuario no-root -RUN addgroup -S spring && adduser -S spring -G spring -USER spring:spring +RUN addgroup -S app && adduser -S app -G app +USER app:app # Copiamos solo el JAR final (ajustado a tu módulo bootstrap) -COPY --from=build /app/bootstrap/target/*.jar app.jar +COPY --from=build /app/bootstrap/target/bootstrap-*.jar app.jar # Configuración de Memoria y Rendimiento para Microservicios # -XX:+UseSerialGC: Menos consumo de RAM para apps < 1GB diff --git a/HELP.md b/HELP.md deleted file mode 100644 index f80f69c..0000000 --- a/HELP.md +++ /dev/null @@ -1,31 +0,0 @@ -# Getting Started - -### Reference Documentation -For further reference, please consider the following sections: - -* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html) -* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/4.0.0-SNAPSHOT/maven-plugin) -* [Create an OCI image](https://docs.spring.io/spring-boot/4.0.0-SNAPSHOT/maven-plugin/build-image.html) -* [Spring Web](https://docs.spring.io/spring-boot/4.0.0-SNAPSHOT/reference/web/servlet.html) -* [Spring Data JPA](https://docs.spring.io/spring-boot/4.0.0-SNAPSHOT/reference/data/sql.html#data.sql.jpa-and-spring-data) -* [Validation](https://docs.spring.io/spring-boot/4.0.0-SNAPSHOT/reference/io/validation.html) -* [Spring Boot DevTools](https://docs.spring.io/spring-boot/4.0.0-SNAPSHOT/reference/using/devtools.html) -* [Spring Boot Actuator](https://docs.spring.io/spring-boot/4.0.0-SNAPSHOT/reference/actuator/index.html) - -### Guides -The following guides illustrate how to use some features concretely: - -* [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/) -* [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/) -* [Building REST services with Spring](https://spring.io/guides/tutorials/rest/) -* [Accessing Data with JPA](https://spring.io/guides/gs/accessing-data-jpa/) -* [Validation](https://spring.io/guides/gs/validating-form-input/) -* [Building a RESTful Web Service with Spring Boot Actuator](https://spring.io/guides/gs/actuator-service/) - -### Maven Parent overrides - -Due to Maven's design, elements are inherited from the parent POM to the project POM. -While most of the inheritance is fine, it also inherits unwanted elements like `` and `` from the parent. -To prevent this, the project POM contains empty overrides for these elements. -If you manually switch to a different parent and actually want the inheritance, you need to remove those overrides. - diff --git a/application/pom.xml b/application/pom.xml index 75ed13b..a6f229f 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -12,9 +12,18 @@ domain ${project.version} + jakarta.validation jakarta.validation-api + 3.0.2 + + + + org.projectlombok + lombok + 1.18.36 + provided \ No newline at end of file diff --git a/bootstrap/pom.xml b/bootstrap/pom.xml index 44dcb76..5107d95 100644 --- a/bootstrap/pom.xml +++ b/bootstrap/pom.xml @@ -1,12 +1,22 @@ - + + 4.0.0 com.pablotj portfolio-api 0.0.1-SNAPSHOT + bootstrap + + 3.6.1 + 42.7.2 + 1.19.7 + + com.pablotj @@ -14,40 +24,84 @@ ${project.version} - + + io.jooby + jooby-netty + ${jooby.version} + + + + io.jooby + jooby-jackson + ${jooby.version} + + + + io.jooby + jooby-jackson + 3.6.1 + + + + io.jooby + jooby-hibernate-validator + 3.6.1 + + + + io.jooby + jooby-openapi + ${jooby.version} + + + + io.jooby + jooby-logback + ${jooby.version} + + org.postgresql postgresql - runtime + ${postgresql.version} com.h2database h2 - runtime + 2.2.224 - - - org.springdoc - springdoc-openapi-starter-webmvc-ui - ${springdoc.version} + io.jooby + jooby-hikari + ${jooby.version} - - - org.springframework.boot - spring-boot-starter-actuator + io.jooby + jooby-guice + ${jooby.version} - - + + + io.jooby + jooby-flyway + ${jooby.version} + + + io.jooby + jooby-test + ${jooby.version} test org.testcontainers postgresql + ${testcontainers.version} test @@ -55,13 +109,34 @@ - org.springframework.boot - spring-boot-maven-plugin + io.jooby + jooby-maven-plugin + ${jooby.version} + + com.pablotj.portfolio.bootstrap.PortfolioApplication + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.1 + jooby-shade + package - repackage + shade + + false + + + + com.pablotj.portfolio.bootstrap.PortfolioApplication + + + diff --git a/bootstrap/src/main/java/com/pablotj/portfolio/bootstrap/PortfolioApplication.java b/bootstrap/src/main/java/com/pablotj/portfolio/bootstrap/PortfolioApplication.java index 019057d..8059992 100644 --- a/bootstrap/src/main/java/com/pablotj/portfolio/bootstrap/PortfolioApplication.java +++ b/bootstrap/src/main/java/com/pablotj/portfolio/bootstrap/PortfolioApplication.java @@ -1,16 +1,79 @@ package com.pablotj.portfolio.bootstrap; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.domain.EntityScan; -import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import com.pablotj.portfolio.bootstrap.certification.CertificationApplicationConfig; +import com.pablotj.portfolio.bootstrap.education.EducationApplicationConfig; +import com.pablotj.portfolio.bootstrap.experience.ExperienceApplicationConfig; +import com.pablotj.portfolio.bootstrap.profile.ProfileApplicationConfig; +import com.pablotj.portfolio.bootstrap.project.ProjectApplicationConfig; +import com.pablotj.portfolio.bootstrap.skill.SkillApplicationConfig; +import com.pablotj.portfolio.infrastructure.config.CorsConfig; +import com.pablotj.portfolio.infrastructure.rest.api.ApiErrorController; +import com.pablotj.portfolio.infrastructure.rest.api.ApiRootController; +import com.pablotj.portfolio.infrastructure.rest.certification.CertificationController; +import com.pablotj.portfolio.infrastructure.rest.education.EducationController; +import com.pablotj.portfolio.infrastructure.rest.experience.ExperienceController; +import com.pablotj.portfolio.infrastructure.rest.profile.ProfileController; +import com.pablotj.portfolio.infrastructure.rest.project.ProjectController; +// Importa el resto de controladores según los tengas listos +import com.pablotj.portfolio.infrastructure.rest.skill.SkillController; +import io.jooby.Jooby; +import io.jooby.flyway.FlywayModule; +import io.jooby.guice.GuiceModule; +import io.jooby.hikari.HikariModule; +import io.jooby.jackson.JacksonModule; +import io.jooby.netty.NettyServer; +import io.jooby.ebean.EbeanModule; +import io.jooby.hibernate.validator.HibernateValidatorModule; -@SpringBootApplication(scanBasePackages = "com.pablotj") -@EnableJpaRepositories(basePackages = {"com.pablotj.portfolio.infrastructure.persistence.*.repo"}) -@EntityScan(basePackages = {"com.pablotj.portfolio.infrastructure.persistence.*.entity"}) -public class PortfolioApplication { +public class PortfolioApplication extends Jooby { + + { + // 1. Servidor y configuración básica + install(new NettyServer()); + + // 2. Módulos de infraestructura + install(new JacksonModule()); + install(new HibernateValidatorModule()); + install(new HikariModule()); + install(new FlywayModule()); + install(new EbeanModule()); + // Instalamos tu configuración de CORS que acabamos de arreglar + install(new CorsConfig()); + + error(ApiErrorController.getHandler()); + // 3. Registro de Dependencias (DI) + // Aquí es donde Jooby sabe que cuando alguien pida un Port, + // debe entregar el Adapter de infraestructura. + // Nota: Si usas módulos de Guice en tus ApplicationConfigs, + // asegúrate de que se instalen aquí. + + // 4. Módulos de Aplicación (Business Logic / Use Cases) + // Estos módulos deberían contener los bindings de Guice para los servicios + install(new ProfileApplicationConfig()); + install(new ProjectApplicationConfig()); + install(new CertificationApplicationConfig()); + install(new EducationApplicationConfig()); + install(new ExperienceApplicationConfig()); + install(new SkillApplicationConfig()); + + // 5. Adaptadores de Entrada (Rutas / Controllers) + // Registramos el controlador raíz para la info de la API + install(new GuiceModule()); // Esto habilita la resolución de @Inject y @Named + + + path("/api", () -> { + mvc(ApiRootController.class); + mvc(CertificationController.class); + mvc(EducationController.class); + mvc(ExperienceController.class); + mvc(ProfileController.class); + mvc(ProjectController.class); + mvc(SkillController.class); + }); - public static void main(String[] args) { - SpringApplication.run(PortfolioApplication.class, args); } -} + + public static void main(final String[] args) { + runApp(args, PortfolioApplication::new); + } +} \ No newline at end of file diff --git a/bootstrap/src/main/java/com/pablotj/portfolio/bootstrap/certification/CertificationApplicationConfig.java b/bootstrap/src/main/java/com/pablotj/portfolio/bootstrap/certification/CertificationApplicationConfig.java index a652302..fe76402 100644 --- a/bootstrap/src/main/java/com/pablotj/portfolio/bootstrap/certification/CertificationApplicationConfig.java +++ b/bootstrap/src/main/java/com/pablotj/portfolio/bootstrap/certification/CertificationApplicationConfig.java @@ -3,19 +3,34 @@ package com.pablotj.portfolio.bootstrap.certification; import com.pablotj.portfolio.application.certification.CreateCertificationUseCase; import com.pablotj.portfolio.application.certification.GetCertificationUseCase; import com.pablotj.portfolio.domain.certification.port.CertificationRepositoryPort; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; +import com.pablotj.portfolio.infrastructure.persistence.certification.adapter.CertificationRepositoryAdapter; +import com.pablotj.portfolio.infrastructure.persistence.certification.mapper.CertificationJpaMapper; +import com.pablotj.portfolio.infrastructure.persistence.profile.adapter.ProfileRepositoryAdapter; +import com.pablotj.portfolio.infrastructure.persistence.profile.mapper.ProfileJpaMapper; +import io.jooby.Extension; +import io.jooby.Jooby; +import jakarta.annotation.Nonnull; +import org.mapstruct.factory.Mappers; -@Configuration -public class CertificationApplicationConfig { +public class CertificationApplicationConfig implements Extension { - @Bean - public GetCertificationUseCase getCertificationUseCase(CertificationRepositoryPort repo) { - return new GetCertificationUseCase(repo); - } + @Override + public void install(@Nonnull Jooby app) { + CertificationJpaMapper mapper = Mappers.getMapper(CertificationJpaMapper.class); + app.getServices().put(CertificationJpaMapper.class, mapper); - @Bean - public CreateCertificationUseCase createCertificationUseCase(CertificationRepositoryPort repo) { - return new CreateCertificationUseCase(repo); + CertificationRepositoryAdapter adapter = new CertificationRepositoryAdapter(mapper); + + // 1. Registramos la IMPLEMENTACIÓN del repositorio (Adapter) + // Usamos app.require para que Jooby le inyecte el objeto Database de Ebean automáticamente + + // 2. Mapeamos la INTERFAZ (Port) a esa instancia en el registro + app.getServices().put(CertificationRepositoryPort.class, adapter); + + // 3. Registramos los Casos de Uso + // Al usar app.require, Jooby busca el CertificationRepositoryPort que acabamos de registrar + // y lo inyecta en el constructor de los Use Cases. + app.getServices().put(GetCertificationUseCase.class, new GetCertificationUseCase(adapter)); + app.getServices().put(CreateCertificationUseCase.class, new CreateCertificationUseCase(adapter)); } } \ No newline at end of file diff --git a/bootstrap/src/main/java/com/pablotj/portfolio/bootstrap/education/EducationApplicationConfig.java b/bootstrap/src/main/java/com/pablotj/portfolio/bootstrap/education/EducationApplicationConfig.java index 91875ca..8645af2 100644 --- a/bootstrap/src/main/java/com/pablotj/portfolio/bootstrap/education/EducationApplicationConfig.java +++ b/bootstrap/src/main/java/com/pablotj/portfolio/bootstrap/education/EducationApplicationConfig.java @@ -3,19 +3,31 @@ package com.pablotj.portfolio.bootstrap.education; import com.pablotj.portfolio.application.education.CreateEducationUseCase; import com.pablotj.portfolio.application.education.GetEducationUseCase; import com.pablotj.portfolio.domain.education.port.EducationRepositoryPort; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; +import com.pablotj.portfolio.infrastructure.persistence.certification.adapter.CertificationRepositoryAdapter; +import com.pablotj.portfolio.infrastructure.persistence.certification.mapper.CertificationJpaMapper; +import com.pablotj.portfolio.infrastructure.persistence.education.adapter.EducationRepositoryAdapter; +import com.pablotj.portfolio.infrastructure.persistence.education.mapper.EducationJpaMapper; +import io.jooby.Extension; +import io.jooby.Jooby; +import jakarta.annotation.Nonnull; +import org.mapstruct.factory.Mappers; -@Configuration -public class EducationApplicationConfig { +public class EducationApplicationConfig implements Extension { - @Bean - public GetEducationUseCase getEducationUseCase(EducationRepositoryPort repo) { - return new GetEducationUseCase(repo); - } + @Override + public void install(@Nonnull Jooby app) { + EducationJpaMapper mapper = Mappers.getMapper(EducationJpaMapper.class); + app.getServices().put(EducationJpaMapper.class, mapper); - @Bean - public CreateEducationUseCase createEducationUseCase(EducationRepositoryPort repo) { - return new CreateEducationUseCase(repo); + EducationRepositoryAdapter adapter = new EducationRepositoryAdapter(mapper); + + // 2. Mapeamos la INTERFAZ (Puerto) a esa instancia concreta + app.getServices().put(EducationRepositoryPort.class, adapter); + + // 3. Registramos los Casos de Uso (Aplicación) + // Usamos app.require() para que Jooby inyecte el EducationRepositoryPort + // en el constructor de los Use Cases. + app.getServices().put(GetEducationUseCase.class, new GetEducationUseCase(adapter)); + app.getServices().put(CreateEducationUseCase.class, new CreateEducationUseCase(adapter)); } } \ No newline at end of file diff --git a/bootstrap/src/main/java/com/pablotj/portfolio/bootstrap/experience/ExperienceApplicationConfig.java b/bootstrap/src/main/java/com/pablotj/portfolio/bootstrap/experience/ExperienceApplicationConfig.java index 31bf306..dbc40a8 100644 --- a/bootstrap/src/main/java/com/pablotj/portfolio/bootstrap/experience/ExperienceApplicationConfig.java +++ b/bootstrap/src/main/java/com/pablotj/portfolio/bootstrap/experience/ExperienceApplicationConfig.java @@ -3,19 +3,30 @@ package com.pablotj.portfolio.bootstrap.experience; import com.pablotj.portfolio.application.experience.CreateExperienceUseCase; import com.pablotj.portfolio.application.experience.GetExperienceUseCase; import com.pablotj.portfolio.domain.experience.port.ExperienceRepositoryPort; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; +import com.pablotj.portfolio.infrastructure.persistence.education.adapter.EducationRepositoryAdapter; +import com.pablotj.portfolio.infrastructure.persistence.education.mapper.EducationJpaMapper; +import com.pablotj.portfolio.infrastructure.persistence.experience.adapter.ExperienceRepositoryAdapter; +import com.pablotj.portfolio.infrastructure.persistence.experience.mapper.ExperienceJpaMapper; +import io.jooby.Extension; +import io.jooby.Jooby; +import io.jooby.ServiceRegistry; +import jakarta.annotation.Nonnull; +import org.mapstruct.factory.Mappers; -@Configuration -public class ExperienceApplicationConfig { +public class ExperienceApplicationConfig implements Extension { + @Override + public void install(@Nonnull Jooby app) { + // 1. Instanciar y registrar el adaptador + ExperienceJpaMapper mapper = Mappers.getMapper(ExperienceJpaMapper.class); + app.getServices().put(ExperienceJpaMapper.class, mapper); - @Bean - public GetExperienceUseCase getExperienceUseCase(ExperienceRepositoryPort repo) { - return new GetExperienceUseCase(repo); - } + ExperienceRepositoryAdapter adapter = new ExperienceRepositoryAdapter(mapper); - @Bean - public CreateExperienceUseCase createExperienceUseCase(ExperienceRepositoryPort repo) { - return new CreateExperienceUseCase(repo); + + app.getServices().put(ExperienceRepositoryPort.class, adapter); + + // 2. Registrar casos de uso usando app.require para inyectar el puerto + app.getServices().put(GetExperienceUseCase.class, new GetExperienceUseCase(adapter)); + app.getServices().put(CreateExperienceUseCase.class, new CreateExperienceUseCase(adapter)); } } \ No newline at end of file diff --git a/bootstrap/src/main/java/com/pablotj/portfolio/bootstrap/profile/ProfileApplicationConfig.java b/bootstrap/src/main/java/com/pablotj/portfolio/bootstrap/profile/ProfileApplicationConfig.java index 6aaaeb6..0e7a782 100644 --- a/bootstrap/src/main/java/com/pablotj/portfolio/bootstrap/profile/ProfileApplicationConfig.java +++ b/bootstrap/src/main/java/com/pablotj/portfolio/bootstrap/profile/ProfileApplicationConfig.java @@ -3,19 +3,29 @@ package com.pablotj.portfolio.bootstrap.profile; import com.pablotj.portfolio.application.profile.CreateProfileUseCase; import com.pablotj.portfolio.application.profile.GetProfileUseCase; import com.pablotj.portfolio.domain.profile.port.ProfileRepositoryPort; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; +import com.pablotj.portfolio.infrastructure.persistence.profile.adapter.ProfileRepositoryAdapter; +import com.pablotj.portfolio.infrastructure.persistence.profile.mapper.ProfileJpaMapper; +import io.jooby.Extension; +import io.jooby.Jooby; +import jakarta.annotation.Nonnull; +import org.mapstruct.factory.Mappers; -@Configuration -public class ProfileApplicationConfig { +public class ProfileApplicationConfig implements Extension { + @Override + public void install(@Nonnull Jooby app) { + // 1. Registramos el Mapper primero (para que esté disponible) + ProfileJpaMapper mapper = Mappers.getMapper(ProfileJpaMapper.class); + app.getServices().put(ProfileJpaMapper.class, mapper); - @Bean - public GetProfileUseCase getHomeUseCase(ProfileRepositoryPort repo) { - return new GetProfileUseCase(repo); - } + // 2. Instanciamos el Adaptador MANUALMENTE + // Jooby NO puede hacer "app.require" de una clase con constructor si no hay un DI Module (Guice) + ProfileRepositoryAdapter adapter = new ProfileRepositoryAdapter(mapper); - @Bean - public CreateProfileUseCase createHomeUseCase(ProfileRepositoryPort repo) { - return new CreateProfileUseCase(repo); + // 3. Registramos el adaptador bajo su Interfaz (Port) + app.getServices().put(ProfileRepositoryPort.class, adapter); + + // 4. Registramos casos de uso instanciándolos con sus dependencias + app.getServices().put(GetProfileUseCase.class, new GetProfileUseCase(adapter)); + app.getServices().put(CreateProfileUseCase.class, new CreateProfileUseCase(adapter)); } } \ No newline at end of file diff --git a/bootstrap/src/main/java/com/pablotj/portfolio/bootstrap/project/ProjectApplicationConfig.java b/bootstrap/src/main/java/com/pablotj/portfolio/bootstrap/project/ProjectApplicationConfig.java index 19bf1ba..e126139 100644 --- a/bootstrap/src/main/java/com/pablotj/portfolio/bootstrap/project/ProjectApplicationConfig.java +++ b/bootstrap/src/main/java/com/pablotj/portfolio/bootstrap/project/ProjectApplicationConfig.java @@ -3,19 +3,29 @@ package com.pablotj.portfolio.bootstrap.project; import com.pablotj.portfolio.application.project.CreateProjectUseCase; import com.pablotj.portfolio.application.project.GetProjectUseCase; import com.pablotj.portfolio.domain.project.port.ProjectRepositoryPort; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; +import com.pablotj.portfolio.infrastructure.persistence.experience.adapter.ExperienceRepositoryAdapter; +import com.pablotj.portfolio.infrastructure.persistence.experience.mapper.ExperienceJpaMapper; +import com.pablotj.portfolio.infrastructure.persistence.project.adapter.ProjectRepositoryAdapter; +import com.pablotj.portfolio.infrastructure.persistence.project.mapper.ProjectJpaMapper; +import io.jooby.Extension; +import io.jooby.Jooby; +import io.jooby.ServiceRegistry; +import jakarta.annotation.Nonnull; +import org.mapstruct.factory.Mappers; -@Configuration -public class ProjectApplicationConfig { +public class ProjectApplicationConfig implements Extension { + @Override + public void install(@Nonnull Jooby app) { - @Bean - public CreateProjectUseCase createProjectUseCase(ProjectRepositoryPort repo) { - return new CreateProjectUseCase(repo); - } + ProjectJpaMapper mapper = Mappers.getMapper(ProjectJpaMapper.class); + app.getServices().put(ProjectJpaMapper.class, mapper); - @Bean - public GetProjectUseCase getProjectUseCase(ProjectRepositoryPort repo) { - return new GetProjectUseCase(repo); + ProjectRepositoryAdapter adapter = new ProjectRepositoryAdapter(mapper); + + app.getServices().put(ProjectRepositoryPort.class, adapter); + + // 2. Registrar casos de uso + app.getServices().put(CreateProjectUseCase.class, new CreateProjectUseCase(adapter)); + app.getServices().put(GetProjectUseCase.class, new GetProjectUseCase(adapter)); } } \ No newline at end of file diff --git a/bootstrap/src/main/java/com/pablotj/portfolio/bootstrap/skill/SkillApplicationConfig.java b/bootstrap/src/main/java/com/pablotj/portfolio/bootstrap/skill/SkillApplicationConfig.java index b78e847..6dae335 100644 --- a/bootstrap/src/main/java/com/pablotj/portfolio/bootstrap/skill/SkillApplicationConfig.java +++ b/bootstrap/src/main/java/com/pablotj/portfolio/bootstrap/skill/SkillApplicationConfig.java @@ -3,19 +3,31 @@ package com.pablotj.portfolio.bootstrap.skill; import com.pablotj.portfolio.application.skill.CreateSkillUseCase; import com.pablotj.portfolio.application.skill.GetSkillUseCase; import com.pablotj.portfolio.domain.skill.port.SkillRepositoryPort; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; +import com.pablotj.portfolio.infrastructure.persistence.project.adapter.ProjectRepositoryAdapter; +import com.pablotj.portfolio.infrastructure.persistence.project.mapper.ProjectJpaMapper; +import com.pablotj.portfolio.infrastructure.persistence.skill.adapter.SkillRepositoryAdapter; +import com.pablotj.portfolio.infrastructure.persistence.skill.mapper.SkillJpaMapper; +import io.jooby.Extension; +import io.jooby.Jooby; +import io.jooby.ServiceRegistry; +import jakarta.annotation.Nonnull; +import org.mapstruct.factory.Mappers; -@Configuration -public class SkillApplicationConfig { +public class SkillApplicationConfig implements Extension { + @Override + public void install(@Nonnull Jooby app) { - @Bean - public GetSkillUseCase getSkillUseCase(SkillRepositoryPort repo) { - return new GetSkillUseCase(repo); - } + SkillJpaMapper mapper = Mappers.getMapper(SkillJpaMapper.class); + app.getServices().put(SkillJpaMapper.class, mapper); - @Bean - public CreateSkillUseCase createSkillUseCase(SkillRepositoryPort repo) { - return new CreateSkillUseCase(repo); + SkillRepositoryAdapter adapter = new SkillRepositoryAdapter(mapper); + + + // 1. Instanciar y registrar el adaptador + app.getServices().put(SkillRepositoryPort.class, adapter); + + // 2. Registrar casos de uso + app.getServices().put(GetSkillUseCase.class, new GetSkillUseCase(adapter)); + app.getServices().put(CreateSkillUseCase.class, new CreateSkillUseCase(adapter)); } } \ No newline at end of file diff --git a/bootstrap/src/main/resources/application.conf b/bootstrap/src/main/resources/application.conf new file mode 100644 index 0000000..ea257e5 --- /dev/null +++ b/bootstrap/src/main/resources/application.conf @@ -0,0 +1,67 @@ +# Configuración de la App (Sustituye a info y app) +info { + app { + version = "0.0.1-SNAPSHOT" # Maven no rellena esto automáticamente en Jooby sin plugins extra, mejor ponlo fijo + } +} + +app { + cors { + allowed-origins = ${?APP_ALLOWED_ORIGINS} + allowed-origins = "http://localhost:8080" # Valor por defecto + } +} + +# Configuración del Servidor +server { + port = 8080 + # En Jooby, el context-path se define habitualmente en la clase App, + # pero puedes usar esta propiedad si la gestionas manualmente. +} + +# Base de Datos (Ebean usa estas propiedades automáticamente) +db { + url = "jdbc:postgresql://"${?DB_HOST}":"${?DB_PORT}"/"${?DB_NAME} + user = ${?DB_USER} + password = ${?DB_PASSWORD} + driverClassName = org.postgresql.Driver + + # HikariCP (Configuración de Pool) + hikari { + maximumPoolSize = 3 + minimumIdle = 1 + idleTimeout = 30000 + connectionTimeout = 10000 + } +} + +flyway { + # Ubicación de tus scripts .sql + locations = ["classpath:db/migration"] + cleanDisabled = true + baselineOnMigrate = true + baselineVersion = ${?FLYWAY_BASELINE_VERSION} + + # Si quieres que se ejecute siempre al arrancar + run = [migrate] +} + +# Ebean (Sustituye a jpa/hibernate) +ebean { + ddl { + generate = false + run = false + } + # Mostrar SQL en consola + debug = true +} + +# Jackson +jackson { + indent_output = true +} + +# Swagger/OpenAPI (Jooby OpenAPI usa estas rutas por defecto o configurables) +swagger { + path = "/swagger-ui" +} \ No newline at end of file diff --git a/bootstrap/src/main/resources/application.yml b/bootstrap/src/main/resources/application.yml deleted file mode 100644 index c4e5bd1..0000000 --- a/bootstrap/src/main/resources/application.yml +++ /dev/null @@ -1,54 +0,0 @@ -info: - app: - version: @project.version@ -app: - cors: - allowed-origins: ${APP_ALLOWED_ORIGINS:http://localhost:8080} -spring: - application: - name: portfolio-api - web: - resources: - add-mappings: false - - datasource: - url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:portfolio} - username: ${DB_USER:postgres} - password: ${DB_PASSWORD:postgres} - driver-class-name: org.postgresql.Driver - hikari: - maximum-pool-size: 3 - minimum-idle: 1 - idle-timeout: 30000 - connection-timeout: 10000 - leak-detection-threshold: 10000 - - jpa: - hibernate: - ddl-auto: validate - properties: - hibernate.transaction.jta.platform: org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform - hibernate: - format_sql: true - dialect: org.hibernate.dialect.PostgreSQLDialect - show-sql: true - - jackson: - serialization: - INDENT_OUTPUT: true - -springdoc: - api-docs: - path: /v3/api-docs - swagger-ui: - path: /swagger-ui - -server: - port: 8080 - servlet: - context-path: /api - forward-headers-strategy: framework - - - - diff --git a/domain/pom.xml b/domain/pom.xml index 0b15ee1..247cf86 100644 --- a/domain/pom.xml +++ b/domain/pom.xml @@ -10,11 +10,14 @@ org.projectlombok lombok - true + 1.18.36 + provided + jakarta.validation jakarta.validation-api + 3.0.2 \ No newline at end of file diff --git a/infrastructure/pom.xml b/infrastructure/pom.xml index c66f6d9..242d20e 100644 --- a/infrastructure/pom.xml +++ b/infrastructure/pom.xml @@ -1,12 +1,23 @@ - + + 4.0.0 com.pablotj portfolio-api 0.0.1-SNAPSHOT + infrastructure + + 3.6.1 + 15.8.0 + 1.18.36 + 1.6.3 + + com.pablotj @@ -19,36 +30,71 @@ ${project.version} - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-validation + io.jooby + jooby + ${jooby.version} - org.springframework.boot - spring-boot-starter-security + io.jooby + jooby-ebean + ${jooby.version} + + + + io.jooby + jooby-hibernate-validator + 3.6.1 + + + + io.jooby + jooby-jackson + ${jooby.version} - org.mapstruct mapstruct + ${mapstruct.version} - org.projectlombok lombok - true + ${lombok.version} + provided + + + + jakarta.validation + jakarta.validation-api + 3.0.2 + + + jakarta.annotation + jakarta.annotation-api + 3.0.0 + + + + + io.ebean + ebean-maven-plugin + ${ebean.version} + + + main + process-classes + + enhance + + + + + + \ No newline at end of file diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/config/CorsConfig.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/config/CorsConfig.java index 09ed57d..a509843 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/config/CorsConfig.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/config/CorsConfig.java @@ -1,34 +1,30 @@ package com.pablotj.portfolio.infrastructure.config; +import io.jooby.Extension; +import io.jooby.Jooby; +import io.jooby.handler.Cors; +import io.jooby.handler.CorsHandler; +import jakarta.annotation.Nonnull; import java.util.Arrays; import java.util.List; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -@Configuration -public class CorsConfig { - - @Value("${app.cors.allowed-origins}") - private String allowedOriginsString; - - @Bean - public CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration config = new CorsConfiguration(); +public class CorsConfig implements Extension { + @Override + public void install(@Nonnull Jooby app) { + // Leemos la configuración + String allowedOriginsString = app.getConfig().getString("app.cors.allowed-origins"); List allowedOrigins = Arrays.asList(allowedOriginsString.split(",")); - config.setAllowedOriginPatterns(allowedOrigins); - config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); - config.setAllowedHeaders(List.of("*")); - config.setAllowCredentials(true); + // Definimos la configuración de CORS + Cors cors = new Cors() + .setOrigin(allowedOrigins) + .setMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")) + .setHeaders(Arrays.asList("Content-Type", "Authorization", "X-Requested-With")) + .setUseCredentials(true); - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", config); - - return source; + // En Jooby 3, los Handlers de este tipo se registran con .use() + // Esto lo añade al pipeline de ejecución antes de llegar a las rutas + app.use(new CorsHandler(cors)); } -} +} \ No newline at end of file diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/config/SecurityConfig.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/config/SecurityConfig.java index 44a3c9d..384da98 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/config/SecurityConfig.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/config/SecurityConfig.java @@ -1,31 +1,22 @@ package com.pablotj.portfolio.infrastructure.config; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.web.cors.CorsConfigurationSource; +import io.jooby.Extension; +import io.jooby.Jooby; +import jakarta.annotation.Nonnull; -@Configuration -@EnableWebSecurity -public class SecurityConfig { +public class SecurityConfig implements Extension { - private final CorsConfigurationSource corsConfigurationSource; + @Override + public void install(@Nonnull Jooby app) { + // En Jooby, al no instalar módulos de seguridad (como pac4j o jwt), + // todas las rutas son accesibles según se definan en los controladores. - public SecurityConfig(CorsConfigurationSource corsConfigurationSource) { - this.corsConfigurationSource = corsConfigurationSource; + // El CSRF en Jooby es un handler que solo se activa si lo instalas, + // así que al no hacer nada aquí, ya está "disabled" por defecto. + + // El CORS ya se gestiona en la clase CorsConfig que instalamos en App.java. + + // Si en el futuro necesitas seguridad (JWT, Basic, etc.), + // aquí es donde añadiríamos los "Before" handlers de Jooby. } - - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .csrf(AbstractHttpConfigurer::disable) - .cors(cors -> cors.configurationSource(corsConfigurationSource)) - .authorizeHttpRequests(auth -> auth - .anyRequest().permitAll() - ); - return http.build(); - } -} +} \ No newline at end of file diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/certification/adapter/CertificationRepositoryAdapter.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/certification/adapter/CertificationRepositoryAdapter.java index 4d00617..3d05573 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/certification/adapter/CertificationRepositoryAdapter.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/certification/adapter/CertificationRepositoryAdapter.java @@ -5,36 +5,49 @@ import com.pablotj.portfolio.domain.certification.CertificationId; import com.pablotj.portfolio.domain.certification.port.CertificationRepositoryPort; import com.pablotj.portfolio.infrastructure.persistence.certification.entity.CertificationJpaEntity; import com.pablotj.portfolio.infrastructure.persistence.certification.mapper.CertificationJpaMapper; -import com.pablotj.portfolio.infrastructure.persistence.certification.repo.SpringDataCertificationRepository; +import io.ebean.DB; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; import java.util.List; import java.util.Optional; -import org.springframework.stereotype.Repository; -@Repository +@Singleton // JSR-330 en lugar de @Repository public class CertificationRepositoryAdapter implements CertificationRepositoryPort { - private final SpringDataCertificationRepository repo; private final CertificationJpaMapper mapper; - public CertificationRepositoryAdapter(SpringDataCertificationRepository repo, CertificationJpaMapper mapper) { - this.repo = repo; + @Inject + public CertificationRepositoryAdapter(CertificationJpaMapper mapper) { this.mapper = mapper; } @Override public Certification save(Certification p) { CertificationJpaEntity entity = mapper.toEntity(p); - CertificationJpaEntity saved = repo.save(entity); - return mapper.toDomain(saved); + // Ebean detecta automáticamente si es insert o update por el ID + DB.save(entity); + return mapper.toDomain(entity); } @Override public Optional findById(CertificationId id) { - return repo.findByProfileIdAndId(id.profileId(), id.certificationId()).map(mapper::toDomain); + CertificationJpaEntity entity = DB.find(CertificationJpaEntity.class) + .where() + .eq("profile.id", id.profileId()) + .eq("id", id.certificationId()) + .findOne(); + + return Optional.ofNullable(entity).map(mapper::toDomain); } @Override public List findAll(Long profileId) { - return repo.findAllByProfileId(profileId).stream().map(mapper::toDomain).toList(); + return DB.find(CertificationJpaEntity.class) + .where() + .eq("profile.id", profileId) + .findList() + .stream() + .map(mapper::toDomain) + .toList(); } } \ No newline at end of file diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/certification/entity/CertificationJpaEntity.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/certification/entity/CertificationJpaEntity.java index 6dc6fc0..b03b5e0 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/certification/entity/CertificationJpaEntity.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/certification/entity/CertificationJpaEntity.java @@ -1,6 +1,7 @@ package com.pablotj.portfolio.infrastructure.persistence.certification.entity; import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileJpaEntity; +import io.ebean.Model; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -17,7 +18,7 @@ import lombok.Setter; @Table(name = "CERTIFICATION") @Getter @Setter -public class CertificationJpaEntity { +public class CertificationJpaEntity extends Model { // Extendemos de Model para soporte Ebean @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/certification/mapper/CertificationJpaMapper.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/certification/mapper/CertificationJpaMapper.java index 59171af..d421f65 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/certification/mapper/CertificationJpaMapper.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/certification/mapper/CertificationJpaMapper.java @@ -1,12 +1,11 @@ package com.pablotj.portfolio.infrastructure.persistence.certification.mapper; import com.pablotj.portfolio.domain.certification.Certification; -import com.pablotj.portfolio.domain.certification.CertificationId; import com.pablotj.portfolio.infrastructure.persistence.certification.entity.CertificationJpaEntity; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -@Mapper(componentModel = "spring") +@Mapper public interface CertificationJpaMapper { @Mapping(target = "id", ignore = true) diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/certification/repo/SpringDataCertificationRepository.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/certification/repo/SpringDataCertificationRepository.java index 756d43f..989973a 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/certification/repo/SpringDataCertificationRepository.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/certification/repo/SpringDataCertificationRepository.java @@ -3,9 +3,8 @@ package com.pablotj.portfolio.infrastructure.persistence.certification.repo; import com.pablotj.portfolio.infrastructure.persistence.certification.entity.CertificationJpaEntity; import java.util.List; import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; -public interface SpringDataCertificationRepository extends JpaRepository { +public interface SpringDataCertificationRepository { Optional findByProfileIdAndId(Long profileId, Long id); diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/education/adapter/EducationRepositoryAdapter.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/education/adapter/EducationRepositoryAdapter.java index 77b9bea..1baf185 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/education/adapter/EducationRepositoryAdapter.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/education/adapter/EducationRepositoryAdapter.java @@ -5,36 +5,48 @@ import com.pablotj.portfolio.domain.education.EducationId; import com.pablotj.portfolio.domain.education.port.EducationRepositoryPort; import com.pablotj.portfolio.infrastructure.persistence.education.entity.EducationJpaEntity; import com.pablotj.portfolio.infrastructure.persistence.education.mapper.EducationJpaMapper; -import com.pablotj.portfolio.infrastructure.persistence.education.repo.SpringDataEducationRepository; +import io.ebean.DB; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; import java.util.List; import java.util.Optional; -import org.springframework.stereotype.Repository; -@Repository +@Singleton public class EducationRepositoryAdapter implements EducationRepositoryPort { - private final SpringDataEducationRepository repo; private final EducationJpaMapper mapper; - public EducationRepositoryAdapter(SpringDataEducationRepository repo, EducationJpaMapper mapper) { - this.repo = repo; + @Inject + public EducationRepositoryAdapter(EducationJpaMapper mapper) { this.mapper = mapper; } @Override public Education save(Education p) { EducationJpaEntity entity = mapper.toEntity(p); - EducationJpaEntity saved = repo.save(entity); - return mapper.toDomain(saved); + DB.save(entity); + return mapper.toDomain(entity); } @Override public Optional findById(EducationId id) { - return repo.findByProfileIdAndId(id.profileId(), id.educationId()).map(mapper::toDomain); + EducationJpaEntity entity = DB.find(EducationJpaEntity.class) + .where() + .eq("profile.id", id.profileId()) + .eq("id", id.educationId()) + .findOne(); + + return Optional.ofNullable(entity).map(mapper::toDomain); } @Override public List findAll(Long profileId) { - return repo.findAllByProfileId(profileId).stream().map(mapper::toDomain).toList(); + return DB.find(EducationJpaEntity.class) + .where() + .eq("profile.id", profileId) + .findList() + .stream() + .map(mapper::toDomain) + .toList(); } } \ No newline at end of file diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/education/entity/EducationJpaEntity.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/education/entity/EducationJpaEntity.java index 754de90..8366cb7 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/education/entity/EducationJpaEntity.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/education/entity/EducationJpaEntity.java @@ -1,6 +1,7 @@ package com.pablotj.portfolio.infrastructure.persistence.education.entity; import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileJpaEntity; +import io.ebean.Model; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -17,7 +18,7 @@ import lombok.Setter; @Table(name = "EDUCATION") @Getter @Setter -public class EducationJpaEntity { +public class EducationJpaEntity extends Model { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -41,4 +42,4 @@ public class EducationJpaEntity { @Column(columnDefinition = "text") private String description; -} +} \ No newline at end of file diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/education/mapper/EducationJpaMapper.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/education/mapper/EducationJpaMapper.java index be6e424..bd7bf22 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/education/mapper/EducationJpaMapper.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/education/mapper/EducationJpaMapper.java @@ -1,12 +1,11 @@ package com.pablotj.portfolio.infrastructure.persistence.education.mapper; import com.pablotj.portfolio.domain.education.Education; -import com.pablotj.portfolio.domain.education.EducationId; import com.pablotj.portfolio.infrastructure.persistence.education.entity.EducationJpaEntity; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -@Mapper(componentModel = "spring") +@Mapper public interface EducationJpaMapper { @Mapping(target = "id", ignore = true) diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/education/repo/SpringDataEducationRepository.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/education/repo/SpringDataEducationRepository.java index f990076..042dbe4 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/education/repo/SpringDataEducationRepository.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/education/repo/SpringDataEducationRepository.java @@ -1,12 +1,10 @@ package com.pablotj.portfolio.infrastructure.persistence.education.repo; -import com.pablotj.portfolio.infrastructure.persistence.certification.entity.CertificationJpaEntity; import com.pablotj.portfolio.infrastructure.persistence.education.entity.EducationJpaEntity; import java.util.List; import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; -public interface SpringDataEducationRepository extends JpaRepository { +public interface SpringDataEducationRepository { Optional findByProfileIdAndId(Long profileId, Long id); List findAllByProfileId(Long profileId); diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/experience/adapter/ExperienceRepositoryAdapter.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/experience/adapter/ExperienceRepositoryAdapter.java index 9ff7444..7d8c198 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/experience/adapter/ExperienceRepositoryAdapter.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/experience/adapter/ExperienceRepositoryAdapter.java @@ -5,36 +5,52 @@ import com.pablotj.portfolio.domain.experience.ExperienceId; import com.pablotj.portfolio.domain.experience.port.ExperienceRepositoryPort; import com.pablotj.portfolio.infrastructure.persistence.experience.entity.ExperienceJpaEntity; import com.pablotj.portfolio.infrastructure.persistence.experience.mapper.ExperienceJpaMapper; -import com.pablotj.portfolio.infrastructure.persistence.experience.repo.SpringDataExperienceRepository; +import io.ebean.DB; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; import java.util.List; import java.util.Optional; -import org.springframework.stereotype.Repository; -@Repository +@Singleton public class ExperienceRepositoryAdapter implements ExperienceRepositoryPort { - private final SpringDataExperienceRepository repo; private final ExperienceJpaMapper mapper; - public ExperienceRepositoryAdapter(SpringDataExperienceRepository repo, ExperienceJpaMapper mapper) { - this.repo = repo; + @Inject + public ExperienceRepositoryAdapter(ExperienceJpaMapper mapper) { this.mapper = mapper; } @Override public Experience save(Experience p) { ExperienceJpaEntity entity = mapper.toEntity(p); - ExperienceJpaEntity saved = repo.save(entity); - return mapper.toDomain(saved); + DB.save(entity); + return mapper.toDomain(entity); } @Override public Optional findById(ExperienceId id) { - return repo.findByProfileIdAndId(id.profileId(), id.experienceId()).map(mapper::toDomain); + ExperienceJpaEntity entity = DB.find(ExperienceJpaEntity.class) + .fetch("technologies") // Carga ansiosa para evitar N+1 + .fetch("achievements") + .where() + .eq("profile.id", id.profileId()) + .eq("id", id.experienceId()) + .findOne(); + + return Optional.ofNullable(entity).map(mapper::toDomain); } @Override public List findAll(Long profileId) { - return repo.findAllByProfileId(profileId).stream().map(mapper::toDomain).toList(); + return DB.find(ExperienceJpaEntity.class) + .fetch("technologies") + .fetch("achievements") + .where() + .eq("profile.id", profileId) + .findList() + .stream() + .map(mapper::toDomain) + .toList(); } } \ No newline at end of file diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/experience/entity/ExperienceAchievementJpaEntity.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/experience/entity/ExperienceAchievementJpaEntity.java index 521111e..6af0bd4 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/experience/entity/ExperienceAchievementJpaEntity.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/experience/entity/ExperienceAchievementJpaEntity.java @@ -1,5 +1,6 @@ package com.pablotj.portfolio.infrastructure.persistence.experience.entity; +import io.ebean.Model; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -20,7 +21,7 @@ import lombok.Setter; @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PRIVATE) @Builder -public class ExperienceAchievementJpaEntity { +public class ExperienceAchievementJpaEntity extends Model { // Adaptado para Ebean @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/experience/entity/ExperienceJpaEntity.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/experience/entity/ExperienceJpaEntity.java index 06ee091..3f96fc6 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/experience/entity/ExperienceJpaEntity.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/experience/entity/ExperienceJpaEntity.java @@ -1,6 +1,7 @@ package com.pablotj.portfolio.infrastructure.persistence.experience.entity; import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileJpaEntity; +import io.ebean.Model; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -27,7 +28,7 @@ import lombok.Setter; @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PRIVATE) @Builder -public class ExperienceJpaEntity { +public class ExperienceJpaEntity extends Model { // Extensión para Ebean @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -52,6 +53,7 @@ public class ExperienceJpaEntity { @Column(columnDefinition = "text") private String description; + // Ebean soporta perfectamente CascadeType.ALL y orphanRemoval @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @JoinColumn(name = "EXPERIENCE_ID") private List technologies; diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/experience/entity/ExperienceSkillJpaEntity.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/experience/entity/ExperienceSkillJpaEntity.java index 24a84c9..5591fe1 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/experience/entity/ExperienceSkillJpaEntity.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/experience/entity/ExperienceSkillJpaEntity.java @@ -1,5 +1,6 @@ package com.pablotj.portfolio.infrastructure.persistence.experience.entity; +import io.ebean.Model; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -19,7 +20,7 @@ import lombok.Setter; @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PRIVATE) @Builder -public class ExperienceSkillJpaEntity { +public class ExperienceSkillJpaEntity extends Model { // Extensión necesaria para el motor Ebean @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/experience/mapper/ExperienceJpaMapper.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/experience/mapper/ExperienceJpaMapper.java index dbce14f..82514bf 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/experience/mapper/ExperienceJpaMapper.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/experience/mapper/ExperienceJpaMapper.java @@ -9,7 +9,7 @@ import com.pablotj.portfolio.infrastructure.persistence.experience.entity.Experi import org.mapstruct.Mapper; import org.mapstruct.Mapping; -@Mapper(componentModel = "spring") +@Mapper public interface ExperienceJpaMapper { @Mapping(target = "id", ignore = true) diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/experience/repo/SpringDataExperienceRepository.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/experience/repo/SpringDataExperienceRepository.java index 3a2361d..470712a 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/experience/repo/SpringDataExperienceRepository.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/experience/repo/SpringDataExperienceRepository.java @@ -1,12 +1,10 @@ package com.pablotj.portfolio.infrastructure.persistence.experience.repo; -import com.pablotj.portfolio.infrastructure.persistence.education.entity.EducationJpaEntity; import com.pablotj.portfolio.infrastructure.persistence.experience.entity.ExperienceJpaEntity; import java.util.List; import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; -public interface SpringDataExperienceRepository extends JpaRepository { +public interface SpringDataExperienceRepository { Optional findByProfileIdAndId(Long profileId, Long id); List findAllByProfileId(Long profileId); diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/profile/adapter/ProfileRepositoryAdapter.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/profile/adapter/ProfileRepositoryAdapter.java index b746cc0..b76388d 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/profile/adapter/ProfileRepositoryAdapter.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/profile/adapter/ProfileRepositoryAdapter.java @@ -4,48 +4,63 @@ import com.pablotj.portfolio.domain.profile.Profile; import com.pablotj.portfolio.domain.profile.ProfileId; import com.pablotj.portfolio.domain.profile.port.ProfileRepositoryPort; import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileJpaEntity; -import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileSocialLinkJpaEntity; import com.pablotj.portfolio.infrastructure.persistence.profile.mapper.ProfileJpaMapper; -import com.pablotj.portfolio.infrastructure.persistence.profile.repo.SpringDataProfileRepository; +import io.ebean.DB; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; import java.util.List; import java.util.Optional; -import org.springframework.stereotype.Repository; -@Repository +@Singleton public class ProfileRepositoryAdapter implements ProfileRepositoryPort { - private final SpringDataProfileRepository repo; private final ProfileJpaMapper mapper; - public ProfileRepositoryAdapter(SpringDataProfileRepository repo, ProfileJpaMapper mapper) { - this.repo = repo; + @Inject + public ProfileRepositoryAdapter(ProfileJpaMapper mapper) { this.mapper = mapper; } @Override public Profile save(Profile p) { ProfileJpaEntity entity = mapper.toEntity(p); - if (entity.getSocial() != null) { - for (ProfileSocialLinkJpaEntity socialLinkJpaEntity : entity.getSocial()) { - socialLinkJpaEntity.setProfile(entity); - } - } - ProfileJpaEntity saved = repo.save(entity); - return mapper.toDomain(saved); + + // Ebean gestiona las relaciones Cascade de forma muy limpia. + // Si el mapper ya asocia los ProfileSocialLinkJpaEntity, DB.save lo procesa todo. + DB.save(entity); + + return mapper.toDomain(entity); } @Override public Optional findBySlug(ProfileId id) { - return repo.findBySlug(id.slug()).map(mapper::toDomain); + ProfileJpaEntity entity = DB.find(ProfileJpaEntity.class) + .fetch("social") // Traemos los links sociales de una vez + .where() + .eq("slug", id.slug()) + .findOne(); + + return Optional.ofNullable(entity).map(mapper::toDomain); } @Override public Optional findById(ProfileId id) { - return repo.findById(id.id()).map(mapper::toDomain); + ProfileJpaEntity entity = DB.find(ProfileJpaEntity.class) + .fetch("social") + .where() + .eq("id", id.id()) + .findOne(); + + return Optional.ofNullable(entity).map(mapper::toDomain); } @Override public List findAll() { - return repo.findAll().stream().map(mapper::toDomain).toList(); + return DB.find(ProfileJpaEntity.class) + .fetch("social") + .findList() + .stream() + .map(mapper::toDomain) + .toList(); } } \ No newline at end of file diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/profile/entity/ProfileJpaEntity.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/profile/entity/ProfileJpaEntity.java index 4de0dda..acd9273 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/profile/entity/ProfileJpaEntity.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/profile/entity/ProfileJpaEntity.java @@ -1,5 +1,6 @@ package com.pablotj.portfolio.infrastructure.persistence.profile.entity; +import io.ebean.Model; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -17,7 +18,7 @@ import lombok.Setter; @Table(name = "profile") @Getter @Setter -public class ProfileJpaEntity { +public class ProfileJpaEntity extends Model { // Adaptación para Ebean @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -47,7 +48,7 @@ public class ProfileJpaEntity { @Column private String avatar; - @Column + @Column(columnDefinition = "text") // Recomendado para bio si es larga private String bio; @OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true) diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/profile/entity/ProfileSocialLinkJpaEntity.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/profile/entity/ProfileSocialLinkJpaEntity.java index a92a078..3b8b070 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/profile/entity/ProfileSocialLinkJpaEntity.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/profile/entity/ProfileSocialLinkJpaEntity.java @@ -1,5 +1,6 @@ package com.pablotj.portfolio.infrastructure.persistence.profile.entity; +import io.ebean.Model; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -16,7 +17,8 @@ import lombok.Setter; @Table(name = "profile_social_link") @Getter @Setter -public class ProfileSocialLinkJpaEntity { +public class ProfileSocialLinkJpaEntity extends Model { // Adaptación para Ebean + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/profile/mapper/ProfileJpaMapper.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/profile/mapper/ProfileJpaMapper.java index 93182b1..8c976ce 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/profile/mapper/ProfileJpaMapper.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/profile/mapper/ProfileJpaMapper.java @@ -7,7 +7,7 @@ import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileSo import org.mapstruct.Mapper; import org.mapstruct.Mapping; -@Mapper(componentModel = "spring") +@Mapper public interface ProfileJpaMapper { @Mapping(target = "id", ignore = true) diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/profile/repo/SpringDataProfileRepository.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/profile/repo/SpringDataProfileRepository.java index b5a6ad4..db62f86 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/profile/repo/SpringDataProfileRepository.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/profile/repo/SpringDataProfileRepository.java @@ -2,9 +2,8 @@ package com.pablotj.portfolio.infrastructure.persistence.profile.repo; import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileJpaEntity; import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; -public interface SpringDataProfileRepository extends JpaRepository { +public interface SpringDataProfileRepository { - Optional findBySlug(String slug); + Optional findBySlug(String slug); } diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/project/adapter/ProjectRepositoryAdapter.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/project/adapter/ProjectRepositoryAdapter.java index bed1735..6fa7707 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/project/adapter/ProjectRepositoryAdapter.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/project/adapter/ProjectRepositoryAdapter.java @@ -5,37 +5,54 @@ import com.pablotj.portfolio.domain.project.ProjectId; import com.pablotj.portfolio.domain.project.port.ProjectRepositoryPort; import com.pablotj.portfolio.infrastructure.persistence.project.entity.ProjectJpaEntity; import com.pablotj.portfolio.infrastructure.persistence.project.mapper.ProjectJpaMapper; -import com.pablotj.portfolio.infrastructure.persistence.project.repo.SpringDataProjectRepository; -import org.springframework.stereotype.Repository; - +import io.ebean.DB; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; import java.util.List; import java.util.Optional; -@Repository +@Singleton public class ProjectRepositoryAdapter implements ProjectRepositoryPort { - private final SpringDataProjectRepository repo; private final ProjectJpaMapper mapper; - public ProjectRepositoryAdapter(SpringDataProjectRepository repo, ProjectJpaMapper mapper) { - this.repo = repo; + @Inject + public ProjectRepositoryAdapter(ProjectJpaMapper mapper) { this.mapper = mapper; } @Override public Project save(Project p) { ProjectJpaEntity entity = mapper.toEntity(p); - ProjectJpaEntity saved = repo.save(entity); - return mapper.toDomain(saved); + // Ebean maneja el grafo de persistencia (tecnologías y características) + // basándose en las anotaciones Cascade que pusimos en la entidad. + DB.save(entity); + return mapper.toDomain(entity); } @Override public Optional findById(ProjectId id) { - return repo.findByProfileIdAndId(id.profileId(), id.projectId()).map(mapper::toDomain); + ProjectJpaEntity entity = DB.find(ProjectJpaEntity.class) + .fetch("technologies") + .fetch("features") + .where() + .eq("profile.id", id.profileId()) + .eq("id", id.projectId()) + .findOne(); + + return Optional.ofNullable(entity).map(mapper::toDomain); } @Override public List findAll(Long profileId) { - return repo.findAllByProfileId(profileId).stream().map(mapper::toDomain).toList(); + return DB.find(ProjectJpaEntity.class) + .fetch("technologies") + .fetch("features") + .where() + .eq("profile.id", profileId) + .findList() + .stream() + .map(mapper::toDomain) + .toList(); } } \ No newline at end of file diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/project/entity/ProjectFeatureJpaEntity.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/project/entity/ProjectFeatureJpaEntity.java index d85afd3..d256637 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/project/entity/ProjectFeatureJpaEntity.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/project/entity/ProjectFeatureJpaEntity.java @@ -1,5 +1,6 @@ package com.pablotj.portfolio.infrastructure.persistence.project.entity; +import io.ebean.Model; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -13,7 +14,7 @@ import lombok.Setter; @Table(name = "PROJECT_FEATURE") @Getter @Setter -public class ProjectFeatureJpaEntity { +public class ProjectFeatureJpaEntity extends Model { // Adaptación para Ebean @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/project/entity/ProjectJpaEntity.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/project/entity/ProjectJpaEntity.java index d1e6b0e..3feab55 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/project/entity/ProjectJpaEntity.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/project/entity/ProjectJpaEntity.java @@ -1,6 +1,7 @@ package com.pablotj.portfolio.infrastructure.persistence.project.entity; import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileJpaEntity; +import io.ebean.Model; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -18,8 +19,9 @@ import lombok.Setter; @Entity @Table(name = "PROJECT") -@Getter @Setter -public class ProjectJpaEntity { +@Getter +@Setter +public class ProjectJpaEntity extends Model { // Adaptación para Ebean @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -32,7 +34,7 @@ public class ProjectJpaEntity { @Column private String title; - @Column + @Column(columnDefinition = "text") private String description; @Column diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/project/entity/ProjectTechnologyJpaEntity.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/project/entity/ProjectTechnologyJpaEntity.java index 1305dde..f7298f4 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/project/entity/ProjectTechnologyJpaEntity.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/project/entity/ProjectTechnologyJpaEntity.java @@ -1,5 +1,6 @@ package com.pablotj.portfolio.infrastructure.persistence.project.entity; +import io.ebean.Model; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -13,7 +14,7 @@ import lombok.Setter; @Table(name = "PROJECT_FEATURE_TECHNOLOGY") @Getter @Setter -public class ProjectTechnologyJpaEntity { +public class ProjectTechnologyJpaEntity extends Model { // Adaptación para el motor Ebean @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/project/mapper/ProjectJpaMapper.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/project/mapper/ProjectJpaMapper.java index d9d6229..6e616cb 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/project/mapper/ProjectJpaMapper.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/project/mapper/ProjectJpaMapper.java @@ -9,7 +9,7 @@ import com.pablotj.portfolio.infrastructure.persistence.project.entity.ProjectTe import org.mapstruct.Mapper; import org.mapstruct.Mapping; -@Mapper(componentModel = "spring") +@Mapper public interface ProjectJpaMapper { @Mapping(target = "id", ignore = true) diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/project/repo/SpringDataProjectRepository.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/project/repo/SpringDataProjectRepository.java index 1a7dd6f..4695165 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/project/repo/SpringDataProjectRepository.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/project/repo/SpringDataProjectRepository.java @@ -1,12 +1,10 @@ package com.pablotj.portfolio.infrastructure.persistence.project.repo; -import com.pablotj.portfolio.infrastructure.persistence.experience.entity.ExperienceJpaEntity; import com.pablotj.portfolio.infrastructure.persistence.project.entity.ProjectJpaEntity; import java.util.List; import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; -public interface SpringDataProjectRepository extends JpaRepository { +public interface SpringDataProjectRepository { Optional findByProfileIdAndId(Long profileId, Long id); List findAllByProfileId(Long profileId); diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/skill/adapter/SkillRepositoryAdapter.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/skill/adapter/SkillRepositoryAdapter.java index e076387..72067e6 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/skill/adapter/SkillRepositoryAdapter.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/skill/adapter/SkillRepositoryAdapter.java @@ -5,36 +5,51 @@ import com.pablotj.portfolio.domain.skill.SkillGroupId; import com.pablotj.portfolio.domain.skill.port.SkillRepositoryPort; import com.pablotj.portfolio.infrastructure.persistence.skill.entity.SkillGroupJpaEntity; import com.pablotj.portfolio.infrastructure.persistence.skill.mapper.SkillJpaMapper; -import com.pablotj.portfolio.infrastructure.persistence.skill.repo.SpringDataSkillRepository; +import io.ebean.DB; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; import java.util.List; import java.util.Optional; -import org.springframework.stereotype.Repository; -@Repository +@Singleton public class SkillRepositoryAdapter implements SkillRepositoryPort { - private final SpringDataSkillRepository repo; private final SkillJpaMapper mapper; - public SkillRepositoryAdapter(SpringDataSkillRepository repo, SkillJpaMapper mapper) { - this.repo = repo; + @Inject + public SkillRepositoryAdapter(SkillJpaMapper mapper) { this.mapper = mapper; } @Override public SkillGroup save(SkillGroup p) { SkillGroupJpaEntity entity = mapper.toEntity(p); - SkillGroupJpaEntity saved = repo.save(entity); - return mapper.toDomain(saved); + // Ebean persiste el SkillGroup y sus SkillJpaEntity asociadas por cascada + DB.save(entity); + return mapper.toDomain(entity); } @Override public Optional findById(SkillGroupId id) { - return repo.findByProfileIdAndId(id.profileId(), id.skillGroupId()).map(mapper::toDomain); + SkillGroupJpaEntity entity = DB.find(SkillGroupJpaEntity.class) + .fetch("skills") // Carga inmediata de la lista de habilidades + .where() + .eq("profile.id", id.profileId()) + .eq("id", id.skillGroupId()) + .findOne(); + + return Optional.ofNullable(entity).map(mapper::toDomain); } @Override public List findAll(Long profileId) { - return repo.findAllByProfileId(profileId).stream().map(mapper::toDomain).toList(); + return DB.find(SkillGroupJpaEntity.class) + .fetch("skills") + .where() + .eq("profile.id", profileId) + .findList() + .stream() + .map(mapper::toDomain) + .toList(); } } \ No newline at end of file diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/skill/entity/SkillGroupJpaEntity.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/skill/entity/SkillGroupJpaEntity.java index dbd431e..66de9b0 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/skill/entity/SkillGroupJpaEntity.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/skill/entity/SkillGroupJpaEntity.java @@ -1,6 +1,7 @@ package com.pablotj.portfolio.infrastructure.persistence.skill.entity; import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileJpaEntity; +import io.ebean.Model; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -20,7 +21,7 @@ import lombok.Setter; @Table(name = "SKILL_GROUP") @Getter @Setter -public class SkillGroupJpaEntity { +public class SkillGroupJpaEntity extends Model { // Adaptación para Ebean @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/skill/entity/SkillJpaEntity.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/skill/entity/SkillJpaEntity.java index 0550e79..80c53f8 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/skill/entity/SkillJpaEntity.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/skill/entity/SkillJpaEntity.java @@ -1,5 +1,6 @@ package com.pablotj.portfolio.infrastructure.persistence.skill.entity; +import io.ebean.Model; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -13,7 +14,7 @@ import lombok.Setter; @Table(name = "SKILL") @Getter @Setter -public class SkillJpaEntity { +public class SkillJpaEntity extends Model { // Adaptación para Ebean @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/skill/mapper/SkillJpaMapper.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/skill/mapper/SkillJpaMapper.java index 72f893b..b781620 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/skill/mapper/SkillJpaMapper.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/skill/mapper/SkillJpaMapper.java @@ -7,7 +7,7 @@ import com.pablotj.portfolio.infrastructure.persistence.skill.entity.SkillJpaEnt import org.mapstruct.Mapper; import org.mapstruct.Mapping; -@Mapper(componentModel = "spring") +@Mapper public interface SkillJpaMapper { @Mapping(target = "id", ignore = true) diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/skill/repo/SpringDataSkillRepository.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/skill/repo/SpringDataSkillRepository.java index 9db0dc3..a395784 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/skill/repo/SpringDataSkillRepository.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/persistence/skill/repo/SpringDataSkillRepository.java @@ -1,12 +1,10 @@ package com.pablotj.portfolio.infrastructure.persistence.skill.repo; -import com.pablotj.portfolio.infrastructure.persistence.project.entity.ProjectJpaEntity; import com.pablotj.portfolio.infrastructure.persistence.skill.entity.SkillGroupJpaEntity; import java.util.List; import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; -public interface SpringDataSkillRepository extends JpaRepository { +public interface SpringDataSkillRepository { Optional findByProfileIdAndId(Long profileId, Long id); List findAllByProfileId(Long profileId); diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/api/ApiErrorController.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/api/ApiErrorController.java index 87df85f..09bf7b7 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/api/ApiErrorController.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/api/ApiErrorController.java @@ -1,30 +1,28 @@ package com.pablotj.portfolio.infrastructure.rest.api; -import org.springframework.boot.web.error.ErrorAttributeOptions; -import org.springframework.boot.web.servlet.error.ErrorAttributes; -import org.springframework.boot.web.servlet.error.ErrorController; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.context.request.WebRequest; - +import io.jooby.ErrorHandler; +import java.util.LinkedHashMap; import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -@Controller -public class ApiErrorController implements ErrorController { +public class ApiErrorController { - private final ErrorAttributes errorAttributes; + private static final Logger log = LoggerFactory.getLogger(ApiErrorController.class); - public ApiErrorController(ErrorAttributes errorAttributes) { - this.errorAttributes = errorAttributes; - } + public static ErrorHandler getHandler() { + return (ctx, cause, statusCode) -> { + log.error("Error en la API: {}", cause.getMessage(), cause); - @RequestMapping("/error") - public ResponseEntity> handleError(WebRequest webRequest) { - Map attributes = errorAttributes.getErrorAttributes(webRequest, - ErrorAttributeOptions.defaults()); - HttpStatus status = HttpStatus.valueOf((int) attributes.getOrDefault("status", 500)); - return new ResponseEntity<>(attributes, status); + Map errorAttributes = new LinkedHashMap<>(); + errorAttributes.put("timestamp", System.currentTimeMillis()); + errorAttributes.put("status", statusCode.value()); + errorAttributes.put("error", statusCode.reason()); + errorAttributes.put("message", cause.getMessage()); + errorAttributes.put("path", ctx.getRequestPath()); + + ctx.setResponseCode(statusCode) + .render(errorAttributes); + }; } } \ No newline at end of file diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/api/ApiRootController.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/api/ApiRootController.java index 4200d84..de8beb7 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/api/ApiRootController.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/api/ApiRootController.java @@ -1,39 +1,41 @@ package com.pablotj.portfolio.infrastructure.rest.api; +import io.jooby.annotation.GET; +import io.jooby.annotation.Path; +import jakarta.inject.Inject; +import jakarta.inject.Named; import java.util.List; import java.util.Map; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -@RestController -@RequestMapping +@Path("/") public class ApiRootController { - @Value("${info.app.version}") - private String appVersion; + private final String appVersion; - @GetMapping - public ResponseEntity> root() { - Map response = Map.of( + @Inject + public ApiRootController(@Named("info.app.version") String appVersion) { + // Jooby/Guice inyecta directamente el valor de la propiedad + // Esto es el equivalente a @Value de Spring + this.appVersion = appVersion; + } + + @GET + public Map root() { + return Map.of( "api", "Portfolio API", "version", appVersion, "doc", "/v3/api-docs", "swagger", "/swagger-ui", "endpoints", List.of( - Map.of("path", "/v1/homes", "description", "Manage projects"), - Map.of("path", "/v1/certifications", "description", "Manage projects"), + Map.of("path", "/v1/homes", "description", "Manage home details"), + Map.of("path", "/v1/certifications", "description", "Manage certifications"), Map.of("path", "/v1/projects", "description", "Manage projects"), - Map.of("path", "/v1/contacts", "description", "Manage projects"), - Map.of("path", "/v1/educations", "description", "Manage projects"), - Map.of("path", "/v1/experiences", "description", "Manage projects"), - Map.of("path", "/v1/projects", "description", "Manage projects"), - Map.of("path", "/v1/technologies", "description", "ProfileSocialLink entries") + Map.of("path", "/v1/contacts", "description", "Manage contact info"), + Map.of("path", "/v1/educations", "description", "Manage education"), + Map.of("path", "/v1/experiences", "description", "Manage experience"), + Map.of("path", "/v1/skills", "description", "Manage skills"), + Map.of("path", "/v1/technologies", "description", "Manage technologies") ) ); - - return ResponseEntity.ok(response); } -} +} \ No newline at end of file diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/certification/CertificationController.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/certification/CertificationController.java index ebe9de7..f8f5dfd 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/certification/CertificationController.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/certification/CertificationController.java @@ -5,54 +5,53 @@ import com.pablotj.portfolio.application.certification.GetCertificationUseCase; import com.pablotj.portfolio.infrastructure.rest.certification.dto.CertificationDto; import com.pablotj.portfolio.infrastructure.rest.certification.dto.CreateCertificationRequest; import com.pablotj.portfolio.infrastructure.rest.certification.mapper.CertificationRestMapper; +import io.jooby.annotation.GET; +import io.jooby.annotation.POST; +import io.jooby.annotation.Path; +import io.jooby.annotation.PathParam; +import jakarta.inject.Inject; import jakarta.validation.Valid; -import java.net.URI; import java.util.List; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.mapstruct.factory.Mappers; -@RestController -@RequestMapping("/v1/profiles/{profileId}/certifications") +@Path("/v1/profiles/{profileId}/certifications") public class CertificationController { + private static final CertificationRestMapper MAPPER = Mappers.getMapper(CertificationRestMapper.class); private final CreateCertificationUseCase createUC; private final GetCertificationUseCase getUC; - private final CertificationRestMapper mapper; - public CertificationController(CreateCertificationUseCase createUC, GetCertificationUseCase getUC, CertificationRestMapper mapper) { + @Inject + public CertificationController(CreateCertificationUseCase createUC, GetCertificationUseCase getUC) { this.createUC = createUC; this.getUC = getUC; - this.mapper = mapper; } - @GetMapping - public List all(@PathVariable Long profileId) { - return getUC.all(profileId).stream().map(mapper::toDto).toList(); + @GET + public List all(@PathParam Long profileId) { + return getUC.all(profileId).stream() + .map(MAPPER::toDto) + .toList(); } - @GetMapping("/{id}") - public ResponseEntity byId(@PathVariable Long profileId, @PathVariable Long id) { + @GET("/{id}") + public CertificationDto byId(@PathParam Long profileId, @PathParam Long id) { + // En Jooby, si devuelves un Optional vacío, automáticamente lanza un 404 return getUC.byId(profileId, id) - .map(mapper::toDto) - .map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); + .map(MAPPER::toDto) + .orElseThrow(() -> new io.jooby.exception.NotFoundException("Certification not found")); } - @PostMapping - public ResponseEntity create(@PathVariable Long profileId, @Valid @RequestBody CreateCertificationRequest request) { + @POST + public CertificationDto create(@PathParam Long profileId, @Valid CreateCertificationRequest request) { var cmd = new CreateCertificationUseCase.Command( request.name(), request.issuer(), request.date(), request.credentialId() ); + var created = createUC.handle(profileId, cmd); - var body = mapper.toDto(created); - return ResponseEntity.created(URI.create("/api/certifications/" + body.id())).body(body); + return MAPPER.toDto(created); } } \ No newline at end of file diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/certification/mapper/CertificationRestMapper.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/certification/mapper/CertificationRestMapper.java index 9d52cfc..ea979ca 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/certification/mapper/CertificationRestMapper.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/certification/mapper/CertificationRestMapper.java @@ -5,7 +5,7 @@ import com.pablotj.portfolio.infrastructure.rest.certification.dto.Certification import org.mapstruct.Mapper; import org.mapstruct.Mapping; -@Mapper(componentModel = "spring") +@Mapper public interface CertificationRestMapper { @Mapping(target = "id", source = "id.certificationId") diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/education/EducationController.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/education/EducationController.java index 7c2a4fd..209a9ca 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/education/EducationController.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/education/EducationController.java @@ -5,46 +5,45 @@ import com.pablotj.portfolio.application.education.GetEducationUseCase; import com.pablotj.portfolio.infrastructure.rest.education.dto.CreateEducationRequest; import com.pablotj.portfolio.infrastructure.rest.education.dto.EducationDto; import com.pablotj.portfolio.infrastructure.rest.education.mapper.EducationRestMapper; +import io.jooby.annotation.GET; +import io.jooby.annotation.POST; +import io.jooby.annotation.Path; +import io.jooby.annotation.PathParam; +import io.jooby.exception.NotFoundException; +import jakarta.inject.Inject; import jakarta.validation.Valid; -import java.net.URI; import java.util.List; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.mapstruct.factory.Mappers; -@RestController -@RequestMapping("/v1/profiles/{profileId}/education") +@Path("/v1/profiles/{profileId}/education") public class EducationController { + private static final EducationRestMapper MAPPER = Mappers.getMapper(EducationRestMapper.class); private final CreateEducationUseCase createUC; private final GetEducationUseCase getUC; - private final EducationRestMapper mapper; - public EducationController(CreateEducationUseCase createUC, GetEducationUseCase getUC, EducationRestMapper mapper) { + @Inject + public EducationController(CreateEducationUseCase createUC, GetEducationUseCase getUC) { this.createUC = createUC; this.getUC = getUC; - this.mapper = mapper; } - @GetMapping - public List all(@PathVariable Long profileId) { - return getUC.all(profileId).stream().map(mapper::toDto).toList(); + @GET + public List all(@PathParam Long profileId) { + return getUC.all(profileId).stream() + .map(MAPPER::toDto) + .toList(); } - @GetMapping("/{id}") - public ResponseEntity byId(@PathVariable Long profileId, @PathVariable Long id) { + @GET("/{id}") + public EducationDto byId(@PathParam Long profileId, @PathParam Long id) { return getUC.byId(profileId, id) - .map(mapper::toDto) - .map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); + .map(MAPPER::toDto) + .orElseThrow(() -> new NotFoundException("Education record not found")); } - @PostMapping - public ResponseEntity create(@PathVariable Long profileId, @Valid @RequestBody CreateEducationRequest request) { + @POST + public EducationDto create(@PathParam Long profileId, @Valid CreateEducationRequest request) { var cmd = new CreateEducationUseCase.Command( request.institution(), request.degree(), @@ -52,8 +51,8 @@ public class EducationController { request.grade(), request.description() ); + var created = createUC.handle(profileId, cmd); - var body = mapper.toDto(created); - return ResponseEntity.created(URI.create("/api/educations/" + body.id())).body(body); + return MAPPER.toDto(created); } } \ No newline at end of file diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/education/mapper/EducationRestMapper.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/education/mapper/EducationRestMapper.java index 6358297..2e78ec5 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/education/mapper/EducationRestMapper.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/education/mapper/EducationRestMapper.java @@ -5,7 +5,7 @@ import com.pablotj.portfolio.infrastructure.rest.education.dto.EducationDto; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -@Mapper(componentModel = "spring") +@Mapper public interface EducationRestMapper { @Mapping(target = "id", source = "id.educationId") diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/experience/ExperienceController.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/experience/ExperienceController.java index aceac1b..0c4fb4e 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/experience/ExperienceController.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/experience/ExperienceController.java @@ -5,46 +5,45 @@ import com.pablotj.portfolio.application.experience.GetExperienceUseCase; import com.pablotj.portfolio.infrastructure.rest.experience.dto.CreateExperienceRequest; import com.pablotj.portfolio.infrastructure.rest.experience.dto.ExperienceDto; import com.pablotj.portfolio.infrastructure.rest.experience.mapper.ExperienceRestMapper; +import io.jooby.annotation.GET; +import io.jooby.annotation.POST; +import io.jooby.annotation.Path; +import io.jooby.annotation.PathParam; +import io.jooby.exception.NotFoundException; +import jakarta.inject.Inject; import jakarta.validation.Valid; -import java.net.URI; import java.util.List; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.mapstruct.factory.Mappers; -@RestController -@RequestMapping("/v1/profiles/{profileId}/experience") +@Path("/v1/profiles/{profileId}/experience") public class ExperienceController { + private static final ExperienceRestMapper MAPPER = Mappers.getMapper(ExperienceRestMapper.class); private final CreateExperienceUseCase createUC; private final GetExperienceUseCase getUC; - private final ExperienceRestMapper mapper; - public ExperienceController(CreateExperienceUseCase createUC, GetExperienceUseCase getUC, ExperienceRestMapper mapper) { + @Inject + public ExperienceController(CreateExperienceUseCase createUC, GetExperienceUseCase getUC) { this.createUC = createUC; this.getUC = getUC; - this.mapper = mapper; } - @GetMapping - public List all(@PathVariable Long profileId) { - return getUC.all(profileId).stream().map(mapper::toDto).toList(); + @GET + public List all(@PathParam Long profileId) { + return getUC.all(profileId).stream() + .map(MAPPER::toDto) + .toList(); } - @GetMapping("/{id}") - public ResponseEntity byId(@PathVariable Long profileId, @PathVariable Long id) { + @GET("/{id}") + public ExperienceDto byId(@PathParam Long profileId, @PathParam Long id) { return getUC.byId(profileId, id) - .map(mapper::toDto) - .map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); + .map(MAPPER::toDto) + .orElseThrow(() -> new NotFoundException("Experience not found")); } - @PostMapping - public ResponseEntity create(@PathVariable Long profileId, @Valid @RequestBody CreateExperienceRequest request) { + @POST + public ExperienceDto create(@PathParam Long profileId, @Valid CreateExperienceRequest request) { var cmd = new CreateExperienceUseCase.Command( request.company(), request.position(), @@ -54,8 +53,8 @@ public class ExperienceController { request.technologies(), request.achievements() ); + var created = createUC.handle(profileId, cmd); - var body = mapper.toDto(created); - return ResponseEntity.created(URI.create("/api/experiences/" + body.id())).body(body); + return MAPPER.toDto(created); } } \ No newline at end of file diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/experience/dto/CreateExperienceRequest.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/experience/dto/CreateExperienceRequest.java index 89404af..b40bef1 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/experience/dto/CreateExperienceRequest.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/experience/dto/CreateExperienceRequest.java @@ -11,4 +11,5 @@ public record CreateExperienceRequest( String description, List technologies, List achievements -) {} \ No newline at end of file +) { +} \ No newline at end of file diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/experience/dto/ExperienceDto.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/experience/dto/ExperienceDto.java index 81450c1..3f16767 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/experience/dto/ExperienceDto.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/experience/dto/ExperienceDto.java @@ -12,4 +12,5 @@ public record ExperienceDto( String description, List technologies, List achievements -) {} \ No newline at end of file +) { +} \ No newline at end of file diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/experience/mapper/ExperienceRestMapper.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/experience/mapper/ExperienceRestMapper.java index 3d67b24..068bf39 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/experience/mapper/ExperienceRestMapper.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/experience/mapper/ExperienceRestMapper.java @@ -8,7 +8,7 @@ import java.util.List; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -@Mapper(componentModel = "spring") +@Mapper public interface ExperienceRestMapper { @Mapping(target = "id", source = "id.experienceId") diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/profile/ProfileController.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/profile/ProfileController.java index c7de454..4eecf57 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/profile/ProfileController.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/profile/ProfileController.java @@ -5,47 +5,45 @@ import com.pablotj.portfolio.application.profile.GetProfileUseCase; import com.pablotj.portfolio.infrastructure.rest.profile.dto.ProfileCreateRequest; import com.pablotj.portfolio.infrastructure.rest.profile.dto.ProfileDto; import com.pablotj.portfolio.infrastructure.rest.profile.mapper.ProfileRestMapper; +import io.jooby.annotation.GET; +import io.jooby.annotation.POST; +import io.jooby.annotation.Path; +import io.jooby.annotation.PathParam; +import io.jooby.exception.NotFoundException; +import jakarta.inject.Inject; import jakarta.validation.Valid; -import java.net.URI; import java.util.List; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.mapstruct.factory.Mappers; -@RestController -@RequestMapping("/v1/profiles") +@Path("/v1/profiles") public class ProfileController { + private static final ProfileRestMapper MAPPER = Mappers.getMapper(ProfileRestMapper.class); private final CreateProfileUseCase createUC; private final GetProfileUseCase getUC; - private final ProfileRestMapper mapper; - public ProfileController(CreateProfileUseCase createUC, GetProfileUseCase getUC, ProfileRestMapper mapper) { + @Inject + public ProfileController(CreateProfileUseCase createUC, GetProfileUseCase getUC) { this.createUC = createUC; this.getUC = getUC; - this.mapper = mapper; } - - @GetMapping + @GET public List all() { - return getUC.all().stream().map(mapper::toDto).toList(); + return getUC.all().stream() + .map(MAPPER::toDto) + .toList(); } - @GetMapping("/{slug}") - public ResponseEntity byId(@PathVariable String slug) { + @GET("/{slug}") + public ProfileDto byId(@PathParam String slug) { return getUC.bySlug(slug) - .map(mapper::toDto) - .map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); + .map(MAPPER::toDto) + .orElseThrow(() -> new NotFoundException("Profile not found")); } - @PostMapping - public ResponseEntity create(@Valid @RequestBody ProfileCreateRequest request) { + @POST + public ProfileDto create(@Valid ProfileCreateRequest request) { var cmd = new CreateProfileUseCase.Command( request.slug(), request.name(), @@ -56,10 +54,12 @@ public class ProfileController { request.location(), request.avatar(), request.bio(), - request.social().stream().map(l -> new CreateProfileUseCase.Link(l.platform(), l.url())).toList() + request.social().stream() + .map(l -> new CreateProfileUseCase.Link(l.platform(), l.url())) + .toList() ); + var created = createUC.handle(cmd); - var body = mapper.toDto(created); - return ResponseEntity.created(URI.create("/api/homes/" + body.id())).body(body); + return MAPPER.toDto(created); } } \ No newline at end of file diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/profile/mapper/ProfileRestMapper.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/profile/mapper/ProfileRestMapper.java index 8a81b29..c6138f6 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/profile/mapper/ProfileRestMapper.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/profile/mapper/ProfileRestMapper.java @@ -8,7 +8,7 @@ import java.util.List; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -@Mapper(componentModel = "spring") +@Mapper public interface ProfileRestMapper { @Mapping(target = "id", source = "id.id") diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/project/ProjectController.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/project/ProjectController.java index e22f0af..245cde0 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/project/ProjectController.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/project/ProjectController.java @@ -5,46 +5,45 @@ import com.pablotj.portfolio.application.project.GetProjectUseCase; import com.pablotj.portfolio.infrastructure.rest.project.dto.CreateProjectRequest; import com.pablotj.portfolio.infrastructure.rest.project.dto.ProjectDto; import com.pablotj.portfolio.infrastructure.rest.project.mapper.ProjectRestMapper; +import io.jooby.annotation.GET; +import io.jooby.annotation.POST; +import io.jooby.annotation.Path; +import io.jooby.annotation.PathParam; +import io.jooby.exception.NotFoundException; +import jakarta.inject.Inject; import jakarta.validation.Valid; -import java.net.URI; import java.util.List; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.mapstruct.factory.Mappers; -@RestController -@RequestMapping("/v1/profiles/{profileId}/projects") +@Path("/v1/profiles/{profileId}/projects") public class ProjectController { + private static final ProjectRestMapper MAPPER = Mappers.getMapper(ProjectRestMapper.class); private final CreateProjectUseCase createUC; private final GetProjectUseCase getUC; - private final ProjectRestMapper mapper; - public ProjectController(CreateProjectUseCase createUC, GetProjectUseCase getUC, ProjectRestMapper mapper) { + @Inject + public ProjectController(CreateProjectUseCase createUC, GetProjectUseCase getUC) { this.createUC = createUC; this.getUC = getUC; - this.mapper = mapper; } - @GetMapping - public List all(@PathVariable Long profileId) { - return getUC.all(profileId).stream().map(mapper::toDto).toList(); + @GET + public List all(@PathParam Long profileId) { + return getUC.all(profileId).stream() + .map(MAPPER::toDto) + .toList(); } - @GetMapping("/{id}") - public ResponseEntity byId(@PathVariable Long profileId, @PathVariable Long id) { + @GET("/{id}") + public ProjectDto byId(@PathParam Long profileId, @PathParam Long id) { return getUC.byId(profileId, id) - .map(mapper::toDto) - .map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); + .map(MAPPER::toDto) + .orElseThrow(() -> new NotFoundException("Project not found")); } - @PostMapping - public ResponseEntity create(@PathVariable Long profileId, @Valid @RequestBody CreateProjectRequest request) { + @POST + public ProjectDto create(@PathParam Long profileId, @Valid CreateProjectRequest request) { var cmd = new CreateProjectUseCase.Command( request.title(), request.description(), @@ -54,8 +53,8 @@ public class ProjectController { request.demo(), request.repository() ); + var created = createUC.handle(profileId, cmd); - var body = mapper.toDto(created); - return ResponseEntity.created(URI.create("/api/projects/" + body.id())).body(body); + return MAPPER.toDto(created); } } \ No newline at end of file diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/project/dto/CreateProjectRequest.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/project/dto/CreateProjectRequest.java index 2553b4e..85ce69b 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/project/dto/CreateProjectRequest.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/project/dto/CreateProjectRequest.java @@ -10,4 +10,5 @@ public record CreateProjectRequest( List features, String demo, String repository -) {} \ No newline at end of file +) { +} \ No newline at end of file diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/project/mapper/ProjectRestMapper.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/project/mapper/ProjectRestMapper.java index b581c8d..565308f 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/project/mapper/ProjectRestMapper.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/project/mapper/ProjectRestMapper.java @@ -8,7 +8,7 @@ import java.util.List; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -@Mapper(componentModel = "spring") +@Mapper public interface ProjectRestMapper { @Mapping(target = "id", source = "id.projectId") diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/skill/SkillController.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/skill/SkillController.java index 7a9d0c4..b58e0a3 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/skill/SkillController.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/skill/SkillController.java @@ -5,53 +5,54 @@ import com.pablotj.portfolio.application.skill.GetSkillUseCase; import com.pablotj.portfolio.infrastructure.rest.skill.dto.CreateSkillGroupRequest; import com.pablotj.portfolio.infrastructure.rest.skill.dto.SkillGroupDto; import com.pablotj.portfolio.infrastructure.rest.skill.mapper.SkillRestMapper; +import io.jooby.annotation.GET; +import io.jooby.annotation.POST; +import io.jooby.annotation.Path; +import io.jooby.annotation.PathParam; +import io.jooby.exception.NotFoundException; +import jakarta.inject.Inject; import jakarta.validation.Valid; -import java.net.URI; import java.util.List; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.mapstruct.factory.Mappers; -@RestController -@RequestMapping("/v1/profiles/{profileId}/skills") +@Path("/v1/profiles/{profileId}/skills") public class SkillController { + private static final SkillRestMapper MAPPER = Mappers.getMapper(SkillRestMapper.class); private final CreateSkillUseCase createUC; private final GetSkillUseCase getUC; - private final SkillRestMapper mapper; - public SkillController(CreateSkillUseCase createUC, GetSkillUseCase getUC, SkillRestMapper mapper) { + @Inject + public SkillController(CreateSkillUseCase createUC, GetSkillUseCase getUC) { this.createUC = createUC; this.getUC = getUC; - this.mapper = mapper; } - @GetMapping - public List all(@PathVariable Long profileId) { - return getUC.all(profileId).stream().map(mapper::toDto).toList(); + @GET + public List all(@PathParam Long profileId) { + return getUC.all(profileId).stream() + .map(MAPPER::toDto) + .toList(); } - @GetMapping("/{id}") - public ResponseEntity byId(@PathVariable Long profileId, @PathVariable Long id) { + @GET("/{id}") + public SkillGroupDto byId(@PathParam Long profileId, @PathParam Long id) { return getUC.byId(profileId, id) - .map(mapper::toDto) - .map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); + .map(MAPPER::toDto) + .orElseThrow(() -> new NotFoundException("Skill group not found")); } - @PostMapping - public ResponseEntity create(@PathVariable Long profileId, @Valid @RequestBody CreateSkillGroupRequest request) { + @POST + public SkillGroupDto create(@PathParam Long profileId, @Valid CreateSkillGroupRequest request) { var cmd = new CreateSkillUseCase.CommandGroup( request.name(), request.icon(), - request.skills().stream().map(s -> new CreateSkillUseCase.CommandSkill(s.name(), s.level(), s.years())).toList() + request.skills().stream() + .map(s -> new CreateSkillUseCase.CommandSkill(s.name(), s.level(), s.years())) + .toList() ); + var created = createUC.handle(profileId, cmd); - var body = mapper.toDto(created); - return ResponseEntity.created(URI.create("/api/skills/" + body.id())).body(body); + return MAPPER.toDto(created); } } \ No newline at end of file diff --git a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/skill/mapper/SkillRestMapper.java b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/skill/mapper/SkillRestMapper.java index 7ac3540..740e191 100644 --- a/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/skill/mapper/SkillRestMapper.java +++ b/infrastructure/src/main/java/com/pablotj/portfolio/infrastructure/rest/skill/mapper/SkillRestMapper.java @@ -7,7 +7,7 @@ import com.pablotj.portfolio.infrastructure.rest.skill.dto.SkillGroupDto; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -@Mapper(componentModel = "spring") +@Mapper public interface SkillRestMapper { @Mapping(target = "id", source = "id.skillGroupId") diff --git a/infrastructure/src/main/resources/db/migration/V1__Initial_Setup.sql b/infrastructure/src/main/resources/db/migration/V1__Initial_Setup.sql new file mode 100644 index 0000000..2be014b --- /dev/null +++ b/infrastructure/src/main/resources/db/migration/V1__Initial_Setup.sql @@ -0,0 +1,168 @@ +create table personal +( + id bigint not null + primary key, + avatar varchar(255), + bio varchar(255), + email varchar(255), + location varchar(255), + name varchar(255), + phone varchar(255), + subtitle varchar(255), + title varchar(255) +); + +create table personal_social_link +( + id bigint not null + primary key, + platform varchar(255), + url varchar(255), + personal_id bigint not null + constraint fkfh1pbfvvg3palcr1yip6jffik + references personal +); + +create table profile +( + id bigint not null + primary key, + avatar varchar(255), + bio varchar(4000), + email varchar(255), + location varchar(255), + name varchar(255), + phone varchar(255), + slug varchar(255), + subtitle varchar(255), + title varchar(255) +); + +create table certification +( + id bigint not null + primary key, + credential_id varchar(255), + date varchar(255), + issuer varchar(255), + name varchar(255), + profile_id bigint not null + constraint fko6ve4ysx15lc2vcjt84sal1yc + references profile +); + +create table education +( + id bigint not null + primary key, + degree varchar(255), + description varchar, + grade varchar(255), + institution varchar(255), + period varchar(255), + profile_id bigint not null + constraint fkelocxwwcyf5acj85hgke1c0fl + references profile +); + +create table experience +( + id bigint not null + primary key, + company varchar(255), + description varchar, + location varchar(255), + period varchar(255), + position varchar(255), + profile_id bigint not null + constraint fkhlkosu9yvtv1ptp01x4tfh9ut + references profile +); + +create table experience_achievement +( + id bigint not null + primary key, + description varchar, + experience_id bigint + constraint fk94xrk6stofkung8skwplo29nd + references experience +); + +create table experience_skill +( + id bigint not null + primary key, + name varchar(255), + experience_id bigint + constraint fkpr3jdfjjlaubuayoafpwyx2al + references experience +); + +create table profile_social_link +( + id bigint not null + primary key, + platform varchar(255), + url varchar(255), + profile_id bigint not null + constraint fkqfxt1g0xm211i7qjnlcuqfes9 + references profile +); + +create table project +( + id bigint not null + primary key, + demo varchar(255), + description varchar(255), + image varchar(255), + repository varchar(255), + title varchar(255), + profile_id bigint not null + constraint fk2i9umkiuu36osx3afamsxq39h + references profile +); + +create table project_feature +( + id bigint not null + primary key, + name varchar(255), + project_id bigint + constraint fkdifppyvrfito5in15ox4db0up + references project +); + +create table project_feature_technology +( + id bigint not null + primary key, + name varchar(255), + project_id bigint + constraint fk15krsajtovetpg5vsaqj3icwf + references project +); + +create table skill_group +( + id bigint not null + primary key, + icon varchar(255), + name varchar(255), + profile_id bigint not null + constraint fko26hcvag49ctl3ciddsqm6mn1 + references profile +); + +create table skill +( + id bigint not null + primary key, + level integer, + name varchar(255), + years integer, + skill_id bigint + constraint fki819li5g5cp5qbsyenhr3kmef + references skill_group +); diff --git a/infrastructure/src/main/resources/ddl.sql b/infrastructure/src/main/resources/ddl.sql deleted file mode 100644 index 48fe643..0000000 --- a/infrastructure/src/main/resources/ddl.sql +++ /dev/null @@ -1,140 +0,0 @@ -create table certification -( - id bigint generated by default as identity, - credential_id varchar(255), - date varchar(255), - issuer varchar(255), - name varchar(255), - primary key (id) -); - -create table education -( - id bigint generated by default as identity, - degree varchar(255), - description text, - grade varchar(255), - institution varchar(255), - period varchar(255), - primary key (id) -); - -create table experience -( - id bigint generated by default as identity, - company varchar(255), - description text, - location varchar(255), - period varchar(255), - position varchar(255), - primary key (id) -); - -create table experience_achievement -( - id bigint generated by default as identity, - description text, - experience_id bigint, - primary key (id) -); - -create table experience_skill -( - id bigint generated by default as identity, - name varchar(255), - experience_id bigint, - primary key (id) -); - -create table personal -( - id bigint generated by default as identity, - avatar varchar(255), - bio varchar(255), - email varchar(255), - location varchar(255), - name varchar(255), - phone varchar(255), - subtitle varchar(255), - title varchar(255), - primary key (id) -); - -create table personal_social_link -( - id bigint generated by default as identity, - platform varchar(255), - url varchar(255), - personal_id bigint not null, - primary key (id) -); - -create table project -( - id bigint generated by default as identity, - demo varchar(255), - description varchar(255), - image varchar(255), - repository varchar(255), - title varchar(255), - primary key (id) -); - -create table project_feature -( - id bigint generated by default as identity, - name varchar(255), - project_id bigint, - primary key (id) -); - -create table project_feature_technology -( - id bigint generated by default as identity, - name varchar(255), - project_id bigint, - primary key (id) -); - -create table skill -( - id bigint generated by default as identity, - level integer, - name varchar(255), - years integer, - skill_id bigint, - primary key (id) -); - -create table skill_group -( - id bigint generated by default as identity, - icon varchar(255), - name varchar(255), - primary key (id) -); - -alter table if exists experience_achievement - add constraint FK94xrk6stofkung8skwplo29nd - foreign key (experience_id) - references experience; - -alter table if exists experience_skill - add constraint FKpr3jdfjjlaubuayoafpwyx2al - foreign key (experience_id) - references experience; - -alter table if exists project_feature - add constraint FKdifppyvrfito5in15ox4db0up - foreign key (project_id) - references project; - -alter table if exists project_feature_technology - add constraint FK15krsajtovetpg5vsaqj3icwf - foreign key (project_id) - references project; - -alter table if exists skill - add constraint FKi819li5g5cp5qbsyenhr3kmef - foreign key (skill_id) - references skill_group; \ No newline at end of file diff --git a/pom.xml b/pom.xml index 9253a6b..be571b7 100644 --- a/pom.xml +++ b/pom.xml @@ -16,30 +16,35 @@ bootstrap - - org.springframework.boot - spring-boot-starter-parent - 3.3.2 - - - 21 + UTF-8 + 3.2.0 1.5.5.Final - 2.6.0 + 1.18.32 + 2.17.2 + + io.jooby + jooby-bom + ${jooby.version} + pom + import + + org.mapstruct mapstruct ${mapstruct.version} - org.mapstruct - mapstruct-processor - ${mapstruct.version} + io.jooby + jooby-apt + ${jooby.version} + provided @@ -50,14 +55,20 @@ org.apache.maven.plugins maven-compiler-plugin + 3.13.0 ${java.version} ${java.version} + + io.jooby + jooby-apt + ${jooby.version} + org.projectlombok lombok - 1.18.32 + ${lombok.version} org.mapstruct