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 707baf8cbc
78 changed files with 1266 additions and 974 deletions

View File

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

3
.gitignore vendored
View File

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

View File

@@ -26,21 +26,26 @@ FROM eclipse-temurin:21-jre-alpine
WORKDIR /app WORKDIR /app
# Seguridad: Usuario no-root # Seguridad: Usuario no-root
RUN addgroup -S spring && adduser -S spring -G spring RUN addgroup -S app && adduser -S app -G app
USER spring:spring
RUN mkdir -p /app/tmp && chown app:app /app/tmp && chmod 777 /app/tmp
RUN mkdir -p /app/logs && chown app:app /app/logs && chmod 777 /app/logs
USER app:app
# Copiamos solo el JAR final (ajustado a tu módulo bootstrap) # 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 # Configuración de Memoria y Rendimiento para Microservicios
# -XX:+UseSerialGC: Menos consumo de RAM para apps < 1GB # -XX:+UseSerialGC: Menos consumo de RAM para apps < 1GB
# -XX:TieredStopAtLevel=1: Arranque más rápido y menos uso de RAM del compilador JIT # -XX:TieredStopAtLevel=1: Arranque más rápido y menos uso de RAM del compilador JIT
# -XX:MaxRAMPercentage: Se ajusta dinámicamente al límite de Docker # -XX:MaxRAMPercentage: Se ajusta dinámicamente al límite de Docker
ENV JAVA_OPTS="-XX:+UseContainerSupport \ ENV JAVA_OPTS="-XX:+UseContainerSupport \
-XX:MaxRAMPercentage=75.0 \ -XX:MaxRAMPercentage=50.0 \
-Xms32m \
-Xmx96m \
-XX:+UseSerialGC \ -XX:+UseSerialGC \
-XX:TieredStopAtLevel=1 \ -XX:TieredStopAtLevel=1"
-Xms128m"
EXPOSE 80 EXPOSE 80

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,17 @@
<artifactId>domain</artifactId> <artifactId>domain</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>jakarta.validation</groupId> <groupId>org.apache.logging.log4j</groupId>
<artifactId>jakarta.validation-api</artifactId> <artifactId>log4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.36</version>
<scope>provided</scope>
</dependency> </dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -33,13 +33,13 @@ public class CreateExperienceUseCase {
} }
public record Command( public record Command(
String position, String position,
String company, String company,
String period, String period,
String location, String location,
String description, String description,
List<String> technologies, List<String> technologies,
List<String> achievements List<String> achievements
) { ) {
} }
} }

View File

@@ -3,7 +3,6 @@ package com.pablotj.portfolio.application.project;
import com.pablotj.portfolio.domain.project.Project; import com.pablotj.portfolio.domain.project.Project;
import com.pablotj.portfolio.domain.project.ProjectId; import com.pablotj.portfolio.domain.project.ProjectId;
import com.pablotj.portfolio.domain.project.port.ProjectRepositoryPort; import com.pablotj.portfolio.domain.project.port.ProjectRepositoryPort;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;

View File

@@ -1,10 +1,14 @@
<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> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>com.pablotj</groupId> <groupId>com.pablotj</groupId>
<artifactId>portfolio-api</artifactId> <artifactId>portfolio-api</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1-SNAPSHOT</version>
</parent> </parent>
<artifactId>bootstrap</artifactId> <artifactId>bootstrap</artifactId>
<dependencies> <dependencies>
@@ -14,56 +18,84 @@
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<!-- Drivers DB -->
<dependency> <dependency>
<groupId>org.postgresql</groupId> <groupId>org.apache.logging.log4j</groupId>
<artifactId>postgresql</artifactId> <artifactId>log4j-api</artifactId>
<scope>runtime</scope> <version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Swagger/OpenAPI -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<!-- Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency> </dependency>
</dependencies> </dependencies>
<build> <build>
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.springframework.boot</groupId> <groupId>io.jooby</groupId>
<artifactId>spring-boot-maven-plugin</artifactId> <artifactId>jooby-maven-plugin</artifactId>
<version>${jooby.version}</version>
<executions> <executions>
<execution> <execution>
<goals> <goals>
<goal>repackage</goal> <goal>openapi</goal>
</goals> </goals>
</execution> </execution>
</executions> </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>
<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>
<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>
<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> </plugin>
</plugins> </plugins>
</build> </build>

View File

@@ -1,16 +1,67 @@
package com.pablotj.portfolio.bootstrap; package com.pablotj.portfolio.bootstrap;
import org.springframework.boot.SpringApplication; import com.pablotj.portfolio.bootstrap.certification.CertificationApplicationConfig;
import org.springframework.boot.autoconfigure.SpringBootApplication; import com.pablotj.portfolio.bootstrap.education.EducationApplicationConfig;
import org.springframework.boot.autoconfigure.domain.EntityScan; import com.pablotj.portfolio.bootstrap.experience.ExperienceApplicationConfig;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 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;
import com.pablotj.portfolio.infrastructure.rest.skill.SkillController;
import io.jooby.Jooby;
import io.jooby.OpenAPIModule;
import io.jooby.ebean.EbeanModule;
import io.jooby.flyway.FlywayModule;
import io.jooby.guice.GuiceModule;
import io.jooby.hibernate.validator.HibernateValidatorModule;
import io.jooby.hikari.HikariModule;
import io.jooby.jackson.JacksonModule;
import io.jooby.netty.NettyServer;
@SpringBootApplication(scanBasePackages = "com.pablotj") public class PortfolioApplication extends Jooby {
@EnableJpaRepositories(basePackages = {"com.pablotj.portfolio.infrastructure.persistence.*.repo"})
@EntityScan(basePackages = {"com.pablotj.portfolio.infrastructure.persistence.*.entity"})
public class PortfolioApplication {
public static void main(String[] args) { {
SpringApplication.run(PortfolioApplication.class, args); install(new NettyServer());
install(new JacksonModule());
install(new HibernateValidatorModule());
install(new HikariModule());
install(new FlywayModule());
install(new EbeanModule());
install(new CorsConfig());
error(ApiErrorController.getHandler());
install(new ProfileApplicationConfig());
install(new ProjectApplicationConfig());
install(new CertificationApplicationConfig());
install(new EducationApplicationConfig());
install(new ExperienceApplicationConfig());
install(new SkillApplicationConfig());
install(new GuiceModule());
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,20 @@ package com.pablotj.portfolio.bootstrap.certification;
import com.pablotj.portfolio.application.certification.CreateCertificationUseCase; import com.pablotj.portfolio.application.certification.CreateCertificationUseCase;
import com.pablotj.portfolio.application.certification.GetCertificationUseCase; import com.pablotj.portfolio.application.certification.GetCertificationUseCase;
import com.pablotj.portfolio.domain.certification.port.CertificationRepositoryPort; import com.pablotj.portfolio.domain.certification.port.CertificationRepositoryPort;
import org.springframework.context.annotation.Bean; import com.pablotj.portfolio.infrastructure.persistence.certification.adapter.CertificationRepositoryAdapter;
import org.springframework.context.annotation.Configuration; import io.jooby.Extension;
import io.jooby.Jooby;
import jakarta.annotation.Nonnull;
@Configuration public class CertificationApplicationConfig implements Extension {
public class CertificationApplicationConfig {
@Bean @Override
public GetCertificationUseCase getCertificationUseCase(CertificationRepositoryPort repo) { public void install(@Nonnull Jooby app) {
return new GetCertificationUseCase(repo); CertificationRepositoryAdapter adapter = new CertificationRepositoryAdapter();
}
@Bean app.getServices().put(CertificationRepositoryPort.class, adapter);
public CreateCertificationUseCase createCertificationUseCase(CertificationRepositoryPort repo) {
return new CreateCertificationUseCase(repo); app.getServices().put(GetCertificationUseCase.class, new GetCertificationUseCase(adapter));
app.getServices().put(CreateCertificationUseCase.class, new CreateCertificationUseCase(adapter));
} }
} }

View File

@@ -3,19 +3,20 @@ package com.pablotj.portfolio.bootstrap.education;
import com.pablotj.portfolio.application.education.CreateEducationUseCase; import com.pablotj.portfolio.application.education.CreateEducationUseCase;
import com.pablotj.portfolio.application.education.GetEducationUseCase; import com.pablotj.portfolio.application.education.GetEducationUseCase;
import com.pablotj.portfolio.domain.education.port.EducationRepositoryPort; import com.pablotj.portfolio.domain.education.port.EducationRepositoryPort;
import org.springframework.context.annotation.Bean; import com.pablotj.portfolio.infrastructure.persistence.education.adapter.EducationRepositoryAdapter;
import org.springframework.context.annotation.Configuration; import io.jooby.Extension;
import io.jooby.Jooby;
import jakarta.annotation.Nonnull;
@Configuration public class EducationApplicationConfig implements Extension {
public class EducationApplicationConfig {
@Bean @Override
public GetEducationUseCase getEducationUseCase(EducationRepositoryPort repo) { public void install(@Nonnull Jooby app) {
return new GetEducationUseCase(repo); EducationRepositoryAdapter adapter = new EducationRepositoryAdapter();
}
@Bean app.getServices().put(EducationRepositoryPort.class, adapter);
public CreateEducationUseCase createEducationUseCase(EducationRepositoryPort repo) {
return new CreateEducationUseCase(repo); app.getServices().put(GetEducationUseCase.class, new GetEducationUseCase(adapter));
app.getServices().put(CreateEducationUseCase.class, new CreateEducationUseCase(adapter));
} }
} }

View File

@@ -3,19 +3,20 @@ package com.pablotj.portfolio.bootstrap.experience;
import com.pablotj.portfolio.application.experience.CreateExperienceUseCase; import com.pablotj.portfolio.application.experience.CreateExperienceUseCase;
import com.pablotj.portfolio.application.experience.GetExperienceUseCase; import com.pablotj.portfolio.application.experience.GetExperienceUseCase;
import com.pablotj.portfolio.domain.experience.port.ExperienceRepositoryPort; import com.pablotj.portfolio.domain.experience.port.ExperienceRepositoryPort;
import org.springframework.context.annotation.Bean; import com.pablotj.portfolio.infrastructure.persistence.experience.adapter.ExperienceRepositoryAdapter;
import org.springframework.context.annotation.Configuration; import io.jooby.Extension;
import io.jooby.Jooby;
import jakarta.annotation.Nonnull;
@Configuration public class ExperienceApplicationConfig implements Extension {
public class ExperienceApplicationConfig { @Override
public void install(@Nonnull Jooby app) {
@Bean ExperienceRepositoryAdapter adapter = new ExperienceRepositoryAdapter();
public GetExperienceUseCase getExperienceUseCase(ExperienceRepositoryPort repo) {
return new GetExperienceUseCase(repo);
}
@Bean app.getServices().put(ExperienceRepositoryPort.class, adapter);
public CreateExperienceUseCase createExperienceUseCase(ExperienceRepositoryPort repo) {
return new CreateExperienceUseCase(repo); app.getServices().put(GetExperienceUseCase.class, new GetExperienceUseCase(adapter));
app.getServices().put(CreateExperienceUseCase.class, new CreateExperienceUseCase(adapter));
} }
} }

View File

@@ -3,19 +3,20 @@ package com.pablotj.portfolio.bootstrap.profile;
import com.pablotj.portfolio.application.profile.CreateProfileUseCase; import com.pablotj.portfolio.application.profile.CreateProfileUseCase;
import com.pablotj.portfolio.application.profile.GetProfileUseCase; import com.pablotj.portfolio.application.profile.GetProfileUseCase;
import com.pablotj.portfolio.domain.profile.port.ProfileRepositoryPort; import com.pablotj.portfolio.domain.profile.port.ProfileRepositoryPort;
import org.springframework.context.annotation.Bean; import com.pablotj.portfolio.infrastructure.persistence.profile.adapter.ProfileRepositoryAdapter;
import org.springframework.context.annotation.Configuration; import io.jooby.Extension;
import io.jooby.Jooby;
import jakarta.annotation.Nonnull;
@Configuration public class ProfileApplicationConfig implements Extension {
public class ProfileApplicationConfig { @Override
public void install(@Nonnull Jooby app) {
@Bean ProfileRepositoryAdapter adapter = new ProfileRepositoryAdapter();
public GetProfileUseCase getHomeUseCase(ProfileRepositoryPort repo) {
return new GetProfileUseCase(repo);
}
@Bean app.getServices().put(ProfileRepositoryPort.class, adapter);
public CreateProfileUseCase createHomeUseCase(ProfileRepositoryPort repo) {
return new CreateProfileUseCase(repo); app.getServices().put(GetProfileUseCase.class, new GetProfileUseCase(adapter));
app.getServices().put(CreateProfileUseCase.class, new CreateProfileUseCase(adapter));
} }
} }

View File

@@ -3,19 +3,20 @@ package com.pablotj.portfolio.bootstrap.project;
import com.pablotj.portfolio.application.project.CreateProjectUseCase; import com.pablotj.portfolio.application.project.CreateProjectUseCase;
import com.pablotj.portfolio.application.project.GetProjectUseCase; import com.pablotj.portfolio.application.project.GetProjectUseCase;
import com.pablotj.portfolio.domain.project.port.ProjectRepositoryPort; import com.pablotj.portfolio.domain.project.port.ProjectRepositoryPort;
import org.springframework.context.annotation.Bean; import com.pablotj.portfolio.infrastructure.persistence.project.adapter.ProjectRepositoryAdapter;
import org.springframework.context.annotation.Configuration; import io.jooby.Extension;
import io.jooby.Jooby;
import jakarta.annotation.Nonnull;
@Configuration public class ProjectApplicationConfig implements Extension {
public class ProjectApplicationConfig { @Override
public void install(@Nonnull Jooby app) {
@Bean ProjectRepositoryAdapter adapter = new ProjectRepositoryAdapter();
public CreateProjectUseCase createProjectUseCase(ProjectRepositoryPort repo) {
return new CreateProjectUseCase(repo);
}
@Bean app.getServices().put(ProjectRepositoryPort.class, adapter);
public GetProjectUseCase getProjectUseCase(ProjectRepositoryPort repo) {
return new GetProjectUseCase(repo); app.getServices().put(CreateProjectUseCase.class, new CreateProjectUseCase(adapter));
app.getServices().put(GetProjectUseCase.class, new GetProjectUseCase(adapter));
} }
} }

View File

@@ -3,19 +3,20 @@ package com.pablotj.portfolio.bootstrap.skill;
import com.pablotj.portfolio.application.skill.CreateSkillUseCase; import com.pablotj.portfolio.application.skill.CreateSkillUseCase;
import com.pablotj.portfolio.application.skill.GetSkillUseCase; import com.pablotj.portfolio.application.skill.GetSkillUseCase;
import com.pablotj.portfolio.domain.skill.port.SkillRepositoryPort; import com.pablotj.portfolio.domain.skill.port.SkillRepositoryPort;
import org.springframework.context.annotation.Bean; import com.pablotj.portfolio.infrastructure.persistence.skill.adapter.SkillRepositoryAdapter;
import org.springframework.context.annotation.Configuration; import io.jooby.Extension;
import io.jooby.Jooby;
import jakarta.annotation.Nonnull;
@Configuration public class SkillApplicationConfig implements Extension {
public class SkillApplicationConfig { @Override
public void install(@Nonnull Jooby app) {
@Bean SkillRepositoryAdapter adapter = new SkillRepositoryAdapter();
public GetSkillUseCase getSkillUseCase(SkillRepositoryPort repo) {
return new GetSkillUseCase(repo);
}
@Bean app.getServices().put(SkillRepositoryPort.class, adapter);
public CreateSkillUseCase createSkillUseCase(SkillRepositoryPort repo) {
return new CreateSkillUseCase(repo); app.getServices().put(GetSkillUseCase.class, new GetSkillUseCase(adapter));
app.getServices().put(CreateSkillUseCase.class, new CreateSkillUseCase(adapter));
} }
} }

View File

@@ -0,0 +1,60 @@
# 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}
}
}
# 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> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<optional>true</optional> <version>1.18.36</version>
<scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>jakarta.validation</groupId> <groupId>org.apache.logging.log4j</groupId>
<artifactId>jakarta.validation-api</artifactId> <artifactId>log4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency> </dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -7,6 +7,7 @@ public record CertificationId(Long profileId, Long certificationId) {
} }
public CertificationId { public CertificationId {
if (certificationId != null && certificationId < 0) throw new IllegalArgumentException("CertificationId must be positive"); if (certificationId != null && certificationId < 0)
throw new IllegalArgumentException("CertificationId must be positive");
} }
} }

View File

@@ -1,6 +1,5 @@
package com.pablotj.portfolio.domain.experience; package com.pablotj.portfolio.domain.experience;
import java.time.LocalDate;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;

View File

@@ -7,6 +7,7 @@ public record ExperienceId(Long profileId, Long experienceId) {
} }
public ExperienceId { public ExperienceId {
if (experienceId != null && experienceId < 0) throw new IllegalArgumentException("ProfileSocialLinkId must be positive"); if (experienceId != null && experienceId < 0)
throw new IllegalArgumentException("ProfileSocialLinkId must be positive");
} }
} }

View File

@@ -2,12 +2,13 @@ package com.pablotj.portfolio.domain.project.port;
import com.pablotj.portfolio.domain.project.Project; import com.pablotj.portfolio.domain.project.Project;
import com.pablotj.portfolio.domain.project.ProjectId; import com.pablotj.portfolio.domain.project.ProjectId;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
public interface ProjectRepositoryPort { public interface ProjectRepositoryPort {
Project save(Project p); Project save(Project p);
Optional<Project> findById(ProjectId id); Optional<Project> findById(ProjectId id);
List<Project> findAll(Long profileId); List<Project> findAll(Long profileId);
} }

View File

@@ -1,54 +1,158 @@
<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> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>com.pablotj</groupId> <groupId>com.pablotj</groupId>
<artifactId>portfolio-api</artifactId> <artifactId>portfolio-api</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1-SNAPSHOT</version>
</parent> </parent>
<artifactId>infrastructure</artifactId> <artifactId>infrastructure</artifactId>
<properties>
<ebean.version>15.8.0</ebean.version>
<postgresql.version>42.7.2</postgresql.version>
<mapstruct.version>1.6.3</mapstruct.version>
<lombok.version>1.18.36</lombok.version>
<testcontainers.version>1.19.7</testcontainers.version>
</properties>
<dependencies> <dependencies>
<dependency>
<groupId>com.pablotj</groupId>
<artifactId>domain</artifactId>
<version>${project.version}</version>
</dependency>
<dependency> <dependency>
<groupId>com.pablotj</groupId> <groupId>com.pablotj</groupId>
<artifactId>application</artifactId> <artifactId>application</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<!-- Spring Boot -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>io.jooby</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>jooby</artifactId>
</dependency> <version>${jooby.version}</version>
<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>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.apache.logging.log4j</groupId>
<artifactId>spring-boot-starter-security</artifactId> <artifactId>log4j-core</artifactId>
<version>${slf4j.version}</version>
</dependency> </dependency>
<!-- MapStruct --> <dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-netty</artifactId>
<version>${jooby.version}</version>
</dependency>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-ebean</artifactId>
<version>${jooby.version}</version>
</dependency>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-hikari</artifactId>
<version>${jooby.version}</version>
</dependency>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-flyway</artifactId>
<version>${jooby.version}</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${postgresql.version}</version>
</dependency>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-jackson</artifactId>
<version>${jooby.version}</version>
</dependency>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-hibernate-validator</artifactId>
<version>${jooby.version}</version>
</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>
<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-guice</artifactId>
<version>${jooby.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.mapstruct</groupId> <groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId> <artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency> </dependency>
<!-- Lombok -->
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<optional>true</optional> <version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency> </dependency>
</dependencies> </dependencies>
<build>
<plugins>
<plugin>
<groupId>io.ebean</groupId>
<artifactId>ebean-maven-plugin</artifactId>
<version>${ebean.version}</version>
<executions>
<execution>
<phase>process-classes</phase>
<goals>
<goal>enhance</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project> </project>

View File

@@ -1,34 +1,26 @@
package com.pablotj.portfolio.infrastructure.config; 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.Arrays;
import java.util.List; 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 implements Extension {
public class CorsConfig {
@Value("${app.cors.allowed-origins}")
private String allowedOriginsString;
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
@Override
public void install(@Nonnull Jooby app) {
String allowedOriginsString = app.getConfig().getString("app.cors.allowed-origins");
List<String> allowedOrigins = Arrays.asList(allowedOriginsString.split(",")); List<String> allowedOrigins = Arrays.asList(allowedOriginsString.split(","));
config.setAllowedOriginPatterns(allowedOrigins);
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); Cors cors = new Cors()
config.setAllowedHeaders(List.of("*")); .setOrigin(allowedOrigins)
config.setAllowCredentials(true); .setMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"))
.setHeaders(Arrays.asList("Content-Type", "Authorization", "X-Requested-With"))
.setUseCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); app.use(new CorsHandler(cors));
source.registerCorsConfiguration("/**", config);
return source;
} }
} }

View File

@@ -1,31 +1,13 @@
package com.pablotj.portfolio.infrastructure.config; package com.pablotj.portfolio.infrastructure.config;
import org.springframework.context.annotation.Bean; import io.jooby.Extension;
import org.springframework.context.annotation.Configuration; import io.jooby.Jooby;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import jakarta.annotation.Nonnull;
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;
@Configuration public class SecurityConfig implements Extension {
@EnableWebSecurity
public class SecurityConfig {
private final CorsConfigurationSource corsConfigurationSource; @Override
public void install(@Nonnull Jooby app) {
public SecurityConfig(CorsConfigurationSource corsConfigurationSource) {
this.corsConfigurationSource = corsConfigurationSource;
}
@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();
} }
} }

View File

@@ -3,38 +3,45 @@ package com.pablotj.portfolio.infrastructure.persistence.certification.adapter;
import com.pablotj.portfolio.domain.certification.Certification; import com.pablotj.portfolio.domain.certification.Certification;
import com.pablotj.portfolio.domain.certification.CertificationId; import com.pablotj.portfolio.domain.certification.CertificationId;
import com.pablotj.portfolio.domain.certification.port.CertificationRepositoryPort; import com.pablotj.portfolio.domain.certification.port.CertificationRepositoryPort;
import com.pablotj.portfolio.infrastructure.persistence.certification.entity.CertificationJpaEntity; import com.pablotj.portfolio.infrastructure.persistence.certification.entity.CertificationEntity;
import com.pablotj.portfolio.infrastructure.persistence.certification.mapper.CertificationJpaMapper; import com.pablotj.portfolio.infrastructure.persistence.certification.mapper.CertificationEntityMapper;
import com.pablotj.portfolio.infrastructure.persistence.certification.repo.SpringDataCertificationRepository; import io.ebean.DB;
import jakarta.inject.Singleton;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import org.springframework.stereotype.Repository; import org.mapstruct.factory.Mappers;
@Repository @Singleton
public class CertificationRepositoryAdapter implements CertificationRepositoryPort { public class CertificationRepositoryAdapter implements CertificationRepositoryPort {
private final SpringDataCertificationRepository repo; private static final CertificationEntityMapper MAPPER = Mappers.getMapper(CertificationEntityMapper.class);
private final CertificationJpaMapper mapper;
public CertificationRepositoryAdapter(SpringDataCertificationRepository repo, CertificationJpaMapper mapper) {
this.repo = repo;
this.mapper = mapper;
}
@Override @Override
public Certification save(Certification p) { public Certification save(Certification p) {
CertificationJpaEntity entity = mapper.toEntity(p); CertificationEntity entity = MAPPER.toEntity(p);
CertificationJpaEntity saved = repo.save(entity); DB.save(entity);
return mapper.toDomain(saved); return MAPPER.toDomain(entity);
} }
@Override @Override
public Optional<Certification> findById(CertificationId id) { 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 @Override
public List<Certification> findAll(Long profileId) { 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; 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.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.FetchType; import jakarta.persistence.FetchType;
@@ -17,7 +18,7 @@ import lombok.Setter;
@Table(name = "CERTIFICATION") @Table(name = "CERTIFICATION")
@Getter @Getter
@Setter @Setter
public class CertificationJpaEntity { public class CertificationEntity extends Model {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -25,7 +26,7 @@ public class CertificationJpaEntity {
@ManyToOne(fetch = FetchType.LAZY) @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "profile_id", nullable = false) @JoinColumn(name = "profile_id", nullable = false)
private ProfileJpaEntity profile; private ProfileEntity profile;
@Column @Column
private String name; private String name;

View File

@@ -1,18 +1,17 @@
package com.pablotj.portfolio.infrastructure.persistence.certification.mapper; package com.pablotj.portfolio.infrastructure.persistence.certification.mapper;
import com.pablotj.portfolio.domain.certification.Certification; import com.pablotj.portfolio.domain.certification.Certification;
import com.pablotj.portfolio.domain.certification.CertificationId; import com.pablotj.portfolio.infrastructure.persistence.certification.entity.CertificationEntity;
import com.pablotj.portfolio.infrastructure.persistence.certification.entity.CertificationJpaEntity;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
@Mapper(componentModel = "spring") @Mapper
public interface CertificationJpaMapper { public interface CertificationEntityMapper {
@Mapping(target = "id", ignore = true) @Mapping(target = "id", ignore = true)
@Mapping(target = "profile.id", source = "id.profileId") @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()))") @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,45 @@ package com.pablotj.portfolio.infrastructure.persistence.education.adapter;
import com.pablotj.portfolio.domain.education.Education; import com.pablotj.portfolio.domain.education.Education;
import com.pablotj.portfolio.domain.education.EducationId; import com.pablotj.portfolio.domain.education.EducationId;
import com.pablotj.portfolio.domain.education.port.EducationRepositoryPort; import com.pablotj.portfolio.domain.education.port.EducationRepositoryPort;
import com.pablotj.portfolio.infrastructure.persistence.education.entity.EducationJpaEntity; import com.pablotj.portfolio.infrastructure.persistence.education.entity.EducationEntity;
import com.pablotj.portfolio.infrastructure.persistence.education.mapper.EducationJpaMapper; import com.pablotj.portfolio.infrastructure.persistence.education.mapper.EducationEntityMapper;
import com.pablotj.portfolio.infrastructure.persistence.education.repo.SpringDataEducationRepository; import io.ebean.DB;
import jakarta.inject.Singleton;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import org.springframework.stereotype.Repository; import org.mapstruct.factory.Mappers;
@Repository @Singleton
public class EducationRepositoryAdapter implements EducationRepositoryPort { public class EducationRepositoryAdapter implements EducationRepositoryPort {
private final SpringDataEducationRepository repo; private static final EducationEntityMapper MAPPER = Mappers.getMapper(EducationEntityMapper.class);
private final EducationJpaMapper mapper;
public EducationRepositoryAdapter(SpringDataEducationRepository repo, EducationJpaMapper mapper) {
this.repo = repo;
this.mapper = mapper;
}
@Override @Override
public Education save(Education p) { public Education save(Education p) {
EducationJpaEntity entity = mapper.toEntity(p); EducationEntity entity = MAPPER.toEntity(p);
EducationJpaEntity saved = repo.save(entity); DB.save(entity);
return mapper.toDomain(saved); return MAPPER.toDomain(entity);
} }
@Override @Override
public Optional<Education> findById(EducationId id) { 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 @Override
public List<Education> findAll(Long profileId) { 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; 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.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.FetchType; import jakarta.persistence.FetchType;
@@ -17,7 +18,7 @@ import lombok.Setter;
@Table(name = "EDUCATION") @Table(name = "EDUCATION")
@Getter @Getter
@Setter @Setter
public class EducationJpaEntity { public class EducationEntity extends Model {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -25,7 +26,7 @@ public class EducationJpaEntity {
@ManyToOne(fetch = FetchType.LAZY) @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "profile_id", nullable = false) @JoinColumn(name = "profile_id", nullable = false)
private ProfileJpaEntity profile; private ProfileEntity profile;
@Column @Column
private String institution; private String institution;

View File

@@ -1,18 +1,17 @@
package com.pablotj.portfolio.infrastructure.persistence.education.mapper; package com.pablotj.portfolio.infrastructure.persistence.education.mapper;
import com.pablotj.portfolio.domain.education.Education; import com.pablotj.portfolio.domain.education.Education;
import com.pablotj.portfolio.domain.education.EducationId; import com.pablotj.portfolio.infrastructure.persistence.education.entity.EducationEntity;
import com.pablotj.portfolio.infrastructure.persistence.education.entity.EducationJpaEntity;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
@Mapper(componentModel = "spring") @Mapper
public interface EducationJpaMapper { public interface EducationEntityMapper {
@Mapping(target = "id", ignore = true) @Mapping(target = "id", ignore = true)
@Mapping(target = "profile.id", source = "id.profileId") @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()))") @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,49 @@ package com.pablotj.portfolio.infrastructure.persistence.experience.adapter;
import com.pablotj.portfolio.domain.experience.Experience; import com.pablotj.portfolio.domain.experience.Experience;
import com.pablotj.portfolio.domain.experience.ExperienceId; import com.pablotj.portfolio.domain.experience.ExperienceId;
import com.pablotj.portfolio.domain.experience.port.ExperienceRepositoryPort; import com.pablotj.portfolio.domain.experience.port.ExperienceRepositoryPort;
import com.pablotj.portfolio.infrastructure.persistence.experience.entity.ExperienceJpaEntity; import com.pablotj.portfolio.infrastructure.persistence.experience.entity.ExperienceEntity;
import com.pablotj.portfolio.infrastructure.persistence.experience.mapper.ExperienceJpaMapper; import com.pablotj.portfolio.infrastructure.persistence.experience.mapper.ExperienceEntityMapper;
import com.pablotj.portfolio.infrastructure.persistence.experience.repo.SpringDataExperienceRepository; import io.ebean.DB;
import jakarta.inject.Singleton;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import org.springframework.stereotype.Repository; import org.mapstruct.factory.Mappers;
@Repository @Singleton
public class ExperienceRepositoryAdapter implements ExperienceRepositoryPort { public class ExperienceRepositoryAdapter implements ExperienceRepositoryPort {
private final SpringDataExperienceRepository repo; private static final ExperienceEntityMapper MAPPER = Mappers.getMapper(ExperienceEntityMapper.class);
private final ExperienceJpaMapper mapper;
public ExperienceRepositoryAdapter(SpringDataExperienceRepository repo, ExperienceJpaMapper mapper) {
this.repo = repo;
this.mapper = mapper;
}
@Override @Override
public Experience save(Experience p) { public Experience save(Experience p) {
ExperienceJpaEntity entity = mapper.toEntity(p); ExperienceEntity entity = MAPPER.toEntity(p);
ExperienceJpaEntity saved = repo.save(entity); DB.save(entity);
return mapper.toDomain(saved); return MAPPER.toDomain(entity);
} }
@Override @Override
public Optional<Experience> findById(ExperienceId id) { public Optional<Experience> findById(ExperienceId id) {
return repo.findByProfileIdAndId(id.profileId(), id.experienceId()).map(mapper::toDomain); ExperienceEntity entity = DB.find(ExperienceEntity.class)
.fetch("technologies")
.fetch("achievements")
.where()
.eq("profile.id", id.profileId())
.eq("id", id.experienceId())
.findOne();
return Optional.ofNullable(entity).map(MAPPER::toDomain);
} }
@Override @Override
public List<Experience> findAll(Long profileId) { 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; package com.pablotj.portfolio.infrastructure.persistence.experience.entity;
import io.ebean.Model;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
@@ -20,7 +21,7 @@ import lombok.Setter;
@NoArgsConstructor(access = AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE) @AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder @Builder
public class ExperienceAchievementJpaEntity { public class ExperienceAchievementEntity extends Model {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)

View File

@@ -1,6 +1,7 @@
package com.pablotj.portfolio.infrastructure.persistence.experience.entity; 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.CascadeType;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
@@ -27,7 +28,7 @@ import lombok.Setter;
@NoArgsConstructor(access = AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE) @AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder @Builder
public class ExperienceJpaEntity { public class ExperienceEntity extends Model {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -35,7 +36,7 @@ public class ExperienceJpaEntity {
@ManyToOne(fetch = FetchType.LAZY) @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "profile_id", nullable = false) @JoinColumn(name = "profile_id", nullable = false)
private ProfileJpaEntity profile; private ProfileEntity profile;
@Column @Column
private String position; private String position;
@@ -52,11 +53,12 @@ public class ExperienceJpaEntity {
@Column(columnDefinition = "text") @Column(columnDefinition = "text")
private String description; private String description;
// Ebean soporta perfectamente CascadeType.ALL y orphanRemoval
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "EXPERIENCE_ID") @JoinColumn(name = "EXPERIENCE_ID")
private List<ExperienceSkillJpaEntity> technologies; private List<ExperienceSkillEntity> technologies;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "EXPERIENCE_ID") @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; package com.pablotj.portfolio.infrastructure.persistence.experience.entity;
import io.ebean.Model;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType; import jakarta.persistence.GenerationType;
@@ -19,7 +20,7 @@ import lombok.Setter;
@NoArgsConstructor(access = AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE) @AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder @Builder
public class ExperienceSkillJpaEntity { public class ExperienceSkillEntity extends Model {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @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.Achievement;
import com.pablotj.portfolio.domain.experience.Experience; import com.pablotj.portfolio.domain.experience.Experience;
import com.pablotj.portfolio.domain.experience.Technology; import com.pablotj.portfolio.domain.experience.Technology;
import com.pablotj.portfolio.infrastructure.persistence.experience.entity.ExperienceAchievementJpaEntity; import com.pablotj.portfolio.infrastructure.persistence.experience.entity.ExperienceAchievementEntity;
import com.pablotj.portfolio.infrastructure.persistence.experience.entity.ExperienceJpaEntity; import com.pablotj.portfolio.infrastructure.persistence.experience.entity.ExperienceEntity;
import com.pablotj.portfolio.infrastructure.persistence.experience.entity.ExperienceSkillJpaEntity; import com.pablotj.portfolio.infrastructure.persistence.experience.entity.ExperienceSkillEntity;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
@Mapper(componentModel = "spring") @Mapper
public interface ExperienceJpaMapper { public interface ExperienceEntityMapper {
@Mapping(target = "id", ignore = true) @Mapping(target = "id", ignore = true)
@Mapping(target = "profile.id", source = "id.profileId") @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()))") @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) @Mapping(target = "id", ignore = true)
ExperienceAchievementJpaEntity toEntity(Achievement entity); ExperienceAchievementEntity toEntity(Achievement entity);
@Mapping(target = "id.value", source = "id") @Mapping(target = "id.value", source = "id")
Achievement toDomain(ExperienceAchievementJpaEntity entity); Achievement toDomain(ExperienceAchievementEntity entity);
@Mapping(target = "id", ignore = true) @Mapping(target = "id", ignore = true)
ExperienceSkillJpaEntity toEntity(Technology entity); ExperienceSkillEntity toEntity(Technology entity);
@Mapping(target = "id.value", source = "id") @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,56 @@ package com.pablotj.portfolio.infrastructure.persistence.profile.adapter;
import com.pablotj.portfolio.domain.profile.Profile; import com.pablotj.portfolio.domain.profile.Profile;
import com.pablotj.portfolio.domain.profile.ProfileId; import com.pablotj.portfolio.domain.profile.ProfileId;
import com.pablotj.portfolio.domain.profile.port.ProfileRepositoryPort; 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.ProfileEntity;
import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileSocialLinkJpaEntity; import com.pablotj.portfolio.infrastructure.persistence.profile.mapper.ProfileEntityMapper;
import com.pablotj.portfolio.infrastructure.persistence.profile.mapper.ProfileJpaMapper; import io.ebean.DB;
import com.pablotj.portfolio.infrastructure.persistence.profile.repo.SpringDataProfileRepository; import jakarta.inject.Singleton;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import org.springframework.stereotype.Repository; import org.mapstruct.factory.Mappers;
@Repository @Singleton
public class ProfileRepositoryAdapter implements ProfileRepositoryPort { public class ProfileRepositoryAdapter implements ProfileRepositoryPort {
private final SpringDataProfileRepository repo; private static final ProfileEntityMapper MAPPER = Mappers.getMapper(ProfileEntityMapper.class);
private final ProfileJpaMapper mapper;
public ProfileRepositoryAdapter(SpringDataProfileRepository repo, ProfileJpaMapper mapper) {
this.repo = repo;
this.mapper = mapper;
}
@Override @Override
public Profile save(Profile p) { public Profile save(Profile p) {
ProfileJpaEntity entity = mapper.toEntity(p); ProfileEntity entity = MAPPER.toEntity(p);
if (entity.getSocial() != null) { DB.save(entity);
for (ProfileSocialLinkJpaEntity socialLinkJpaEntity : entity.getSocial()) {
socialLinkJpaEntity.setProfile(entity); return MAPPER.toDomain(entity);
}
}
ProfileJpaEntity saved = repo.save(entity);
return mapper.toDomain(saved);
} }
@Override @Override
public Optional<Profile> findBySlug(ProfileId id) { public Optional<Profile> findBySlug(ProfileId id) {
return repo.findBySlug(id.slug()).map(mapper::toDomain); ProfileEntity entity = DB.find(ProfileEntity.class)
.fetch("social")
.where()
.eq("slug", id.slug())
.findOne();
return Optional.ofNullable(entity).map(MAPPER::toDomain);
} }
@Override @Override
public Optional<Profile> findById(ProfileId id) { 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 @Override
public List<Profile> findAll() { 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; package com.pablotj.portfolio.infrastructure.persistence.profile.entity;
import io.ebean.Model;
import jakarta.persistence.CascadeType; import jakarta.persistence.CascadeType;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
@@ -17,7 +18,7 @@ import lombok.Setter;
@Table(name = "profile") @Table(name = "profile")
@Getter @Getter
@Setter @Setter
public class ProfileJpaEntity { public class ProfileEntity extends Model {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -47,10 +48,10 @@ public class ProfileJpaEntity {
@Column @Column
private String avatar; private String avatar;
@Column @Column(columnDefinition = "text")
private String bio; private String bio;
@OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true) @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; package com.pablotj.portfolio.infrastructure.persistence.profile.entity;
import io.ebean.Model;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.FetchType; import jakarta.persistence.FetchType;
@@ -16,14 +17,15 @@ import lombok.Setter;
@Table(name = "profile_social_link") @Table(name = "profile_social_link")
@Getter @Getter
@Setter @Setter
public class ProfileSocialLinkJpaEntity { public class ProfileSocialLinkEntity extends Model {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private Long id;
@ManyToOne(fetch = FetchType.LAZY) @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "profile_id", nullable = false) @JoinColumn(name = "profile_id", nullable = false)
private ProfileJpaEntity profile; private ProfileEntity profile;
@Column @Column
private String url; 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.Profile;
import com.pablotj.portfolio.domain.profile.ProfileSocialLink; import com.pablotj.portfolio.domain.profile.ProfileSocialLink;
import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileJpaEntity; import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileEntity;
import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileSocialLinkJpaEntity; import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileSocialLinkEntity;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
@Mapper(componentModel = "spring") @Mapper
public interface ProfileJpaMapper { public interface ProfileEntityMapper {
@Mapping(target = "id", ignore = true) @Mapping(target = "id", ignore = true)
@Mapping(target = "social", source = "social") @Mapping(target = "social", source = "social")
ProfileJpaEntity toEntity(Profile domain); ProfileEntity toEntity(Profile domain);
@Mapping(target = "id.id", source = "id") @Mapping(target = "id.id", source = "id")
@Mapping(target = "social", source = "social") @Mapping(target = "social", source = "social")
Profile toDomain(ProfileJpaEntity e); Profile toDomain(ProfileEntity e);
@Mapping(target = "id", ignore = true) @Mapping(target = "id", ignore = true)
ProfileSocialLinkJpaEntity toEntitySocial(ProfileSocialLink entity); ProfileSocialLinkEntity toEntitySocial(ProfileSocialLink entity);
@Mapping(target = "id.value", source = "id") @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,50 @@ package com.pablotj.portfolio.infrastructure.persistence.project.adapter;
import com.pablotj.portfolio.domain.project.Project; import com.pablotj.portfolio.domain.project.Project;
import com.pablotj.portfolio.domain.project.ProjectId; import com.pablotj.portfolio.domain.project.ProjectId;
import com.pablotj.portfolio.domain.project.port.ProjectRepositoryPort; import com.pablotj.portfolio.domain.project.port.ProjectRepositoryPort;
import com.pablotj.portfolio.infrastructure.persistence.project.entity.ProjectJpaEntity; import com.pablotj.portfolio.infrastructure.persistence.project.entity.ProjectEntity;
import com.pablotj.portfolio.infrastructure.persistence.project.mapper.ProjectJpaMapper; import com.pablotj.portfolio.infrastructure.persistence.project.mapper.ProjectEntityMapper;
import com.pablotj.portfolio.infrastructure.persistence.project.repo.SpringDataProjectRepository; import io.ebean.DB;
import org.springframework.stereotype.Repository; import jakarta.inject.Singleton;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import org.mapstruct.factory.Mappers;
@Repository @Singleton
public class ProjectRepositoryAdapter implements ProjectRepositoryPort { public class ProjectRepositoryAdapter implements ProjectRepositoryPort {
private final SpringDataProjectRepository repo; private static final ProjectEntityMapper MAPPER = Mappers.getMapper(ProjectEntityMapper.class);
private final ProjectJpaMapper mapper;
public ProjectRepositoryAdapter(SpringDataProjectRepository repo, ProjectJpaMapper mapper) {
this.repo = repo;
this.mapper = mapper;
}
@Override @Override
public Project save(Project p) { public Project save(Project p) {
ProjectJpaEntity entity = mapper.toEntity(p); ProjectEntity entity = MAPPER.toEntity(p);
ProjectJpaEntity saved = repo.save(entity); DB.save(entity);
return mapper.toDomain(saved); return MAPPER.toDomain(entity);
} }
@Override @Override
public Optional<Project> findById(ProjectId id) { 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 @Override
public List<Project> findAll(Long profileId) { 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; 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.CascadeType;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
@@ -18,8 +19,9 @@ import lombok.Setter;
@Entity @Entity
@Table(name = "PROJECT") @Table(name = "PROJECT")
@Getter @Setter @Getter
public class ProjectJpaEntity { @Setter
public class ProjectEntity extends Model {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -27,12 +29,12 @@ public class ProjectJpaEntity {
@ManyToOne(fetch = FetchType.LAZY) @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "profile_id", nullable = false) @JoinColumn(name = "profile_id", nullable = false)
private ProfileJpaEntity profile; private ProfileEntity profile;
@Column @Column
private String title; private String title;
@Column @Column(columnDefinition = "text")
private String description; private String description;
@Column @Column
@@ -40,11 +42,11 @@ public class ProjectJpaEntity {
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "PROJECT_ID") @JoinColumn(name = "PROJECT_ID")
private List<ProjectTechnologyJpaEntity> technologies; private List<ProjectTechnologyEntity> technologies;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "PROJECT_ID") @JoinColumn(name = "PROJECT_ID")
private List<ProjectFeatureJpaEntity> features; private List<ProjectFeatureEntity> features;
@Column @Column
private String demo; private String demo;

View File

@@ -1,5 +1,6 @@
package com.pablotj.portfolio.infrastructure.persistence.project.entity; package com.pablotj.portfolio.infrastructure.persistence.project.entity;
import io.ebean.Model;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
@@ -13,7 +14,7 @@ import lombok.Setter;
@Table(name = "PROJECT_FEATURE") @Table(name = "PROJECT_FEATURE")
@Getter @Getter
@Setter @Setter
public class ProjectFeatureJpaEntity { public class ProjectFeatureEntity extends Model {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)

View File

@@ -1,5 +1,6 @@
package com.pablotj.portfolio.infrastructure.persistence.project.entity; package com.pablotj.portfolio.infrastructure.persistence.project.entity;
import io.ebean.Model;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
@@ -13,7 +14,7 @@ import lombok.Setter;
@Table(name = "PROJECT_FEATURE_TECHNOLOGY") @Table(name = "PROJECT_FEATURE_TECHNOLOGY")
@Getter @Getter
@Setter @Setter
public class ProjectTechnologyJpaEntity { public class ProjectTechnologyEntity extends Model {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @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.Project;
import com.pablotj.portfolio.domain.project.ProjectFeature; import com.pablotj.portfolio.domain.project.ProjectFeature;
import com.pablotj.portfolio.domain.project.ProjectTechnology; import com.pablotj.portfolio.domain.project.ProjectTechnology;
import com.pablotj.portfolio.infrastructure.persistence.project.entity.ProjectFeatureJpaEntity; import com.pablotj.portfolio.infrastructure.persistence.project.entity.ProjectEntity;
import com.pablotj.portfolio.infrastructure.persistence.project.entity.ProjectJpaEntity; import com.pablotj.portfolio.infrastructure.persistence.project.entity.ProjectFeatureEntity;
import com.pablotj.portfolio.infrastructure.persistence.project.entity.ProjectTechnologyJpaEntity; import com.pablotj.portfolio.infrastructure.persistence.project.entity.ProjectTechnologyEntity;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
@Mapper(componentModel = "spring") @Mapper
public interface ProjectJpaMapper { public interface ProjectEntityMapper {
@Mapping(target = "id", ignore = true) @Mapping(target = "id", ignore = true)
@Mapping(target = "profile.id", source = "id.profileId") @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()))") @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) @Mapping(target = "id", ignore = true)
ProjectTechnologyJpaEntity toEntity(ProjectTechnology entity); ProjectTechnologyEntity toEntity(ProjectTechnology entity);
@Mapping(target = "id.value", source = "id") @Mapping(target = "id.value", source = "id")
ProjectTechnology toDomain(ProjectTechnologyJpaEntity entity); ProjectTechnology toDomain(ProjectTechnologyEntity entity);
@Mapping(target = "id", ignore = true) @Mapping(target = "id", ignore = true)
ProjectFeatureJpaEntity toEntity(ProjectFeature entity); ProjectFeatureEntity toEntity(ProjectFeature entity);
@Mapping(target = "id.value", source = "id") @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,47 @@ package com.pablotj.portfolio.infrastructure.persistence.skill.adapter;
import com.pablotj.portfolio.domain.skill.SkillGroup; import com.pablotj.portfolio.domain.skill.SkillGroup;
import com.pablotj.portfolio.domain.skill.SkillGroupId; import com.pablotj.portfolio.domain.skill.SkillGroupId;
import com.pablotj.portfolio.domain.skill.port.SkillRepositoryPort; import com.pablotj.portfolio.domain.skill.port.SkillRepositoryPort;
import com.pablotj.portfolio.infrastructure.persistence.skill.entity.SkillGroupJpaEntity; import com.pablotj.portfolio.infrastructure.persistence.skill.entity.SkillGroupEntity;
import com.pablotj.portfolio.infrastructure.persistence.skill.mapper.SkillJpaMapper; import com.pablotj.portfolio.infrastructure.persistence.skill.mapper.SkillMapper;
import com.pablotj.portfolio.infrastructure.persistence.skill.repo.SpringDataSkillRepository; import io.ebean.DB;
import jakarta.inject.Singleton;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import org.springframework.stereotype.Repository; import org.mapstruct.factory.Mappers;
@Repository @Singleton
public class SkillRepositoryAdapter implements SkillRepositoryPort { public class SkillRepositoryAdapter implements SkillRepositoryPort {
private final SpringDataSkillRepository repo; private static final SkillMapper MAPPER = Mappers.getMapper(SkillMapper.class);
private final SkillJpaMapper mapper;
public SkillRepositoryAdapter(SpringDataSkillRepository repo, SkillJpaMapper mapper) {
this.repo = repo;
this.mapper = mapper;
}
@Override @Override
public SkillGroup save(SkillGroup p) { public SkillGroup save(SkillGroup p) {
SkillGroupJpaEntity entity = mapper.toEntity(p); SkillGroupEntity entity = MAPPER.toEntity(p);
SkillGroupJpaEntity saved = repo.save(entity); DB.save(entity);
return mapper.toDomain(saved); return MAPPER.toDomain(entity);
} }
@Override @Override
public Optional<SkillGroup> findById(SkillGroupId id) { 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 @Override
public List<SkillGroup> findAll(Long profileId) { 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; package com.pablotj.portfolio.infrastructure.persistence.skill.entity;
import io.ebean.Model;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
@@ -13,7 +14,7 @@ import lombok.Setter;
@Table(name = "SKILL") @Table(name = "SKILL")
@Getter @Getter
@Setter @Setter
public class SkillJpaEntity { public class SkillEntity extends Model {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)

View File

@@ -1,6 +1,7 @@
package com.pablotj.portfolio.infrastructure.persistence.skill.entity; 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.CascadeType;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
@@ -20,7 +21,7 @@ import lombok.Setter;
@Table(name = "SKILL_GROUP") @Table(name = "SKILL_GROUP")
@Getter @Getter
@Setter @Setter
public class SkillGroupJpaEntity { public class SkillGroupEntity extends Model {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -28,7 +29,7 @@ public class SkillGroupJpaEntity {
@ManyToOne(fetch = FetchType.LAZY) @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "profile_id", nullable = false) @JoinColumn(name = "profile_id", nullable = false)
private ProfileJpaEntity profile; private ProfileEntity profile;
@Column @Column
private String name; private String name;
@@ -38,5 +39,5 @@ public class SkillGroupJpaEntity {
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "SKILL_ID") @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.Skill;
import com.pablotj.portfolio.domain.skill.SkillGroup; import com.pablotj.portfolio.domain.skill.SkillGroup;
import com.pablotj.portfolio.infrastructure.persistence.skill.entity.SkillGroupJpaEntity; import com.pablotj.portfolio.infrastructure.persistence.skill.entity.SkillEntity;
import com.pablotj.portfolio.infrastructure.persistence.skill.entity.SkillJpaEntity; import com.pablotj.portfolio.infrastructure.persistence.skill.entity.SkillGroupEntity;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
@Mapper(componentModel = "spring") @Mapper
public interface SkillJpaMapper { public interface SkillMapper {
@Mapping(target = "id", ignore = true) @Mapping(target = "id", ignore = true)
@Mapping(target = "profile.id", source = "id.profileId") @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()))") @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) @Mapping(target = "id", ignore = true)
SkillJpaEntity toEntity(Skill entity); SkillEntity toEntity(Skill entity);
@Mapping(target = "id.value", source = "id") @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; package com.pablotj.portfolio.infrastructure.rest.api;
import org.springframework.boot.web.error.ErrorAttributeOptions; import io.jooby.ErrorHandler;
import org.springframework.boot.web.servlet.error.ErrorAttributes; import java.util.LinkedHashMap;
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 java.util.Map; import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Controller public class ApiErrorController {
public class ApiErrorController implements ErrorController {
private final ErrorAttributes errorAttributes; private static final Logger log = LoggerFactory.getLogger(ApiErrorController.class);
public ApiErrorController(ErrorAttributes errorAttributes) { public static ErrorHandler getHandler() {
this.errorAttributes = errorAttributes; return (ctx, cause, statusCode) -> {
} log.error("Error en la API: {}", cause.getMessage(), cause);
@RequestMapping("/error") Map<String, Object> errorAttributes = new LinkedHashMap<>();
public ResponseEntity<Map<String, Object>> handleError(WebRequest webRequest) { errorAttributes.put("timestamp", System.currentTimeMillis());
Map<String, Object> attributes = errorAttributes.getErrorAttributes(webRequest, errorAttributes.put("status", statusCode.value());
ErrorAttributeOptions.defaults()); errorAttributes.put("error", statusCode.reason());
HttpStatus status = HttpStatus.valueOf((int) attributes.getOrDefault("status", 500)); errorAttributes.put("message", cause.getMessage());
return new ResponseEntity<>(attributes, status); errorAttributes.put("path", ctx.getRequestPath());
ctx.setResponseCode(statusCode)
.render(errorAttributes);
};
} }
} }

View File

@@ -1,39 +1,39 @@
package com.pablotj.portfolio.infrastructure.rest.api; 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.List;
import java.util.Map; 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 @Path("/")
@RequestMapping
public class ApiRootController { public class ApiRootController {
@Value("${info.app.version}") private final String appVersion;
private String appVersion;
@GetMapping @Inject
public ResponseEntity<Map<String, Object>> root() { public ApiRootController(@Named("info.app.version") String appVersion) {
Map<String, Object> response = Map.of( this.appVersion = appVersion;
}
@GET
public Map<String, Object> root() {
return Map.of(
"api", "Portfolio API", "api", "Portfolio API",
"version", appVersion, "version", appVersion,
"doc", "/v3/api-docs", "doc", "/v3/api-docs",
"swagger", "/swagger-ui", "swagger", "/swagger-ui",
"endpoints", List.of( "endpoints", List.of(
Map.of("path", "/v1/homes", "description", "Manage projects"), Map.of("path", "/v1/homes", "description", "Manage home details"),
Map.of("path", "/v1/certifications", "description", "Manage projects"), Map.of("path", "/v1/certifications", "description", "Manage certifications"),
Map.of("path", "/v1/projects", "description", "Manage projects"), Map.of("path", "/v1/projects", "description", "Manage projects"),
Map.of("path", "/v1/contacts", "description", "Manage projects"), Map.of("path", "/v1/contacts", "description", "Manage contact info"),
Map.of("path", "/v1/educations", "description", "Manage projects"), Map.of("path", "/v1/educations", "description", "Manage education"),
Map.of("path", "/v1/experiences", "description", "Manage projects"), Map.of("path", "/v1/experiences", "description", "Manage experience"),
Map.of("path", "/v1/projects", "description", "Manage projects"), Map.of("path", "/v1/skills", "description", "Manage skills"),
Map.of("path", "/v1/technologies", "description", "ProfileSocialLink entries") 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.CertificationDto;
import com.pablotj.portfolio.infrastructure.rest.certification.dto.CreateCertificationRequest; import com.pablotj.portfolio.infrastructure.rest.certification.dto.CreateCertificationRequest;
import com.pablotj.portfolio.infrastructure.rest.certification.mapper.CertificationRestMapper; 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 jakarta.validation.Valid;
import java.net.URI;
import java.util.List; import java.util.List;
import org.springframework.http.ResponseEntity; import org.mapstruct.factory.Mappers;
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;
@RestController @Path("/v1/profiles/{profileId}/certifications")
@RequestMapping("/v1/profiles/{profileId}/certifications")
public class CertificationController { public class CertificationController {
private static final CertificationRestMapper MAPPER = Mappers.getMapper(CertificationRestMapper.class);
private final CreateCertificationUseCase createUC; private final CreateCertificationUseCase createUC;
private final GetCertificationUseCase getUC; 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.createUC = createUC;
this.getUC = getUC; this.getUC = getUC;
this.mapper = mapper;
} }
@GetMapping @GET
public List<CertificationDto> all(@PathVariable Long profileId) { public List<CertificationDto> all(@PathParam Long profileId) {
return getUC.all(profileId).stream().map(mapper::toDto).toList(); return getUC.all(profileId).stream()
.map(MAPPER::toDto)
.toList();
} }
@GetMapping("/{id}") @GET("/{id}")
public ResponseEntity<CertificationDto> byId(@PathVariable Long profileId, @PathVariable Long 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) return getUC.byId(profileId, id)
.map(mapper::toDto) .map(MAPPER::toDto)
.map(ResponseEntity::ok) .orElseThrow(() -> new io.jooby.exception.NotFoundException("Certification not found"));
.orElse(ResponseEntity.notFound().build());
} }
@PostMapping @POST
public ResponseEntity<CertificationDto> create(@PathVariable Long profileId, @Valid @RequestBody CreateCertificationRequest request) { public CertificationDto create(@PathParam Long profileId, @Valid CreateCertificationRequest request) {
var cmd = new CreateCertificationUseCase.Command( var cmd = new CreateCertificationUseCase.Command(
request.name(), request.name(),
request.issuer(), request.issuer(),
request.date(), request.date(),
request.credentialId() request.credentialId()
); );
var created = createUC.handle(profileId, cmd); var created = createUC.handle(profileId, cmd);
var body = mapper.toDto(created); return MAPPER.toDto(created);
return ResponseEntity.created(URI.create("/api/certifications/" + body.id())).body(body);
} }
} }

View File

@@ -5,7 +5,7 @@ import com.pablotj.portfolio.infrastructure.rest.certification.dto.Certification
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
@Mapper(componentModel = "spring") @Mapper
public interface CertificationRestMapper { public interface CertificationRestMapper {
@Mapping(target = "id", source = "id.certificationId") @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.CreateEducationRequest;
import com.pablotj.portfolio.infrastructure.rest.education.dto.EducationDto; import com.pablotj.portfolio.infrastructure.rest.education.dto.EducationDto;
import com.pablotj.portfolio.infrastructure.rest.education.mapper.EducationRestMapper; 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 jakarta.validation.Valid;
import java.net.URI;
import java.util.List; import java.util.List;
import org.springframework.http.ResponseEntity; import org.mapstruct.factory.Mappers;
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;
@RestController @Path("/v1/profiles/{profileId}/education")
@RequestMapping("/v1/profiles/{profileId}/education")
public class EducationController { public class EducationController {
private static final EducationRestMapper MAPPER = Mappers.getMapper(EducationRestMapper.class);
private final CreateEducationUseCase createUC; private final CreateEducationUseCase createUC;
private final GetEducationUseCase getUC; 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.createUC = createUC;
this.getUC = getUC; this.getUC = getUC;
this.mapper = mapper;
} }
@GetMapping @GET
public List<EducationDto> all(@PathVariable Long profileId) { public List<EducationDto> all(@PathParam Long profileId) {
return getUC.all(profileId).stream().map(mapper::toDto).toList(); return getUC.all(profileId).stream()
.map(MAPPER::toDto)
.toList();
} }
@GetMapping("/{id}") @GET("/{id}")
public ResponseEntity<EducationDto> byId(@PathVariable Long profileId, @PathVariable Long id) { public EducationDto byId(@PathParam Long profileId, @PathParam Long id) {
return getUC.byId(profileId, id) return getUC.byId(profileId, id)
.map(mapper::toDto) .map(MAPPER::toDto)
.map(ResponseEntity::ok) .orElseThrow(() -> new NotFoundException("Education record not found"));
.orElse(ResponseEntity.notFound().build());
} }
@PostMapping @POST
public ResponseEntity<EducationDto> create(@PathVariable Long profileId, @Valid @RequestBody CreateEducationRequest request) { public EducationDto create(@PathParam Long profileId, @Valid CreateEducationRequest request) {
var cmd = new CreateEducationUseCase.Command( var cmd = new CreateEducationUseCase.Command(
request.institution(), request.institution(),
request.degree(), request.degree(),
@@ -52,8 +51,8 @@ public class EducationController {
request.grade(), request.grade(),
request.description() request.description()
); );
var created = createUC.handle(profileId, cmd); var created = createUC.handle(profileId, cmd);
var body = mapper.toDto(created); return MAPPER.toDto(created);
return ResponseEntity.created(URI.create("/api/educations/" + body.id())).body(body);
} }
} }

View File

@@ -5,7 +5,7 @@ import com.pablotj.portfolio.infrastructure.rest.education.dto.EducationDto;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
@Mapper(componentModel = "spring") @Mapper
public interface EducationRestMapper { public interface EducationRestMapper {
@Mapping(target = "id", source = "id.educationId") @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.CreateExperienceRequest;
import com.pablotj.portfolio.infrastructure.rest.experience.dto.ExperienceDto; import com.pablotj.portfolio.infrastructure.rest.experience.dto.ExperienceDto;
import com.pablotj.portfolio.infrastructure.rest.experience.mapper.ExperienceRestMapper; 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 jakarta.validation.Valid;
import java.net.URI;
import java.util.List; import java.util.List;
import org.springframework.http.ResponseEntity; import org.mapstruct.factory.Mappers;
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;
@RestController @Path("/v1/profiles/{profileId}/experience")
@RequestMapping("/v1/profiles/{profileId}/experience")
public class ExperienceController { public class ExperienceController {
private static final ExperienceRestMapper MAPPER = Mappers.getMapper(ExperienceRestMapper.class);
private final CreateExperienceUseCase createUC; private final CreateExperienceUseCase createUC;
private final GetExperienceUseCase getUC; 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.createUC = createUC;
this.getUC = getUC; this.getUC = getUC;
this.mapper = mapper;
} }
@GetMapping @GET
public List<ExperienceDto> all(@PathVariable Long profileId) { public List<ExperienceDto> all(@PathParam Long profileId) {
return getUC.all(profileId).stream().map(mapper::toDto).toList(); return getUC.all(profileId).stream()
.map(MAPPER::toDto)
.toList();
} }
@GetMapping("/{id}") @GET("/{id}")
public ResponseEntity<ExperienceDto> byId(@PathVariable Long profileId, @PathVariable Long id) { public ExperienceDto byId(@PathParam Long profileId, @PathParam Long id) {
return getUC.byId(profileId, id) return getUC.byId(profileId, id)
.map(mapper::toDto) .map(MAPPER::toDto)
.map(ResponseEntity::ok) .orElseThrow(() -> new NotFoundException("Experience not found"));
.orElse(ResponseEntity.notFound().build());
} }
@PostMapping @POST
public ResponseEntity<ExperienceDto> create(@PathVariable Long profileId, @Valid @RequestBody CreateExperienceRequest request) { public ExperienceDto create(@PathParam Long profileId, @Valid CreateExperienceRequest request) {
var cmd = new CreateExperienceUseCase.Command( var cmd = new CreateExperienceUseCase.Command(
request.company(), request.company(),
request.position(), request.position(),
@@ -54,8 +53,8 @@ public class ExperienceController {
request.technologies(), request.technologies(),
request.achievements() request.achievements()
); );
var created = createUC.handle(profileId, cmd); var created = createUC.handle(profileId, cmd);
var body = mapper.toDto(created); return MAPPER.toDto(created);
return ResponseEntity.created(URI.create("/api/experiences/" + body.id())).body(body);
} }
} }

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ import java.util.List;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
@Mapper(componentModel = "spring") @Mapper
public interface ExperienceRestMapper { public interface ExperienceRestMapper {
@Mapping(target = "id", source = "id.experienceId") @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.ProfileCreateRequest;
import com.pablotj.portfolio.infrastructure.rest.profile.dto.ProfileDto; import com.pablotj.portfolio.infrastructure.rest.profile.dto.ProfileDto;
import com.pablotj.portfolio.infrastructure.rest.profile.mapper.ProfileRestMapper; 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 jakarta.validation.Valid;
import java.net.URI;
import java.util.List; import java.util.List;
import org.springframework.http.ResponseEntity; import org.mapstruct.factory.Mappers;
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;
@RestController @Path("/v1/profiles")
@RequestMapping("/v1/profiles")
public class ProfileController { public class ProfileController {
private static final ProfileRestMapper MAPPER = Mappers.getMapper(ProfileRestMapper.class);
private final CreateProfileUseCase createUC; private final CreateProfileUseCase createUC;
private final GetProfileUseCase getUC; 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.createUC = createUC;
this.getUC = getUC; this.getUC = getUC;
this.mapper = mapper;
} }
@GET
@GetMapping
public List<ProfileDto> all() { public List<ProfileDto> all() {
return getUC.all().stream().map(mapper::toDto).toList(); return getUC.all().stream()
.map(MAPPER::toDto)
.toList();
} }
@GetMapping("/{slug}") @GET("/{slug}")
public ResponseEntity<ProfileDto> byId(@PathVariable String slug) { public ProfileDto byId(@PathParam String slug) {
return getUC.bySlug(slug) return getUC.bySlug(slug)
.map(mapper::toDto) .map(MAPPER::toDto)
.map(ResponseEntity::ok) .orElseThrow(() -> new NotFoundException("Profile not found"));
.orElse(ResponseEntity.notFound().build());
} }
@PostMapping @POST
public ResponseEntity<ProfileDto> create(@Valid @RequestBody ProfileCreateRequest request) { public ProfileDto create(@Valid ProfileCreateRequest request) {
var cmd = new CreateProfileUseCase.Command( var cmd = new CreateProfileUseCase.Command(
request.slug(), request.slug(),
request.name(), request.name(),
@@ -56,10 +54,12 @@ public class ProfileController {
request.location(), request.location(),
request.avatar(), request.avatar(),
request.bio(), 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 created = createUC.handle(cmd);
var body = mapper.toDto(created); return MAPPER.toDto(created);
return ResponseEntity.created(URI.create("/api/homes/" + body.id())).body(body);
} }
} }

View File

@@ -8,7 +8,7 @@ import java.util.List;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
@Mapper(componentModel = "spring") @Mapper
public interface ProfileRestMapper { public interface ProfileRestMapper {
@Mapping(target = "id", source = "id.id") @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.CreateProjectRequest;
import com.pablotj.portfolio.infrastructure.rest.project.dto.ProjectDto; import com.pablotj.portfolio.infrastructure.rest.project.dto.ProjectDto;
import com.pablotj.portfolio.infrastructure.rest.project.mapper.ProjectRestMapper; 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 jakarta.validation.Valid;
import java.net.URI;
import java.util.List; import java.util.List;
import org.springframework.http.ResponseEntity; import org.mapstruct.factory.Mappers;
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;
@RestController @Path("/v1/profiles/{profileId}/projects")
@RequestMapping("/v1/profiles/{profileId}/projects")
public class ProjectController { public class ProjectController {
private static final ProjectRestMapper MAPPER = Mappers.getMapper(ProjectRestMapper.class);
private final CreateProjectUseCase createUC; private final CreateProjectUseCase createUC;
private final GetProjectUseCase getUC; 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.createUC = createUC;
this.getUC = getUC; this.getUC = getUC;
this.mapper = mapper;
} }
@GetMapping @GET
public List<ProjectDto> all(@PathVariable Long profileId) { public List<ProjectDto> all(@PathParam Long profileId) {
return getUC.all(profileId).stream().map(mapper::toDto).toList(); return getUC.all(profileId).stream()
.map(MAPPER::toDto)
.toList();
} }
@GetMapping("/{id}") @GET("/{id}")
public ResponseEntity<ProjectDto> byId(@PathVariable Long profileId, @PathVariable Long id) { public ProjectDto byId(@PathParam Long profileId, @PathParam Long id) {
return getUC.byId(profileId, id) return getUC.byId(profileId, id)
.map(mapper::toDto) .map(MAPPER::toDto)
.map(ResponseEntity::ok) .orElseThrow(() -> new NotFoundException("Project not found"));
.orElse(ResponseEntity.notFound().build());
} }
@PostMapping @POST
public ResponseEntity<ProjectDto> create(@PathVariable Long profileId, @Valid @RequestBody CreateProjectRequest request) { public ProjectDto create(@PathParam Long profileId, @Valid CreateProjectRequest request) {
var cmd = new CreateProjectUseCase.Command( var cmd = new CreateProjectUseCase.Command(
request.title(), request.title(),
request.description(), request.description(),
@@ -54,8 +53,8 @@ public class ProjectController {
request.demo(), request.demo(),
request.repository() request.repository()
); );
var created = createUC.handle(profileId, cmd); var created = createUC.handle(profileId, cmd);
var body = mapper.toDto(created); return MAPPER.toDto(created);
return ResponseEntity.created(URI.create("/api/projects/" + body.id())).body(body);
} }
} }

View File

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

View File

@@ -8,7 +8,7 @@ import java.util.List;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
@Mapper(componentModel = "spring") @Mapper
public interface ProjectRestMapper { public interface ProjectRestMapper {
@Mapping(target = "id", source = "id.projectId") @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.CreateSkillGroupRequest;
import com.pablotj.portfolio.infrastructure.rest.skill.dto.SkillGroupDto; import com.pablotj.portfolio.infrastructure.rest.skill.dto.SkillGroupDto;
import com.pablotj.portfolio.infrastructure.rest.skill.mapper.SkillRestMapper; 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 jakarta.validation.Valid;
import java.net.URI;
import java.util.List; import java.util.List;
import org.springframework.http.ResponseEntity; import org.mapstruct.factory.Mappers;
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;
@RestController @Path("/v1/profiles/{profileId}/skills")
@RequestMapping("/v1/profiles/{profileId}/skills")
public class SkillController { public class SkillController {
private static final SkillRestMapper MAPPER = Mappers.getMapper(SkillRestMapper.class);
private final CreateSkillUseCase createUC; private final CreateSkillUseCase createUC;
private final GetSkillUseCase getUC; 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.createUC = createUC;
this.getUC = getUC; this.getUC = getUC;
this.mapper = mapper;
} }
@GetMapping @GET
public List<SkillGroupDto> all(@PathVariable Long profileId) { public List<SkillGroupDto> all(@PathParam Long profileId) {
return getUC.all(profileId).stream().map(mapper::toDto).toList(); return getUC.all(profileId).stream()
.map(MAPPER::toDto)
.toList();
} }
@GetMapping("/{id}") @GET("/{id}")
public ResponseEntity<SkillGroupDto> byId(@PathVariable Long profileId, @PathVariable Long id) { public SkillGroupDto byId(@PathParam Long profileId, @PathParam Long id) {
return getUC.byId(profileId, id) return getUC.byId(profileId, id)
.map(mapper::toDto) .map(MAPPER::toDto)
.map(ResponseEntity::ok) .orElseThrow(() -> new NotFoundException("Skill group not found"));
.orElse(ResponseEntity.notFound().build());
} }
@PostMapping @POST
public ResponseEntity<SkillGroupDto> create(@PathVariable Long profileId, @Valid @RequestBody CreateSkillGroupRequest request) { public SkillGroupDto create(@PathParam Long profileId, @Valid CreateSkillGroupRequest request) {
var cmd = new CreateSkillUseCase.CommandGroup( var cmd = new CreateSkillUseCase.CommandGroup(
request.name(), request.name(),
request.icon(), 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 created = createUC.handle(profileId, cmd);
var body = mapper.toDto(created); return MAPPER.toDto(created);
return ResponseEntity.created(URI.create("/api/skills/" + body.id())).body(body);
} }
} }

View File

@@ -7,7 +7,7 @@ import com.pablotj.portfolio.infrastructure.rest.skill.dto.SkillGroupDto;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
@Mapper(componentModel = "spring") @Mapper
public interface SkillRestMapper { public interface SkillRestMapper {
@Mapping(target = "id", source = "id.skillGroupId") @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

View File

@@ -0,0 +1,101 @@
# ========== Log4j2 Properties Configuration ==========
status = warn
name = JoobyPortfolioConfig
property.basePath = logs
# ========== Appenders ==========
# Console
appender.console.type = Console
appender.console.name = ConsoleAppender
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n
# Rolling File INFO (DEBUG e INFO)
appender.info.type = RollingFile
appender.info.name = InfoFileAppender
appender.info.fileName = ${basePath}/info.log
appender.info.filePattern = ${basePath}/info-%d{yyyy-MM-dd}-%i.log.gz
appender.info.layout.type = PatternLayout
appender.info.layout.pattern = [%d{yyyy-MM-dd HH:mm:ss}] [%t] %-5level %logger{36} - %msg%n
appender.info.policies.type = Policies
appender.info.policies.time.type = TimeBasedTriggeringPolicy
appender.info.policies.time.interval = 1
appender.info.policies.size.type = SizeBasedTriggeringPolicy
appender.info.policies.size.size = 10MB
appender.info.filter.type = LevelRangeFilter
appender.info.filter.levelMin = debug
appender.info.filter.levelMax = info
appender.info.filter.onMatch = ACCEPT
appender.info.filter.onMismatch = DENY
# Rolling File ERROR (ERROR y FATAL)
appender.error.type = RollingFile
appender.error.name = ErrorFileAppender
appender.error.fileName = ${basePath}/error.log
appender.error.filePattern = ${basePath}/error-%d{yyyy-MM-dd}-%i.log.gz
appender.error.layout.type = PatternLayout
appender.error.layout.pattern = [%d{yyyy-MM-dd HH:mm:ss}] [%t] %-5level %logger{36} - %msg%n
appender.error.policies.type = Policies
appender.error.policies.time.type = TimeBasedTriggeringPolicy
appender.error.policies.time.interval = 1
appender.error.policies.size.type = SizeBasedTriggeringPolicy
appender.error.policies.size.size = 10MB
appender.error.filter.threshold.type = ThresholdFilter
appender.error.filter.threshold.level = error
appender.error.filter.threshold.onMatch = ACCEPT
appender.error.filter.threshold.onMismatch = DENY
# Rolling File for Database logs (Ebean SQL)
appender.db.type = RollingFile
appender.db.name = DBFileAppender
appender.db.fileName = ${basePath}/db.log
appender.db.filePattern = ${basePath}/db-%d{yyyy-MM-dd}-%i.log.gz
appender.db.layout.type = PatternLayout
appender.db.layout.pattern = [%d{yyyy-MM-dd HH:mm:ss}] [%t] %-5level %logger{36} - %msg%n
appender.db.policies.type = Policies
appender.db.policies.time.type = TimeBasedTriggeringPolicy
appender.db.policies.time.interval = 1
appender.db.policies.size.type = SizeBasedTriggeringPolicy
appender.db.policies.size.size = 10MB
# ========== Loggers ==========
# App logs (Tu paquete base)
logger.app.name = com.pablotj
logger.app.level = debug
logger.app.additivity = false
logger.app.appenderRefs = console, info, error
logger.app.appenderRef.console.ref = ConsoleAppender
logger.app.appenderRef.info.ref = InfoFileAppender
logger.app.appenderRef.error.ref = ErrorFileAppender
# Ebean SQL (Sustituye a Hibernate SQL)
# io.ebean.SQL muestra las sentencias ejecutadas
logger.ebean.name = io.ebean.SQL
logger.ebean.level = debug
logger.ebean.additivity = false
logger.ebean.appenderRefs = db
logger.ebean.appenderRef.db.ref = DBFileAppender
# Ebean TX (Opcional: muestra transacciones)
logger.ebean_tx.name = io.ebean.TX
logger.ebean_tx.level = info
logger.ebean_tx.additivity = false
logger.ebean_tx.appenderRefs = db
logger.ebean_tx.appenderRef.db.ref = DBFileAppender
# Jooby Framework logs
logger.jooby.name = io.jooby
logger.jooby.level = info
logger.jooby.additivity = false
logger.jooby.appenderRefs = console
logger.jooby.appenderRef.console.ref = ConsoleAppender
# ========== Root Logger ==========
rootLogger.level = info
rootLogger.appenderRefs = console, info, error
rootLogger.appenderRef.console.ref = ConsoleAppender
rootLogger.appenderRef.info.ref = InfoFileAppender
rootLogger.appenderRef.error.ref = ErrorFileAppender

141
pom.xml
View File

@@ -1,73 +1,86 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" <project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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"> 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> <modelVersion>4.0.0</modelVersion>
<groupId>com.pablotj</groupId> <groupId>com.pablotj</groupId>
<artifactId>portfolio-api</artifactId> <artifactId>portfolio-api</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<modules> <modules>
<module>domain</module> <module>domain</module>
<module>application</module> <module>application</module>
<module>infrastructure</module> <module>infrastructure</module>
<module>bootstrap</module> <module>bootstrap</module>
</modules> </modules>
<parent> <properties>
<groupId>org.springframework.boot</groupId> <java.version>21</java.version>
<artifactId>spring-boot-starter-parent</artifactId> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<version>3.3.2</version> <jooby.version>3.6.1</jooby.version>
<relativePath/> <mapstruct.version>1.5.5.Final</mapstruct.version>
</parent> <lombok.version>1.18.32</lombok.version>
<jackson.version>2.17.2</jackson.version>
<slf4j.version>2.24.1</slf4j.version>
</properties>
<properties> <dependencyManagement>
<java.version>21</java.version> <dependencies>
<mapstruct.version>1.5.5.Final</mapstruct.version> <dependency>
<springdoc.version>2.6.0</springdoc.version> <groupId>io.jooby</groupId>
</properties> <artifactId>jooby-bom</artifactId>
<version>${jooby.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependencyManagement> <dependency>
<dependencies> <groupId>org.mapstruct</groupId>
<dependency> <artifactId>mapstruct</artifactId>
<groupId>org.mapstruct</groupId> <version>${mapstruct.version}</version>
<artifactId>mapstruct</artifactId> </dependency>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build> <dependency>
<pluginManagement> <groupId>io.jooby</groupId>
<plugins> <artifactId>jooby-apt</artifactId>
<plugin> <version>${jooby.version}</version>
<groupId>org.apache.maven.plugins</groupId> <scope>provided</scope>
<artifactId>maven-compiler-plugin</artifactId> </dependency>
<configuration> </dependencies>
<source>${java.version}</source> </dependencyManagement>
<target>${java.version}</target>
<annotationProcessorPaths> <build>
<path> <pluginManagement>
<groupId>org.projectlombok</groupId> <plugins>
<artifactId>lombok</artifactId> <plugin>
<version>1.18.32</version> <groupId>org.apache.maven.plugins</groupId>
</path> <artifactId>maven-compiler-plugin</artifactId>
<path> <version>3.13.0</version>
<groupId>org.mapstruct</groupId> <configuration>
<artifactId>mapstruct-processor</artifactId> <source>${java.version}</source>
<version>${mapstruct.version}</version> <target>${java.version}</target>
</path> <annotationProcessorPaths>
</annotationProcessorPaths> <path>
</configuration> <groupId>io.jooby</groupId>
</plugin> <artifactId>jooby-apt</artifactId>
</plugins> <version>${jooby.version}</version>
</pluginManagement> </path>
</build> <path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project> </project>