Compare commits

...

6 Commits

Author SHA1 Message Date
16f10e101a 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.
2026-03-03 08:49:55 +01:00
83070ccbda ci: implements jenkins deploy 2026-03-02 12:40:22 +01:00
4e72e6da77 ci: implements jenkins deploy 2026-02-21 10:44:49 +01:00
5a26b299f2 feat: move CORS configuration to Spring Security 2025-09-20 11:22:48 +02:00
6b3585da5e chore: remove active Spring profile from Dockerfile 2025-09-20 10:48:46 +02:00
e7b6712ad3 fix: correct Docker Compose network configuration 2025-09-20 10:30:19 +02:00
81 changed files with 1433 additions and 953 deletions

View File

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

3
.gitignore vendored
View File

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

View File

@@ -1,16 +1,51 @@
# Stage 1: Build
# --- Stage 1: Build & Dependencies ---
FROM maven:3.9-eclipse-temurin-21-alpine AS build
WORKDIR /app
# Copiamos todo el proyecto
COPY . .
# 1. Copiamos los archivos de configuración (Estructura de poms)
COPY pom.xml .
COPY application/pom.xml ./application/
COPY bootstrap/pom.xml ./bootstrap/
COPY domain/pom.xml ./domain/
COPY infrastructure/pom.xml ./infrastructure/
# Compilamos todo el proyecto (todos los módulos)
RUN mvn clean package -DskipTests
# 2. Descargamos dependencias (Cacheamos esta capa)
# Usamos install de los poms para que los módulos se reconozcan entre sí
RUN mvn dependency:go-offline -B
# Stage 2: Run
FROM openjdk:21-jdk
# 3. Copiamos el código fuente y compilamos
COPY application/src ./application/src
COPY bootstrap/src ./bootstrap/src
COPY domain/src ./domain/src
COPY infrastructure/src ./infrastructure/src
RUN mvn clean package -DskipTests -B
# --- Stage 2: Run (Imagen final ligera) ---
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /app/bootstrap/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005", "-Xmx512m", "-Xms256m", "-jar", "app.jar","--spring.profiles.active=prod"]
# Seguridad: Usuario no-root
RUN addgroup -S app && adduser -S app -G app
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)
COPY --from=build /app/bootstrap/target/bootstrap-*.jar app.jar
# Configuración de Memoria y Rendimiento para Microservicios
# -XX:+UseSerialGC: Menos consumo de RAM para apps < 1GB
# -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
ENV JAVA_OPTS="-XX:+UseContainerSupport \
-XX:MaxRAMPercentage=75.0 \
-XX:+UseSerialGC \
-XX:TieredStopAtLevel=1 \
-Xms128m"
EXPOSE 80
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

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.

77
Jenkinsfile vendored Normal file
View File

@@ -0,0 +1,77 @@
pipeline {
agent any
environment {
REGISTRY_URL = "registry.pablotj.com"
USER = "andromeda"
PASS = credentials('docker-registry-password')
}
stages {
stage('Prepare Workspace & Checkout') {
steps {
echo "Cleaning workspace"
deleteDir()
}
}
stage('Checkout') {
steps {
echo "Checking out repo..."
checkout scm
}
}
stage('Load Environment') {
steps {
echo "Loading .env secret from Jenkins..."
withCredentials([file(credentialsId: 'env', variable: 'SECRET_ENV')]) {
sh 'cp $SECRET_ENV .env'
}
}
}
stage('Build and Tag Docker Image') {
steps {
echo "Building Docker image..."
sh '''
make build
make tag
'''
}
}
stage('ush Docker Image') {
steps {
echo "Tagging and pushing Docker image to registry..."
sh '''
echo $PASS | docker login ${REGISTRY_URL} -u ${USER} --password-stdin
make push
'''
}
}
stage('Deploy Docker Container') {
steps {
echo "Stopping old container and running new container..."
sh '''
make stop
make run
'''
}
}
}
post {
success {
echo "✅ Deployment completed successfully!"
}
failure {
echo "❌ Pipeline failed!"
}
always {
deleteDir()
}
}
}

37
Makefile Normal file
View File

@@ -0,0 +1,37 @@
STACK=pablotj-portfolio
APP_NAME=pablotj-portfolio-api
IMAGE_NAME=$(APP_NAME)
REGISTRY_URL=registry.pablotj.com
NAMESPACE=andromeda
TAG?=latest
HOST_PORT=8181
CONTAINER_PORT=8080
IMAGE_FULL=$(REGISTRY_URL)/$(NAMESPACE)/$(IMAGE_NAME):$(TAG)
build:
docker build -t $(IMAGE_NAME):$(TAG) .
tag:
docker tag $(IMAGE_NAME):$(TAG) $(IMAGE_FULL)
push:
docker push $(IMAGE_FULL)
run:
docker run -d \
--name $(APP_NAME) \
--label com.docker.compose.service="$(APP_NAME)" \
--label com.docker.compose.project="$(STACK)" \
--network andromeda \
-p $(HOST_PORT):$(CONTAINER_PORT) \
--env-file .env \
$(IMAGE_FULL)
stop:
docker stop $(APP_NAME) || true
docker rm $(APP_NAME) || true
deploy: build tag push stop run

View File

@@ -12,9 +12,17 @@
<artifactId>domain</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<groupId>org.apache.logging.log4j</groupId>
<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>
</dependencies>
</project>

View File

@@ -33,13 +33,13 @@ public class CreateExperienceUseCase {
}
public record Command(
String position,
String company,
String period,
String location,
String description,
List<String> technologies,
List<String> achievements
String position,
String company,
String period,
String location,
String description,
List<String> technologies,
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.ProjectId;
import com.pablotj.portfolio.domain.project.port.ProjectRepositoryPort;
import java.util.List;
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>
<parent>
<groupId>com.pablotj</groupId>
<artifactId>portfolio-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>bootstrap</artifactId>
<dependencies>
@@ -14,56 +18,84 @@
<version>${project.version}</version>
</dependency>
<!-- Drivers DB -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</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>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<groupId>io.jooby</groupId>
<artifactId>jooby-maven-plugin</artifactId>
<version>${jooby.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
<goal>openapi</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>com.pablotj.portfolio.bootstrap.PortfolioApplication</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<id>jooby-shade</id>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<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>
</plugins>
</build>

View File

@@ -1,16 +1,67 @@
package com.pablotj.portfolio.bootstrap;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import com.pablotj.portfolio.bootstrap.certification.CertificationApplicationConfig;
import com.pablotj.portfolio.bootstrap.education.EducationApplicationConfig;
import com.pablotj.portfolio.bootstrap.experience.ExperienceApplicationConfig;
import com.pablotj.portfolio.bootstrap.profile.ProfileApplicationConfig;
import com.pablotj.portfolio.bootstrap.project.ProjectApplicationConfig;
import com.pablotj.portfolio.bootstrap.skill.SkillApplicationConfig;
import com.pablotj.portfolio.infrastructure.config.CorsConfig;
import com.pablotj.portfolio.infrastructure.rest.api.ApiErrorController;
import com.pablotj.portfolio.infrastructure.rest.api.ApiRootController;
import com.pablotj.portfolio.infrastructure.rest.certification.CertificationController;
import com.pablotj.portfolio.infrastructure.rest.education.EducationController;
import com.pablotj.portfolio.infrastructure.rest.experience.ExperienceController;
import com.pablotj.portfolio.infrastructure.rest.profile.ProfileController;
import com.pablotj.portfolio.infrastructure.rest.project.ProjectController;
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")
@EnableJpaRepositories(basePackages = {"com.pablotj.portfolio.infrastructure.persistence.*.repo"})
@EntityScan(basePackages = {"com.pablotj.portfolio.infrastructure.persistence.*.entity"})
public class PortfolioApplication {
public class PortfolioApplication extends Jooby {
public static void main(String[] args) {
SpringApplication.run(PortfolioApplication.class, args);
{
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.GetCertificationUseCase;
import com.pablotj.portfolio.domain.certification.port.CertificationRepositoryPort;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.pablotj.portfolio.infrastructure.persistence.certification.adapter.CertificationRepositoryAdapter;
import io.jooby.Extension;
import io.jooby.Jooby;
import jakarta.annotation.Nonnull;
@Configuration
public class CertificationApplicationConfig {
public class CertificationApplicationConfig implements Extension {
@Bean
public GetCertificationUseCase getCertificationUseCase(CertificationRepositoryPort repo) {
return new GetCertificationUseCase(repo);
}
@Override
public void install(@Nonnull Jooby app) {
CertificationRepositoryAdapter adapter = new CertificationRepositoryAdapter();
@Bean
public CreateCertificationUseCase createCertificationUseCase(CertificationRepositoryPort repo) {
return new CreateCertificationUseCase(repo);
app.getServices().put(CertificationRepositoryPort.class, adapter);
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.GetEducationUseCase;
import com.pablotj.portfolio.domain.education.port.EducationRepositoryPort;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.pablotj.portfolio.infrastructure.persistence.education.adapter.EducationRepositoryAdapter;
import io.jooby.Extension;
import io.jooby.Jooby;
import jakarta.annotation.Nonnull;
@Configuration
public class EducationApplicationConfig {
public class EducationApplicationConfig implements Extension {
@Bean
public GetEducationUseCase getEducationUseCase(EducationRepositoryPort repo) {
return new GetEducationUseCase(repo);
}
@Override
public void install(@Nonnull Jooby app) {
EducationRepositoryAdapter adapter = new EducationRepositoryAdapter();
@Bean
public CreateEducationUseCase createEducationUseCase(EducationRepositoryPort repo) {
return new CreateEducationUseCase(repo);
app.getServices().put(EducationRepositoryPort.class, adapter);
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.GetExperienceUseCase;
import com.pablotj.portfolio.domain.experience.port.ExperienceRepositoryPort;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.pablotj.portfolio.infrastructure.persistence.experience.adapter.ExperienceRepositoryAdapter;
import io.jooby.Extension;
import io.jooby.Jooby;
import jakarta.annotation.Nonnull;
@Configuration
public class ExperienceApplicationConfig {
public class ExperienceApplicationConfig implements Extension {
@Override
public void install(@Nonnull Jooby app) {
@Bean
public GetExperienceUseCase getExperienceUseCase(ExperienceRepositoryPort repo) {
return new GetExperienceUseCase(repo);
}
ExperienceRepositoryAdapter adapter = new ExperienceRepositoryAdapter();
@Bean
public CreateExperienceUseCase createExperienceUseCase(ExperienceRepositoryPort repo) {
return new CreateExperienceUseCase(repo);
app.getServices().put(ExperienceRepositoryPort.class, adapter);
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.GetProfileUseCase;
import com.pablotj.portfolio.domain.profile.port.ProfileRepositoryPort;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.pablotj.portfolio.infrastructure.persistence.profile.adapter.ProfileRepositoryAdapter;
import io.jooby.Extension;
import io.jooby.Jooby;
import jakarta.annotation.Nonnull;
@Configuration
public class ProfileApplicationConfig {
public class ProfileApplicationConfig implements Extension {
@Override
public void install(@Nonnull Jooby app) {
@Bean
public GetProfileUseCase getHomeUseCase(ProfileRepositoryPort repo) {
return new GetProfileUseCase(repo);
}
ProfileRepositoryAdapter adapter = new ProfileRepositoryAdapter();
@Bean
public CreateProfileUseCase createHomeUseCase(ProfileRepositoryPort repo) {
return new CreateProfileUseCase(repo);
app.getServices().put(ProfileRepositoryPort.class, adapter);
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.GetProjectUseCase;
import com.pablotj.portfolio.domain.project.port.ProjectRepositoryPort;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.pablotj.portfolio.infrastructure.persistence.project.adapter.ProjectRepositoryAdapter;
import io.jooby.Extension;
import io.jooby.Jooby;
import jakarta.annotation.Nonnull;
@Configuration
public class ProjectApplicationConfig {
public class ProjectApplicationConfig implements Extension {
@Override
public void install(@Nonnull Jooby app) {
@Bean
public CreateProjectUseCase createProjectUseCase(ProjectRepositoryPort repo) {
return new CreateProjectUseCase(repo);
}
ProjectRepositoryAdapter adapter = new ProjectRepositoryAdapter();
@Bean
public GetProjectUseCase getProjectUseCase(ProjectRepositoryPort repo) {
return new GetProjectUseCase(repo);
app.getServices().put(ProjectRepositoryPort.class, adapter);
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.GetSkillUseCase;
import com.pablotj.portfolio.domain.skill.port.SkillRepositoryPort;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.pablotj.portfolio.infrastructure.persistence.skill.adapter.SkillRepositoryAdapter;
import io.jooby.Extension;
import io.jooby.Jooby;
import jakarta.annotation.Nonnull;
@Configuration
public class SkillApplicationConfig {
public class SkillApplicationConfig implements Extension {
@Override
public void install(@Nonnull Jooby app) {
@Bean
public GetSkillUseCase getSkillUseCase(SkillRepositoryPort repo) {
return new GetSkillUseCase(repo);
}
SkillRepositoryAdapter adapter = new SkillRepositoryAdapter();
@Bean
public CreateSkillUseCase createSkillUseCase(SkillRepositoryPort repo) {
return new CreateSkillUseCase(repo);
app.getServices().put(SkillRepositoryPort.class, adapter);
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

@@ -9,8 +9,7 @@ services:
ports:
- "5432:5432"
networks:
common_network:
ipv4_address: 10.1.0.121
network:
volumes:
- db_data:/var/lib/postgresql/data
@@ -20,16 +19,20 @@ services:
ports:
- "8095:8080"
networks:
common_network:
ipv4_address: 10.1.0.121
network:
environment:
DB_NAME: ${DB_NAME}
DB_HOST: ${DB_HOST}
DB_PORT: ${DB_PORT}
DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASSWORD}
APP_ALLOWED_ORIGINS: ${APP_ALLOWED_ORIGINS}
depends_on:
- db
volumes:
db_data:
networks:
network:
driver: bridge

View File

@@ -10,11 +10,14 @@
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<version>1.18.36</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -7,6 +7,7 @@ public record CertificationId(Long profileId, Long 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;
import java.time.LocalDate;
import lombok.Builder;
import lombok.Getter;

View File

@@ -7,6 +7,7 @@ public record ExperienceId(Long profileId, Long 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.ProjectId;
import java.util.List;
import java.util.Optional;
public interface ProjectRepositoryPort {
Project save(Project p);
Optional<Project> findById(ProjectId id);
List<Project> findAll(Long profileId);
}

View File

@@ -1,49 +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>
<parent>
<groupId>com.pablotj</groupId>
<artifactId>portfolio-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<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>
<dependency>
<groupId>com.pablotj</groupId>
<artifactId>domain</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.pablotj</groupId>
<artifactId>application</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<groupId>io.jooby</groupId>
<artifactId>jooby</artifactId>
<version>${jooby.version}</version>
</dependency>
<!-- MapStruct -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${slf4j.version}</version>
</dependency>
<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>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
</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>

View File

@@ -1,24 +1,26 @@
package com.pablotj.portfolio.infrastructure.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import io.jooby.Extension;
import io.jooby.Jooby;
import io.jooby.handler.Cors;
import io.jooby.handler.CorsHandler;
import jakarta.annotation.Nonnull;
import java.util.Arrays;
import java.util.List;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Value("${app.cors.allowed-origins}")
private String allowedOriginsString;
public class CorsConfig implements Extension {
@Override
public void addCorsMappings(CorsRegistry registry) {
public void install(@Nonnull Jooby app) {
String allowedOriginsString = app.getConfig().getString("app.cors.allowed-origins");
List<String> allowedOrigins = Arrays.asList(allowedOriginsString.split(","));
String [] allowedOrigins = allowedOriginsString.split(",");
Cors cors = new Cors()
.setOrigin(allowedOrigins)
.setMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"))
.setHeaders(Arrays.asList("Content-Type", "Authorization", "X-Requested-With"))
.setUseCredentials(true);
registry.addMapping("/**")
.allowedOrigins(allowedOrigins)
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*");
app.use(new CorsHandler(cors));
}
}

View File

@@ -0,0 +1,13 @@
package com.pablotj.portfolio.infrastructure.config;
import io.jooby.Extension;
import io.jooby.Jooby;
import jakarta.annotation.Nonnull;
public class SecurityConfig implements Extension {
@Override
public void install(@Nonnull Jooby app) {
}
}

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.CertificationId;
import com.pablotj.portfolio.domain.certification.port.CertificationRepositoryPort;
import com.pablotj.portfolio.infrastructure.persistence.certification.entity.CertificationJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.certification.mapper.CertificationJpaMapper;
import com.pablotj.portfolio.infrastructure.persistence.certification.repo.SpringDataCertificationRepository;
import com.pablotj.portfolio.infrastructure.persistence.certification.entity.CertificationEntity;
import com.pablotj.portfolio.infrastructure.persistence.certification.mapper.CertificationEntityMapper;
import io.ebean.DB;
import jakarta.inject.Singleton;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Repository;
import org.mapstruct.factory.Mappers;
@Repository
@Singleton
public class CertificationRepositoryAdapter implements CertificationRepositoryPort {
private final SpringDataCertificationRepository repo;
private final CertificationJpaMapper mapper;
public CertificationRepositoryAdapter(SpringDataCertificationRepository repo, CertificationJpaMapper mapper) {
this.repo = repo;
this.mapper = mapper;
}
private static final CertificationEntityMapper MAPPER = Mappers.getMapper(CertificationEntityMapper.class);
@Override
public Certification save(Certification p) {
CertificationJpaEntity entity = mapper.toEntity(p);
CertificationJpaEntity saved = repo.save(entity);
return mapper.toDomain(saved);
CertificationEntity entity = MAPPER.toEntity(p);
DB.save(entity);
return MAPPER.toDomain(entity);
}
@Override
public Optional<Certification> findById(CertificationId id) {
return repo.findByProfileIdAndId(id.profileId(), id.certificationId()).map(mapper::toDomain);
CertificationEntity entity = DB.find(CertificationEntity.class)
.where()
.eq("profile.id", id.profileId())
.eq("id", id.certificationId())
.findOne();
return Optional.ofNullable(entity).map(MAPPER::toDomain);
}
@Override
public List<Certification> findAll(Long profileId) {
return repo.findAllByProfileId(profileId).stream().map(mapper::toDomain).toList();
return DB.find(CertificationEntity.class)
.where()
.eq("profile.id", profileId)
.findList()
.stream()
.map(MAPPER::toDomain)
.toList();
}
}

View File

@@ -1,6 +1,7 @@
package com.pablotj.portfolio.infrastructure.persistence.certification.entity;
import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileEntity;
import io.ebean.Model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
@@ -17,7 +18,7 @@ import lombok.Setter;
@Table(name = "CERTIFICATION")
@Getter
@Setter
public class CertificationJpaEntity {
public class CertificationEntity extends Model {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -25,7 +26,7 @@ public class CertificationJpaEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "profile_id", nullable = false)
private ProfileJpaEntity profile;
private ProfileEntity profile;
@Column
private String name;

View File

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

View File

@@ -1,14 +0,0 @@
package com.pablotj.portfolio.infrastructure.persistence.certification.repo;
import com.pablotj.portfolio.infrastructure.persistence.certification.entity.CertificationJpaEntity;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface SpringDataCertificationRepository extends JpaRepository<CertificationJpaEntity, Long> {
Optional<CertificationJpaEntity> findByProfileIdAndId(Long profileId, Long id);
List<CertificationJpaEntity> findAllByProfileId(Long profileId);
}

View File

@@ -3,38 +3,45 @@ package com.pablotj.portfolio.infrastructure.persistence.education.adapter;
import com.pablotj.portfolio.domain.education.Education;
import com.pablotj.portfolio.domain.education.EducationId;
import com.pablotj.portfolio.domain.education.port.EducationRepositoryPort;
import com.pablotj.portfolio.infrastructure.persistence.education.entity.EducationJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.education.mapper.EducationJpaMapper;
import com.pablotj.portfolio.infrastructure.persistence.education.repo.SpringDataEducationRepository;
import com.pablotj.portfolio.infrastructure.persistence.education.entity.EducationEntity;
import com.pablotj.portfolio.infrastructure.persistence.education.mapper.EducationEntityMapper;
import io.ebean.DB;
import jakarta.inject.Singleton;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Repository;
import org.mapstruct.factory.Mappers;
@Repository
@Singleton
public class EducationRepositoryAdapter implements EducationRepositoryPort {
private final SpringDataEducationRepository repo;
private final EducationJpaMapper mapper;
public EducationRepositoryAdapter(SpringDataEducationRepository repo, EducationJpaMapper mapper) {
this.repo = repo;
this.mapper = mapper;
}
private static final EducationEntityMapper MAPPER = Mappers.getMapper(EducationEntityMapper.class);
@Override
public Education save(Education p) {
EducationJpaEntity entity = mapper.toEntity(p);
EducationJpaEntity saved = repo.save(entity);
return mapper.toDomain(saved);
EducationEntity entity = MAPPER.toEntity(p);
DB.save(entity);
return MAPPER.toDomain(entity);
}
@Override
public Optional<Education> findById(EducationId id) {
return repo.findByProfileIdAndId(id.profileId(), id.educationId()).map(mapper::toDomain);
EducationEntity entity = DB.find(EducationEntity.class)
.where()
.eq("profile.id", id.profileId())
.eq("id", id.educationId())
.findOne();
return Optional.ofNullable(entity).map(MAPPER::toDomain);
}
@Override
public List<Education> findAll(Long profileId) {
return repo.findAllByProfileId(profileId).stream().map(mapper::toDomain).toList();
return DB.find(EducationEntity.class)
.where()
.eq("profile.id", profileId)
.findList()
.stream()
.map(MAPPER::toDomain)
.toList();
}
}

View File

@@ -1,6 +1,7 @@
package com.pablotj.portfolio.infrastructure.persistence.education.entity;
import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileEntity;
import io.ebean.Model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
@@ -17,7 +18,7 @@ import lombok.Setter;
@Table(name = "EDUCATION")
@Getter
@Setter
public class EducationJpaEntity {
public class EducationEntity extends Model {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -25,7 +26,7 @@ public class EducationJpaEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "profile_id", nullable = false)
private ProfileJpaEntity profile;
private ProfileEntity profile;
@Column
private String institution;

View File

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

View File

@@ -1,13 +0,0 @@
package com.pablotj.portfolio.infrastructure.persistence.education.repo;
import com.pablotj.portfolio.infrastructure.persistence.certification.entity.CertificationJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.education.entity.EducationJpaEntity;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface SpringDataEducationRepository extends JpaRepository<EducationJpaEntity, Long> {
Optional<EducationJpaEntity> findByProfileIdAndId(Long profileId, Long id);
List<EducationJpaEntity> findAllByProfileId(Long profileId);
}

View File

@@ -3,38 +3,49 @@ package com.pablotj.portfolio.infrastructure.persistence.experience.adapter;
import com.pablotj.portfolio.domain.experience.Experience;
import com.pablotj.portfolio.domain.experience.ExperienceId;
import com.pablotj.portfolio.domain.experience.port.ExperienceRepositoryPort;
import com.pablotj.portfolio.infrastructure.persistence.experience.entity.ExperienceJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.experience.mapper.ExperienceJpaMapper;
import com.pablotj.portfolio.infrastructure.persistence.experience.repo.SpringDataExperienceRepository;
import com.pablotj.portfolio.infrastructure.persistence.experience.entity.ExperienceEntity;
import com.pablotj.portfolio.infrastructure.persistence.experience.mapper.ExperienceEntityMapper;
import io.ebean.DB;
import jakarta.inject.Singleton;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Repository;
import org.mapstruct.factory.Mappers;
@Repository
@Singleton
public class ExperienceRepositoryAdapter implements ExperienceRepositoryPort {
private final SpringDataExperienceRepository repo;
private final ExperienceJpaMapper mapper;
public ExperienceRepositoryAdapter(SpringDataExperienceRepository repo, ExperienceJpaMapper mapper) {
this.repo = repo;
this.mapper = mapper;
}
private static final ExperienceEntityMapper MAPPER = Mappers.getMapper(ExperienceEntityMapper.class);
@Override
public Experience save(Experience p) {
ExperienceJpaEntity entity = mapper.toEntity(p);
ExperienceJpaEntity saved = repo.save(entity);
return mapper.toDomain(saved);
ExperienceEntity entity = MAPPER.toEntity(p);
DB.save(entity);
return MAPPER.toDomain(entity);
}
@Override
public Optional<Experience> findById(ExperienceId id) {
return repo.findByProfileIdAndId(id.profileId(), id.experienceId()).map(mapper::toDomain);
ExperienceEntity entity = DB.find(ExperienceEntity.class)
.fetch("technologies")
.fetch("achievements")
.where()
.eq("profile.id", id.profileId())
.eq("id", id.experienceId())
.findOne();
return Optional.ofNullable(entity).map(MAPPER::toDomain);
}
@Override
public List<Experience> findAll(Long profileId) {
return repo.findAllByProfileId(profileId).stream().map(mapper::toDomain).toList();
return DB.find(ExperienceEntity.class)
.fetch("technologies")
.fetch("achievements")
.where()
.eq("profile.id", profileId)
.findList()
.stream()
.map(MAPPER::toDomain)
.toList();
}
}

View File

@@ -1,5 +1,6 @@
package com.pablotj.portfolio.infrastructure.persistence.experience.entity;
import io.ebean.Model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
@@ -20,7 +21,7 @@ import lombok.Setter;
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder
public class ExperienceAchievementJpaEntity {
public class ExperienceAchievementEntity extends Model {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)

View File

@@ -1,6 +1,7 @@
package com.pablotj.portfolio.infrastructure.persistence.experience.entity;
import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileEntity;
import io.ebean.Model;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
@@ -27,7 +28,7 @@ import lombok.Setter;
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder
public class ExperienceJpaEntity {
public class ExperienceEntity extends Model {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -35,7 +36,7 @@ public class ExperienceJpaEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "profile_id", nullable = false)
private ProfileJpaEntity profile;
private ProfileEntity profile;
@Column
private String position;
@@ -52,11 +53,12 @@ public class ExperienceJpaEntity {
@Column(columnDefinition = "text")
private String description;
// Ebean soporta perfectamente CascadeType.ALL y orphanRemoval
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "EXPERIENCE_ID")
private List<ExperienceSkillJpaEntity> technologies;
private List<ExperienceSkillEntity> technologies;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "EXPERIENCE_ID")
private List<ExperienceAchievementJpaEntity> achievements;
private List<ExperienceAchievementEntity> achievements;
}

View File

@@ -1,5 +1,6 @@
package com.pablotj.portfolio.infrastructure.persistence.experience.entity;
import io.ebean.Model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
@@ -19,7 +20,7 @@ import lombok.Setter;
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder
public class ExperienceSkillJpaEntity {
public class ExperienceSkillEntity extends Model {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)

View File

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

View File

@@ -1,13 +0,0 @@
package com.pablotj.portfolio.infrastructure.persistence.experience.repo;
import com.pablotj.portfolio.infrastructure.persistence.education.entity.EducationJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.experience.entity.ExperienceJpaEntity;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface SpringDataExperienceRepository extends JpaRepository<ExperienceJpaEntity, Long> {
Optional<ExperienceJpaEntity> findByProfileIdAndId(Long profileId, Long id);
List<ExperienceJpaEntity> findAllByProfileId(Long profileId);
}

View File

@@ -3,49 +3,56 @@ package com.pablotj.portfolio.infrastructure.persistence.profile.adapter;
import com.pablotj.portfolio.domain.profile.Profile;
import com.pablotj.portfolio.domain.profile.ProfileId;
import com.pablotj.portfolio.domain.profile.port.ProfileRepositoryPort;
import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileSocialLinkJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.profile.mapper.ProfileJpaMapper;
import com.pablotj.portfolio.infrastructure.persistence.profile.repo.SpringDataProfileRepository;
import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileEntity;
import com.pablotj.portfolio.infrastructure.persistence.profile.mapper.ProfileEntityMapper;
import io.ebean.DB;
import jakarta.inject.Singleton;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Repository;
import org.mapstruct.factory.Mappers;
@Repository
@Singleton
public class ProfileRepositoryAdapter implements ProfileRepositoryPort {
private final SpringDataProfileRepository repo;
private final ProfileJpaMapper mapper;
public ProfileRepositoryAdapter(SpringDataProfileRepository repo, ProfileJpaMapper mapper) {
this.repo = repo;
this.mapper = mapper;
}
private static final ProfileEntityMapper MAPPER = Mappers.getMapper(ProfileEntityMapper.class);
@Override
public Profile save(Profile p) {
ProfileJpaEntity entity = mapper.toEntity(p);
if (entity.getSocial() != null) {
for (ProfileSocialLinkJpaEntity socialLinkJpaEntity : entity.getSocial()) {
socialLinkJpaEntity.setProfile(entity);
}
}
ProfileJpaEntity saved = repo.save(entity);
return mapper.toDomain(saved);
ProfileEntity entity = MAPPER.toEntity(p);
DB.save(entity);
return MAPPER.toDomain(entity);
}
@Override
public Optional<Profile> findBySlug(ProfileId id) {
return repo.findBySlug(id.slug()).map(mapper::toDomain);
ProfileEntity entity = DB.find(ProfileEntity.class)
.fetch("social")
.where()
.eq("slug", id.slug())
.findOne();
return Optional.ofNullable(entity).map(MAPPER::toDomain);
}
@Override
public Optional<Profile> findById(ProfileId id) {
return repo.findById(id.id()).map(mapper::toDomain);
ProfileEntity entity = DB.find(ProfileEntity.class)
.fetch("social")
.where()
.eq("id", id.id())
.findOne();
return Optional.ofNullable(entity).map(MAPPER::toDomain);
}
@Override
public List<Profile> findAll() {
return repo.findAll().stream().map(mapper::toDomain).toList();
return DB.find(ProfileEntity.class)
.fetch("social")
.findList()
.stream()
.map(MAPPER::toDomain)
.toList();
}
}

View File

@@ -1,5 +1,6 @@
package com.pablotj.portfolio.infrastructure.persistence.profile.entity;
import io.ebean.Model;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
@@ -17,7 +18,7 @@ import lombok.Setter;
@Table(name = "profile")
@Getter
@Setter
public class ProfileJpaEntity {
public class ProfileEntity extends Model {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -47,10 +48,10 @@ public class ProfileJpaEntity {
@Column
private String avatar;
@Column
@Column(columnDefinition = "text")
private String bio;
@OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ProfileSocialLinkJpaEntity> social = new ArrayList<>();
private List<ProfileSocialLinkEntity> social = new ArrayList<>();
}

View File

@@ -1,5 +1,6 @@
package com.pablotj.portfolio.infrastructure.persistence.profile.entity;
import io.ebean.Model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
@@ -16,14 +17,15 @@ import lombok.Setter;
@Table(name = "profile_social_link")
@Getter
@Setter
public class ProfileSocialLinkJpaEntity {
public class ProfileSocialLinkEntity extends Model {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "profile_id", nullable = false)
private ProfileJpaEntity profile;
private ProfileEntity profile;
@Column
private String url;

View File

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

View File

@@ -1,10 +0,0 @@
package com.pablotj.portfolio.infrastructure.persistence.profile.repo;
import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileJpaEntity;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface SpringDataProfileRepository extends JpaRepository<ProfileJpaEntity, Long> {
Optional<ProfileJpaEntity> findBySlug(String slug);
}

View File

@@ -3,39 +3,50 @@ package com.pablotj.portfolio.infrastructure.persistence.project.adapter;
import com.pablotj.portfolio.domain.project.Project;
import com.pablotj.portfolio.domain.project.ProjectId;
import com.pablotj.portfolio.domain.project.port.ProjectRepositoryPort;
import com.pablotj.portfolio.infrastructure.persistence.project.entity.ProjectJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.project.mapper.ProjectJpaMapper;
import com.pablotj.portfolio.infrastructure.persistence.project.repo.SpringDataProjectRepository;
import org.springframework.stereotype.Repository;
import com.pablotj.portfolio.infrastructure.persistence.project.entity.ProjectEntity;
import com.pablotj.portfolio.infrastructure.persistence.project.mapper.ProjectEntityMapper;
import io.ebean.DB;
import jakarta.inject.Singleton;
import java.util.List;
import java.util.Optional;
import org.mapstruct.factory.Mappers;
@Repository
@Singleton
public class ProjectRepositoryAdapter implements ProjectRepositoryPort {
private final SpringDataProjectRepository repo;
private final ProjectJpaMapper mapper;
private static final ProjectEntityMapper MAPPER = Mappers.getMapper(ProjectEntityMapper.class);
public ProjectRepositoryAdapter(SpringDataProjectRepository repo, ProjectJpaMapper mapper) {
this.repo = repo;
this.mapper = mapper;
}
@Override
public Project save(Project p) {
ProjectJpaEntity entity = mapper.toEntity(p);
ProjectJpaEntity saved = repo.save(entity);
return mapper.toDomain(saved);
ProjectEntity entity = MAPPER.toEntity(p);
DB.save(entity);
return MAPPER.toDomain(entity);
}
@Override
public Optional<Project> findById(ProjectId id) {
return repo.findByProfileIdAndId(id.profileId(), id.projectId()).map(mapper::toDomain);
ProjectEntity entity = DB.find(ProjectEntity.class)
.fetch("technologies")
.fetch("features")
.where()
.eq("profile.id", id.profileId())
.eq("id", id.projectId())
.findOne();
return Optional.ofNullable(entity).map(MAPPER::toDomain);
}
@Override
public List<Project> findAll(Long profileId) {
return repo.findAllByProfileId(profileId).stream().map(mapper::toDomain).toList();
return DB.find(ProjectEntity.class)
.fetch("technologies")
.fetch("features")
.where()
.eq("profile.id", profileId)
.findList()
.stream()
.map(MAPPER::toDomain)
.toList();
}
}

View File

@@ -1,6 +1,7 @@
package com.pablotj.portfolio.infrastructure.persistence.project.entity;
import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileEntity;
import io.ebean.Model;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
@@ -18,8 +19,9 @@ import lombok.Setter;
@Entity
@Table(name = "PROJECT")
@Getter @Setter
public class ProjectJpaEntity {
@Getter
@Setter
public class ProjectEntity extends Model {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -27,12 +29,12 @@ public class ProjectJpaEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "profile_id", nullable = false)
private ProfileJpaEntity profile;
private ProfileEntity profile;
@Column
private String title;
@Column
@Column(columnDefinition = "text")
private String description;
@Column
@@ -40,11 +42,11 @@ public class ProjectJpaEntity {
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "PROJECT_ID")
private List<ProjectTechnologyJpaEntity> technologies;
private List<ProjectTechnologyEntity> technologies;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "PROJECT_ID")
private List<ProjectFeatureJpaEntity> features;
private List<ProjectFeatureEntity> features;
@Column
private String demo;

View File

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

View File

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

View File

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

View File

@@ -1,13 +0,0 @@
package com.pablotj.portfolio.infrastructure.persistence.project.repo;
import com.pablotj.portfolio.infrastructure.persistence.experience.entity.ExperienceJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.project.entity.ProjectJpaEntity;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface SpringDataProjectRepository extends JpaRepository<ProjectJpaEntity, Long> {
Optional<ProjectJpaEntity> findByProfileIdAndId(Long profileId, Long id);
List<ProjectJpaEntity> findAllByProfileId(Long profileId);
}

View File

@@ -3,38 +3,47 @@ package com.pablotj.portfolio.infrastructure.persistence.skill.adapter;
import com.pablotj.portfolio.domain.skill.SkillGroup;
import com.pablotj.portfolio.domain.skill.SkillGroupId;
import com.pablotj.portfolio.domain.skill.port.SkillRepositoryPort;
import com.pablotj.portfolio.infrastructure.persistence.skill.entity.SkillGroupJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.skill.mapper.SkillJpaMapper;
import com.pablotj.portfolio.infrastructure.persistence.skill.repo.SpringDataSkillRepository;
import com.pablotj.portfolio.infrastructure.persistence.skill.entity.SkillGroupEntity;
import com.pablotj.portfolio.infrastructure.persistence.skill.mapper.SkillMapper;
import io.ebean.DB;
import jakarta.inject.Singleton;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Repository;
import org.mapstruct.factory.Mappers;
@Repository
@Singleton
public class SkillRepositoryAdapter implements SkillRepositoryPort {
private final SpringDataSkillRepository repo;
private final SkillJpaMapper mapper;
public SkillRepositoryAdapter(SpringDataSkillRepository repo, SkillJpaMapper mapper) {
this.repo = repo;
this.mapper = mapper;
}
private static final SkillMapper MAPPER = Mappers.getMapper(SkillMapper.class);
@Override
public SkillGroup save(SkillGroup p) {
SkillGroupJpaEntity entity = mapper.toEntity(p);
SkillGroupJpaEntity saved = repo.save(entity);
return mapper.toDomain(saved);
SkillGroupEntity entity = MAPPER.toEntity(p);
DB.save(entity);
return MAPPER.toDomain(entity);
}
@Override
public Optional<SkillGroup> findById(SkillGroupId id) {
return repo.findByProfileIdAndId(id.profileId(), id.skillGroupId()).map(mapper::toDomain);
SkillGroupEntity entity = DB.find(SkillGroupEntity.class)
.fetch("skills") // Carga inmediata de la lista de habilidades
.where()
.eq("profile.id", id.profileId())
.eq("id", id.skillGroupId())
.findOne();
return Optional.ofNullable(entity).map(MAPPER::toDomain);
}
@Override
public List<SkillGroup> findAll(Long profileId) {
return repo.findAllByProfileId(profileId).stream().map(mapper::toDomain).toList();
return DB.find(SkillGroupEntity.class)
.fetch("skills")
.where()
.eq("profile.id", profileId)
.findList()
.stream()
.map(MAPPER::toDomain)
.toList();
}
}

View File

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

View File

@@ -1,6 +1,7 @@
package com.pablotj.portfolio.infrastructure.persistence.skill.entity;
import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.profile.entity.ProfileEntity;
import io.ebean.Model;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
@@ -20,7 +21,7 @@ import lombok.Setter;
@Table(name = "SKILL_GROUP")
@Getter
@Setter
public class SkillGroupJpaEntity {
public class SkillGroupEntity extends Model {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -28,7 +29,7 @@ public class SkillGroupJpaEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "profile_id", nullable = false)
private ProfileJpaEntity profile;
private ProfileEntity profile;
@Column
private String name;
@@ -38,5 +39,5 @@ public class SkillGroupJpaEntity {
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "SKILL_ID")
private List<SkillJpaEntity> skills;
private List<SkillEntity> skills;
}

View File

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

View File

@@ -1,13 +0,0 @@
package com.pablotj.portfolio.infrastructure.persistence.skill.repo;
import com.pablotj.portfolio.infrastructure.persistence.project.entity.ProjectJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.skill.entity.SkillGroupJpaEntity;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface SpringDataSkillRepository extends JpaRepository<SkillGroupJpaEntity, Long> {
Optional<SkillGroupJpaEntity> findByProfileIdAndId(Long profileId, Long id);
List<SkillGroupJpaEntity> findAllByProfileId(Long profileId);
}

View File

@@ -1,30 +1,28 @@
package com.pablotj.portfolio.infrastructure.rest.api;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.WebRequest;
import io.jooby.ErrorHandler;
import java.util.LinkedHashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Controller
public class ApiErrorController implements ErrorController {
public class ApiErrorController {
private final ErrorAttributes errorAttributes;
private static final Logger log = LoggerFactory.getLogger(ApiErrorController.class);
public ApiErrorController(ErrorAttributes errorAttributes) {
this.errorAttributes = errorAttributes;
}
public static ErrorHandler getHandler() {
return (ctx, cause, statusCode) -> {
log.error("Error en la API: {}", cause.getMessage(), cause);
@RequestMapping("/error")
public ResponseEntity<Map<String, Object>> handleError(WebRequest webRequest) {
Map<String, Object> attributes = errorAttributes.getErrorAttributes(webRequest,
ErrorAttributeOptions.defaults());
HttpStatus status = HttpStatus.valueOf((int) attributes.getOrDefault("status", 500));
return new ResponseEntity<>(attributes, status);
Map<String, Object> errorAttributes = new LinkedHashMap<>();
errorAttributes.put("timestamp", System.currentTimeMillis());
errorAttributes.put("status", statusCode.value());
errorAttributes.put("error", statusCode.reason());
errorAttributes.put("message", cause.getMessage());
errorAttributes.put("path", ctx.getRequestPath());
ctx.setResponseCode(statusCode)
.render(errorAttributes);
};
}
}

View File

@@ -1,39 +1,39 @@
package com.pablotj.portfolio.infrastructure.rest.api;
import io.jooby.annotation.GET;
import io.jooby.annotation.Path;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping
@Path("/")
public class ApiRootController {
@Value("${info.app.version}")
private String appVersion;
private final String appVersion;
@GetMapping
public ResponseEntity<Map<String, Object>> root() {
Map<String, Object> response = Map.of(
@Inject
public ApiRootController(@Named("info.app.version") String appVersion) {
this.appVersion = appVersion;
}
@GET
public Map<String, Object> root() {
return Map.of(
"api", "Portfolio API",
"version", appVersion,
"doc", "/v3/api-docs",
"swagger", "/swagger-ui",
"endpoints", List.of(
Map.of("path", "/v1/homes", "description", "Manage projects"),
Map.of("path", "/v1/certifications", "description", "Manage projects"),
Map.of("path", "/v1/homes", "description", "Manage home details"),
Map.of("path", "/v1/certifications", "description", "Manage certifications"),
Map.of("path", "/v1/projects", "description", "Manage projects"),
Map.of("path", "/v1/contacts", "description", "Manage projects"),
Map.of("path", "/v1/educations", "description", "Manage projects"),
Map.of("path", "/v1/experiences", "description", "Manage projects"),
Map.of("path", "/v1/projects", "description", "Manage projects"),
Map.of("path", "/v1/technologies", "description", "ProfileSocialLink entries")
Map.of("path", "/v1/contacts", "description", "Manage contact info"),
Map.of("path", "/v1/educations", "description", "Manage education"),
Map.of("path", "/v1/experiences", "description", "Manage experience"),
Map.of("path", "/v1/skills", "description", "Manage skills"),
Map.of("path", "/v1/technologies", "description", "Manage technologies")
)
);
return ResponseEntity.ok(response);
}
}

View File

@@ -5,54 +5,53 @@ import com.pablotj.portfolio.application.certification.GetCertificationUseCase;
import com.pablotj.portfolio.infrastructure.rest.certification.dto.CertificationDto;
import com.pablotj.portfolio.infrastructure.rest.certification.dto.CreateCertificationRequest;
import com.pablotj.portfolio.infrastructure.rest.certification.mapper.CertificationRestMapper;
import io.jooby.annotation.GET;
import io.jooby.annotation.POST;
import io.jooby.annotation.Path;
import io.jooby.annotation.PathParam;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import java.net.URI;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.mapstruct.factory.Mappers;
@RestController
@RequestMapping("/v1/profiles/{profileId}/certifications")
@Path("/v1/profiles/{profileId}/certifications")
public class CertificationController {
private static final CertificationRestMapper MAPPER = Mappers.getMapper(CertificationRestMapper.class);
private final CreateCertificationUseCase createUC;
private final GetCertificationUseCase getUC;
private final CertificationRestMapper mapper;
public CertificationController(CreateCertificationUseCase createUC, GetCertificationUseCase getUC, CertificationRestMapper mapper) {
@Inject
public CertificationController(CreateCertificationUseCase createUC, GetCertificationUseCase getUC) {
this.createUC = createUC;
this.getUC = getUC;
this.mapper = mapper;
}
@GetMapping
public List<CertificationDto> all(@PathVariable Long profileId) {
return getUC.all(profileId).stream().map(mapper::toDto).toList();
@GET
public List<CertificationDto> all(@PathParam Long profileId) {
return getUC.all(profileId).stream()
.map(MAPPER::toDto)
.toList();
}
@GetMapping("/{id}")
public ResponseEntity<CertificationDto> byId(@PathVariable Long profileId, @PathVariable Long id) {
@GET("/{id}")
public CertificationDto byId(@PathParam Long profileId, @PathParam Long id) {
// En Jooby, si devuelves un Optional vacío, automáticamente lanza un 404
return getUC.byId(profileId, id)
.map(mapper::toDto)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
.map(MAPPER::toDto)
.orElseThrow(() -> new io.jooby.exception.NotFoundException("Certification not found"));
}
@PostMapping
public ResponseEntity<CertificationDto> create(@PathVariable Long profileId, @Valid @RequestBody CreateCertificationRequest request) {
@POST
public CertificationDto create(@PathParam Long profileId, @Valid CreateCertificationRequest request) {
var cmd = new CreateCertificationUseCase.Command(
request.name(),
request.issuer(),
request.date(),
request.credentialId()
);
var created = createUC.handle(profileId, cmd);
var body = mapper.toDto(created);
return ResponseEntity.created(URI.create("/api/certifications/" + body.id())).body(body);
return MAPPER.toDto(created);
}
}

View File

@@ -5,7 +5,7 @@ import com.pablotj.portfolio.infrastructure.rest.certification.dto.Certification
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(componentModel = "spring")
@Mapper
public interface CertificationRestMapper {
@Mapping(target = "id", source = "id.certificationId")

View File

@@ -5,46 +5,45 @@ import com.pablotj.portfolio.application.education.GetEducationUseCase;
import com.pablotj.portfolio.infrastructure.rest.education.dto.CreateEducationRequest;
import com.pablotj.portfolio.infrastructure.rest.education.dto.EducationDto;
import com.pablotj.portfolio.infrastructure.rest.education.mapper.EducationRestMapper;
import io.jooby.annotation.GET;
import io.jooby.annotation.POST;
import io.jooby.annotation.Path;
import io.jooby.annotation.PathParam;
import io.jooby.exception.NotFoundException;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import java.net.URI;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.mapstruct.factory.Mappers;
@RestController
@RequestMapping("/v1/profiles/{profileId}/education")
@Path("/v1/profiles/{profileId}/education")
public class EducationController {
private static final EducationRestMapper MAPPER = Mappers.getMapper(EducationRestMapper.class);
private final CreateEducationUseCase createUC;
private final GetEducationUseCase getUC;
private final EducationRestMapper mapper;
public EducationController(CreateEducationUseCase createUC, GetEducationUseCase getUC, EducationRestMapper mapper) {
@Inject
public EducationController(CreateEducationUseCase createUC, GetEducationUseCase getUC) {
this.createUC = createUC;
this.getUC = getUC;
this.mapper = mapper;
}
@GetMapping
public List<EducationDto> all(@PathVariable Long profileId) {
return getUC.all(profileId).stream().map(mapper::toDto).toList();
@GET
public List<EducationDto> all(@PathParam Long profileId) {
return getUC.all(profileId).stream()
.map(MAPPER::toDto)
.toList();
}
@GetMapping("/{id}")
public ResponseEntity<EducationDto> byId(@PathVariable Long profileId, @PathVariable Long id) {
@GET("/{id}")
public EducationDto byId(@PathParam Long profileId, @PathParam Long id) {
return getUC.byId(profileId, id)
.map(mapper::toDto)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
.map(MAPPER::toDto)
.orElseThrow(() -> new NotFoundException("Education record not found"));
}
@PostMapping
public ResponseEntity<EducationDto> create(@PathVariable Long profileId, @Valid @RequestBody CreateEducationRequest request) {
@POST
public EducationDto create(@PathParam Long profileId, @Valid CreateEducationRequest request) {
var cmd = new CreateEducationUseCase.Command(
request.institution(),
request.degree(),
@@ -52,8 +51,8 @@ public class EducationController {
request.grade(),
request.description()
);
var created = createUC.handle(profileId, cmd);
var body = mapper.toDto(created);
return ResponseEntity.created(URI.create("/api/educations/" + body.id())).body(body);
return MAPPER.toDto(created);
}
}

View File

@@ -5,7 +5,7 @@ import com.pablotj.portfolio.infrastructure.rest.education.dto.EducationDto;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(componentModel = "spring")
@Mapper
public interface EducationRestMapper {
@Mapping(target = "id", source = "id.educationId")

View File

@@ -5,46 +5,45 @@ import com.pablotj.portfolio.application.experience.GetExperienceUseCase;
import com.pablotj.portfolio.infrastructure.rest.experience.dto.CreateExperienceRequest;
import com.pablotj.portfolio.infrastructure.rest.experience.dto.ExperienceDto;
import com.pablotj.portfolio.infrastructure.rest.experience.mapper.ExperienceRestMapper;
import io.jooby.annotation.GET;
import io.jooby.annotation.POST;
import io.jooby.annotation.Path;
import io.jooby.annotation.PathParam;
import io.jooby.exception.NotFoundException;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import java.net.URI;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.mapstruct.factory.Mappers;
@RestController
@RequestMapping("/v1/profiles/{profileId}/experience")
@Path("/v1/profiles/{profileId}/experience")
public class ExperienceController {
private static final ExperienceRestMapper MAPPER = Mappers.getMapper(ExperienceRestMapper.class);
private final CreateExperienceUseCase createUC;
private final GetExperienceUseCase getUC;
private final ExperienceRestMapper mapper;
public ExperienceController(CreateExperienceUseCase createUC, GetExperienceUseCase getUC, ExperienceRestMapper mapper) {
@Inject
public ExperienceController(CreateExperienceUseCase createUC, GetExperienceUseCase getUC) {
this.createUC = createUC;
this.getUC = getUC;
this.mapper = mapper;
}
@GetMapping
public List<ExperienceDto> all(@PathVariable Long profileId) {
return getUC.all(profileId).stream().map(mapper::toDto).toList();
@GET
public List<ExperienceDto> all(@PathParam Long profileId) {
return getUC.all(profileId).stream()
.map(MAPPER::toDto)
.toList();
}
@GetMapping("/{id}")
public ResponseEntity<ExperienceDto> byId(@PathVariable Long profileId, @PathVariable Long id) {
@GET("/{id}")
public ExperienceDto byId(@PathParam Long profileId, @PathParam Long id) {
return getUC.byId(profileId, id)
.map(mapper::toDto)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
.map(MAPPER::toDto)
.orElseThrow(() -> new NotFoundException("Experience not found"));
}
@PostMapping
public ResponseEntity<ExperienceDto> create(@PathVariable Long profileId, @Valid @RequestBody CreateExperienceRequest request) {
@POST
public ExperienceDto create(@PathParam Long profileId, @Valid CreateExperienceRequest request) {
var cmd = new CreateExperienceUseCase.Command(
request.company(),
request.position(),
@@ -54,8 +53,8 @@ public class ExperienceController {
request.technologies(),
request.achievements()
);
var created = createUC.handle(profileId, cmd);
var body = mapper.toDto(created);
return ResponseEntity.created(URI.create("/api/experiences/" + body.id())).body(body);
return MAPPER.toDto(created);
}
}

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ import java.util.List;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(componentModel = "spring")
@Mapper
public interface ExperienceRestMapper {
@Mapping(target = "id", source = "id.experienceId")

View File

@@ -5,47 +5,45 @@ import com.pablotj.portfolio.application.profile.GetProfileUseCase;
import com.pablotj.portfolio.infrastructure.rest.profile.dto.ProfileCreateRequest;
import com.pablotj.portfolio.infrastructure.rest.profile.dto.ProfileDto;
import com.pablotj.portfolio.infrastructure.rest.profile.mapper.ProfileRestMapper;
import io.jooby.annotation.GET;
import io.jooby.annotation.POST;
import io.jooby.annotation.Path;
import io.jooby.annotation.PathParam;
import io.jooby.exception.NotFoundException;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import java.net.URI;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.mapstruct.factory.Mappers;
@RestController
@RequestMapping("/v1/profiles")
@Path("/v1/profiles")
public class ProfileController {
private static final ProfileRestMapper MAPPER = Mappers.getMapper(ProfileRestMapper.class);
private final CreateProfileUseCase createUC;
private final GetProfileUseCase getUC;
private final ProfileRestMapper mapper;
public ProfileController(CreateProfileUseCase createUC, GetProfileUseCase getUC, ProfileRestMapper mapper) {
@Inject
public ProfileController(CreateProfileUseCase createUC, GetProfileUseCase getUC) {
this.createUC = createUC;
this.getUC = getUC;
this.mapper = mapper;
}
@GetMapping
@GET
public List<ProfileDto> all() {
return getUC.all().stream().map(mapper::toDto).toList();
return getUC.all().stream()
.map(MAPPER::toDto)
.toList();
}
@GetMapping("/{slug}")
public ResponseEntity<ProfileDto> byId(@PathVariable String slug) {
@GET("/{slug}")
public ProfileDto byId(@PathParam String slug) {
return getUC.bySlug(slug)
.map(mapper::toDto)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
.map(MAPPER::toDto)
.orElseThrow(() -> new NotFoundException("Profile not found"));
}
@PostMapping
public ResponseEntity<ProfileDto> create(@Valid @RequestBody ProfileCreateRequest request) {
@POST
public ProfileDto create(@Valid ProfileCreateRequest request) {
var cmd = new CreateProfileUseCase.Command(
request.slug(),
request.name(),
@@ -56,10 +54,12 @@ public class ProfileController {
request.location(),
request.avatar(),
request.bio(),
request.social().stream().map(l -> new CreateProfileUseCase.Link(l.platform(), l.url())).toList()
request.social().stream()
.map(l -> new CreateProfileUseCase.Link(l.platform(), l.url()))
.toList()
);
var created = createUC.handle(cmd);
var body = mapper.toDto(created);
return ResponseEntity.created(URI.create("/api/homes/" + body.id())).body(body);
return MAPPER.toDto(created);
}
}

View File

@@ -8,7 +8,7 @@ import java.util.List;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(componentModel = "spring")
@Mapper
public interface ProfileRestMapper {
@Mapping(target = "id", source = "id.id")

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ import java.util.List;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(componentModel = "spring")
@Mapper
public interface ProjectRestMapper {
@Mapping(target = "id", source = "id.projectId")

View File

@@ -5,53 +5,54 @@ import com.pablotj.portfolio.application.skill.GetSkillUseCase;
import com.pablotj.portfolio.infrastructure.rest.skill.dto.CreateSkillGroupRequest;
import com.pablotj.portfolio.infrastructure.rest.skill.dto.SkillGroupDto;
import com.pablotj.portfolio.infrastructure.rest.skill.mapper.SkillRestMapper;
import io.jooby.annotation.GET;
import io.jooby.annotation.POST;
import io.jooby.annotation.Path;
import io.jooby.annotation.PathParam;
import io.jooby.exception.NotFoundException;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import java.net.URI;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.mapstruct.factory.Mappers;
@RestController
@RequestMapping("/v1/profiles/{profileId}/skills")
@Path("/v1/profiles/{profileId}/skills")
public class SkillController {
private static final SkillRestMapper MAPPER = Mappers.getMapper(SkillRestMapper.class);
private final CreateSkillUseCase createUC;
private final GetSkillUseCase getUC;
private final SkillRestMapper mapper;
public SkillController(CreateSkillUseCase createUC, GetSkillUseCase getUC, SkillRestMapper mapper) {
@Inject
public SkillController(CreateSkillUseCase createUC, GetSkillUseCase getUC) {
this.createUC = createUC;
this.getUC = getUC;
this.mapper = mapper;
}
@GetMapping
public List<SkillGroupDto> all(@PathVariable Long profileId) {
return getUC.all(profileId).stream().map(mapper::toDto).toList();
@GET
public List<SkillGroupDto> all(@PathParam Long profileId) {
return getUC.all(profileId).stream()
.map(MAPPER::toDto)
.toList();
}
@GetMapping("/{id}")
public ResponseEntity<SkillGroupDto> byId(@PathVariable Long profileId, @PathVariable Long id) {
@GET("/{id}")
public SkillGroupDto byId(@PathParam Long profileId, @PathParam Long id) {
return getUC.byId(profileId, id)
.map(mapper::toDto)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
.map(MAPPER::toDto)
.orElseThrow(() -> new NotFoundException("Skill group not found"));
}
@PostMapping
public ResponseEntity<SkillGroupDto> create(@PathVariable Long profileId, @Valid @RequestBody CreateSkillGroupRequest request) {
@POST
public SkillGroupDto create(@PathParam Long profileId, @Valid CreateSkillGroupRequest request) {
var cmd = new CreateSkillUseCase.CommandGroup(
request.name(),
request.icon(),
request.skills().stream().map(s -> new CreateSkillUseCase.CommandSkill(s.name(), s.level(), s.years())).toList()
request.skills().stream()
.map(s -> new CreateSkillUseCase.CommandSkill(s.name(), s.level(), s.years()))
.toList()
);
var created = createUC.handle(profileId, cmd);
var body = mapper.toDto(created);
return ResponseEntity.created(URI.create("/api/skills/" + body.id())).body(body);
return MAPPER.toDto(created);
}
}

View File

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

View File

@@ -0,0 +1,168 @@
create table personal
(
id bigint not null
primary key,
avatar varchar(255),
bio varchar(255),
email varchar(255),
location varchar(255),
name varchar(255),
phone varchar(255),
subtitle varchar(255),
title varchar(255)
);
create table personal_social_link
(
id bigint not null
primary key,
platform varchar(255),
url varchar(255),
personal_id bigint not null
constraint fkfh1pbfvvg3palcr1yip6jffik
references personal
);
create table profile
(
id bigint not null
primary key,
avatar varchar(255),
bio varchar(4000),
email varchar(255),
location varchar(255),
name varchar(255),
phone varchar(255),
slug varchar(255),
subtitle varchar(255),
title varchar(255)
);
create table certification
(
id bigint not null
primary key,
credential_id varchar(255),
date varchar(255),
issuer varchar(255),
name varchar(255),
profile_id bigint not null
constraint fko6ve4ysx15lc2vcjt84sal1yc
references profile
);
create table education
(
id bigint not null
primary key,
degree varchar(255),
description varchar,
grade varchar(255),
institution varchar(255),
period varchar(255),
profile_id bigint not null
constraint fkelocxwwcyf5acj85hgke1c0fl
references profile
);
create table experience
(
id bigint not null
primary key,
company varchar(255),
description varchar,
location varchar(255),
period varchar(255),
position varchar(255),
profile_id bigint not null
constraint fkhlkosu9yvtv1ptp01x4tfh9ut
references profile
);
create table experience_achievement
(
id bigint not null
primary key,
description varchar,
experience_id bigint
constraint fk94xrk6stofkung8skwplo29nd
references experience
);
create table experience_skill
(
id bigint not null
primary key,
name varchar(255),
experience_id bigint
constraint fkpr3jdfjjlaubuayoafpwyx2al
references experience
);
create table profile_social_link
(
id bigint not null
primary key,
platform varchar(255),
url varchar(255),
profile_id bigint not null
constraint fkqfxt1g0xm211i7qjnlcuqfes9
references profile
);
create table project
(
id bigint not null
primary key,
demo varchar(255),
description varchar(255),
image varchar(255),
repository varchar(255),
title varchar(255),
profile_id bigint not null
constraint fk2i9umkiuu36osx3afamsxq39h
references profile
);
create table project_feature
(
id bigint not null
primary key,
name varchar(255),
project_id bigint
constraint fkdifppyvrfito5in15ox4db0up
references project
);
create table project_feature_technology
(
id bigint not null
primary key,
name varchar(255),
project_id bigint
constraint fk15krsajtovetpg5vsaqj3icwf
references project
);
create table skill_group
(
id bigint not null
primary key,
icon varchar(255),
name varchar(255),
profile_id bigint not null
constraint fko26hcvag49ctl3ciddsqm6mn1
references profile
);
create table skill
(
id bigint not null
primary key,
level integer,
name varchar(255),
years integer,
skill_id bigint
constraint fki819li5g5cp5qbsyenhr3kmef
references skill_group
);

View File

@@ -1,145 +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 profile_social_link
add constraint FKfh1pbfvvg3palcr1yip6jffik
foreign key (PROFILE_ID)
references profile;
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"
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">
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>
<groupId>com.pablotj</groupId>
<artifactId>portfolio-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<groupId>com.pablotj</groupId>
<artifactId>portfolio-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>domain</module>
<module>application</module>
<module>infrastructure</module>
<module>bootstrap</module>
</modules>
<modules>
<module>domain</module>
<module>application</module>
<module>infrastructure</module>
<module>bootstrap</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
<relativePath/>
</parent>
<properties>
<java.version>21</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jooby.version>3.6.1</jooby.version>
<mapstruct.version>1.5.5.Final</mapstruct.version>
<lombok.version>1.18.32</lombok.version>
<jackson.version>2.17.2</jackson.version>
<slf4j.version>2.24.1</slf4j.version>
</properties>
<properties>
<java.version>21</java.version>
<mapstruct.version>1.5.5.Final</mapstruct.version>
<springdoc.version>2.6.0</springdoc.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-bom</artifactId>
<version>${jooby.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-apt</artifactId>
<version>${jooby.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<annotationProcessorPaths>
<path>
<groupId>io.jooby</groupId>
<artifactId>jooby-apt</artifactId>
<version>${jooby.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${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>