refactor: replace Spring Boot with Jooby framework

- Remove Spring Boot dependencies and annotations.
- Implement Jooby MVC controllers and Guice dependency injection.
- Migrate persistence layer to Ebean ORM.
- Configure Flyway migrations and ApiErrorController.
- Update application configuration to HOCON format.
This commit is contained in:
2026-03-02 16:38:11 +01:00
parent 83070ccbda
commit cb4e0c2b78
70 changed files with 1190 additions and 862 deletions

View File

@@ -1,5 +1,3 @@
SPRING_PROFILES_ACTIVE=dev
APP_ALLOWED_ORIGINS='http://127.0.0.1:3000, http://localhost:3000'
DB_NAME=EXAMPLE_DB

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@
target
.env
Icon?
.docker

View File

@@ -26,11 +26,14 @@ 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
RUN mkdir -p /app/tmp && chown app:app /app/tmp && chmod 777 /app/tmp
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

31
HELP.md
View File

@@ -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 `<license>` and `<developers>` 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.

View File

@@ -12,9 +12,18 @@
<artifactId>domain</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>3.0.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.36</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@@ -1,12 +1,22 @@
<project>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.pablotj</groupId>
<artifactId>portfolio-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>bootstrap</artifactId>
<properties>
<jooby.version>3.6.1</jooby.version>
<postgresql.version>42.7.2</postgresql.version>
<testcontainers.version>1.19.7</testcontainers.version>
</properties>
<dependencies>
<dependency>
<groupId>com.pablotj</groupId>
@@ -14,40 +24,90 @@
<version>${project.version}</version>
</dependency>
<!-- Drivers DB -->
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-netty</artifactId>
<version>${jooby.version}</version>
</dependency>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-jackson</artifactId>
<version>${jooby.version}</version>
</dependency>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-jackson</artifactId>
<version>3.6.1</version>
</dependency>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-hibernate-validator</artifactId>
<version>3.6.1</version>
</dependency>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-openapi</artifactId>
<version>${jooby.version}</version>
</dependency>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-swagger-ui</artifactId>
<version>${jooby.version}</version>
</dependency>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-logback</artifactId>
<version>${jooby.version}</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
<version>${postgresql.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
<version>2.2.224</version>
</dependency>
<!-- Swagger/OpenAPI -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
<groupId>io.jooby</groupId>
<artifactId>jooby-hikari</artifactId>
<version>${jooby.version}</version>
</dependency>
<!-- Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<groupId>io.jooby</groupId>
<artifactId>jooby-guice</artifactId>
<version>${jooby.version}</version>
</dependency>
<!-- Test -->
<!--
install(new DotenvModule());
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<groupId>io.jooby</groupId>
<artifactId>jooby-dotenv</artifactId>
<version>${jooby.version}</version>
</dependency>-->
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-flyway</artifactId>
<version>${jooby.version}</version>
</dependency>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-test</artifactId>
<version>${jooby.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
@@ -55,15 +115,74 @@
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<groupId>io.jooby</groupId>
<artifactId>jooby-maven-plugin</artifactId>
<version>${jooby.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
<goal>openapi</goal> <!-- Este goal genera openapi.json/yaml -->
</goals>
</execution>
</executions>
<configuration>
<mainClass>com.pablotj.portfolio.bootstrap.PortfolioApplication</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<id>jooby-shade</id>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<!-- 1. GENÉRICO: Elimina TODO en META-INF excepto servicios y archivos ebean -->
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
<exclude>META-INF/MANIFEST.MF</exclude>
<exclude>META-INF/DEPENDENCIES*</exclude>
<exclude>META-INF/LICENSE*</exclude>
<exclude>META-INF/NOTICE*</exclude>
<exclude>META-INF/*.txt</exclude>
<exclude>META-INF/*.md</exclude>
<!-- 2. ESPECÍFICO: Limpia los avisos de Netty, JSON Schema y Módulos -->
<exclude>META-INF/io.netty.versions.properties</exclude>
<exclude>draftv3/schema</exclude>
<exclude>draftv4/schema</exclude>
<exclude>**/module-info.class</exclude>
<exclude>META-INF/versions/**</exclude>
<!-- 3. GENÉRICO: Archivos de licencia sueltos en la raíz -->
<exclude>LICENSE*</exclude>
<exclude>NOTICE*</exclude>
</excludes>
</filter>
</filters>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/ebean.mf</resource>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.pablotj.portfolio.bootstrap.PortfolioApplication</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

View File

@@ -1,16 +1,80 @@
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.OpenAPIModule;
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 {
public static void main(String[] args) {
SpringApplication.run(PortfolioApplication.class, args);
{
// 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);
});
install(new OpenAPIModule().swaggerUI("/docs"));
}
public static void main(final String[] args) {
runApp(args, PortfolioApplication::new);
}
}

View File

@@ -3,19 +3,32 @@ 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.CertificationEntityMapper;
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) {
CertificationEntityMapper mapper = Mappers.getMapper(CertificationEntityMapper.class);
app.getServices().put(CertificationEntityMapper.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));
}
}

View File

@@ -3,19 +3,29 @@ 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.education.adapter.EducationRepositoryAdapter;
import com.pablotj.portfolio.infrastructure.persistence.education.mapper.EducationEntityMapper;
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) {
EducationEntityMapper mapper = Mappers.getMapper(EducationEntityMapper.class);
app.getServices().put(EducationEntityMapper.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));
}
}

View File

@@ -3,19 +3,27 @@ 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.experience.adapter.ExperienceRepositoryAdapter;
import com.pablotj.portfolio.infrastructure.persistence.experience.mapper.ExperienceEntityMapper;
import io.jooby.Extension;
import io.jooby.Jooby;
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
ExperienceEntityMapper mapper = Mappers.getMapper(ExperienceEntityMapper.class);
app.getServices().put(ExperienceEntityMapper.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));
}
}

View File

@@ -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.ProfileEntityMapper;
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)
ProfileEntityMapper mapper = Mappers.getMapper(ProfileEntityMapper.class);
app.getServices().put(ProfileEntityMapper.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));
}
}

View File

@@ -3,19 +3,26 @@ 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.project.adapter.ProjectRepositoryAdapter;
import com.pablotj.portfolio.infrastructure.persistence.project.mapper.ProjectEntityMapper;
import io.jooby.Extension;
import io.jooby.Jooby;
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);
}
ProjectEntityMapper mapper = Mappers.getMapper(ProjectEntityMapper.class);
app.getServices().put(ProjectEntityMapper.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));
}
}

View File

@@ -3,19 +3,28 @@ 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.skill.adapter.SkillRepositoryAdapter;
import com.pablotj.portfolio.infrastructure.persistence.skill.mapper.SkillMapper;
import io.jooby.Extension;
import io.jooby.Jooby;
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);
}
SkillMapper mapper = Mappers.getMapper(SkillMapper.class);
app.getServices().put(SkillMapper.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));
}
}

View File

@@ -0,0 +1,61 @@
# 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 {
ddl {
generate = false
run = false
}
# Mostrar SQL en consola
debug = true
}
# Jackson
jackson {
indent_output = true
}

View File

@@ -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

View File

@@ -10,11 +10,14 @@
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<version>1.18.36</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>3.0.2</version>
</dependency>
</dependencies>
</project>

View File

@@ -1,12 +1,23 @@
<project>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.pablotj</groupId>
<artifactId>portfolio-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>infrastructure</artifactId>
<properties>
<jooby.version>3.6.1</jooby.version>
<ebean.version>15.8.0</ebean.version>
<lombok.version>1.18.36</lombok.version>
<mapstruct.version>1.6.3</mapstruct.version>
</properties>
<dependencies>
<dependency>
<groupId>com.pablotj</groupId>
@@ -19,36 +30,71 @@
<version>${project.version}</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<groupId>io.jooby</groupId>
<artifactId>jooby</artifactId>
<version>${jooby.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<groupId>io.jooby</groupId>
<artifactId>jooby-ebean</artifactId>
<version>${jooby.version}</version>
</dependency>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-hibernate-validator</artifactId>
<version>3.6.1</version>
</dependency>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-jackson</artifactId>
<version>${jooby.version}</version>
</dependency>
<!-- MapStruct -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>3.0.2</version>
</dependency>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>3.0.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.ebean</groupId>
<artifactId>ebean-maven-plugin</artifactId>
<version>${ebean.version}</version>
<executions>
<execution>
<id>main</id>
<phase>process-classes</phase>
<goals>
<goal>enhance</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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<String> 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));
}
}

View File

@@ -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.
@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();
// 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.
}
}

View File

@@ -3,38 +3,51 @@ package com.pablotj.portfolio.infrastructure.persistence.certification.adapter;
import com.pablotj.portfolio.domain.certification.Certification;
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 com.pablotj.portfolio.infrastructure.persistence.certification.entity.CertificationEntity;
import com.pablotj.portfolio.infrastructure.persistence.certification.mapper.CertificationEntityMapper;
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;
private final CertificationEntityMapper mapper;
public CertificationRepositoryAdapter(SpringDataCertificationRepository repo, CertificationJpaMapper mapper) {
this.repo = repo;
@Inject
public CertificationRepositoryAdapter(CertificationEntityMapper mapper) {
this.mapper = mapper;
}
@Override
public Certification save(Certification p) {
CertificationJpaEntity entity = mapper.toEntity(p);
CertificationJpaEntity saved = repo.save(entity);
return mapper.toDomain(saved);
CertificationEntity entity = mapper.toEntity(p);
// Ebean detecta automáticamente si es insert o update por el ID
DB.save(entity);
return mapper.toDomain(entity);
}
@Override
public Optional<Certification> findById(CertificationId id) {
return repo.findByProfileIdAndId(id.profileId(), id.certificationId()).map(mapper::toDomain);
CertificationEntity entity = DB.find(CertificationEntity.class)
.where()
.eq("profile.id", id.profileId())
.eq("id", id.certificationId())
.findOne();
return Optional.ofNullable(entity).map(mapper::toDomain);
}
@Override
public List<Certification> findAll(Long profileId) {
return repo.findAllByProfileId(profileId).stream().map(mapper::toDomain).toList();
return DB.find(CertificationEntity.class)
.where()
.eq("profile.id", profileId)
.findList()
.stream()
.map(mapper::toDomain)
.toList();
}
}

View File

@@ -1,6 +1,7 @@
package com.pablotj.portfolio.infrastructure.persistence.certification.entity;
import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileEntity;
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 CertificationEntity extends Model { // Extendemos de Model para soporte Ebean
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -25,7 +26,7 @@ public class CertificationJpaEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "profile_id", nullable = false)
private ProfileJpaEntity profile;
private ProfileEntity profile;
@Column
private String name;

View File

@@ -1,18 +1,17 @@
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 com.pablotj.portfolio.infrastructure.persistence.certification.entity.CertificationEntity;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(componentModel = "spring")
public interface CertificationJpaMapper {
@Mapper
public interface CertificationEntityMapper {
@Mapping(target = "id", ignore = true)
@Mapping(target = "profile.id", source = "id.profileId")
CertificationJpaEntity toEntity(Certification domain);
CertificationEntity toEntity(Certification domain);
@Mapping(target = "id", expression = "java(new CertificationId(e.getProfile().getId(), e.getId()))")
Certification toDomain(CertificationJpaEntity e);
Certification toDomain(CertificationEntity e);
}

View File

@@ -1,14 +0,0 @@
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<CertificationJpaEntity, Long> {
Optional<CertificationJpaEntity> findByProfileIdAndId(Long profileId, Long id);
List<CertificationJpaEntity> findAllByProfileId(Long profileId);
}

View File

@@ -3,38 +3,50 @@ package com.pablotj.portfolio.infrastructure.persistence.education.adapter;
import com.pablotj.portfolio.domain.education.Education;
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 com.pablotj.portfolio.infrastructure.persistence.education.entity.EducationEntity;
import com.pablotj.portfolio.infrastructure.persistence.education.mapper.EducationEntityMapper;
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;
private final EducationEntityMapper mapper;
public EducationRepositoryAdapter(SpringDataEducationRepository repo, EducationJpaMapper mapper) {
this.repo = repo;
@Inject
public EducationRepositoryAdapter(EducationEntityMapper mapper) {
this.mapper = mapper;
}
@Override
public Education save(Education p) {
EducationJpaEntity entity = mapper.toEntity(p);
EducationJpaEntity saved = repo.save(entity);
return mapper.toDomain(saved);
EducationEntity entity = mapper.toEntity(p);
DB.save(entity);
return mapper.toDomain(entity);
}
@Override
public Optional<Education> findById(EducationId id) {
return repo.findByProfileIdAndId(id.profileId(), id.educationId()).map(mapper::toDomain);
EducationEntity entity = DB.find(EducationEntity.class)
.where()
.eq("profile.id", id.profileId())
.eq("id", id.educationId())
.findOne();
return Optional.ofNullable(entity).map(mapper::toDomain);
}
@Override
public List<Education> findAll(Long profileId) {
return repo.findAllByProfileId(profileId).stream().map(mapper::toDomain).toList();
return DB.find(EducationEntity.class)
.where()
.eq("profile.id", profileId)
.findList()
.stream()
.map(mapper::toDomain)
.toList();
}
}

View File

@@ -1,6 +1,7 @@
package com.pablotj.portfolio.infrastructure.persistence.education.entity;
import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileEntity;
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 EducationEntity extends Model {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -25,7 +26,7 @@ public class EducationJpaEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "profile_id", nullable = false)
private ProfileJpaEntity profile;
private ProfileEntity profile;
@Column
private String institution;

View File

@@ -1,18 +1,17 @@
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 com.pablotj.portfolio.infrastructure.persistence.education.entity.EducationEntity;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(componentModel = "spring")
public interface EducationJpaMapper {
@Mapper
public interface EducationEntityMapper {
@Mapping(target = "id", ignore = true)
@Mapping(target = "profile.id", source = "id.profileId")
EducationJpaEntity toEntity(Education domain);
EducationEntity toEntity(Education domain);
@Mapping(target = "id", expression = "java(new EducationId(e.getProfile().getId(), e.getId()))")
Education toDomain(EducationJpaEntity e);
Education toDomain(EducationEntity e);
}

View File

@@ -1,13 +0,0 @@
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<EducationJpaEntity, Long> {
Optional<EducationJpaEntity> findByProfileIdAndId(Long profileId, Long id);
List<EducationJpaEntity> findAllByProfileId(Long profileId);
}

View File

@@ -3,38 +3,54 @@ package com.pablotj.portfolio.infrastructure.persistence.experience.adapter;
import com.pablotj.portfolio.domain.experience.Experience;
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 com.pablotj.portfolio.infrastructure.persistence.experience.entity.ExperienceEntity;
import com.pablotj.portfolio.infrastructure.persistence.experience.mapper.ExperienceEntityMapper;
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;
private final ExperienceEntityMapper mapper;
public ExperienceRepositoryAdapter(SpringDataExperienceRepository repo, ExperienceJpaMapper mapper) {
this.repo = repo;
@Inject
public ExperienceRepositoryAdapter(ExperienceEntityMapper mapper) {
this.mapper = mapper;
}
@Override
public Experience save(Experience p) {
ExperienceJpaEntity entity = mapper.toEntity(p);
ExperienceJpaEntity saved = repo.save(entity);
return mapper.toDomain(saved);
ExperienceEntity entity = mapper.toEntity(p);
DB.save(entity);
return mapper.toDomain(entity);
}
@Override
public Optional<Experience> findById(ExperienceId id) {
return repo.findByProfileIdAndId(id.profileId(), id.experienceId()).map(mapper::toDomain);
ExperienceEntity entity = DB.find(ExperienceEntity.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<Experience> findAll(Long profileId) {
return repo.findAllByProfileId(profileId).stream().map(mapper::toDomain).toList();
return DB.find(ExperienceEntity.class)
.fetch("technologies")
.fetch("achievements")
.where()
.eq("profile.id", profileId)
.findList()
.stream()
.map(mapper::toDomain)
.toList();
}
}

View File

@@ -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 ExperienceAchievementEntity extends Model { // Adaptado para Ebean
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)

View File

@@ -1,6 +1,7 @@
package com.pablotj.portfolio.infrastructure.persistence.experience.entity;
import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileEntity;
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 ExperienceEntity extends Model { // Extensión para Ebean
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -35,7 +36,7 @@ public class ExperienceJpaEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "profile_id", nullable = false)
private ProfileJpaEntity profile;
private ProfileEntity profile;
@Column
private String position;
@@ -52,11 +53,12 @@ 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<ExperienceSkillJpaEntity> technologies;
private List<ExperienceSkillEntity> technologies;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "EXPERIENCE_ID")
private List<ExperienceAchievementJpaEntity> achievements;
private List<ExperienceAchievementEntity> achievements;
}

View File

@@ -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 ExperienceSkillEntity extends Model { // Extensión necesaria para el motor Ebean
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)

View File

@@ -3,33 +3,33 @@ package com.pablotj.portfolio.infrastructure.persistence.experience.mapper;
import com.pablotj.portfolio.domain.experience.Achievement;
import com.pablotj.portfolio.domain.experience.Experience;
import com.pablotj.portfolio.domain.experience.Technology;
import com.pablotj.portfolio.infrastructure.persistence.experience.entity.ExperienceAchievementJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.experience.entity.ExperienceJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.experience.entity.ExperienceSkillJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.experience.entity.ExperienceAchievementEntity;
import com.pablotj.portfolio.infrastructure.persistence.experience.entity.ExperienceEntity;
import com.pablotj.portfolio.infrastructure.persistence.experience.entity.ExperienceSkillEntity;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(componentModel = "spring")
public interface ExperienceJpaMapper {
@Mapper
public interface ExperienceEntityMapper {
@Mapping(target = "id", ignore = true)
@Mapping(target = "profile.id", source = "id.profileId")
ExperienceJpaEntity toEntity(Experience domain);
ExperienceEntity toEntity(Experience domain);
@Mapping(target = "id", expression = "java(new ExperienceId(entity.getProfile().getId(), entity.getId()))")
Experience toDomain(ExperienceJpaEntity entity);
Experience toDomain(ExperienceEntity entity);
@Mapping(target = "id", ignore = true)
ExperienceAchievementJpaEntity toEntity(Achievement entity);
ExperienceAchievementEntity toEntity(Achievement entity);
@Mapping(target = "id.value", source = "id")
Achievement toDomain(ExperienceAchievementJpaEntity entity);
Achievement toDomain(ExperienceAchievementEntity entity);
@Mapping(target = "id", ignore = true)
ExperienceSkillJpaEntity toEntity(Technology entity);
ExperienceSkillEntity toEntity(Technology entity);
@Mapping(target = "id.value", source = "id")
Technology toDomain(ExperienceSkillJpaEntity entity);
Technology toDomain(ExperienceSkillEntity entity);
}

View File

@@ -1,13 +0,0 @@
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<ExperienceJpaEntity, Long> {
Optional<ExperienceJpaEntity> findByProfileIdAndId(Long profileId, Long id);
List<ExperienceJpaEntity> findAllByProfileId(Long profileId);
}

View File

@@ -3,49 +3,64 @@ package com.pablotj.portfolio.infrastructure.persistence.profile.adapter;
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 com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileEntity;
import com.pablotj.portfolio.infrastructure.persistence.profile.mapper.ProfileEntityMapper;
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;
private final ProfileEntityMapper mapper;
public ProfileRepositoryAdapter(SpringDataProfileRepository repo, ProfileJpaMapper mapper) {
this.repo = repo;
@Inject
public ProfileRepositoryAdapter(ProfileEntityMapper 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);
ProfileEntity entity = mapper.toEntity(p);
// Ebean gestiona las relaciones Cascade de forma muy limpia.
// Si el mapper ya asocia los ProfileSocialLinkEntity, DB.save lo procesa todo.
DB.save(entity);
return mapper.toDomain(entity);
}
@Override
public Optional<Profile> findBySlug(ProfileId id) {
return repo.findBySlug(id.slug()).map(mapper::toDomain);
ProfileEntity entity = DB.find(ProfileEntity.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<Profile> findById(ProfileId id) {
return repo.findById(id.id()).map(mapper::toDomain);
ProfileEntity entity = DB.find(ProfileEntity.class)
.fetch("social")
.where()
.eq("id", id.id())
.findOne();
return Optional.ofNullable(entity).map(mapper::toDomain);
}
@Override
public List<Profile> findAll() {
return repo.findAll().stream().map(mapper::toDomain).toList();
return DB.find(ProfileEntity.class)
.fetch("social")
.findList()
.stream()
.map(mapper::toDomain)
.toList();
}
}

View File

@@ -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 ProfileEntity extends Model { // Adaptación para Ebean
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -47,10 +48,10 @@ 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)
private List<ProfileSocialLinkJpaEntity> social = new ArrayList<>();
private List<ProfileSocialLinkEntity> social = new ArrayList<>();
}

View File

@@ -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,14 +17,15 @@ import lombok.Setter;
@Table(name = "profile_social_link")
@Getter
@Setter
public class ProfileSocialLinkJpaEntity {
public class ProfileSocialLinkEntity extends Model { // Adaptación para Ebean
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "profile_id", nullable = false)
private ProfileJpaEntity profile;
private ProfileEntity profile;
@Column
private String url;

View File

@@ -2,25 +2,25 @@ package com.pablotj.portfolio.infrastructure.persistence.profile.mapper;
import com.pablotj.portfolio.domain.profile.Profile;
import com.pablotj.portfolio.domain.profile.ProfileSocialLink;
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.entity.ProfileEntity;
import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileSocialLinkEntity;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(componentModel = "spring")
public interface ProfileJpaMapper {
@Mapper
public interface ProfileEntityMapper {
@Mapping(target = "id", ignore = true)
@Mapping(target = "social", source = "social")
ProfileJpaEntity toEntity(Profile domain);
ProfileEntity toEntity(Profile domain);
@Mapping(target = "id.id", source = "id")
@Mapping(target = "social", source = "social")
Profile toDomain(ProfileJpaEntity e);
Profile toDomain(ProfileEntity e);
@Mapping(target = "id", ignore = true)
ProfileSocialLinkJpaEntity toEntitySocial(ProfileSocialLink entity);
ProfileSocialLinkEntity toEntitySocial(ProfileSocialLink entity);
@Mapping(target = "id.value", source = "id")
ProfileSocialLink toDomainSocial(ProfileSocialLinkJpaEntity entity);
ProfileSocialLink toDomainSocial(ProfileSocialLinkEntity entity);
}

View File

@@ -1,10 +0,0 @@
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<ProfileJpaEntity, Long> {
Optional<ProfileJpaEntity> findBySlug(String slug);
}

View File

@@ -3,39 +3,56 @@ package com.pablotj.portfolio.infrastructure.persistence.project.adapter;
import com.pablotj.portfolio.domain.project.Project;
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 com.pablotj.portfolio.infrastructure.persistence.project.entity.ProjectEntity;
import com.pablotj.portfolio.infrastructure.persistence.project.mapper.ProjectEntityMapper;
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;
private final ProjectEntityMapper mapper;
public ProjectRepositoryAdapter(SpringDataProjectRepository repo, ProjectJpaMapper mapper) {
this.repo = repo;
@Inject
public ProjectRepositoryAdapter(ProjectEntityMapper mapper) {
this.mapper = mapper;
}
@Override
public Project save(Project p) {
ProjectJpaEntity entity = mapper.toEntity(p);
ProjectJpaEntity saved = repo.save(entity);
return mapper.toDomain(saved);
ProjectEntity entity = mapper.toEntity(p);
// 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<Project> findById(ProjectId id) {
return repo.findByProfileIdAndId(id.profileId(), id.projectId()).map(mapper::toDomain);
ProjectEntity entity = DB.find(ProjectEntity.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<Project> findAll(Long profileId) {
return repo.findAllByProfileId(profileId).stream().map(mapper::toDomain).toList();
return DB.find(ProjectEntity.class)
.fetch("technologies")
.fetch("features")
.where()
.eq("profile.id", profileId)
.findList()
.stream()
.map(mapper::toDomain)
.toList();
}
}

View File

@@ -1,6 +1,7 @@
package com.pablotj.portfolio.infrastructure.persistence.project.entity;
import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileEntity;
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 ProjectEntity extends Model { // Adaptación para Ebean
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -27,12 +29,12 @@ public class ProjectJpaEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "profile_id", nullable = false)
private ProfileJpaEntity profile;
private ProfileEntity profile;
@Column
private String title;
@Column
@Column(columnDefinition = "text")
private String description;
@Column
@@ -40,11 +42,11 @@ public class ProjectJpaEntity {
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "PROJECT_ID")
private List<ProjectTechnologyJpaEntity> technologies;
private List<ProjectTechnologyEntity> technologies;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "PROJECT_ID")
private List<ProjectFeatureJpaEntity> features;
private List<ProjectFeatureEntity> features;
@Column
private String demo;

View File

@@ -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 ProjectFeatureEntity extends Model { // Adaptación para Ebean
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)

View File

@@ -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 ProjectTechnologyEntity extends Model { // Adaptación para el motor Ebean
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)

View File

@@ -3,31 +3,31 @@ package com.pablotj.portfolio.infrastructure.persistence.project.mapper;
import com.pablotj.portfolio.domain.project.Project;
import com.pablotj.portfolio.domain.project.ProjectFeature;
import com.pablotj.portfolio.domain.project.ProjectTechnology;
import com.pablotj.portfolio.infrastructure.persistence.project.entity.ProjectFeatureJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.project.entity.ProjectJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.project.entity.ProjectTechnologyJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.project.entity.ProjectEntity;
import com.pablotj.portfolio.infrastructure.persistence.project.entity.ProjectFeatureEntity;
import com.pablotj.portfolio.infrastructure.persistence.project.entity.ProjectTechnologyEntity;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(componentModel = "spring")
public interface ProjectJpaMapper {
@Mapper
public interface ProjectEntityMapper {
@Mapping(target = "id", ignore = true)
@Mapping(target = "profile.id", source = "id.profileId")
ProjectJpaEntity toEntity(Project domain);
ProjectEntity toEntity(Project domain);
@Mapping(target = "id", expression = "java(new ProjectId(e.getProfile().getId(), e.getId()))")
Project toDomain(ProjectJpaEntity e);
Project toDomain(ProjectEntity e);
@Mapping(target = "id", ignore = true)
ProjectTechnologyJpaEntity toEntity(ProjectTechnology entity);
ProjectTechnologyEntity toEntity(ProjectTechnology entity);
@Mapping(target = "id.value", source = "id")
ProjectTechnology toDomain(ProjectTechnologyJpaEntity entity);
ProjectTechnology toDomain(ProjectTechnologyEntity entity);
@Mapping(target = "id", ignore = true)
ProjectFeatureJpaEntity toEntity(ProjectFeature entity);
ProjectFeatureEntity toEntity(ProjectFeature entity);
@Mapping(target = "id.value", source = "id")
ProjectFeature toDomain(ProjectFeatureJpaEntity entity);
ProjectFeature toDomain(ProjectFeatureEntity entity);
}

View File

@@ -1,13 +0,0 @@
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<ProjectJpaEntity, Long> {
Optional<ProjectJpaEntity> findByProfileIdAndId(Long profileId, Long id);
List<ProjectJpaEntity> findAllByProfileId(Long profileId);
}

View File

@@ -3,38 +3,53 @@ package com.pablotj.portfolio.infrastructure.persistence.skill.adapter;
import com.pablotj.portfolio.domain.skill.SkillGroup;
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 com.pablotj.portfolio.infrastructure.persistence.skill.entity.SkillGroupEntity;
import com.pablotj.portfolio.infrastructure.persistence.skill.mapper.SkillMapper;
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;
private final SkillMapper mapper;
public SkillRepositoryAdapter(SpringDataSkillRepository repo, SkillJpaMapper mapper) {
this.repo = repo;
@Inject
public SkillRepositoryAdapter(SkillMapper mapper) {
this.mapper = mapper;
}
@Override
public SkillGroup save(SkillGroup p) {
SkillGroupJpaEntity entity = mapper.toEntity(p);
SkillGroupJpaEntity saved = repo.save(entity);
return mapper.toDomain(saved);
SkillGroupEntity entity = mapper.toEntity(p);
// Ebean persiste el SkillGroup y sus SkillEntity asociadas por cascada
DB.save(entity);
return mapper.toDomain(entity);
}
@Override
public Optional<SkillGroup> findById(SkillGroupId id) {
return repo.findByProfileIdAndId(id.profileId(), id.skillGroupId()).map(mapper::toDomain);
SkillGroupEntity entity = DB.find(SkillGroupEntity.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<SkillGroup> findAll(Long profileId) {
return repo.findAllByProfileId(profileId).stream().map(mapper::toDomain).toList();
return DB.find(SkillGroupEntity.class)
.fetch("skills")
.where()
.eq("profile.id", profileId)
.findList()
.stream()
.map(mapper::toDomain)
.toList();
}
}

View File

@@ -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 SkillEntity extends Model { // Adaptación para Ebean
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)

View File

@@ -1,6 +1,7 @@
package com.pablotj.portfolio.infrastructure.persistence.skill.entity;
import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileEntity;
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 SkillGroupEntity extends Model { // Adaptación para Ebean
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -28,7 +29,7 @@ public class SkillGroupJpaEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "profile_id", nullable = false)
private ProfileJpaEntity profile;
private ProfileEntity profile;
@Column
private String name;
@@ -38,5 +39,5 @@ public class SkillGroupJpaEntity {
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "SKILL_ID")
private List<SkillJpaEntity> skills;
private List<SkillEntity> skills;
}

View File

@@ -2,24 +2,24 @@ package com.pablotj.portfolio.infrastructure.persistence.skill.mapper;
import com.pablotj.portfolio.domain.skill.Skill;
import com.pablotj.portfolio.domain.skill.SkillGroup;
import com.pablotj.portfolio.infrastructure.persistence.skill.entity.SkillGroupJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.skill.entity.SkillJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.skill.entity.SkillGroupEntity;
import com.pablotj.portfolio.infrastructure.persistence.skill.entity.SkillEntity;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(componentModel = "spring")
public interface SkillJpaMapper {
@Mapper
public interface SkillMapper {
@Mapping(target = "id", ignore = true)
@Mapping(target = "profile.id", source = "id.profileId")
SkillGroupJpaEntity toEntity(SkillGroup domain);
SkillGroupEntity toEntity(SkillGroup domain);
@Mapping(target = "id", expression = "java(new SkillGroupId(entity.getProfile().getId(), entity.getId()))")
SkillGroup toDomain(SkillGroupJpaEntity entity);
SkillGroup toDomain(SkillGroupEntity entity);
@Mapping(target = "id", ignore = true)
SkillJpaEntity toEntity(Skill entity);
SkillEntity toEntity(Skill entity);
@Mapping(target = "id.value", source = "id")
Skill toDomain(SkillJpaEntity entity);
Skill toDomain(SkillEntity entity);
}

View File

@@ -1,13 +0,0 @@
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<SkillGroupJpaEntity, Long> {
Optional<SkillGroupJpaEntity> findByProfileIdAndId(Long profileId, Long id);
List<SkillGroupJpaEntity> findAllByProfileId(Long profileId);
}

View File

@@ -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<Map<String, Object>> handleError(WebRequest webRequest) {
Map<String, Object> attributes = errorAttributes.getErrorAttributes(webRequest,
ErrorAttributeOptions.defaults());
HttpStatus status = HttpStatus.valueOf((int) attributes.getOrDefault("status", 500));
return new ResponseEntity<>(attributes, status);
Map<String, Object> 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);
};
}
}

View File

@@ -1,39 +1,39 @@
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<Map<String, Object>> root() {
Map<String, Object> response = Map.of(
@Inject
public ApiRootController(@Named("info.app.version") String appVersion) {
this.appVersion = appVersion;
}
@GET
public Map<String, Object> 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);
}
}

View File

@@ -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<CertificationDto> all(@PathVariable Long profileId) {
return getUC.all(profileId).stream().map(mapper::toDto).toList();
@GET
public List<CertificationDto> all(@PathParam Long profileId) {
return getUC.all(profileId).stream()
.map(MAPPER::toDto)
.toList();
}
@GetMapping("/{id}")
public ResponseEntity<CertificationDto> 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<CertificationDto> 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);
}
}

View File

@@ -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")

View File

@@ -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<EducationDto> all(@PathVariable Long profileId) {
return getUC.all(profileId).stream().map(mapper::toDto).toList();
@GET
public List<EducationDto> all(@PathParam Long profileId) {
return getUC.all(profileId).stream()
.map(MAPPER::toDto)
.toList();
}
@GetMapping("/{id}")
public ResponseEntity<EducationDto> 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<EducationDto> 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);
}
}

View File

@@ -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")

View File

@@ -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<ExperienceDto> all(@PathVariable Long profileId) {
return getUC.all(profileId).stream().map(mapper::toDto).toList();
@GET
public List<ExperienceDto> all(@PathParam Long profileId) {
return getUC.all(profileId).stream()
.map(MAPPER::toDto)
.toList();
}
@GetMapping("/{id}")
public ResponseEntity<ExperienceDto> 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<ExperienceDto> 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);
}
}

View File

@@ -11,4 +11,5 @@ public record CreateExperienceRequest(
String description,
List<String> technologies,
List<String> achievements
) {}
) {
}

View File

@@ -12,4 +12,5 @@ public record ExperienceDto(
String description,
List<String> technologies,
List<String> achievements
) {}
) {
}

View File

@@ -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")

View File

@@ -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<ProfileDto> all() {
return getUC.all().stream().map(mapper::toDto).toList();
return getUC.all().stream()
.map(MAPPER::toDto)
.toList();
}
@GetMapping("/{slug}")
public ResponseEntity<ProfileDto> 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<ProfileDto> 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);
}
}

View File

@@ -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")

View File

@@ -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<ProjectDto> all(@PathVariable Long profileId) {
return getUC.all(profileId).stream().map(mapper::toDto).toList();
@GET
public List<ProjectDto> all(@PathParam Long profileId) {
return getUC.all(profileId).stream()
.map(MAPPER::toDto)
.toList();
}
@GetMapping("/{id}")
public ResponseEntity<ProjectDto> 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<ProjectDto> 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);
}
}

View File

@@ -10,4 +10,5 @@ public record CreateProjectRequest(
List<String> features,
String demo,
String repository
) {}
) {
}

View File

@@ -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")

View File

@@ -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<SkillGroupDto> all(@PathVariable Long profileId) {
return getUC.all(profileId).stream().map(mapper::toDto).toList();
@GET
public List<SkillGroupDto> all(@PathParam Long profileId) {
return getUC.all(profileId).stream()
.map(MAPPER::toDto)
.toList();
}
@GetMapping("/{id}")
public ResponseEntity<SkillGroupDto> 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<SkillGroupDto> 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);
}
}

View File

@@ -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")

View File

@@ -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
);

View File

@@ -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;

View File

@@ -0,0 +1,3 @@
entity-packages: com.pablotj.portfolio.infrastructure.persistence
transactional-packages: com.pablotj.portfolio
querybean-packages: com.pablotj.portfolio

35
pom.xml
View File

@@ -16,30 +16,35 @@
<module>bootstrap</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
<relativePath/>
</parent>
<properties>
<java.version>21</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jooby.version>3.2.0</jooby.version>
<mapstruct.version>1.5.5.Final</mapstruct.version>
<springdoc.version>2.6.0</springdoc.version>
<lombok.version>1.18.32</lombok.version>
<jackson.version>2.17.2</jackson.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-bom</artifactId>
<version>${jooby.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
<groupId>io.jooby</groupId>
<artifactId>jooby-apt</artifactId>
<version>${jooby.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
</dependencyManagement>
@@ -50,14 +55,20 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<annotationProcessorPaths>
<path>
<groupId>io.jooby</groupId>
<artifactId>jooby-apt</artifactId>
<version>${jooby.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.mapstruct</groupId>