Compare commits

..

17 Commits

Author SHA1 Message Date
Pablo de la Torre
17e8f83bf7 ci(config): update infrastructure configuration 2026-05-13 19:40:15 +02:00
4eddee6561 ci(config): update infrastructure configuration 2026-05-01 10:34:26 +02:00
707baf8cbc 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-11 11:32:58 +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
ca41aee64f chore: configure CORS 2025-09-20 10:22:10 +02:00
2959c68bf3 chore: configure Docker to deploy project using environment-based properties 2025-09-20 10:21:52 +02:00
8b2795d518 Merge remote-tracking branch 'origin/main' 2025-09-20 10:08:10 +02:00
8ef605ea2d chore: optimize HikariCP pool and JVM memory for Raspberry Pi 2025-09-20 10:07:59 +02:00
Pablo de la Torre
ee1820960e chore(gitignore): add macOS system files to ignore list 2025-09-10 08:02:47 +02:00
1b55d9ab29 refactor(api): refactor endpoints, services, and domain logic 2025-09-09 19:53:42 +02:00
9f5306545e feat: implement final Experience API 2025-08-27 16:28:23 +02:00
ab40e9a497 feat: implement final Contact API 2025-08-26 17:50:20 +02:00
9ff4b21dd9 feat: implement final Resume API 2025-08-26 17:41:07 +02:00
180 changed files with 2823 additions and 1913 deletions

7
.env.example Normal file
View File

@@ -0,0 +1,7 @@
APP_ALLOWED_ORIGINS='http://127.0.0.1:3000, http://localhost:3000'
DB_NAME=EXAMPLE_DB
DB_USER=EXAMPLE
DB_PASSWORD=SECRET
DB_HOST=127.0.0.1
DB_PORT=5432

8
.gitignore vendored
View File

@@ -1,3 +1,9 @@
.idea
*.toml
*.db
target
.env
Icon?
.docker
tmp
logs

52
Dockerfile Normal file
View File

@@ -0,0 +1,52 @@
# --- Stage 1: Build & Dependencies ---
FROM maven:3.9-eclipse-temurin-21-alpine AS build
WORKDIR /app
# 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/
# 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
# 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
# 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=50.0 \
-Xms32m \
-Xmx96m \
-XX:+UseSerialGC \
-XX:TieredStopAtLevel=1"
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.nortapp.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()
}
}
}

33
Makefile Normal file
View File

@@ -0,0 +1,33 @@
STACK=pablotj-portfolio
APP_NAME=pablotj-portfolio-api
IMAGE_NAME=$(APP_NAME)
REGISTRY_URL=registry.nortapp.com
NAMESPACE=andromeda
TAG?=latest
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 \
--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

@@ -1,26 +0,0 @@
package com.pablotj.portfolio.application.about;
import com.pablotj.portfolio.domain.about.About;
import com.pablotj.portfolio.domain.about.port.AboutRepositoryPort;
public class CreateAboutUseCase {
private final AboutRepositoryPort repository;
public CreateAboutUseCase(AboutRepositoryPort repository) {
this.repository = repository;
}
public About handle(Command cmd) {
var about = About.builder()
.id(null)
.title(cmd.title())
.description(cmd.description())
.url(cmd.url())
.build();
return repository.save(about);
}
public record Command(String title, String description, String url) {
}
}

View File

@@ -1,24 +0,0 @@
package com.pablotj.portfolio.application.about;
import com.pablotj.portfolio.domain.about.About;
import com.pablotj.portfolio.domain.about.AboutId;
import com.pablotj.portfolio.domain.about.port.AboutRepositoryPort;
import java.util.List;
import java.util.Optional;
public class GetAboutUseCase {
private final AboutRepositoryPort repository;
public GetAboutUseCase(AboutRepositoryPort repository) {
this.repository = repository;
}
public Optional<About> byId(Long id) {
return repository.findById(new AboutId(id));
}
public List<About> all() {
return repository.findAll();
}
}

View File

@@ -1,26 +1,33 @@
package com.pablotj.portfolio.application.certification;
import com.pablotj.portfolio.domain.certification.Certification;
import com.pablotj.portfolio.domain.certification.CertificationId;
import com.pablotj.portfolio.domain.certification.port.CertificationRepositoryPort;
public class CreateCertificationUseCase {
private final CertificationRepositoryPort repository;
CertificationRepositoryPort repository;
public CreateCertificationUseCase(CertificationRepositoryPort repository) {
this.repository = repository;
}
public Certification handle(Command cmd) {
public Certification handle(Long profileId, Command cmd) {
var certification = Certification.builder()
.id(null)
.title(cmd.title())
.description(cmd.description())
.url(cmd.url())
.id(new CertificationId(profileId))
.name(cmd.name())
.issuer(cmd.issuer())
.date(cmd.date())
.credentialId(cmd.credentialId())
.build();
return repository.save(certification);
}
public record Command(String title, String description, String url) {
public record Command(
String name,
String issuer,
String date,
String credentialId
) {
}
}

View File

@@ -14,11 +14,11 @@ public class GetCertificationUseCase {
this.repository = repository;
}
public Optional<Certification> byId(Long id) {
return repository.findById(new CertificationId(id));
public Optional<Certification> byId(Long profileId, Long id) {
return repository.findById(new CertificationId(profileId, id));
}
public List<Certification> all() {
return repository.findAll();
public List<Certification> all(Long profileId) {
return repository.findAll(profileId);
}
}

View File

@@ -1,26 +0,0 @@
package com.pablotj.portfolio.application.contact;
import com.pablotj.portfolio.domain.contact.Contact;
import com.pablotj.portfolio.domain.contact.port.ContactRepositoryPort;
public class CreateContactUseCase {
private final ContactRepositoryPort repository;
public CreateContactUseCase(ContactRepositoryPort repository) {
this.repository = repository;
}
public Contact handle(Command cmd) {
var contact = Contact.builder()
.id(null)
.title(cmd.title())
.description(cmd.description())
.url(cmd.url())
.build();
return repository.save(contact);
}
public record Command(String title, String description, String url) {
}
}

View File

@@ -1,24 +0,0 @@
package com.pablotj.portfolio.application.contact;
import com.pablotj.portfolio.domain.contact.Contact;
import com.pablotj.portfolio.domain.contact.ContactId;
import com.pablotj.portfolio.domain.contact.port.ContactRepositoryPort;
import java.util.List;
import java.util.Optional;
public class GetContactUseCase {
private final ContactRepositoryPort repository;
public GetContactUseCase(ContactRepositoryPort repository) {
this.repository = repository;
}
public Optional<Contact> byId(Long id) {
return repository.findById(new ContactId(id));
}
public List<Contact> all() {
return repository.findAll();
}
}

View File

@@ -1,6 +1,7 @@
package com.pablotj.portfolio.application.education;
import com.pablotj.portfolio.domain.education.Education;
import com.pablotj.portfolio.domain.education.EducationId;
import com.pablotj.portfolio.domain.education.port.EducationRepositoryPort;
public class CreateEducationUseCase {
@@ -11,16 +12,24 @@ public class CreateEducationUseCase {
this.repository = repository;
}
public Education handle(Command cmd) {
public Education handle(Long profileId, Command cmd) {
var education = Education.builder()
.id(null)
.title(cmd.title())
.id(new EducationId(profileId))
.institution(cmd.institution())
.degree(cmd.degree())
.period(cmd.period())
.grade(cmd.grade())
.description(cmd.description())
.url(cmd.url())
.build();
return repository.save(education);
}
public record Command(String title, String description, String url) {
public record Command(
String institution,
String degree,
String period,
String grade,
String description
) {
}
}

View File

@@ -14,11 +14,11 @@ public class GetEducationUseCase {
this.repository = repository;
}
public Optional<Education> byId(Long id) {
return repository.findById(new EducationId(id));
public Optional<Education> byId(Long profileId, Long id) {
return repository.findById(new EducationId(profileId, id));
}
public List<Education> all() {
return repository.findAll();
public List<Education> all(Long profileId) {
return repository.findAll(profileId);
}
}

View File

@@ -1,7 +1,12 @@
package com.pablotj.portfolio.application.experience;
import com.pablotj.portfolio.domain.experience.Achievement;
import com.pablotj.portfolio.domain.experience.Experience;
import com.pablotj.portfolio.domain.experience.ExperienceId;
import com.pablotj.portfolio.domain.experience.Technology;
import com.pablotj.portfolio.domain.experience.port.ExperienceRepositoryPort;
import java.util.ArrayList;
import java.util.List;
public class CreateExperienceUseCase {
@@ -11,16 +16,30 @@ public class CreateExperienceUseCase {
this.repository = repository;
}
public Experience handle(Command cmd) {
public Experience handle(Long profileId, Command cmd) {
var experience = Experience.builder()
.id(null)
.title(cmd.title())
.id(new ExperienceId(profileId))
.company(cmd.company())
.position(cmd.position())
.period(cmd.period())
.location(cmd.location())
.description(cmd.description())
.url(cmd.url())
.technologies(new ArrayList<>())
.achievements(new ArrayList<>())
.build();
cmd.technologies.forEach(name -> experience.getTechnologies().add(Technology.builder().id(null).name(name).build()));
cmd.achievements.forEach(description -> experience.getAchievements().add(Achievement.builder().id(null).description(description).build()));
return repository.save(experience);
}
public record Command(String title, String description, String url) {
public record Command(
String position,
String company,
String period,
String location,
String description,
List<String> technologies,
List<String> achievements
) {
}
}

View File

@@ -14,11 +14,11 @@ public class GetExperienceUseCase {
this.repository = repository;
}
public Optional<Experience> byId(Long id) {
return repository.findById(new ExperienceId(id));
public Optional<Experience> byId(Long profileId, Long id) {
return repository.findById(new ExperienceId(profileId, id));
}
public List<Experience> all() {
return repository.findAll();
public List<Experience> all(Long profileId) {
return repository.findAll(profileId);
}
}

View File

@@ -1,26 +0,0 @@
package com.pablotj.portfolio.application.home;
import com.pablotj.portfolio.domain.home.Home;
import com.pablotj.portfolio.domain.home.port.HomeRepositoryPort;
public class CreateHomeUseCase {
private final HomeRepositoryPort repository;
public CreateHomeUseCase(HomeRepositoryPort repository) {
this.repository = repository;
}
public Home handle(Command cmd) {
var home = Home.builder()
.id(null)
.title(cmd.title())
.description(cmd.description())
.url(cmd.url())
.build();
return repository.save(home);
}
public record Command(String title, String description, String url) {
}
}

View File

@@ -1,24 +0,0 @@
package com.pablotj.portfolio.application.home;
import com.pablotj.portfolio.domain.home.Home;
import com.pablotj.portfolio.domain.home.HomeId;
import com.pablotj.portfolio.domain.home.port.HomeRepositoryPort;
import java.util.List;
import java.util.Optional;
public class GetHomeUseCase {
private final HomeRepositoryPort repository;
public GetHomeUseCase(HomeRepositoryPort repository) {
this.repository = repository;
}
public Optional<Home> byId(Long id) {
return repository.findById(new HomeId(id));
}
public List<Home> all() {
return repository.findAll();
}
}

View File

@@ -0,0 +1,50 @@
package com.pablotj.portfolio.application.profile;
import com.pablotj.portfolio.domain.profile.Profile;
import com.pablotj.portfolio.domain.profile.ProfileSocialLink;
import com.pablotj.portfolio.domain.profile.port.ProfileRepositoryPort;
import java.util.List;
public class CreateProfileUseCase {
private final ProfileRepositoryPort repository;
public CreateProfileUseCase(ProfileRepositoryPort repository) {
this.repository = repository;
}
public Profile handle(Command cmd) {
var personalBuilder = Profile.builder()
.id(null)
.slug(cmd.slug())
.name(cmd.name())
.title(cmd.title())
.subtitle(cmd.subtitle())
.email(cmd.email())
.phone(cmd.phone())
.location(cmd.location())
.avatar(cmd.avatar())
.bio(cmd.bio());
if (cmd.socialLinks != null) {
cmd.socialLinks.forEach(l -> personalBuilder.social(ProfileSocialLink.builder().id(null).platform(l.platform()).url(l.url()).build()));
}
return repository.save(personalBuilder.build());
}
public record Command(
String slug,
String name,
String title,
String subtitle,
String email,
String phone,
String location,
String avatar,
String bio,
List<Link> socialLinks
) {
}
public record Link(String platform, String url) {
}
}

View File

@@ -0,0 +1,30 @@
package com.pablotj.portfolio.application.profile;
import com.pablotj.portfolio.domain.profile.Profile;
import com.pablotj.portfolio.domain.profile.ProfileId;
import com.pablotj.portfolio.domain.profile.port.ProfileRepositoryPort;
import java.util.List;
import java.util.Optional;
public class GetProfileUseCase {
private final ProfileRepositoryPort repository;
public GetProfileUseCase(ProfileRepositoryPort repository) {
this.repository = repository;
}
public Optional<Profile> byId(Long id) {
return repository.findById(new ProfileId(id));
}
public Optional<Profile> bySlug(String slug) {
return repository.findBySlug(new ProfileId(slug));
}
public List<Profile> all() {
return repository.findAll();
}
}

View File

@@ -1,25 +1,44 @@
package com.pablotj.portfolio.application.project;
import com.pablotj.portfolio.domain.project.Project;
import com.pablotj.portfolio.domain.project.ProjectFeature;
import com.pablotj.portfolio.domain.project.ProjectId;
import com.pablotj.portfolio.domain.project.ProjectTechnology;
import com.pablotj.portfolio.domain.project.port.ProjectRepositoryPort;
import java.util.ArrayList;
import java.util.List;
public class CreateProjectUseCase {
private final ProjectRepositoryPort repository;
public record Command(String title, String description, String url) {}
public CreateProjectUseCase(ProjectRepositoryPort repository) {
this.repository = repository;
}
public Project handle(Command cmd) {
public Project handle(Long profileId, Command cmd) {
var project = Project.builder()
.id(null)
.id(new ProjectId(profileId))
.title(cmd.title())
.description(cmd.description())
.url(cmd.url())
.image(cmd.image())
.technologies(new ArrayList<>())
.features(new ArrayList<>())
.demo(cmd.demo())
.repository(cmd.repository())
.build();
cmd.technologies.forEach(name -> project.getTechnologies().add(ProjectTechnology.builder().id(null).name(name).build()));
cmd.features.forEach(description -> project.getFeatures().add(ProjectFeature.builder().id(null).name(description).build()));
return repository.save(project);
}
public record Command(String title,
String description,
String image,
List<String> technologies,
List<String> features,
String demo,
String repository) {
}
}

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;
@@ -15,11 +14,11 @@ public class GetProjectUseCase {
this.repository = repository;
}
public Optional<Project> byId(Long id) {
return repository.findById(new ProjectId(id));
public Optional<Project> byId(Long profileId, Long id) {
return repository.findById(new ProjectId(profileId, id));
}
public List<Project> all() {
return repository.findAll();
public List<Project> all(Long profileId) {
return repository.findAll(profileId);
}
}

View File

@@ -1,7 +1,11 @@
package com.pablotj.portfolio.application.skill;
import com.pablotj.portfolio.domain.skill.Skill;
import com.pablotj.portfolio.domain.skill.SkillGroup;
import com.pablotj.portfolio.domain.skill.SkillGroupId;
import com.pablotj.portfolio.domain.skill.port.SkillRepositoryPort;
import java.util.ArrayList;
import java.util.List;
public class CreateSkillUseCase {
@@ -11,16 +15,29 @@ public class CreateSkillUseCase {
this.repository = repository;
}
public Skill handle(Command cmd) {
var skill = Skill.builder()
.id(null)
.title(cmd.title())
.description(cmd.description())
.url(cmd.url())
public SkillGroup handle(Long profileId, CommandGroup cmd) {
var skillGroup = SkillGroup.builder()
.id(new SkillGroupId(profileId))
.name(cmd.name())
.icon(cmd.icon())
.skills(new ArrayList<>())
.build();
return repository.save(skill);
cmd.skills.forEach(s -> skillGroup.getSkills().add(
Skill.builder().id(null).name(s.name).level(s.level).years(s.years).build()));
return repository.save(skillGroup);
}
public record Command(String title, String description, String url) {
public record CommandGroup(
String name,
String icon,
List<CommandSkill> skills
) {
}
public record CommandSkill(
String name,
Integer level,
Integer years
) {
}
}

View File

@@ -1,7 +1,7 @@
package com.pablotj.portfolio.application.skill;
import com.pablotj.portfolio.domain.skill.Skill;
import com.pablotj.portfolio.domain.skill.SkillId;
import com.pablotj.portfolio.domain.skill.SkillGroup;
import com.pablotj.portfolio.domain.skill.SkillGroupId;
import com.pablotj.portfolio.domain.skill.port.SkillRepositoryPort;
import java.util.List;
import java.util.Optional;
@@ -14,11 +14,11 @@ public class GetSkillUseCase {
this.repository = repository;
}
public Optional<Skill> byId(Long id) {
return repository.findById(new SkillId(id));
public Optional<SkillGroup> byId(Long profileId, Long id) {
return repository.findById(new SkillGroupId(profileId, id));
}
public List<Skill> all() {
return repository.findAll();
public List<SkillGroup> all(Long profileId) {
return repository.findAll(profileId);
}
}

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,41 +18,85 @@
<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>io.jooby</groupId>
<artifactId>jooby-maven-plugin</artifactId>
<version>${jooby.version}</version>
<executions>
<execution>
<goals>
<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>
</project>

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

@@ -1,21 +0,0 @@
package com.pablotj.portfolio.bootstrap.about;
import com.pablotj.portfolio.application.about.CreateAboutUseCase;
import com.pablotj.portfolio.application.about.GetAboutUseCase;
import com.pablotj.portfolio.domain.about.port.AboutRepositoryPort;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AboutApplicationConfig {
@Bean
public GetAboutUseCase getAboutUseCase(AboutRepositoryPort repo) {
return new GetAboutUseCase(repo);
}
@Bean
public CreateAboutUseCase createAboutUseCase(AboutRepositoryPort repo) {
return new CreateAboutUseCase(repo);
}
}

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

@@ -1,21 +0,0 @@
package com.pablotj.portfolio.bootstrap.contact;
import com.pablotj.portfolio.application.contact.CreateContactUseCase;
import com.pablotj.portfolio.application.contact.GetContactUseCase;
import com.pablotj.portfolio.domain.contact.port.ContactRepositoryPort;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ContactApplicationConfig {
@Bean
public GetContactUseCase getContactUseCase(ContactRepositoryPort repo) {
return new GetContactUseCase(repo);
}
@Bean
public CreateContactUseCase createContactUseCase(ContactRepositoryPort repo) {
return new CreateContactUseCase(repo);
}
}

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

@@ -1,21 +0,0 @@
package com.pablotj.portfolio.bootstrap.home;
import com.pablotj.portfolio.application.home.CreateHomeUseCase;
import com.pablotj.portfolio.application.home.GetHomeUseCase;
import com.pablotj.portfolio.domain.home.port.HomeRepositoryPort;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class HomeApplicationConfig {
@Bean
public GetHomeUseCase getHomeUseCase(HomeRepositoryPort repo) {
return new GetHomeUseCase(repo);
}
@Bean
public CreateHomeUseCase createHomeUseCase(HomeRepositoryPort repo) {
return new CreateHomeUseCase(repo);
}
}

View File

@@ -0,0 +1,22 @@
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 com.pablotj.portfolio.infrastructure.persistence.profile.adapter.ProfileRepositoryAdapter;
import io.jooby.Extension;
import io.jooby.Jooby;
import jakarta.annotation.Nonnull;
public class ProfileApplicationConfig implements Extension {
@Override
public void install(@Nonnull Jooby app) {
ProfileRepositoryAdapter adapter = new ProfileRepositoryAdapter();
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,64 +0,0 @@
spring:
application:
name: portfolio-api
web:
resources:
add-mappings: false
jpa:
hibernate:
ddl-auto: update
properties:
hibernate.transaction.jta.platform: org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform
hibernate:
format_sql: true
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
---
spring:
config:
activate:
on-profile: default
datasource:
url: jdbc:h2:mem:portfolio_db;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
username: sa
password:
h2:
console:
enabled: true
path: /h2-console
---
spring:
config:
activate:
on-profile: prod
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
jpa:
hibernate:
ddl-auto: validate

38
docker-compose.yml Normal file
View File

@@ -0,0 +1,38 @@
services:
db:
image: postgres:15-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
ports:
- "5432:5432"
networks:
network:
volumes:
- db_data:/var/lib/postgresql/data
api:
build: .
restart: unless-stopped
ports:
- "8095:8080"
networks:
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

@@ -1,13 +0,0 @@
package com.pablotj.portfolio.domain.about;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class About {
private final AboutId id;
private final String title;
private final String description;
private final String url;
}

View File

@@ -1,7 +0,0 @@
package com.pablotj.portfolio.domain.about;
public record AboutId(Long value) {
public AboutId {
if (value != null && value < 0) throw new IllegalArgumentException("AboutId must be positive");
}
}

View File

@@ -1,15 +0,0 @@
package com.pablotj.portfolio.domain.about.port;
import com.pablotj.portfolio.domain.about.About;
import com.pablotj.portfolio.domain.about.AboutId;
import java.util.List;
import java.util.Optional;
public interface AboutRepositoryPort {
About save(About p);
Optional<About> findById(AboutId id);
List<About> findAll();
}

View File

@@ -7,7 +7,8 @@ import lombok.Getter;
@Builder
public class Certification {
private final CertificationId id;
private final String title;
private final String description;
private final String url;
private final String name;
private final String issuer;
private final String date;
private final String credentialId;
}

View File

@@ -1,7 +1,13 @@
package com.pablotj.portfolio.domain.certification;
public record CertificationId(Long value) {
public record CertificationId(Long profileId, Long certificationId) {
public CertificationId(Long profileId) {
this(profileId, null);
}
public CertificationId {
if (value != null && value < 0) throw new IllegalArgumentException("CertificationId must be positive");
if (certificationId != null && certificationId < 0)
throw new IllegalArgumentException("CertificationId must be positive");
}
}

View File

@@ -10,5 +10,5 @@ public interface CertificationRepositoryPort {
Optional<Certification> findById(CertificationId id);
List<Certification> findAll();
List<Certification> findAll(Long profileId);
}

View File

@@ -1,13 +0,0 @@
package com.pablotj.portfolio.domain.contact;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class Contact {
private final ContactId id;
private final String title;
private final String description;
private final String url;
}

View File

@@ -1,7 +0,0 @@
package com.pablotj.portfolio.domain.contact;
public record ContactId(Long value) {
public ContactId {
if (value != null && value < 0) throw new IllegalArgumentException("ContactId must be positive");
}
}

View File

@@ -1,14 +0,0 @@
package com.pablotj.portfolio.domain.contact.port;
import com.pablotj.portfolio.domain.contact.Contact;
import com.pablotj.portfolio.domain.contact.ContactId;
import java.util.List;
import java.util.Optional;
public interface ContactRepositoryPort {
Contact save(Contact p);
Optional<Contact> findById(ContactId id);
List<Contact> findAll();
}

View File

@@ -7,7 +7,9 @@ import lombok.Getter;
@Builder
public class Education {
private final EducationId id;
private final String title;
private final String institution;
private final String degree;
private final String period;
private final String grade;
private final String description;
private final String url;
}

View File

@@ -1,7 +1,11 @@
package com.pablotj.portfolio.domain.education;
public record EducationId(Long value) {
public record EducationId(Long profileId, Long educationId) {
public EducationId(Long profileId) {
this(profileId, null);
}
public EducationId {
if (value != null && value < 0) throw new IllegalArgumentException("EducationId must be positive");
if (educationId != null && educationId < 0) throw new IllegalArgumentException("EducationId must be positive");
}
}

View File

@@ -10,5 +10,5 @@ public interface EducationRepositoryPort {
Optional<Education> findById(EducationId id);
List<Education> findAll();
List<Education> findAll(Long profileId);
}

View File

@@ -0,0 +1,11 @@
package com.pablotj.portfolio.domain.experience;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class Achievement {
private final ArchivementId id;
private String description;
}

View File

@@ -0,0 +1,7 @@
package com.pablotj.portfolio.domain.experience;
public record ArchivementId(Long value) {
public ArchivementId {
if (value != null && value < 0) throw new IllegalArgumentException("ArchivementId must be positive");
}
}

View File

@@ -1,5 +1,6 @@
package com.pablotj.portfolio.domain.experience;
import java.util.List;
import lombok.Builder;
import lombok.Getter;
@@ -7,7 +8,11 @@ import lombok.Getter;
@Builder
public class Experience {
private final ExperienceId id;
private final String title;
private final String company;
private final String position;
private final String period;
private final String location;
private final String description;
private final String url;
private final List<Technology> technologies;
private final List<Achievement> achievements;
}

View File

@@ -1,7 +1,13 @@
package com.pablotj.portfolio.domain.experience;
public record ExperienceId(Long value) {
public record ExperienceId(Long profileId, Long experienceId) {
public ExperienceId(Long profileId) {
this(profileId, null);
}
public ExperienceId {
if (value != null && value < 0) throw new IllegalArgumentException("ExperienceId must be positive");
if (experienceId != null && experienceId < 0)
throw new IllegalArgumentException("ProfileSocialLinkId must be positive");
}
}

View File

@@ -0,0 +1,11 @@
package com.pablotj.portfolio.domain.experience;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class Technology {
private final TechnologyId id;
private String name;
}

View File

@@ -0,0 +1,7 @@
package com.pablotj.portfolio.domain.experience;
public record TechnologyId(Long value) {
public TechnologyId {
if (value != null && value < 0) throw new IllegalArgumentException("TechnologyId must be positive");
}
}

View File

@@ -10,5 +10,6 @@ public interface ExperienceRepositoryPort {
Optional<Experience> findById(ExperienceId id);
List<Experience> findAll();
List<Experience> findAll(Long profileId);
}

View File

@@ -1,13 +0,0 @@
package com.pablotj.portfolio.domain.home;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class Home {
private final HomeId id;
private final String title;
private final String description;
private final String url;
}

View File

@@ -1,7 +0,0 @@
package com.pablotj.portfolio.domain.home;
public record HomeId(Long value) {
public HomeId {
if (value != null && value < 0) throw new IllegalArgumentException("HomeId must be positive");
}
}

View File

@@ -1,14 +0,0 @@
package com.pablotj.portfolio.domain.home.port;
import com.pablotj.portfolio.domain.home.Home;
import com.pablotj.portfolio.domain.home.HomeId;
import java.util.List;
import java.util.Optional;
public interface HomeRepositoryPort {
Home save(Home p);
Optional<Home> findById(HomeId id);
List<Home> findAll();
}

View File

@@ -0,0 +1,23 @@
package com.pablotj.portfolio.domain.profile;
import java.util.List;
import lombok.Builder;
import lombok.Getter;
import lombok.Singular;
@Getter
@Builder
public class Profile {
private final ProfileId id;
private final String slug;
private final String name;
private final String title;
private final String subtitle;
private final String email;
private final String phone;
private final String location;
private final String avatar;
private final String bio;
@Singular("social")
private final List<ProfileSocialLink> social;
}

View File

@@ -0,0 +1,16 @@
package com.pablotj.portfolio.domain.profile;
public record ProfileId(Long id, String slug) {
public ProfileId(Long id) {
this(id, null);
}
public ProfileId(String slug) {
this(null, slug);
}
public ProfileId {
if (id != null && id < 0) throw new IllegalArgumentException("ProfileId must be positive");
}
}

View File

@@ -0,0 +1,12 @@
package com.pablotj.portfolio.domain.profile;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class ProfileSocialLink {
private final ProfileSocialLinkId id;
private final String url;
private final String platform;
}

View File

@@ -0,0 +1,7 @@
package com.pablotj.portfolio.domain.profile;
public record ProfileSocialLinkId(Long value) {
public ProfileSocialLinkId {
if (value != null && value < 0) throw new IllegalArgumentException("ProfileSocialLinkId must be positive");
}
}

View File

@@ -0,0 +1,16 @@
package com.pablotj.portfolio.domain.profile.port;
import com.pablotj.portfolio.domain.profile.Profile;
import com.pablotj.portfolio.domain.profile.ProfileId;
import java.util.List;
import java.util.Optional;
public interface ProfileRepositoryPort {
Profile save(Profile p);
Optional<Profile> findBySlug(ProfileId id);
Optional<Profile> findById(ProfileId id);
List<Profile> findAll();
}

View File

@@ -1,5 +1,6 @@
package com.pablotj.portfolio.domain.project;
import java.util.List;
import lombok.Builder;
import lombok.Getter;
@@ -9,5 +10,9 @@ public class Project {
private final ProjectId id;
private final String title;
private final String description;
private final String url;
private final String image;
private final List<ProjectTechnology> technologies;
private final List<ProjectFeature> features;
private final String demo;
private final String repository;
}

View File

@@ -0,0 +1,11 @@
package com.pablotj.portfolio.domain.project;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class ProjectFeature {
private final ProjectFeatureId id;
private final String name;
}

View File

@@ -0,0 +1,7 @@
package com.pablotj.portfolio.domain.project;
public record ProjectFeatureId(Long value) {
public ProjectFeatureId {
if (value != null && value < 0) throw new IllegalArgumentException("ProjectFeatureId must be positive");
}
}

View File

@@ -1,7 +1,12 @@
package com.pablotj.portfolio.domain.project;
public record ProjectId(Long value) {
public record ProjectId(Long profileId, Long projectId) {
public ProjectId(Long profileId) {
this(profileId, null);
}
public ProjectId {
if (value != null && value < 0) throw new IllegalArgumentException("ProjectId must be positive");
if (projectId != null && projectId < 0) throw new IllegalArgumentException("ProjectId must be positive");
}
}

View File

@@ -0,0 +1,11 @@
package com.pablotj.portfolio.domain.project;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class ProjectTechnology {
private final ProjectTechnologyId id;
private final String name;
}

View File

@@ -0,0 +1,7 @@
package com.pablotj.portfolio.domain.project;
public record ProjectTechnologyId(Long value) {
public ProjectTechnologyId {
if (value != null && value < 0) throw new IllegalArgumentException("ProjectTechnologyId 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();
List<Project> findAll(Long profileId);
}

View File

@@ -7,7 +7,7 @@ import lombok.Getter;
@Builder
public class Skill {
private final SkillId id;
private final String title;
private final String description;
private final String url;
private final String name;
private final Integer level;
private final Integer years;
}

View File

@@ -0,0 +1,14 @@
package com.pablotj.portfolio.domain.skill;
import java.util.List;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class SkillGroup {
private final SkillGroupId id;
private final String name;
private final String icon;
private final List<Skill> skills;
}

View File

@@ -0,0 +1,12 @@
package com.pablotj.portfolio.domain.skill;
public record SkillGroupId(Long profileId, Long skillGroupId) {
public SkillGroupId(Long profileId) {
this(profileId, null);
}
public SkillGroupId {
if (skillGroupId != null && skillGroupId < 0) throw new IllegalArgumentException("SkillId must be positive");
}
}

View File

@@ -1,14 +1,14 @@
package com.pablotj.portfolio.domain.skill.port;
import com.pablotj.portfolio.domain.skill.Skill;
import com.pablotj.portfolio.domain.skill.SkillId;
import com.pablotj.portfolio.domain.skill.SkillGroup;
import com.pablotj.portfolio.domain.skill.SkillGroupId;
import java.util.List;
import java.util.Optional;
public interface SkillRepositoryPort {
Skill save(Skill p);
SkillGroup save(SkillGroup p);
Optional<Skill> findById(SkillId id);
Optional<SkillGroup> findById(SkillGroupId id);
List<Skill> findAll();
List<SkillGroup> 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

@@ -0,0 +1,26 @@
package com.pablotj.portfolio.infrastructure.config;
import io.jooby.Extension;
import io.jooby.Jooby;
import io.jooby.handler.Cors;
import io.jooby.handler.CorsHandler;
import jakarta.annotation.Nonnull;
import java.util.Arrays;
import java.util.List;
public class CorsConfig implements Extension {
@Override
public void install(@Nonnull Jooby app) {
String allowedOriginsString = app.getConfig().getString("app.cors.allowed-origins");
List<String> allowedOrigins = Arrays.asList(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);
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

@@ -1,40 +0,0 @@
package com.pablotj.portfolio.infrastructure.persistence.about.adapter;
import com.pablotj.portfolio.domain.about.About;
import com.pablotj.portfolio.domain.about.AboutId;
import com.pablotj.portfolio.domain.about.port.AboutRepositoryPort;
import com.pablotj.portfolio.infrastructure.persistence.about.entity.AboutJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.about.mapper.AboutJpaMapper;
import com.pablotj.portfolio.infrastructure.persistence.about.repo.SpringDataAboutRepository;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Repository;
@Repository
public class AboutRepositoryAdapter implements AboutRepositoryPort {
private final SpringDataAboutRepository repo;
private final AboutJpaMapper mapper;
public AboutRepositoryAdapter(SpringDataAboutRepository repo, AboutJpaMapper mapper) {
this.repo = repo;
this.mapper = mapper;
}
@Override
public About save(About p) {
AboutJpaEntity entity = mapper.toEntity(p);
AboutJpaEntity saved = repo.save(entity);
return mapper.toDomain(saved);
}
@Override
public Optional<About> findById(AboutId id) {
return repo.findById(id.value()).map(mapper::toDomain);
}
@Override
public List<About> findAll() {
return repo.findAll().stream().map(mapper::toDomain).toList();
}
}

View File

@@ -1,29 +0,0 @@
package com.pablotj.portfolio.infrastructure.persistence.about.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name = "abouts")
@Getter
@Setter
public class AboutJpaEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(columnDefinition = "text")
private String description;
private String url;
}

View File

@@ -1,24 +0,0 @@
package com.pablotj.portfolio.infrastructure.persistence.about.mapper;
import com.pablotj.portfolio.domain.about.About;
import com.pablotj.portfolio.domain.about.AboutId;
import com.pablotj.portfolio.infrastructure.persistence.about.entity.AboutJpaEntity;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(componentModel = "spring")
public interface AboutJpaMapper {
@Mapping(target = "id", ignore = true)
AboutJpaEntity toEntity(About domain);
default About toDomain(AboutJpaEntity e) {
if (e == null) return null;
return About.builder()
.id(e.getId() == null ? null : new AboutId(e.getId()))
.title(e.getTitle())
.description(e.getDescription())
.url(e.getUrl())
.build();
}
}

View File

@@ -1,7 +0,0 @@
package com.pablotj.portfolio.infrastructure.persistence.about.repo;
import com.pablotj.portfolio.infrastructure.persistence.about.entity.AboutJpaEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface SpringDataAboutRepository extends JpaRepository<AboutJpaEntity, Long> {
}

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.findById(id.value()).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() {
return repo.findAll().stream().map(mapper::toDomain).toList();
public List<Certification> findAll(Long profileId) {
return DB.find(CertificationEntity.class)
.where()
.eq("profile.id", profileId)
.findList()
.stream()
.map(MAPPER::toDomain)
.toList();
}
}

View File

@@ -0,0 +1,42 @@
package com.pablotj.portfolio.infrastructure.persistence.certification.entity;
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;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name = "CERTIFICATION")
@Getter
@Setter
public class CertificationEntity extends Model {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "profile_id", nullable = false)
private ProfileEntity profile;
@Column
private String name;
@Column
private String issuer;
@Column
private String date;
@Column
private String credentialId;
}

View File

@@ -1,29 +0,0 @@
package com.pablotj.portfolio.infrastructure.persistence.certification.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name = "certifications")
@Getter
@Setter
public class CertificationJpaEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(columnDefinition = "text")
private String description;
private String url;
}

View File

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

View File

@@ -1,24 +0,0 @@
package com.pablotj.portfolio.infrastructure.persistence.certification.mapper;
import com.pablotj.portfolio.domain.certification.Certification;
import com.pablotj.portfolio.domain.certification.CertificationId;
import com.pablotj.portfolio.infrastructure.persistence.certification.entity.CertificationJpaEntity;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(componentModel = "spring")
public interface CertificationJpaMapper {
@Mapping(target = "id", ignore = true)
CertificationJpaEntity toEntity(Certification domain);
default Certification toDomain(CertificationJpaEntity e) {
if (e == null) return null;
return Certification.builder()
.id(e.getId() == null ? null : new CertificationId(e.getId()))
.title(e.getTitle())
.description(e.getDescription())
.url(e.getUrl())
.build();
}
}

View File

@@ -1,7 +0,0 @@
package com.pablotj.portfolio.infrastructure.persistence.certification.repo;
import com.pablotj.portfolio.infrastructure.persistence.certification.entity.CertificationJpaEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface SpringDataCertificationRepository extends JpaRepository<CertificationJpaEntity, Long> {
}

View File

@@ -1,40 +0,0 @@
package com.pablotj.portfolio.infrastructure.persistence.contact.adapter;
import com.pablotj.portfolio.domain.contact.Contact;
import com.pablotj.portfolio.domain.contact.ContactId;
import com.pablotj.portfolio.domain.contact.port.ContactRepositoryPort;
import com.pablotj.portfolio.infrastructure.persistence.contact.entity.ContactJpaEntity;
import com.pablotj.portfolio.infrastructure.persistence.contact.mapper.ContactJpaMapper;
import com.pablotj.portfolio.infrastructure.persistence.contact.repo.SpringDataContactRepository;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Repository;
@Repository
public class ContactRepositoryAdapter implements ContactRepositoryPort {
private final SpringDataContactRepository repo;
private final ContactJpaMapper mapper;
public ContactRepositoryAdapter(SpringDataContactRepository repo, ContactJpaMapper mapper) {
this.repo = repo;
this.mapper = mapper;
}
@Override
public Contact save(Contact p) {
ContactJpaEntity entity = mapper.toEntity(p);
ContactJpaEntity saved = repo.save(entity);
return mapper.toDomain(saved);
}
@Override
public Optional<Contact> findById(ContactId id) {
return repo.findById(id.value()).map(mapper::toDomain);
}
@Override
public List<Contact> findAll() {
return repo.findAll().stream().map(mapper::toDomain).toList();
}
}

View File

@@ -1,24 +0,0 @@
package com.pablotj.portfolio.infrastructure.persistence.contact.mapper;
import com.pablotj.portfolio.domain.contact.Contact;
import com.pablotj.portfolio.domain.contact.ContactId;
import com.pablotj.portfolio.infrastructure.persistence.contact.entity.ContactJpaEntity;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(componentModel = "spring")
public interface ContactJpaMapper {
@Mapping(target = "id", ignore = true)
ContactJpaEntity toEntity(Contact domain);
default Contact toDomain(ContactJpaEntity e) {
if (e == null) return null;
return Contact.builder()
.id(e.getId() == null ? null : new ContactId(e.getId()))
.title(e.getTitle())
.description(e.getDescription())
.url(e.getUrl())
.build();
}
}

View File

@@ -1,7 +0,0 @@
package com.pablotj.portfolio.infrastructure.persistence.contact.repo;
import com.pablotj.portfolio.infrastructure.persistence.contact.entity.ContactJpaEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface SpringDataContactRepository extends JpaRepository<ContactJpaEntity, Long> {
}

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.findById(id.value()).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() {
return repo.findAll().stream().map(mapper::toDomain).toList();
public List<Education> findAll(Long profileId) {
return DB.find(EducationEntity.class)
.where()
.eq("profile.id", profileId)
.findList()
.stream()
.map(MAPPER::toDomain)
.toList();
}
}

View File

@@ -0,0 +1,45 @@
package com.pablotj.portfolio.infrastructure.persistence.education.entity;
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;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name = "EDUCATION")
@Getter
@Setter
public class EducationEntity extends Model {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "profile_id", nullable = false)
private ProfileEntity profile;
@Column
private String institution;
@Column
private String degree;
@Column
private String period;
@Column
private String grade;
@Column(columnDefinition = "text")
private String description;
}

View File

@@ -1,29 +0,0 @@
package com.pablotj.portfolio.infrastructure.persistence.education.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name = "educations")
@Getter
@Setter
public class EducationJpaEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(columnDefinition = "text")
private String description;
private String url;
}

View File

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

View File

@@ -1,24 +0,0 @@
package com.pablotj.portfolio.infrastructure.persistence.education.mapper;
import com.pablotj.portfolio.domain.education.Education;
import com.pablotj.portfolio.domain.education.EducationId;
import com.pablotj.portfolio.infrastructure.persistence.education.entity.EducationJpaEntity;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(componentModel = "spring")
public interface EducationJpaMapper {
@Mapping(target = "id", ignore = true)
EducationJpaEntity toEntity(Education domain);
default Education toDomain(EducationJpaEntity e) {
if (e == null) return null;
return Education.builder()
.id(e.getId() == null ? null : new EducationId(e.getId()))
.title(e.getTitle())
.description(e.getDescription())
.url(e.getUrl())
.build();
}
}

View File

@@ -1,7 +0,0 @@
package com.pablotj.portfolio.infrastructure.persistence.education.repo;
import com.pablotj.portfolio.infrastructure.persistence.education.entity.EducationJpaEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface SpringDataEducationRepository extends JpaRepository<EducationJpaEntity, Long> {
}

Some files were not shown because too many files have changed in this diff Show More