From 433838eee1a36a0c00f622121683d1d59684ef4f Mon Sep 17 00:00:00 2001 From: Pol Pinol Castuera Date: Tue, 27 May 2025 18:44:02 +0200 Subject: [PATCH] Fix mongo timeout --- build.gradle | 20 ++++-- .../application/PasswordService.java | 18 ------ .../RegisterUserCommandHandler.java | 43 ++++++++----- .../GetUserByIdCommandHandler.java | 16 ----- .../GetUserByIdQueryHandler.java | 27 ++++++++ .../GetUserQueryHandler.java | 20 ++++-- .../RequestUserUseCase/UserNotFound.java | 12 ---- .../io/autoinvestor/application/UserDTO.java | 8 +++ .../application/UserNotFound.java | 12 ++++ .../application/UsersReadModel.java | 12 ++-- .../io/autoinvestor/domain/AggregateRoot.java | 39 ----------- .../java/io/autoinvestor/domain/Event.java | 49 -------------- .../io/autoinvestor/domain/EventStore.java | 10 +++ src/main/java/io/autoinvestor/domain/Id.java | 5 ++ .../autoinvestor/domain/UserRepository.java | 7 -- .../io/autoinvestor/domain/events/Event.java | 34 ++++++++++ .../domain/{ => events}/EventId.java | 8 ++- .../domain/{ => events}/EventPayload.java | 2 +- .../domain/{ => events}/EventPublisher.java | 2 +- .../domain/events/EventSourcedEntity.java | 36 +++++++++++ .../domain/{users => model}/FirstName.java | 12 +++- .../InvalidPasswordLength.java | 2 +- .../domain/{users => model}/LastName.java | 12 +++- .../domain/{users => model}/RiskLevel.java | 21 ++++-- .../io/autoinvestor/domain/model/User.java | 64 +++++++++++++++++++ .../domain/{users => model}/UserEmail.java | 14 +++- .../domain/{users => model}/UserId.java | 10 ++- .../autoinvestor/domain/model/UserState.java | 28 ++++++++ .../domain/model/UserWasRegisteredEvent.java | 43 +++++++++++++ .../UserWasRegisteredEventPayload.java | 13 ++-- .../io/autoinvestor/domain/users/User.java | 50 --------------- .../autoinvestor/domain/users/UserName.java | 22 ------- .../autoinvestor/domain/users/UserState.java | 17 ----- .../domain/users/UserWasRegisteredEvent.java | 22 ------- .../CreateUserEventMapperDocument.java | 24 ------- .../InMemoryUserRepository.java | 17 ----- .../infrastructure/MongoUserRepository.java | 34 ---------- .../UserCreatedEventDocument.java | 20 ------ .../UserCreatedEventMessageMapper.java | 27 -------- .../infrastructure/UserCreatedMessage.java | 12 ---- .../UserCreatedMessagePayload.java | 8 --- .../infrastructure/UserReadModelInMemory.java | 36 ----------- .../infrastructure/UserReadModelMongo.java | 45 ------------- .../UserResponseDocumentMapper.java | 12 ---- .../infrastructure/UsersEventPublisher.java | 31 --------- .../infrastructure/UsersProjection.java | 32 ---------- .../event_publishers/EventMessageMapper.java | 42 ++++++++++++ .../InMemoryEventPublisher.java | 8 ++- .../PubsubEventPublisher.java | 58 +++++++++++++++++ .../read_models/InMemoryUsersReadModel.java | 34 ++++++++++ .../read_models/MongoUsersReadModel.java | 49 ++++++++++++++ .../UserDocument.java} | 6 +- .../read_models/UserMapper.java | 29 +++++++++ .../repositories/EventDocument.java | 50 +++++++++++++++ .../repositories/EventMapper.java | 54 ++++++++++++++++ .../InMemoryEventStoreRepository.java | 43 +++++++++++++ .../MongoEventStoreRepository.java | 62 ++++++++++++++++++ .../io/autoinvestor/ui/GetUserController.java | 31 +++++++++ .../ui/GlobalExceptionHandler.java | 7 +- ...oller.java => RegisterUserController.java} | 21 +----- .../ui/{user => }/RegisterUserRequest.java | 2 +- .../ui/{user => }/UserResponse.java | 4 +- .../resources/application-local.properties | 7 ++ .../resources/application-prod.properties | 4 ++ src/main/resources/application.properties | 4 +- 65 files changed, 877 insertions(+), 646 deletions(-) delete mode 100644 src/main/java/io/autoinvestor/application/PasswordService.java delete mode 100644 src/main/java/io/autoinvestor/application/RequestUserById/GetUserByIdCommandHandler.java create mode 100644 src/main/java/io/autoinvestor/application/RequestUserById/GetUserByIdQueryHandler.java delete mode 100644 src/main/java/io/autoinvestor/application/RequestUserUseCase/UserNotFound.java create mode 100644 src/main/java/io/autoinvestor/application/UserDTO.java create mode 100644 src/main/java/io/autoinvestor/application/UserNotFound.java delete mode 100644 src/main/java/io/autoinvestor/domain/AggregateRoot.java delete mode 100644 src/main/java/io/autoinvestor/domain/Event.java create mode 100644 src/main/java/io/autoinvestor/domain/EventStore.java delete mode 100644 src/main/java/io/autoinvestor/domain/UserRepository.java create mode 100644 src/main/java/io/autoinvestor/domain/events/Event.java rename src/main/java/io/autoinvestor/domain/{ => events}/EventId.java (52%) rename src/main/java/io/autoinvestor/domain/{ => events}/EventPayload.java (70%) rename src/main/java/io/autoinvestor/domain/{ => events}/EventPublisher.java (72%) create mode 100644 src/main/java/io/autoinvestor/domain/events/EventSourcedEntity.java rename src/main/java/io/autoinvestor/domain/{users => model}/FirstName.java (64%) rename src/main/java/io/autoinvestor/domain/{users => model}/InvalidPasswordLength.java (92%) rename src/main/java/io/autoinvestor/domain/{users => model}/LastName.java (65%) rename src/main/java/io/autoinvestor/domain/{users => model}/RiskLevel.java (51%) create mode 100644 src/main/java/io/autoinvestor/domain/model/User.java rename src/main/java/io/autoinvestor/domain/{users => model}/UserEmail.java (75%) rename src/main/java/io/autoinvestor/domain/{users => model}/UserId.java (51%) create mode 100644 src/main/java/io/autoinvestor/domain/model/UserState.java create mode 100644 src/main/java/io/autoinvestor/domain/model/UserWasRegisteredEvent.java rename src/main/java/io/autoinvestor/domain/{users => model}/UserWasRegisteredEventPayload.java (58%) delete mode 100644 src/main/java/io/autoinvestor/domain/users/User.java delete mode 100644 src/main/java/io/autoinvestor/domain/users/UserName.java delete mode 100644 src/main/java/io/autoinvestor/domain/users/UserState.java delete mode 100644 src/main/java/io/autoinvestor/domain/users/UserWasRegisteredEvent.java delete mode 100644 src/main/java/io/autoinvestor/infrastructure/CreateUserEventMapperDocument.java delete mode 100644 src/main/java/io/autoinvestor/infrastructure/InMemoryUserRepository.java delete mode 100644 src/main/java/io/autoinvestor/infrastructure/MongoUserRepository.java delete mode 100644 src/main/java/io/autoinvestor/infrastructure/UserCreatedEventDocument.java delete mode 100644 src/main/java/io/autoinvestor/infrastructure/UserCreatedEventMessageMapper.java delete mode 100644 src/main/java/io/autoinvestor/infrastructure/UserCreatedMessage.java delete mode 100644 src/main/java/io/autoinvestor/infrastructure/UserCreatedMessagePayload.java delete mode 100644 src/main/java/io/autoinvestor/infrastructure/UserReadModelInMemory.java delete mode 100644 src/main/java/io/autoinvestor/infrastructure/UserReadModelMongo.java delete mode 100644 src/main/java/io/autoinvestor/infrastructure/UserResponseDocumentMapper.java delete mode 100644 src/main/java/io/autoinvestor/infrastructure/UsersEventPublisher.java delete mode 100644 src/main/java/io/autoinvestor/infrastructure/UsersProjection.java create mode 100644 src/main/java/io/autoinvestor/infrastructure/event_publishers/EventMessageMapper.java rename src/main/java/io/autoinvestor/infrastructure/{ => event_publishers}/InMemoryEventPublisher.java (80%) create mode 100644 src/main/java/io/autoinvestor/infrastructure/event_publishers/PubsubEventPublisher.java create mode 100644 src/main/java/io/autoinvestor/infrastructure/read_models/InMemoryUsersReadModel.java create mode 100644 src/main/java/io/autoinvestor/infrastructure/read_models/MongoUsersReadModel.java rename src/main/java/io/autoinvestor/infrastructure/{UserReadModelDocument.java => read_models/UserDocument.java} (69%) create mode 100644 src/main/java/io/autoinvestor/infrastructure/read_models/UserMapper.java create mode 100644 src/main/java/io/autoinvestor/infrastructure/repositories/EventDocument.java create mode 100644 src/main/java/io/autoinvestor/infrastructure/repositories/EventMapper.java create mode 100644 src/main/java/io/autoinvestor/infrastructure/repositories/InMemoryEventStoreRepository.java create mode 100644 src/main/java/io/autoinvestor/infrastructure/repositories/MongoEventStoreRepository.java create mode 100644 src/main/java/io/autoinvestor/ui/GetUserController.java rename src/main/java/io/autoinvestor/ui/{user/UserController.java => RegisterUserController.java} (58%) rename src/main/java/io/autoinvestor/ui/{user => }/RegisterUserRequest.java (81%) rename src/main/java/io/autoinvestor/ui/{user => }/UserResponse.java (68%) create mode 100644 src/main/resources/application-local.properties create mode 100644 src/main/resources/application-prod.properties diff --git a/build.gradle b/build.gradle index 9003f6d..04e753c 100644 --- a/build.gradle +++ b/build.gradle @@ -21,15 +21,23 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - implementation 'org.springframework.security:spring-security-core:' - implementation 'io.jsonwebtoken:jjwt:0.12.6' - implementation 'io.github.cdimascio:java-dotenv:5.2.2' - implementation platform("com.google.cloud:spring-cloud-gcp-dependencies:6.1.1") - implementation 'com.google.cloud:spring-cloud-gcp-starter-pubsub' + + implementation 'com.google.cloud:google-cloud-pubsub:1.123.0' + implementation "com.google.cloud:spring-cloud-gcp-starter-pubsub:6.1.1" + implementation 'com.fasterxml.jackson.core:jackson-databind' implementation 'org.springframework.integration:spring-integration-core' - implementation 'com.google.cloud:google-cloud-pubsub:1.113.7' implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' + testImplementation 'org.springframework.boot:spring-boot-testcontainers' + testImplementation 'org.testcontainers:testcontainers' + testImplementation 'org.testcontainers:junit-jupiter' + testImplementation 'org.testcontainers:gcloud' + + compileOnly 'org.projectlombok:lombok:1.18.38' + annotationProcessor 'org.projectlombok:lombok:1.18.38' + + testCompileOnly 'org.projectlombok:lombok:1.18.38' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.38' } tasks.named('test') { diff --git a/src/main/java/io/autoinvestor/application/PasswordService.java b/src/main/java/io/autoinvestor/application/PasswordService.java deleted file mode 100644 index fecb19e..0000000 --- a/src/main/java/io/autoinvestor/application/PasswordService.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.autoinvestor.application; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.stereotype.Service; - -@Service -public class PasswordService { - - private static final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); - - public static String hashPassword(String rawPassword) { - return encoder.encode(rawPassword); - } - - public static boolean matches(String passwordRaw, String passwordEncrypted) { - return encoder.matches(passwordRaw, passwordEncrypted); - } -} diff --git a/src/main/java/io/autoinvestor/application/RegisterUserUseCase/RegisterUserCommandHandler.java b/src/main/java/io/autoinvestor/application/RegisterUserUseCase/RegisterUserCommandHandler.java index b531d44..127065e 100644 --- a/src/main/java/io/autoinvestor/application/RegisterUserUseCase/RegisterUserCommandHandler.java +++ b/src/main/java/io/autoinvestor/application/RegisterUserUseCase/RegisterUserCommandHandler.java @@ -1,14 +1,12 @@ package io.autoinvestor.application.RegisterUserUseCase; +import io.autoinvestor.application.UserDTO; import io.autoinvestor.application.UsersReadModel; -import io.autoinvestor.domain.Event; -import io.autoinvestor.domain.EventPublisher; -import io.autoinvestor.domain.UserRepository; -import io.autoinvestor.domain.users.User; +import io.autoinvestor.domain.events.Event; +import io.autoinvestor.domain.events.EventPublisher; +import io.autoinvestor.domain.EventStore; +import io.autoinvestor.domain.model.User; import io.autoinvestor.exceptions.BadRequestException; -import io.autoinvestor.infrastructure.UserCreatedEventMessageMapper; -import io.autoinvestor.infrastructure.UserCreatedMessage; -import io.autoinvestor.infrastructure.UsersEventPublisher; import java.util.List; import lombok.RequiredArgsConstructor; @@ -18,25 +16,36 @@ @RequiredArgsConstructor public class RegisterUserCommandHandler { - private final UserRepository repository; + private final EventStore eventStore; private final EventPublisher eventPublisher; - private final UsersEventPublisher usersEventPublisher; - private final UserCreatedEventMessageMapper userCreatedEventMessageMapper; - private final UsersReadModel usersReadModel; + private final UsersReadModel readModel; public void handle(RegisterUserCommand command) { if (command.email() == null || command.email().isBlank()) { throw new BadRequestException("Email cannot be null or empty"); } - if (usersReadModel.get(command.email()) != null) { + + if (this.readModel.get(command.email()).isPresent()) { throw UserRegisteredAlreadyExists.with(command.email()); } User user = User.create(command.firstName(), command.lastName(), command.email(), command.riskLevel()); - List> uncommittedEvents = user.releaseEvents(); - this.repository.save(uncommittedEvents); - this.eventPublisher.publish(uncommittedEvents); - List userCreatedMessages = this.userCreatedEventMessageMapper.map(uncommittedEvents); - this.usersEventPublisher.publishUserCreated(userCreatedMessages); + + List> events = user.getUncommittedEvents(); + + this.eventStore.save(user); + + UserDTO dto = new UserDTO( + user.getState().userId().value(), + command.email(), + command.firstName(), + command.lastName(), + command.riskLevel() + ); + this.readModel.save(dto); + + this.eventPublisher.publish(events); + + user.markEventsAsCommitted(); } } diff --git a/src/main/java/io/autoinvestor/application/RequestUserById/GetUserByIdCommandHandler.java b/src/main/java/io/autoinvestor/application/RequestUserById/GetUserByIdCommandHandler.java deleted file mode 100644 index bbd01ff..0000000 --- a/src/main/java/io/autoinvestor/application/RequestUserById/GetUserByIdCommandHandler.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.autoinvestor.application.RequestUserById; - -import io.autoinvestor.application.UsersReadModel; -import io.autoinvestor.ui.user.UserResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class GetUserByIdCommandHandler { - private final UsersReadModel readModel; - - public UserResponse handle (GetUserByIdQuery query) { - return readModel.getById(query.userId()); - } -} diff --git a/src/main/java/io/autoinvestor/application/RequestUserById/GetUserByIdQueryHandler.java b/src/main/java/io/autoinvestor/application/RequestUserById/GetUserByIdQueryHandler.java new file mode 100644 index 0000000..971f308 --- /dev/null +++ b/src/main/java/io/autoinvestor/application/RequestUserById/GetUserByIdQueryHandler.java @@ -0,0 +1,27 @@ +package io.autoinvestor.application.RequestUserById; + +import io.autoinvestor.application.UserNotFound; +import io.autoinvestor.application.UserDTO; +import io.autoinvestor.application.UsersReadModel; +import io.autoinvestor.ui.UserResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class GetUserByIdQueryHandler { + private final UsersReadModel readModel; + + public UserResponse handle (GetUserByIdQuery query) { + UserDTO dto = this.readModel.getById(query.userId()) + .orElseThrow(() -> UserNotFound.with(query.userId())); + + return new UserResponse( + dto.userId(), + dto.firstName(), + dto.lastName(), + dto.email(), + dto.riskLevel() + ); + } +} diff --git a/src/main/java/io/autoinvestor/application/RequestUserUseCase/GetUserQueryHandler.java b/src/main/java/io/autoinvestor/application/RequestUserUseCase/GetUserQueryHandler.java index 960479b..1fbc616 100644 --- a/src/main/java/io/autoinvestor/application/RequestUserUseCase/GetUserQueryHandler.java +++ b/src/main/java/io/autoinvestor/application/RequestUserUseCase/GetUserQueryHandler.java @@ -1,7 +1,9 @@ package io.autoinvestor.application.RequestUserUseCase; +import io.autoinvestor.application.UserDTO; +import io.autoinvestor.application.UserNotFound; import io.autoinvestor.application.UsersReadModel; -import io.autoinvestor.ui.user.UserResponse; +import io.autoinvestor.ui.UserResponse; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -11,10 +13,16 @@ public class GetUserQueryHandler { private final UsersReadModel usersReadModel; - public UserResponse handle(GetUserQuery getUserQuery) { - if (usersReadModel.get(getUserQuery.email()) == null) { - throw UserNotFound.with(getUserQuery.email()); - } - return usersReadModel.get(getUserQuery.email()); + public UserResponse handle(GetUserQuery query) { + UserDTO dto = this.usersReadModel.get(query.email()) + .orElseThrow(() -> UserNotFound.with(query.email())); + + return new UserResponse( + dto.userId(), + dto.firstName(), + dto.lastName(), + dto.email(), + dto.riskLevel() + ); } } diff --git a/src/main/java/io/autoinvestor/application/RequestUserUseCase/UserNotFound.java b/src/main/java/io/autoinvestor/application/RequestUserUseCase/UserNotFound.java deleted file mode 100644 index 60f3d93..0000000 --- a/src/main/java/io/autoinvestor/application/RequestUserUseCase/UserNotFound.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.autoinvestor.application.RequestUserUseCase; - -public class UserNotFound extends RuntimeException { - private UserNotFound(String message) { - super(message); - } - - public static UserNotFound with(String email) { - String message = "User with mail " + email + " doesn't exist."; - return new UserNotFound(message); - } -} diff --git a/src/main/java/io/autoinvestor/application/UserDTO.java b/src/main/java/io/autoinvestor/application/UserDTO.java new file mode 100644 index 0000000..572700f --- /dev/null +++ b/src/main/java/io/autoinvestor/application/UserDTO.java @@ -0,0 +1,8 @@ +package io.autoinvestor.application; + +public record UserDTO(String userId, + String email, + String firstName, + String lastName, + int riskLevel) { +} diff --git a/src/main/java/io/autoinvestor/application/UserNotFound.java b/src/main/java/io/autoinvestor/application/UserNotFound.java new file mode 100644 index 0000000..4687064 --- /dev/null +++ b/src/main/java/io/autoinvestor/application/UserNotFound.java @@ -0,0 +1,12 @@ +package io.autoinvestor.application; + +public class UserNotFound extends RuntimeException { + private UserNotFound(String message) { + super(message); + } + + public static UserNotFound with(String name) { + String message = "User with id/email " + name + " doesn't exist."; + return new UserNotFound(message); + } +} diff --git a/src/main/java/io/autoinvestor/application/UsersReadModel.java b/src/main/java/io/autoinvestor/application/UsersReadModel.java index a546ccc..2b025bd 100644 --- a/src/main/java/io/autoinvestor/application/UsersReadModel.java +++ b/src/main/java/io/autoinvestor/application/UsersReadModel.java @@ -1,12 +1,10 @@ package io.autoinvestor.application; -import io.autoinvestor.infrastructure.UserReadModelDocument; -import io.autoinvestor.ui.user.UserResponse; -public interface UsersReadModel { - - void add(UserReadModelDocument document); +import java.util.Optional; - UserResponse get(String email); - UserResponse getById(String userId); +public interface UsersReadModel { + void save(UserDTO user); + Optional get(String email); + Optional getById(String userId); } diff --git a/src/main/java/io/autoinvestor/domain/AggregateRoot.java b/src/main/java/io/autoinvestor/domain/AggregateRoot.java deleted file mode 100644 index 34af3dd..0000000 --- a/src/main/java/io/autoinvestor/domain/AggregateRoot.java +++ /dev/null @@ -1,39 +0,0 @@ -package io.autoinvestor.domain; - -import java.util.ArrayList; -import java.util.List; - -public class AggregateRoot { - private final List> appliedEvents; - private Integer version; - - protected AggregateRoot(List> stream) { - if (!stream.isEmpty()) { - for (Event event : stream) { - this.when(event); - } - this.version = stream.size(); - } else { - this.version = 0; - } - appliedEvents = new ArrayList<>(); - } - - protected void when(Event event) { - } - - protected void recordEvent(Event event) { - this.appliedEvents.add(event); - } - - protected void apply(Event event) { - this.when(event); - this.appliedEvents.add(event); - } - - public List> releaseEvents() { - List> events = new ArrayList<>(this.appliedEvents); - this.appliedEvents.clear(); - return events; - } -} diff --git a/src/main/java/io/autoinvestor/domain/Event.java b/src/main/java/io/autoinvestor/domain/Event.java deleted file mode 100644 index 167f225..0000000 --- a/src/main/java/io/autoinvestor/domain/Event.java +++ /dev/null @@ -1,49 +0,0 @@ -package io.autoinvestor.domain; - -import java.util.Date; - -public abstract class Event { - private final EventId id; - private final Id aggregateId; - private final String type; - private final EventPayload payload; - private final Date occurredAt; - private final int version; - - protected Event(Id aggregateId, String type, EventPayload payload) { - this(aggregateId, type, payload, 1); - } - - protected Event(Id aggregateId, String type, EventPayload payload, int version) { - this.id = EventId.generate(); - this.aggregateId = aggregateId; - this.type = type; - this.payload = payload; - this.occurredAt = new Date(); - this.version = version; - } - - public EventId getId() { - return id; - } - - public Id getAggregateId() { - return aggregateId; - } - - public String getType() { - return type; - } - - public EventPayload getPayload() { - return payload; - } - - public Date getOccurredAt() { - return occurredAt; - } - - public int getVersion() { - return version; - } -} diff --git a/src/main/java/io/autoinvestor/domain/EventStore.java b/src/main/java/io/autoinvestor/domain/EventStore.java new file mode 100644 index 0000000..726af11 --- /dev/null +++ b/src/main/java/io/autoinvestor/domain/EventStore.java @@ -0,0 +1,10 @@ +package io.autoinvestor.domain; + +import io.autoinvestor.domain.model.User; +import io.autoinvestor.domain.model.UserId; + + +public interface EventStore { + void save(User user); + User get(UserId userId); +} diff --git a/src/main/java/io/autoinvestor/domain/Id.java b/src/main/java/io/autoinvestor/domain/Id.java index 2960264..ea90fdb 100644 --- a/src/main/java/io/autoinvestor/domain/Id.java +++ b/src/main/java/io/autoinvestor/domain/Id.java @@ -31,4 +31,9 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(id); } + + @Override + public String toString() { + return value(); + } } diff --git a/src/main/java/io/autoinvestor/domain/UserRepository.java b/src/main/java/io/autoinvestor/domain/UserRepository.java deleted file mode 100644 index 0e0aab3..0000000 --- a/src/main/java/io/autoinvestor/domain/UserRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.autoinvestor.domain; - -import java.util.List; - -public interface UserRepository { - void save(List> userEvents); -} diff --git a/src/main/java/io/autoinvestor/domain/events/Event.java b/src/main/java/io/autoinvestor/domain/events/Event.java new file mode 100644 index 0000000..ea579ff --- /dev/null +++ b/src/main/java/io/autoinvestor/domain/events/Event.java @@ -0,0 +1,34 @@ +package io.autoinvestor.domain.events; + +import io.autoinvestor.domain.Id; +import lombok.Getter; + +import java.util.Date; + +@Getter +public abstract class Event

{ + private final EventId id; + private final Id aggregateId; + private final String type; + private final P payload; + private final Date occurredAt; + private final int version; + + protected Event(Id aggregateId, String type, P payload, int version) { + this.id = EventId.generate(); + this.aggregateId = aggregateId; + this.type = type; + this.payload = payload; + this.occurredAt = new Date(); + this.version = version; + } + + protected Event(EventId id, Id aggregateId, String type, P payload, Date occurredAt, int version) { + this.id = id; + this.aggregateId = aggregateId; + this.type = type; + this.payload = payload; + this.occurredAt = occurredAt; + this.version = version; + } +} diff --git a/src/main/java/io/autoinvestor/domain/EventId.java b/src/main/java/io/autoinvestor/domain/events/EventId.java similarity index 52% rename from src/main/java/io/autoinvestor/domain/EventId.java rename to src/main/java/io/autoinvestor/domain/events/EventId.java index 8601083..1fef05e 100644 --- a/src/main/java/io/autoinvestor/domain/EventId.java +++ b/src/main/java/io/autoinvestor/domain/events/EventId.java @@ -1,4 +1,6 @@ -package io.autoinvestor.domain; +package io.autoinvestor.domain.events; + +import io.autoinvestor.domain.Id; public class EventId extends Id { EventId(String id) { @@ -8,4 +10,8 @@ public class EventId extends Id { public static EventId generate() { return new EventId(generateId()); } + + public static EventId from(String id) { + return new EventId(id); + } } diff --git a/src/main/java/io/autoinvestor/domain/EventPayload.java b/src/main/java/io/autoinvestor/domain/events/EventPayload.java similarity index 70% rename from src/main/java/io/autoinvestor/domain/EventPayload.java rename to src/main/java/io/autoinvestor/domain/events/EventPayload.java index 48b01d4..afa0a55 100644 --- a/src/main/java/io/autoinvestor/domain/EventPayload.java +++ b/src/main/java/io/autoinvestor/domain/events/EventPayload.java @@ -1,4 +1,4 @@ -package io.autoinvestor.domain; +package io.autoinvestor.domain.events; import java.util.Map; diff --git a/src/main/java/io/autoinvestor/domain/EventPublisher.java b/src/main/java/io/autoinvestor/domain/events/EventPublisher.java similarity index 72% rename from src/main/java/io/autoinvestor/domain/EventPublisher.java rename to src/main/java/io/autoinvestor/domain/events/EventPublisher.java index 799870e..623689f 100644 --- a/src/main/java/io/autoinvestor/domain/EventPublisher.java +++ b/src/main/java/io/autoinvestor/domain/events/EventPublisher.java @@ -1,4 +1,4 @@ -package io.autoinvestor.domain; +package io.autoinvestor.domain.events; import java.util.List; diff --git a/src/main/java/io/autoinvestor/domain/events/EventSourcedEntity.java b/src/main/java/io/autoinvestor/domain/events/EventSourcedEntity.java new file mode 100644 index 0000000..abfef71 --- /dev/null +++ b/src/main/java/io/autoinvestor/domain/events/EventSourcedEntity.java @@ -0,0 +1,36 @@ +package io.autoinvestor.domain.events; + +import java.util.ArrayList; +import java.util.List; + +public abstract class EventSourcedEntity { + private final List> appliedEvents = new ArrayList<>(); + protected int version; + + protected EventSourcedEntity(List> stream) { + if (!stream.isEmpty()) { + for (Event e : stream) { + when(e); + } + version = stream.size(); + } else { + version = 0; + } + } + + protected void apply(Event e) { + appliedEvents.add(e); + when(e); + } + + protected abstract void when(Event e); + + public List> getUncommittedEvents() { + return new ArrayList<>(appliedEvents); + } + + public void markEventsAsCommitted() { + appliedEvents.clear(); + } +} + diff --git a/src/main/java/io/autoinvestor/domain/users/FirstName.java b/src/main/java/io/autoinvestor/domain/model/FirstName.java similarity index 64% rename from src/main/java/io/autoinvestor/domain/users/FirstName.java rename to src/main/java/io/autoinvestor/domain/model/FirstName.java index d5c7c50..2e7ebb7 100644 --- a/src/main/java/io/autoinvestor/domain/users/FirstName.java +++ b/src/main/java/io/autoinvestor/domain/model/FirstName.java @@ -1,4 +1,4 @@ -package io.autoinvestor.domain.users; +package io.autoinvestor.domain.model; import com.fasterxml.jackson.annotation.JsonValue; @@ -6,10 +6,18 @@ public class FirstName { private final String firstName; - public FirstName(String firstName) { + FirstName(String firstName) { this.firstName = firstName; } + public static FirstName empty() { + return new FirstName(""); + } + + public static FirstName from(String firstName) { + return new FirstName(firstName); + } + @JsonValue public String value() { return firstName; diff --git a/src/main/java/io/autoinvestor/domain/users/InvalidPasswordLength.java b/src/main/java/io/autoinvestor/domain/model/InvalidPasswordLength.java similarity index 92% rename from src/main/java/io/autoinvestor/domain/users/InvalidPasswordLength.java rename to src/main/java/io/autoinvestor/domain/model/InvalidPasswordLength.java index c63d832..deb824b 100644 --- a/src/main/java/io/autoinvestor/domain/users/InvalidPasswordLength.java +++ b/src/main/java/io/autoinvestor/domain/model/InvalidPasswordLength.java @@ -1,4 +1,4 @@ -package io.autoinvestor.domain.users; +package io.autoinvestor.domain.model; public class InvalidPasswordLength extends RuntimeException { private InvalidPasswordLength(String message) { diff --git a/src/main/java/io/autoinvestor/domain/users/LastName.java b/src/main/java/io/autoinvestor/domain/model/LastName.java similarity index 65% rename from src/main/java/io/autoinvestor/domain/users/LastName.java rename to src/main/java/io/autoinvestor/domain/model/LastName.java index 5a38d77..293ee12 100644 --- a/src/main/java/io/autoinvestor/domain/users/LastName.java +++ b/src/main/java/io/autoinvestor/domain/model/LastName.java @@ -1,4 +1,4 @@ -package io.autoinvestor.domain.users; +package io.autoinvestor.domain.model; import com.fasterxml.jackson.annotation.JsonValue; @@ -6,10 +6,18 @@ public class LastName { private final String lastName; - public LastName(String lastName) { + LastName(String lastName) { this.lastName = lastName; } + public static LastName empty() { + return new LastName(""); + } + + public static LastName from(String lastName) { + return new LastName(lastName); + } + @JsonValue public String value() { return lastName; diff --git a/src/main/java/io/autoinvestor/domain/users/RiskLevel.java b/src/main/java/io/autoinvestor/domain/model/RiskLevel.java similarity index 51% rename from src/main/java/io/autoinvestor/domain/users/RiskLevel.java rename to src/main/java/io/autoinvestor/domain/model/RiskLevel.java index 8f3f34f..1953b5f 100644 --- a/src/main/java/io/autoinvestor/domain/users/RiskLevel.java +++ b/src/main/java/io/autoinvestor/domain/model/RiskLevel.java @@ -1,25 +1,32 @@ -package io.autoinvestor.domain.users; +package io.autoinvestor.domain.model; import com.fasterxml.jackson.annotation.JsonValue; import io.autoinvestor.exceptions.RiskLevelNotValid; public class RiskLevel { - private final Integer riskLevel; + private final int riskLevel; - public RiskLevel(Integer riskLevel) { - validate(riskLevel); + RiskLevel(int riskLevel) { this.riskLevel = riskLevel; } - private static void validate(Integer riskLevel) { - + private static void validate(int riskLevel) { if (riskLevel < 1 || riskLevel > 4) { throw RiskLevelNotValid.with(riskLevel); } } + public static RiskLevel empty() { + return new RiskLevel(-1); + } + + public static RiskLevel from(int riskLevel) { + validate(riskLevel); + return new RiskLevel(riskLevel); + } + @JsonValue - public Integer value() { + public int value() { return this.riskLevel; } } diff --git a/src/main/java/io/autoinvestor/domain/model/User.java b/src/main/java/io/autoinvestor/domain/model/User.java new file mode 100644 index 0000000..e5ed88b --- /dev/null +++ b/src/main/java/io/autoinvestor/domain/model/User.java @@ -0,0 +1,64 @@ +package io.autoinvestor.domain.model; + +import io.autoinvestor.domain.events.Event; +import io.autoinvestor.domain.events.EventSourcedEntity; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.List; + +@Getter +public class User extends EventSourcedEntity { + + private UserState state; + + private User(List> stream) { + super(stream); + + if (stream.isEmpty()) { + this.state = UserState.empty(); + } + } + + public static User empty() { + return new User(new ArrayList<>()); + } + + public static User from(List> stream) { + return new User(stream); + } + + public static User create(String firstName, String lastName, String email, int riskLevel) { + User user = User.empty(); + + user.apply(UserWasRegisteredEvent.with( + user.getState().userId(), + FirstName.from(firstName), + LastName.from(lastName), + UserEmail.from(email), + RiskLevel.from(riskLevel), + user.version + )); + + return user; + } + + @Override + protected void when(Event event) { + switch (event.getType()) { + case UserWasRegisteredEvent.TYPE : + whenUserCreated((UserWasRegisteredEvent) event); + break; + default: + throw new IllegalArgumentException("Unknown event type"); + } + } + + private void whenUserCreated(UserWasRegisteredEvent event) { + if (this.state != null) { + this.state = UserState.empty(); + } + assert this.state != null; + this.state = this.state.withUserCreated(event); + } +} diff --git a/src/main/java/io/autoinvestor/domain/users/UserEmail.java b/src/main/java/io/autoinvestor/domain/model/UserEmail.java similarity index 75% rename from src/main/java/io/autoinvestor/domain/users/UserEmail.java rename to src/main/java/io/autoinvestor/domain/model/UserEmail.java index d49f1d4..c3c6ed9 100644 --- a/src/main/java/io/autoinvestor/domain/users/UserEmail.java +++ b/src/main/java/io/autoinvestor/domain/model/UserEmail.java @@ -1,4 +1,4 @@ -package io.autoinvestor.domain.users; +package io.autoinvestor.domain.model; import com.fasterxml.jackson.annotation.JsonValue; import io.autoinvestor.exceptions.EmailNotValid; @@ -8,8 +8,7 @@ public class UserEmail { private final String email; - public UserEmail(String email) { - validate(email); + UserEmail(String email) { this.email = email; } @@ -22,6 +21,15 @@ private static void validate(String email) { } } + public static UserEmail empty() { + return new UserEmail(""); + } + + public static UserEmail from(String email) { + validate(email); + return new UserEmail(email); + } + @JsonValue public String value() { return this.email; diff --git a/src/main/java/io/autoinvestor/domain/users/UserId.java b/src/main/java/io/autoinvestor/domain/model/UserId.java similarity index 51% rename from src/main/java/io/autoinvestor/domain/users/UserId.java rename to src/main/java/io/autoinvestor/domain/model/UserId.java index d44ac8e..2e50995 100644 --- a/src/main/java/io/autoinvestor/domain/users/UserId.java +++ b/src/main/java/io/autoinvestor/domain/model/UserId.java @@ -1,4 +1,4 @@ -package io.autoinvestor.domain.users; +package io.autoinvestor.domain.model; import io.autoinvestor.domain.Id; @@ -10,4 +10,12 @@ public class UserId extends Id { public static UserId generate() { return new UserId(generateId()); } + + public static UserId from(String id) { + return new UserId(id); + } + + public static UserId empty() { + return new UserId(""); + } } diff --git a/src/main/java/io/autoinvestor/domain/model/UserState.java b/src/main/java/io/autoinvestor/domain/model/UserState.java new file mode 100644 index 0000000..e3432d9 --- /dev/null +++ b/src/main/java/io/autoinvestor/domain/model/UserState.java @@ -0,0 +1,28 @@ +package io.autoinvestor.domain.model; + +import java.util.Date; + +public record UserState( + UserId userId, + FirstName firstName, + LastName lastName, + UserEmail userEmail, + RiskLevel riskLevel, + Date createdAt, + Date updatedAt +) { + public static UserState empty() { + return new UserState(UserId.empty(), FirstName.empty(), LastName.empty(), UserEmail.empty(), RiskLevel.empty(), new Date(), new Date()); + } + + public UserState withUserCreated(UserWasRegisteredEvent event) { + UserWasRegisteredEventPayload payload = event.getPayload(); + return new UserState(UserId.from(event.getAggregateId().value()), + FirstName.from(payload.firstName()), + LastName.from(payload.lastName()), + UserEmail.from(payload.email()), + RiskLevel.from(payload.riskLevel()), + event.getOccurredAt(), + new Date()); + } +} diff --git a/src/main/java/io/autoinvestor/domain/model/UserWasRegisteredEvent.java b/src/main/java/io/autoinvestor/domain/model/UserWasRegisteredEvent.java new file mode 100644 index 0000000..3502709 --- /dev/null +++ b/src/main/java/io/autoinvestor/domain/model/UserWasRegisteredEvent.java @@ -0,0 +1,43 @@ +package io.autoinvestor.domain.model; + +import io.autoinvestor.domain.events.Event; +import io.autoinvestor.domain.Id; +import io.autoinvestor.domain.events.EventId; + +import java.util.Date; + +public class UserWasRegisteredEvent extends Event { + + public static final String TYPE = "USER_CREATED"; + + private UserWasRegisteredEvent(Id aggregateId, UserWasRegisteredEventPayload payload, int version) { + super(aggregateId, TYPE, payload, version); + } + + protected UserWasRegisteredEvent(EventId id, + Id aggregateId, + UserWasRegisteredEventPayload payload, + Date occurredAt, + int version) { + super(id, aggregateId, TYPE, payload, occurredAt, version); + } + + public static UserWasRegisteredEvent with( + UserId userId, + FirstName firstName, + LastName lastName, + UserEmail userEmail, + RiskLevel riskLevel, + int version) { + var payload = new UserWasRegisteredEventPayload(firstName.value(), lastName.value(), userEmail.value(), riskLevel.value()); + return new UserWasRegisteredEvent(userId, payload, version); + } + + public static UserWasRegisteredEvent hydrate(EventId id, + Id aggregateId, + UserWasRegisteredEventPayload payload, + Date occurredAt, + int version) { + return new UserWasRegisteredEvent(id, aggregateId, payload, occurredAt, version); + } +} diff --git a/src/main/java/io/autoinvestor/domain/users/UserWasRegisteredEventPayload.java b/src/main/java/io/autoinvestor/domain/model/UserWasRegisteredEventPayload.java similarity index 58% rename from src/main/java/io/autoinvestor/domain/users/UserWasRegisteredEventPayload.java rename to src/main/java/io/autoinvestor/domain/model/UserWasRegisteredEventPayload.java index 8ff1a34..20d3386 100644 --- a/src/main/java/io/autoinvestor/domain/users/UserWasRegisteredEventPayload.java +++ b/src/main/java/io/autoinvestor/domain/model/UserWasRegisteredEventPayload.java @@ -1,16 +1,15 @@ -package io.autoinvestor.domain.users; +package io.autoinvestor.domain.model; -import io.autoinvestor.domain.EventPayload; +import io.autoinvestor.domain.events.EventPayload; import java.util.Map; public record UserWasRegisteredEventPayload( - FirstName firstName, - LastName lastName, - UserEmail email, - RiskLevel riskLevel + String firstName, + String lastName, + String email, + int riskLevel ) implements EventPayload { - @Override public Map asMap() { return Map.of("firstName", firstName, "lastName", lastName, "email", email, "riskLevel", riskLevel); diff --git a/src/main/java/io/autoinvestor/domain/users/User.java b/src/main/java/io/autoinvestor/domain/users/User.java deleted file mode 100644 index cbbee5e..0000000 --- a/src/main/java/io/autoinvestor/domain/users/User.java +++ /dev/null @@ -1,50 +0,0 @@ -package io.autoinvestor.domain.users; - -import io.autoinvestor.domain.AggregateRoot; -import io.autoinvestor.domain.Event; -import lombok.Getter; - -import java.util.ArrayList; -import java.util.List; - -public class User extends AggregateRoot { - - @Getter - private UserState userState; - - private User(List> stream) { - super(stream); - } - - @Override - protected void when(Event event) { - switch (event.getType()) { - case "UserWasRegisteredEvent" : - this.whenUserCreated((UserWasRegisteredEvent) event); - break; - } - } - - public static User empty() { - return new User(new ArrayList<>()); - } - - private void whenUserCreated(UserWasRegisteredEvent event) { - this.userState = UserState.withUserCreated(event); - } - - public static User create(String firstName, String lastName, String email, Integer riskLevel) { - UserId id = UserId.generate(); - FirstName firstNameDTO = new FirstName(firstName); - LastName lastNameDTO = new LastName(lastName); - RiskLevel riskLevelDTO = new RiskLevel(riskLevel); - UserEmail userEmail = new UserEmail(email); - User user = User.empty(); - user.createUser(id, firstNameDTO, lastNameDTO, userEmail, riskLevelDTO); - return user; - } - - public void createUser(UserId userId, FirstName firstName, LastName lastName, UserEmail userEmail, RiskLevel riskLevel) { - this.apply(UserWasRegisteredEvent.with(userId, firstName, lastName, userEmail, riskLevel)); - } -} diff --git a/src/main/java/io/autoinvestor/domain/users/UserName.java b/src/main/java/io/autoinvestor/domain/users/UserName.java deleted file mode 100644 index 487a32d..0000000 --- a/src/main/java/io/autoinvestor/domain/users/UserName.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.autoinvestor.domain.users; - -public class UserName { - private final String name; - - public UserName(String name) { - this.name = name; - } - - public String value() { - return name; - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (!(o instanceof UserName that)) - return false; - return name.equals(that.name); - } -} diff --git a/src/main/java/io/autoinvestor/domain/users/UserState.java b/src/main/java/io/autoinvestor/domain/users/UserState.java deleted file mode 100644 index 9c4528f..0000000 --- a/src/main/java/io/autoinvestor/domain/users/UserState.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.autoinvestor.domain.users; - -import java.util.Date; - -public record UserState( - UserId userId, - FirstName firstName, - LastName lastName, - UserEmail userEmail, - Date createdAt, - Date updatedAt -) { - public static UserState withUserCreated(UserWasRegisteredEvent event) { - UserWasRegisteredEventPayload payload = event.getPayload(); - return new UserState((UserId) event.getAggregateId(), payload.firstName(), payload.lastName(), payload.email(), new Date(), new Date()); - } -} diff --git a/src/main/java/io/autoinvestor/domain/users/UserWasRegisteredEvent.java b/src/main/java/io/autoinvestor/domain/users/UserWasRegisteredEvent.java deleted file mode 100644 index 5cb7df5..0000000 --- a/src/main/java/io/autoinvestor/domain/users/UserWasRegisteredEvent.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.autoinvestor.domain.users; - -import io.autoinvestor.domain.Event; -import io.autoinvestor.domain.Id; - -public class UserWasRegisteredEvent extends Event { - - private UserWasRegisteredEvent(Id aggregateId, UserWasRegisteredEventPayload payload) { - super(aggregateId, "USER_CREATED", payload); - } - - public static UserWasRegisteredEvent with( - UserId userId, - FirstName firstName, - LastName lastName, - UserEmail userEmail, - RiskLevel riskLevel - ) { - var payload = new UserWasRegisteredEventPayload(firstName, lastName, userEmail, riskLevel); - return new UserWasRegisteredEvent(userId, payload); - } -} diff --git a/src/main/java/io/autoinvestor/infrastructure/CreateUserEventMapperDocument.java b/src/main/java/io/autoinvestor/infrastructure/CreateUserEventMapperDocument.java deleted file mode 100644 index 2ab1568..0000000 --- a/src/main/java/io/autoinvestor/infrastructure/CreateUserEventMapperDocument.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.autoinvestor.infrastructure; - -import io.autoinvestor.domain.Event; -import io.autoinvestor.domain.users.UserWasRegisteredEvent; -import io.autoinvestor.domain.users.UserWasRegisteredEventPayload; -import org.springframework.stereotype.Component; - -@Component -public class CreateUserEventMapperDocument { - UserCreatedEventDocument toDocument(Event userCreatedEvent){ - UserWasRegisteredEventPayload payload = (UserWasRegisteredEventPayload) userCreatedEvent.getPayload(); - return new UserCreatedEventDocument( - userCreatedEvent.getId().value(), - userCreatedEvent.getAggregateId().value(), - payload.firstName().value(), - payload.lastName().value(), - payload.email().value(), - payload.riskLevel().value(), - userCreatedEvent.getOccurredAt().toInstant(), - userCreatedEvent.getVersion() - - ); - } -} diff --git a/src/main/java/io/autoinvestor/infrastructure/InMemoryUserRepository.java b/src/main/java/io/autoinvestor/infrastructure/InMemoryUserRepository.java deleted file mode 100644 index b727ff4..0000000 --- a/src/main/java/io/autoinvestor/infrastructure/InMemoryUserRepository.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.autoinvestor.infrastructure; - -import io.autoinvestor.domain.Event; -import io.autoinvestor.domain.UserRepository; -import java.util.ArrayList; -import java.util.List; -import org.springframework.stereotype.Repository; - -@Repository -public class InMemoryUserRepository implements UserRepository { - private final List> eventStore = new ArrayList<>(); - - @Override - public void save(List> userEvents) { - eventStore.addAll(userEvents); - } -} diff --git a/src/main/java/io/autoinvestor/infrastructure/MongoUserRepository.java b/src/main/java/io/autoinvestor/infrastructure/MongoUserRepository.java deleted file mode 100644 index 9b3d469..0000000 --- a/src/main/java/io/autoinvestor/infrastructure/MongoUserRepository.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.autoinvestor.infrastructure; - -import io.autoinvestor.domain.Event; -import io.autoinvestor.domain.UserRepository; -import io.grpc.alts.internal.Identity; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.annotation.Primary; -import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.stream.Collectors; - - -@Repository -@Primary -public class MongoUserRepository implements UserRepository { - - private final MongoTemplate template; - private final CreateUserEventMapperDocument mapper; - - public MongoUserRepository(MongoTemplate template, CreateUserEventMapperDocument mapper) { - this.template = template; - this.mapper = mapper; - } - - @Override - public void save(List> userEvents) { - List documents = userEvents.stream() - .map(mapper::toDocument) - .collect(Collectors.toList()); - template.insertAll(documents); - } -} diff --git a/src/main/java/io/autoinvestor/infrastructure/UserCreatedEventDocument.java b/src/main/java/io/autoinvestor/infrastructure/UserCreatedEventDocument.java deleted file mode 100644 index c2b97fc..0000000 --- a/src/main/java/io/autoinvestor/infrastructure/UserCreatedEventDocument.java +++ /dev/null @@ -1,20 +0,0 @@ -package io.autoinvestor.infrastructure; - -import org.springframework.data.annotation.Id; -import org.springframework.data.mongodb.core.mapping.Document; -import org.springframework.data.mongodb.core.mapping.Field; - -import java.time.Instant; - -@Document(collection = "events") -public record UserCreatedEventDocument( - @Id String id, - String aggregateId, - String firstName, - String lastName, - String email, - Integer riskLevel, - @Field("occurredAt") Instant occurredAt, - Integer version - -) {} diff --git a/src/main/java/io/autoinvestor/infrastructure/UserCreatedEventMessageMapper.java b/src/main/java/io/autoinvestor/infrastructure/UserCreatedEventMessageMapper.java deleted file mode 100644 index 658d6dc..0000000 --- a/src/main/java/io/autoinvestor/infrastructure/UserCreatedEventMessageMapper.java +++ /dev/null @@ -1,27 +0,0 @@ -package io.autoinvestor.infrastructure; - -import io.autoinvestor.domain.Event; -import io.autoinvestor.domain.users.UserWasRegisteredEventPayload; -import java.util.ArrayList; -import java.util.List; -import org.springframework.stereotype.Component; - -@Component -public class UserCreatedEventMessageMapper { - - public List map(List> userRegisteredEvent) { - List userCreatedMessages = new ArrayList<>(); - for (Event userWasRegistered : userRegisteredEvent) { - - UserWasRegisteredEventPayload payload = (UserWasRegisteredEventPayload) userWasRegistered.getPayload(); - UserCreatedMessagePayload userCreatedMessagePayload = new UserCreatedMessagePayload( - payload.firstName().value(), payload.lastName().value(), payload.email().value(), - payload.riskLevel().value()); - UserCreatedMessage userCreatedMessage = new UserCreatedMessage(userWasRegistered.getId().value(), - userWasRegistered.getOccurredAt(), userWasRegistered.getAggregateId().value(), - userWasRegistered.getVersion(), userWasRegistered.getType(), userCreatedMessagePayload); - userCreatedMessages.add(userCreatedMessage); - } - return userCreatedMessages; - } -} diff --git a/src/main/java/io/autoinvestor/infrastructure/UserCreatedMessage.java b/src/main/java/io/autoinvestor/infrastructure/UserCreatedMessage.java deleted file mode 100644 index da5f03f..0000000 --- a/src/main/java/io/autoinvestor/infrastructure/UserCreatedMessage.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.autoinvestor.infrastructure; - -import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.Date; - -public record UserCreatedMessage(@JsonProperty("eventId") String eventId, @JsonProperty("occurredAt") Date occurredAt, - @JsonProperty("aggregateId") String aggregateId, @JsonProperty("version") int version, - @JsonProperty("type") String type, @JsonProperty("payload") UserCreatedMessagePayload payload) { - public record Payload(@JsonProperty("email") String email, @JsonProperty("firstName") String firstName, - @JsonProperty("lastName") String lastName, @JsonProperty("riskLevel") int riskLevel) { - } -} diff --git a/src/main/java/io/autoinvestor/infrastructure/UserCreatedMessagePayload.java b/src/main/java/io/autoinvestor/infrastructure/UserCreatedMessagePayload.java deleted file mode 100644 index df94084..0000000 --- a/src/main/java/io/autoinvestor/infrastructure/UserCreatedMessagePayload.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.autoinvestor.infrastructure; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public record UserCreatedMessagePayload(@JsonProperty("firstName") String firstName, - @JsonProperty("lastName") String lastName, @JsonProperty("email") String email, - @JsonProperty("riskLevel") Integer riskLevel) { -} diff --git a/src/main/java/io/autoinvestor/infrastructure/UserReadModelInMemory.java b/src/main/java/io/autoinvestor/infrastructure/UserReadModelInMemory.java deleted file mode 100644 index 779cc39..0000000 --- a/src/main/java/io/autoinvestor/infrastructure/UserReadModelInMemory.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.autoinvestor.infrastructure; - -import io.autoinvestor.application.UsersReadModel; -import io.autoinvestor.ui.user.UserResponse; -import org.springframework.stereotype.Repository; - -import java.util.ArrayList; -import java.util.List; - -@Repository -public class UserReadModelInMemory implements UsersReadModel { - - private final List userReadModelDocuments; - - public UserReadModelInMemory() { - userReadModelDocuments = new ArrayList<>(); - } - - public void add(UserReadModelDocument document) { - userReadModelDocuments.add(document); - } - - @Override - public UserResponse get(String email) { - return userReadModelDocuments.stream() - .filter(document -> document.email().equals(email)) - .findFirst() - .map(UserResponseDocumentMapper::map) - .orElse(null); - } - - @Override - public UserResponse getById(String userId) { - return null; - } -} diff --git a/src/main/java/io/autoinvestor/infrastructure/UserReadModelMongo.java b/src/main/java/io/autoinvestor/infrastructure/UserReadModelMongo.java deleted file mode 100644 index 394630c..0000000 --- a/src/main/java/io/autoinvestor/infrastructure/UserReadModelMongo.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.autoinvestor.infrastructure; - -import io.autoinvestor.application.UsersReadModel; -import io.autoinvestor.ui.user.UserResponse; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Primary; -import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.data.mongodb.core.query.Criteria; -import org.springframework.data.mongodb.core.query.Query; -import org.springframework.stereotype.Repository; - -import java.util.ArrayList; -import java.util.List; - -@Repository -@Primary -public class UserReadModelMongo implements UsersReadModel { - - @Autowired - private MongoTemplate mongoTemplate; - - public void add(UserReadModelDocument document) { - mongoTemplate.save(document); - } - - @Override - public UserResponse get(String email) { - Query query = new Query(Criteria.where("email").is(email)); - UserReadModelDocument document = mongoTemplate.findOne(query, UserReadModelDocument.class); - if (document == null) { - return null; - } - return UserResponseDocumentMapper.map(document); - } - - @Override - public UserResponse getById(String userId) { - Query query = new Query(Criteria.where("userId").is(userId)); - UserReadModelDocument document = mongoTemplate.findOne(query, UserReadModelDocument.class, "users"); - if (document == null) { - return null; - } - return UserResponseDocumentMapper.map(document); - } -} diff --git a/src/main/java/io/autoinvestor/infrastructure/UserResponseDocumentMapper.java b/src/main/java/io/autoinvestor/infrastructure/UserResponseDocumentMapper.java deleted file mode 100644 index 7d7e6ba..0000000 --- a/src/main/java/io/autoinvestor/infrastructure/UserResponseDocumentMapper.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.autoinvestor.infrastructure; - -import io.autoinvestor.ui.user.UserResponse; -import org.springframework.stereotype.Service; - -@Service -public class UserResponseDocumentMapper { - - public static UserResponse map(UserReadModelDocument document) { - return new UserResponse(document.userId(), document.email(), document.firstName(), document.lastName(), document.riskLevel()); - } -} diff --git a/src/main/java/io/autoinvestor/infrastructure/UsersEventPublisher.java b/src/main/java/io/autoinvestor/infrastructure/UsersEventPublisher.java deleted file mode 100644 index 93e7992..0000000 --- a/src/main/java/io/autoinvestor/infrastructure/UsersEventPublisher.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.autoinvestor.infrastructure; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.cloud.spring.pubsub.core.PubSubTemplate; -import java.util.List; -import org.springframework.stereotype.Service; - -@Service -public class UsersEventPublisher { - - private static final String TOPIC = "users"; - private final PubSubTemplate pubSubTemplate; - - public UsersEventPublisher(PubSubTemplate pubSubTemplate) { - this.pubSubTemplate = pubSubTemplate; - } - - public void publishUserCreated(List userCreatedMessage) { - ObjectMapper objectMapper = new ObjectMapper(); - for (UserCreatedMessage message : userCreatedMessage) { - - try { - String jsonMessage = objectMapper.writeValueAsString(message); - pubSubTemplate.publish(TOPIC, jsonMessage); - } catch (JsonProcessingException e) { - throw new RuntimeException("Error serializing message to JSON", e); - } - } - } -} diff --git a/src/main/java/io/autoinvestor/infrastructure/UsersProjection.java b/src/main/java/io/autoinvestor/infrastructure/UsersProjection.java deleted file mode 100644 index 4545da8..0000000 --- a/src/main/java/io/autoinvestor/infrastructure/UsersProjection.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.autoinvestor.infrastructure; - -import io.autoinvestor.domain.users.UserWasRegisteredEvent; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.event.EventListener; -import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.stereotype.Component; - -@Component -public class UsersProjection { - - private final UserReadModelInMemory userReadModelInMemory; - private final UserReadModelMongo userReadModelMongo; - - public UsersProjection(UserReadModelInMemory userReadModelInMemory, UserReadModelMongo userReadModelMongo) { - this.userReadModelInMemory = userReadModelInMemory; - this.userReadModelMongo = userReadModelMongo; - } - - @EventListener - public void onUserRegistered(UserWasRegisteredEvent userWasRegisteredEvent) { - String userId = userWasRegisteredEvent.getAggregateId().value(); - String email = userWasRegisteredEvent.getPayload().email().value(); - String firstName = userWasRegisteredEvent.getPayload().firstName().value(); - String lastName = userWasRegisteredEvent.getPayload().lastName().value(); - Integer riskLevel = userWasRegisteredEvent.getPayload().riskLevel().value(); - - UserReadModelDocument userReadModelDocument = new UserReadModelDocument(userId, email, firstName, lastName, riskLevel); - //this.userReadModelInMemory.add(userReadModelDocument); - this.userReadModelMongo.add(userReadModelDocument); - } -} diff --git a/src/main/java/io/autoinvestor/infrastructure/event_publishers/EventMessageMapper.java b/src/main/java/io/autoinvestor/infrastructure/event_publishers/EventMessageMapper.java new file mode 100644 index 0000000..90fc31c --- /dev/null +++ b/src/main/java/io/autoinvestor/infrastructure/event_publishers/EventMessageMapper.java @@ -0,0 +1,42 @@ +package io.autoinvestor.infrastructure.event_publishers; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.protobuf.ByteString; +import com.google.pubsub.v1.PubsubMessage; +import io.autoinvestor.domain.events.Event; +import io.autoinvestor.exceptions.InternalErrorException; + +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; + + +final class EventMessageMapper { + + private final ObjectMapper objectMapper; + + EventMessageMapper(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + PubsubMessage toMessage(Event event) { + try { + Map envelope = new HashMap<>(); + envelope.put("payload", event.getPayload().asMap()); + envelope.put("eventId", event.getId().value()); + envelope.put("type", event.getType()); + envelope.put("aggregateId", event.getAggregateId().value()); + envelope.put("occurredAt", + Instant.ofEpochMilli(event.getOccurredAt().getTime()).toString()); + envelope.put("version", event.getVersion()); + + String json = objectMapper.writeValueAsString(envelope); + return PubsubMessage.newBuilder() + .setData(ByteString.copyFromUtf8(json)) + .build(); + } catch (JsonProcessingException ex) { + throw new InternalErrorException("Failed to serialise domain event"); + } + } +} diff --git a/src/main/java/io/autoinvestor/infrastructure/InMemoryEventPublisher.java b/src/main/java/io/autoinvestor/infrastructure/event_publishers/InMemoryEventPublisher.java similarity index 80% rename from src/main/java/io/autoinvestor/infrastructure/InMemoryEventPublisher.java rename to src/main/java/io/autoinvestor/infrastructure/event_publishers/InMemoryEventPublisher.java index 929bf30..edbe2f1 100644 --- a/src/main/java/io/autoinvestor/infrastructure/InMemoryEventPublisher.java +++ b/src/main/java/io/autoinvestor/infrastructure/event_publishers/InMemoryEventPublisher.java @@ -1,13 +1,15 @@ -package io.autoinvestor.infrastructure; +package io.autoinvestor.infrastructure.event_publishers; -import io.autoinvestor.domain.Event; -import io.autoinvestor.domain.EventPublisher; +import io.autoinvestor.domain.events.Event; +import io.autoinvestor.domain.events.EventPublisher; import java.util.ArrayList; import java.util.List; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; @Component +@Profile("local") public class InMemoryEventPublisher implements EventPublisher { private final ApplicationEventPublisher eventPublisher; diff --git a/src/main/java/io/autoinvestor/infrastructure/event_publishers/PubsubEventPublisher.java b/src/main/java/io/autoinvestor/infrastructure/event_publishers/PubsubEventPublisher.java new file mode 100644 index 0000000..ffa5296 --- /dev/null +++ b/src/main/java/io/autoinvestor/infrastructure/event_publishers/PubsubEventPublisher.java @@ -0,0 +1,58 @@ +package io.autoinvestor.infrastructure.event_publishers; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.pubsub.v1.ProjectTopicName; +import com.google.cloud.pubsub.v1.Publisher; +import io.autoinvestor.domain.events.Event; +import io.autoinvestor.domain.events.EventPublisher; +import jakarta.annotation.PreDestroy; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +@Profile("prod") +public class PubsubEventPublisher implements EventPublisher { + + private final Publisher publisher; + private final EventMessageMapper mapper; + + public PubsubEventPublisher( + @Value("${GCP_PROJECT}") String projectId, + @Value("${PUBSUB_TOPIC}") String topic, + ObjectMapper objectMapper + ) throws Exception { + this.mapper = new EventMessageMapper(objectMapper); + ProjectTopicName topicName = ProjectTopicName.of(projectId, topic); + this.publisher = Publisher.newBuilder(topicName).build(); + + log.info("Pub/Sub publisher created for topic {}", topicName); + } + + @Override + public void publish(List> events) { + if (events.isEmpty()) { + log.debug("publish invoked with empty list — nothing to do"); + return; + } + + log.info("Publishing {} domain event(s)", events.size()); + + events.stream() + .map(mapper::toMessage) + .forEach(publisher::publish); + } + + @PreDestroy + public void shutdown() throws Exception { + log.info("Shutting down Pub/Sub publisher..."); + publisher.shutdown(); + publisher.awaitTermination(1, TimeUnit.MINUTES); + } +} + diff --git a/src/main/java/io/autoinvestor/infrastructure/read_models/InMemoryUsersReadModel.java b/src/main/java/io/autoinvestor/infrastructure/read_models/InMemoryUsersReadModel.java new file mode 100644 index 0000000..199268d --- /dev/null +++ b/src/main/java/io/autoinvestor/infrastructure/read_models/InMemoryUsersReadModel.java @@ -0,0 +1,34 @@ +package io.autoinvestor.infrastructure.read_models; + +import io.autoinvestor.application.UserDTO; +import io.autoinvestor.application.UsersReadModel; +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Repository +public class InMemoryUsersReadModel implements UsersReadModel { + + private final List users = new ArrayList<>(); + + @Override + public void save(UserDTO dto) { + users.add(dto); + } + + @Override + public Optional get(String email) { + return users.stream() + .filter(user -> user.email().equals(email)) + .findFirst(); + } + + @Override + public Optional getById(String userId) { + return users.stream() + .filter(user -> user.userId().equals(userId)) + .findFirst(); + } +} diff --git a/src/main/java/io/autoinvestor/infrastructure/read_models/MongoUsersReadModel.java b/src/main/java/io/autoinvestor/infrastructure/read_models/MongoUsersReadModel.java new file mode 100644 index 0000000..4931158 --- /dev/null +++ b/src/main/java/io/autoinvestor/infrastructure/read_models/MongoUsersReadModel.java @@ -0,0 +1,49 @@ +package io.autoinvestor.infrastructure.read_models; + +import io.autoinvestor.application.UserDTO; +import io.autoinvestor.application.UsersReadModel; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Profile; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +@Primary +@Profile("prod") +public class MongoUsersReadModel implements UsersReadModel { + + private static final String COLLECTION = "users"; + + private final MongoTemplate template; + private final UserMapper mapper; + + public MongoUsersReadModel(MongoTemplate template, UserMapper mapper) { + this.template = template; + this.mapper = mapper; + } + + @Override + public void save(UserDTO dto) { + template.save(mapper.toDocument(dto), COLLECTION); + } + + @Override + public Optional get(String email) { + Query query = new Query(Criteria.where("email").is(email)); + UserDocument doc = template.findOne(query, UserDocument.class, COLLECTION); + return Optional.ofNullable(doc) + .map(mapper::toDTO); + } + + @Override + public Optional getById(String userId) { + Query query = new Query(Criteria.where("_id").is(userId)); + UserDocument doc = template.findOne(query, UserDocument.class, COLLECTION); + return Optional.ofNullable(doc) + .map(mapper::toDTO); + } +} diff --git a/src/main/java/io/autoinvestor/infrastructure/UserReadModelDocument.java b/src/main/java/io/autoinvestor/infrastructure/read_models/UserDocument.java similarity index 69% rename from src/main/java/io/autoinvestor/infrastructure/UserReadModelDocument.java rename to src/main/java/io/autoinvestor/infrastructure/read_models/UserDocument.java index db34155..5cf2780 100644 --- a/src/main/java/io/autoinvestor/infrastructure/UserReadModelDocument.java +++ b/src/main/java/io/autoinvestor/infrastructure/read_models/UserDocument.java @@ -1,11 +1,11 @@ -package io.autoinvestor.infrastructure; +package io.autoinvestor.infrastructure.read_models; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; @Document(collection = "users") -public record UserReadModelDocument( - String userId, +public record UserDocument( + @Id String userId, String email, String firstName, String lastName, diff --git a/src/main/java/io/autoinvestor/infrastructure/read_models/UserMapper.java b/src/main/java/io/autoinvestor/infrastructure/read_models/UserMapper.java new file mode 100644 index 0000000..93196fb --- /dev/null +++ b/src/main/java/io/autoinvestor/infrastructure/read_models/UserMapper.java @@ -0,0 +1,29 @@ +package io.autoinvestor.infrastructure.read_models; + +import io.autoinvestor.application.UserDTO; +import org.springframework.stereotype.Component; + + +@Component +public class UserMapper { + + public UserDocument toDocument(UserDTO dto) { + return new UserDocument( + dto.userId(), + dto.email(), + dto.firstName(), + dto.lastName(), + dto.riskLevel() + ); + } + + public UserDTO toDTO(UserDocument doc) { + return new UserDTO( + doc.userId(), + doc.email(), + doc.firstName(), + doc.lastName(), + doc.riskLevel() + ); + } +} diff --git a/src/main/java/io/autoinvestor/infrastructure/repositories/EventDocument.java b/src/main/java/io/autoinvestor/infrastructure/repositories/EventDocument.java new file mode 100644 index 0000000..8db9923 --- /dev/null +++ b/src/main/java/io/autoinvestor/infrastructure/repositories/EventDocument.java @@ -0,0 +1,50 @@ +package io.autoinvestor.infrastructure.repositories; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.Field; + +import java.util.Date; +import java.util.Map; + +@Getter +@Setter +@Document(collection = "events") +public class EventDocument { + + @Id + private String id; + + @Field + private String aggregateId; + + @Field + private String type; + + @Field + private Map payload; + + @Field + private Date occurredAt; + + @Field + private int version; + + public EventDocument() { } + + public EventDocument(String id, + String aggregateId, + String type, + Map payload, + Date occurredAt, + int version) { + this.id = id; + this.aggregateId = aggregateId; + this.type = type; + this.payload = payload; + this.occurredAt = occurredAt; + this.version = version; + } +} diff --git a/src/main/java/io/autoinvestor/infrastructure/repositories/EventMapper.java b/src/main/java/io/autoinvestor/infrastructure/repositories/EventMapper.java new file mode 100644 index 0000000..29fc695 --- /dev/null +++ b/src/main/java/io/autoinvestor/infrastructure/repositories/EventMapper.java @@ -0,0 +1,54 @@ +package io.autoinvestor.infrastructure.repositories; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.autoinvestor.domain.events.Event; +import io.autoinvestor.domain.events.EventId; +import io.autoinvestor.domain.events.EventPayload; +import io.autoinvestor.domain.model.UserId; +import io.autoinvestor.domain.model.UserWasRegisteredEvent; +import io.autoinvestor.domain.model.UserWasRegisteredEventPayload; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.Map; + +@Component +public class EventMapper { + + private final ObjectMapper json = new ObjectMapper(); + + public

EventDocument toDocument(Event

evt) { + Map payloadMap = + json.convertValue(evt.getPayload(), new TypeReference>() {}); + + return new EventDocument( + evt.getId().value(), + evt.getAggregateId().value(), + evt.getType(), + payloadMap, + evt.getOccurredAt(), + evt.getVersion() + ); + } + + public Event toDomain(EventDocument doc) { + EventId id = EventId.from(doc.getId()); + UserId aggId = UserId.from(doc.getAggregateId()); + Date occurred = doc.getOccurredAt(); + int version = doc.getVersion(); + + switch (doc.getType()) { + case UserWasRegisteredEvent.TYPE -> { + UserWasRegisteredEventPayload payload = + json.convertValue(doc.getPayload(), UserWasRegisteredEventPayload.class); + + return UserWasRegisteredEvent.hydrate(id, aggId, payload, occurred, version); + } + + default -> throw new IllegalArgumentException( + "Unknown event type: " + doc.getType() + ); + } + } +} diff --git a/src/main/java/io/autoinvestor/infrastructure/repositories/InMemoryEventStoreRepository.java b/src/main/java/io/autoinvestor/infrastructure/repositories/InMemoryEventStoreRepository.java new file mode 100644 index 0000000..c4afa4b --- /dev/null +++ b/src/main/java/io/autoinvestor/infrastructure/repositories/InMemoryEventStoreRepository.java @@ -0,0 +1,43 @@ +package io.autoinvestor.infrastructure.repositories; + +import io.autoinvestor.domain.events.Event; +import io.autoinvestor.domain.EventStore; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import io.autoinvestor.domain.model.User; +import io.autoinvestor.domain.model.UserId; +import org.springframework.context.annotation.Profile; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.stereotype.Repository; + +@Repository +@Profile("local") +public class InMemoryEventStoreRepository implements EventStore { + private final List> eventStore = new ArrayList<>(); + + @Override + public void save(User user) { + List> uncommittedEvents = user.getUncommittedEvents(); + if (!uncommittedEvents.isEmpty()) { + eventStore.addAll(uncommittedEvents); + user.markEventsAsCommitted(); + } + } + + @Override + public User get(UserId userId) { + if (eventStore.isEmpty()) { + return null; + } + + List> userEvents = eventStore.stream() + .filter(event -> event.getAggregateId().value().equals(userId.value())) + .toList(); + + return User.from(userEvents); + } +} diff --git a/src/main/java/io/autoinvestor/infrastructure/repositories/MongoEventStoreRepository.java b/src/main/java/io/autoinvestor/infrastructure/repositories/MongoEventStoreRepository.java new file mode 100644 index 0000000..50e50b3 --- /dev/null +++ b/src/main/java/io/autoinvestor/infrastructure/repositories/MongoEventStoreRepository.java @@ -0,0 +1,62 @@ +package io.autoinvestor.infrastructure.repositories; + +import io.autoinvestor.domain.events.Event; +import io.autoinvestor.domain.EventStore; +import io.autoinvestor.domain.model.User; +import io.autoinvestor.domain.model.UserId; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Profile; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.stream.Collectors; + + +@Repository +@Primary +@Profile("prod") +public class MongoEventStoreRepository implements EventStore { + private static final String COLLECTION = "events"; + + private final MongoTemplate template; + private final EventMapper mapper; + + public MongoEventStoreRepository(MongoTemplate template, EventMapper mapper) { + this.template = template; + this.mapper = mapper; + } + + @Override + public void save(User user) { + List docs = user.getUncommittedEvents() + .stream() + .map(mapper::toDocument) + .collect(Collectors.toList()); + template.insertAll(docs); + } + + @Override + public User get(UserId userId) { + Query q = Query.query( + Criteria.where("aggregateId") + .is(userId.value()) + ) + .with(Sort.by("version")); + + List docs = template.find(q, EventDocument.class, COLLECTION); + + if (docs.isEmpty()) { + return null; + } + + List> events = docs.stream() + .map(mapper::toDomain) + .collect(Collectors.toList()); + + return User.from(events); + } +} diff --git a/src/main/java/io/autoinvestor/ui/GetUserController.java b/src/main/java/io/autoinvestor/ui/GetUserController.java new file mode 100644 index 0000000..1f85bdf --- /dev/null +++ b/src/main/java/io/autoinvestor/ui/GetUserController.java @@ -0,0 +1,31 @@ +package io.autoinvestor.ui; + +import io.autoinvestor.application.RequestUserById.GetUserByIdQueryHandler; +import io.autoinvestor.application.RequestUserById.GetUserByIdQuery; +import io.autoinvestor.application.RequestUserUseCase.GetUserQuery; +import io.autoinvestor.application.RequestUserUseCase.GetUserQueryHandler; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/user") +@RequiredArgsConstructor +public class GetUserController { + + private final GetUserQueryHandler getUserCommandHandler; + private final GetUserByIdQueryHandler getUserByIdQueryHandler; + + @GetMapping + public ResponseEntity getUser( + @RequestHeader(value = "X-User-Id", required = false) String userId, + @RequestParam(value = "email", required = false) String email + ) { + if (userId != null) { + return ResponseEntity.ok(getUserByIdQueryHandler.handle(new GetUserByIdQuery(userId))); + } if (email != null) { + return ResponseEntity.ok(getUserCommandHandler.handle(new GetUserQuery(email))); + } + return ResponseEntity.badRequest().build(); + } +} diff --git a/src/main/java/io/autoinvestor/ui/GlobalExceptionHandler.java b/src/main/java/io/autoinvestor/ui/GlobalExceptionHandler.java index 74a3856..d51d5c9 100644 --- a/src/main/java/io/autoinvestor/ui/GlobalExceptionHandler.java +++ b/src/main/java/io/autoinvestor/ui/GlobalExceptionHandler.java @@ -1,16 +1,15 @@ package io.autoinvestor.ui; import io.autoinvestor.application.RegisterUserUseCase.UserRegisteredAlreadyExists; -import io.autoinvestor.application.RequestUserUseCase.UserNotFound; -import io.autoinvestor.domain.users.InvalidPasswordLength; +import io.autoinvestor.application.UserNotFound; +import io.autoinvestor.domain.model.InvalidPasswordLength; import io.autoinvestor.exceptions.*; -import io.autoinvestor.ui.user.UserController; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -@RestControllerAdvice(assignableTypes = {UserController.class}) +@RestControllerAdvice(assignableTypes = {RegisterUserController.class, GetUserController.class}) public class GlobalExceptionHandler { @ExceptionHandler(DuplicatedException.class) diff --git a/src/main/java/io/autoinvestor/ui/user/UserController.java b/src/main/java/io/autoinvestor/ui/RegisterUserController.java similarity index 58% rename from src/main/java/io/autoinvestor/ui/user/UserController.java rename to src/main/java/io/autoinvestor/ui/RegisterUserController.java index b3d96b8..474e2c0 100644 --- a/src/main/java/io/autoinvestor/ui/user/UserController.java +++ b/src/main/java/io/autoinvestor/ui/RegisterUserController.java @@ -1,8 +1,8 @@ -package io.autoinvestor.ui.user; +package io.autoinvestor.ui; import io.autoinvestor.application.RegisterUserUseCase.RegisterUserCommand; import io.autoinvestor.application.RegisterUserUseCase.RegisterUserCommandHandler; -import io.autoinvestor.application.RequestUserById.GetUserByIdCommandHandler; +import io.autoinvestor.application.RequestUserById.GetUserByIdQueryHandler; import io.autoinvestor.application.RequestUserById.GetUserByIdQuery; import io.autoinvestor.application.RequestUserUseCase.GetUserQuery; import io.autoinvestor.application.RequestUserUseCase.GetUserQueryHandler; @@ -14,27 +14,12 @@ @RestController @RequestMapping("/user") @RequiredArgsConstructor -public class UserController { +public class RegisterUserController { private static final Integer DEFAULT_RISK_LEVEL = 1; - private final GetUserQueryHandler getUserCommandHandler; - private final GetUserByIdCommandHandler getUserByIdCommandHandler; private final RegisterUserCommandHandler registerUserCommandHandler; - @GetMapping - public ResponseEntity getUser( - @RequestHeader(value = "X-User-Id", required = false) String userId, - @RequestParam(value = "email", required = false) String email - ) { - if (userId != null) { - return ResponseEntity.ok(getUserByIdCommandHandler.handle(new GetUserByIdQuery(userId))); - } if (email != null) { - return ResponseEntity.ok(getUserCommandHandler.handle(new GetUserQuery(email))); - } - return ResponseEntity.badRequest().build(); - } - @PostMapping public ResponseEntity handle(@RequestBody RegisterUserRequest dto) { registerUserCommandHandler.handle(new RegisterUserCommand( diff --git a/src/main/java/io/autoinvestor/ui/user/RegisterUserRequest.java b/src/main/java/io/autoinvestor/ui/RegisterUserRequest.java similarity index 81% rename from src/main/java/io/autoinvestor/ui/user/RegisterUserRequest.java rename to src/main/java/io/autoinvestor/ui/RegisterUserRequest.java index 0d12f2f..003362e 100644 --- a/src/main/java/io/autoinvestor/ui/user/RegisterUserRequest.java +++ b/src/main/java/io/autoinvestor/ui/RegisterUserRequest.java @@ -1,4 +1,4 @@ -package io.autoinvestor.ui.user; +package io.autoinvestor.ui; public record RegisterUserRequest( String firstName, diff --git a/src/main/java/io/autoinvestor/ui/user/UserResponse.java b/src/main/java/io/autoinvestor/ui/UserResponse.java similarity index 68% rename from src/main/java/io/autoinvestor/ui/user/UserResponse.java rename to src/main/java/io/autoinvestor/ui/UserResponse.java index 8869d64..39e006f 100644 --- a/src/main/java/io/autoinvestor/ui/user/UserResponse.java +++ b/src/main/java/io/autoinvestor/ui/UserResponse.java @@ -1,10 +1,10 @@ -package io.autoinvestor.ui.user; +package io.autoinvestor.ui; public record UserResponse( String userId, String email, String firstName, String lastName, - Integer riskLevel + int riskLevel ) { } diff --git a/src/main/resources/application-local.properties b/src/main/resources/application-local.properties new file mode 100644 index 0000000..1ed44a7 --- /dev/null +++ b/src/main/resources/application-local.properties @@ -0,0 +1,7 @@ +spring.autoconfigure.exclude=\ + org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\ + org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration + +spring.autoconfigure.exclude+=,\ + com.google.cloud.spring.autoconfigure.core.GcpContextAutoConfiguration,\ + com.google.cloud.spring.autoconfigure.pubsub.GcpPubSubAutoConfiguration \ No newline at end of file diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties new file mode 100644 index 0000000..e3fe723 --- /dev/null +++ b/src/main/resources/application-prod.properties @@ -0,0 +1,4 @@ +spring.data.mongodb.uri=${MONGODB_URI} +spring.data.mongodb.database=${MONGODB_DB} +GCP_PROJECT=${GCP_PROJECT} +PUBSUB_TOPIC=${PUBSUB_TOPIC} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a0f2694..ea9c919 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,4 +1,2 @@ spring.application.name=users -spring.cloud.gcp.pubsub.project-id=autoinvestor-tfm -spring.data.mongodb.uri=mongodb://localhost:27017 -spring.data.mongodb.database=mydatabase \ No newline at end of file +spring.profiles.active=local