From eae07c329ca2f987e3cf396221bdb79366ec7bcf Mon Sep 17 00:00:00 2001 From: Dusan Date: Mon, 16 Feb 2026 18:18:59 +0100 Subject: [PATCH] feat: Add Rating CRUD with MongoDb --- build.gradle.kts | 17 ++- environment/.local.env | 6 + .../com/devoops/rating/RatingApplication.java | 3 +- .../devoops/rating/config/MongoConfig.java | 10 ++ .../rating/controller/RatingController.java | 71 ++++++++++++ .../dto/request/CreateRatingRequest.java | 29 +++++ .../dto/request/UpdateRatingRequest.java | 14 +++ .../rating/dto/response/RatingResponse.java | 16 +++ .../exception/GlobalExceptionHandler.java | 64 +++++++++++ .../exception/RatingNotFoundException.java | 11 ++ .../devoops/rating/mapper/RatingMapper.java | 25 +++++ .../devoops/rating/model/BaseDocument.java | 44 ++++++++ .../java/com/devoops/rating/model/Rating.java | 30 +++++ .../rating/repository/RatingRepository.java | 23 ++++ .../devoops/rating/service/RatingService.java | 26 +++++ .../rating/service/RatingServiceImpl.java | 104 ++++++++++++++++++ src/main/resources/application.properties | 10 ++ 17 files changed, 497 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/devoops/rating/config/MongoConfig.java create mode 100644 src/main/java/com/devoops/rating/controller/RatingController.java create mode 100644 src/main/java/com/devoops/rating/dto/request/CreateRatingRequest.java create mode 100644 src/main/java/com/devoops/rating/dto/request/UpdateRatingRequest.java create mode 100644 src/main/java/com/devoops/rating/dto/response/RatingResponse.java create mode 100644 src/main/java/com/devoops/rating/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/com/devoops/rating/exception/RatingNotFoundException.java create mode 100644 src/main/java/com/devoops/rating/mapper/RatingMapper.java create mode 100644 src/main/java/com/devoops/rating/model/BaseDocument.java create mode 100644 src/main/java/com/devoops/rating/model/Rating.java create mode 100644 src/main/java/com/devoops/rating/repository/RatingRepository.java create mode 100644 src/main/java/com/devoops/rating/service/RatingService.java create mode 100644 src/main/java/com/devoops/rating/service/RatingServiceImpl.java diff --git a/build.gradle.kts b/build.gradle.kts index 06c2892..88ccae7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,21 +19,30 @@ repositories { } dependencies { - implementation("org.springframework.boot:spring-boot-starter-flyway") + // MongoDB + implementation("org.springframework.boot:spring-boot-starter-data-mongodb") + + // Lombok + compileOnly("org.projectlombok:lombok") + annotationProcessor("org.projectlombok:lombok") + + // MapStruct + implementation("org.mapstruct:mapstruct:1.6.3") + annotationProcessor("org.mapstruct:mapstruct-processor:1.6.3") + annotationProcessor("org.projectlombok:lombok-mapstruct-binding:0.2.0") + // implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-actuator") implementation("io.micrometer:micrometer-registry-prometheus") implementation("org.springframework.boot:spring-boot-starter-webmvc") + implementation("org.springframework.boot:spring-boot-starter-validation") implementation("net.logstash.logback:logstash-logback-encoder:8.0") - implementation("org.flywaydb:flyway-database-postgresql") //zipkin(tracing) implementation("org.springframework.boot:spring-boot-micrometer-tracing-brave") implementation("org.springframework.boot:spring-boot-starter-zipkin") implementation("io.micrometer:micrometer-tracing-bridge-brave") implementation("io.zipkin.reporter2:zipkin-reporter-brave") - runtimeOnly("org.postgresql:postgresql") - testImplementation("org.springframework.boot:spring-boot-starter-flyway-test") // testImplementation("org.springframework.boot:spring-boot-starter-security-test") testImplementation("org.springframework.boot:spring-boot-starter-webmvc-test") testRuntimeOnly("org.junit.platform:junit-platform-launcher") diff --git a/environment/.local.env b/environment/.local.env index a3dc869..1b704a2 100644 --- a/environment/.local.env +++ b/environment/.local.env @@ -2,3 +2,9 @@ SERVER_PORT=8080 LOGSTASH_HOST=logstash:5000 ZIPKIN_HOST=zipkin ZIPKIN_PORT=9411 + +# MongoDB +MONGODB_HOST=devoops-mongodb +MONGODB_PORT=27017 +MONGODB_USERNAME=devoops +MONGODB_PASSWORD=devoops diff --git a/src/main/java/com/devoops/rating/RatingApplication.java b/src/main/java/com/devoops/rating/RatingApplication.java index f0e90b9..41053ae 100644 --- a/src/main/java/com/devoops/rating/RatingApplication.java +++ b/src/main/java/com/devoops/rating/RatingApplication.java @@ -2,9 +2,8 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration; -@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) +@SpringBootApplication public class RatingApplication { public static void main(String[] args) { diff --git a/src/main/java/com/devoops/rating/config/MongoConfig.java b/src/main/java/com/devoops/rating/config/MongoConfig.java new file mode 100644 index 0000000..6695581 --- /dev/null +++ b/src/main/java/com/devoops/rating/config/MongoConfig.java @@ -0,0 +1,10 @@ +package com.devoops.rating.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.config.EnableMongoAuditing; + +@Configuration +@EnableMongoAuditing +public class MongoConfig { + +} diff --git a/src/main/java/com/devoops/rating/controller/RatingController.java b/src/main/java/com/devoops/rating/controller/RatingController.java new file mode 100644 index 0000000..14735e5 --- /dev/null +++ b/src/main/java/com/devoops/rating/controller/RatingController.java @@ -0,0 +1,71 @@ +package com.devoops.rating.controller; + +import com.devoops.rating.dto.request.CreateRatingRequest; +import com.devoops.rating.dto.response.RatingResponse; +import com.devoops.rating.dto.request.UpdateRatingRequest; +import com.devoops.rating.service.RatingService; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/api/v1/ratings") +public class RatingController { + + private final RatingService ratingService; + + public RatingController(RatingService ratingService) { + this.ratingService = ratingService; + } + + @PostMapping + public ResponseEntity createRating(@Valid @RequestBody CreateRatingRequest request) { + RatingResponse response = ratingService.createRating(request); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + + @GetMapping("/{id}") + public ResponseEntity getRatingById(@PathVariable UUID id) { + RatingResponse response = ratingService.getRatingById(id); + return ResponseEntity.ok(response); + } + + @GetMapping + public ResponseEntity> getAllRatings() { + List responses = ratingService.getAllRatings(); + return ResponseEntity.ok(responses); + } + + @GetMapping("/target/{targetId}") + public ResponseEntity> getRatingsByTargetId(@PathVariable UUID targetId) { + List responses = ratingService.getRatingsByTargetId(targetId); + return ResponseEntity.ok(responses); + } + + + + @GetMapping("/guest/{guestId}") + public ResponseEntity> getRatingsByGuestId(@PathVariable UUID guestId) { + List responses = ratingService.getRatingsByGuestId(guestId); + return ResponseEntity.ok(responses); + } + + @PutMapping("/{id}") + public ResponseEntity updateRating( + @PathVariable UUID id, + @Valid @RequestBody UpdateRatingRequest request) { + RatingResponse response = ratingService.updateRating(id, request); + return ResponseEntity.ok(response); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteRating(@PathVariable UUID id) { + ratingService.deleteRating(id); + return ResponseEntity.noContent().build(); + } +} + diff --git a/src/main/java/com/devoops/rating/dto/request/CreateRatingRequest.java b/src/main/java/com/devoops/rating/dto/request/CreateRatingRequest.java new file mode 100644 index 0000000..ebb6657 --- /dev/null +++ b/src/main/java/com/devoops/rating/dto/request/CreateRatingRequest.java @@ -0,0 +1,29 @@ +package com.devoops.rating.dto.request; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.util.UUID; + +public record CreateRatingRequest( + @NotNull(message = "Target ID is required") + UUID targetId, + + @NotBlank(message = "Guest first name is required") + String guestFirstName, + + @NotBlank(message = "Guest last name is required") + String guestLastName, + + @NotNull(message = "Guest ID is required") + UUID guestId, + + @NotNull(message = "Score is required") + @Min(value = 1, message = "Score must be at least 1") + @Max(value = 5, message = "Score must be at most 5") + Integer score +) { +} + diff --git a/src/main/java/com/devoops/rating/dto/request/UpdateRatingRequest.java b/src/main/java/com/devoops/rating/dto/request/UpdateRatingRequest.java new file mode 100644 index 0000000..05c7732 --- /dev/null +++ b/src/main/java/com/devoops/rating/dto/request/UpdateRatingRequest.java @@ -0,0 +1,14 @@ +package com.devoops.rating.dto.request; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; + +public record UpdateRatingRequest( + @NotNull(message = "Score is required") + @Min(value = 1, message = "Score must be at least 1") + @Max(value = 5, message = "Score must be at most 5") + Integer score +) { +} + diff --git a/src/main/java/com/devoops/rating/dto/response/RatingResponse.java b/src/main/java/com/devoops/rating/dto/response/RatingResponse.java new file mode 100644 index 0000000..4b481fe --- /dev/null +++ b/src/main/java/com/devoops/rating/dto/response/RatingResponse.java @@ -0,0 +1,16 @@ +package com.devoops.rating.dto.response; + +import java.time.LocalDateTime; +import java.util.UUID; + +public record RatingResponse( + UUID id, + UUID targetId, + String guestFirstName, + String guestLastName, + UUID guestId, + Integer score, + LocalDateTime createdAt, + LocalDateTime updatedAt +) { } + diff --git a/src/main/java/com/devoops/rating/exception/GlobalExceptionHandler.java b/src/main/java/com/devoops/rating/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..688c52f --- /dev/null +++ b/src/main/java/com/devoops/rating/exception/GlobalExceptionHandler.java @@ -0,0 +1,64 @@ +package com.devoops.rating.exception; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + @ExceptionHandler(RatingNotFoundException.class) + public ResponseEntity handleRatingNotFound(RatingNotFoundException ex) { + ErrorResponse error = new ErrorResponse( + HttpStatus.NOT_FOUND.value(), + ex.getMessage(), + Instant.now() + ); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidationErrors(MethodArgumentNotValidException ex) { + Map errors = new HashMap<>(); + ex.getBindingResult().getAllErrors().forEach(error -> { + String fieldName = ((FieldError) error).getField(); + String errorMessage = error.getDefaultMessage(); + errors.put(fieldName, errorMessage); + }); + + ValidationErrorResponse response = new ValidationErrorResponse( + HttpStatus.BAD_REQUEST.value(), + "Validation failed", + errors, + Instant.now() + ); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleGenericException(Exception ex) { + log.error("Unexpected error occurred: ", ex); + ErrorResponse error = new ErrorResponse( + HttpStatus.INTERNAL_SERVER_ERROR.value(), + ex.getMessage(), + Instant.now() + ); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); + } + + public record ErrorResponse(int status, String message, Instant timestamp) {} + + public record ValidationErrorResponse(int status, String message, Map errors, Instant timestamp) {} +} + diff --git a/src/main/java/com/devoops/rating/exception/RatingNotFoundException.java b/src/main/java/com/devoops/rating/exception/RatingNotFoundException.java new file mode 100644 index 0000000..859fe72 --- /dev/null +++ b/src/main/java/com/devoops/rating/exception/RatingNotFoundException.java @@ -0,0 +1,11 @@ +package com.devoops.rating.exception; + +import java.util.UUID; + +public class RatingNotFoundException extends RuntimeException { + + public RatingNotFoundException(UUID id) { + super("Rating not found with id: " + id); + } +} + diff --git a/src/main/java/com/devoops/rating/mapper/RatingMapper.java b/src/main/java/com/devoops/rating/mapper/RatingMapper.java new file mode 100644 index 0000000..516199a --- /dev/null +++ b/src/main/java/com/devoops/rating/mapper/RatingMapper.java @@ -0,0 +1,25 @@ +package com.devoops.rating.mapper; + +import com.devoops.rating.dto.request.CreateRatingRequest; +import com.devoops.rating.dto.response.RatingResponse; +import com.devoops.rating.model.Rating; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + + +import java.util.List; + +@Mapper(componentModel = "spring") +public interface RatingMapper { + + @Mapping(target = "id", ignore = true) + @Mapping(target = "createdAt", ignore = true) + @Mapping(target = "updatedAt", ignore = true) + @Mapping(target = "isDeleted", ignore = true) + Rating toEntity(CreateRatingRequest request); + + RatingResponse toResponse(Rating rating); + + List toResponseList(List ratings); +} + diff --git a/src/main/java/com/devoops/rating/model/BaseDocument.java b/src/main/java/com/devoops/rating/model/BaseDocument.java new file mode 100644 index 0000000..a3e4097 --- /dev/null +++ b/src/main/java/com/devoops/rating/model/BaseDocument.java @@ -0,0 +1,44 @@ +package com.devoops.rating.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.annotation.Transient; +import org.springframework.data.domain.Persistable; +import org.springframework.data.mongodb.core.mapping.MongoId; +import org.springframework.data.mongodb.core.mapping.FieldType; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@SuperBuilder +public abstract class BaseDocument implements Persistable { + + @Id + @Builder.Default + private UUID id = UUID.randomUUID(); + + @CreatedDate + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; + + @Builder.Default + private boolean isDeleted = false; + + @Override + @Transient + public boolean isNew() { + return createdAt == null; + } +} + diff --git a/src/main/java/com/devoops/rating/model/Rating.java b/src/main/java/com/devoops/rating/model/Rating.java new file mode 100644 index 0000000..7851216 --- /dev/null +++ b/src/main/java/com/devoops/rating/model/Rating.java @@ -0,0 +1,30 @@ +package com.devoops.rating.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@AllArgsConstructor +@SuperBuilder +@Document(collection = "ratings") +public class Rating extends BaseDocument { + + private UUID targetId; + + private String guestFirstName; + + private String guestLastName; + + private UUID guestId; + + private Integer score; +} + diff --git a/src/main/java/com/devoops/rating/repository/RatingRepository.java b/src/main/java/com/devoops/rating/repository/RatingRepository.java new file mode 100644 index 0000000..64539e0 --- /dev/null +++ b/src/main/java/com/devoops/rating/repository/RatingRepository.java @@ -0,0 +1,23 @@ +package com.devoops.rating.repository; + +import com.devoops.rating.model.Rating; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Repository +public interface RatingRepository extends MongoRepository { + + Optional findByIdAndIsDeletedFalse(UUID id); + + List findAllByIsDeletedFalse(); + + List findAllByTargetIdAndIsDeletedFalse(UUID targetId); + + List findAllByGuestIdAndIsDeletedFalse(UUID guestId); + +} + diff --git a/src/main/java/com/devoops/rating/service/RatingService.java b/src/main/java/com/devoops/rating/service/RatingService.java new file mode 100644 index 0000000..bf3fc66 --- /dev/null +++ b/src/main/java/com/devoops/rating/service/RatingService.java @@ -0,0 +1,26 @@ +package com.devoops.rating.service; + +import com.devoops.rating.dto.request.CreateRatingRequest; +import com.devoops.rating.dto.response.RatingResponse; +import com.devoops.rating.dto.request.UpdateRatingRequest; + +import java.util.List; +import java.util.UUID; + +public interface RatingService { + + RatingResponse createRating(CreateRatingRequest request); + + RatingResponse getRatingById(UUID id); + + List getAllRatings(); + + List getRatingsByTargetId(UUID targetId); + + List getRatingsByGuestId(UUID guestId); + + RatingResponse updateRating(UUID id, UpdateRatingRequest request); + + void deleteRating(UUID id); +} + diff --git a/src/main/java/com/devoops/rating/service/RatingServiceImpl.java b/src/main/java/com/devoops/rating/service/RatingServiceImpl.java new file mode 100644 index 0000000..88e19cd --- /dev/null +++ b/src/main/java/com/devoops/rating/service/RatingServiceImpl.java @@ -0,0 +1,104 @@ +package com.devoops.rating.service; + +import com.devoops.rating.dto.request.CreateRatingRequest; +import com.devoops.rating.dto.response.RatingResponse; +import com.devoops.rating.dto.request.UpdateRatingRequest; +import com.devoops.rating.exception.RatingNotFoundException; +import com.devoops.rating.mapper.RatingMapper; +import com.devoops.rating.model.Rating; +import com.devoops.rating.repository.RatingRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +@Service +public class RatingServiceImpl implements RatingService { + + private static final Logger log = LoggerFactory.getLogger(RatingServiceImpl.class); + + private final RatingRepository ratingRepository; + private final RatingMapper ratingMapper; + + public RatingServiceImpl(RatingRepository ratingRepository, RatingMapper ratingMapper) { + this.ratingRepository = ratingRepository; + this.ratingMapper = ratingMapper; + } + + @Override + public RatingResponse createRating(CreateRatingRequest request) { + log.debug("Creating new rating for target: {}", request.targetId()); + + Rating rating = ratingMapper.toEntity(request); + + Rating savedRating = ratingRepository.save(rating); + log.info("Created rating with id: {}", savedRating.getId()); + + return ratingMapper.toResponse(savedRating); + } + + @Override + public RatingResponse getRatingById(UUID id) { + log.debug("Fetching rating with id: {}", id); + + Rating rating = ratingRepository.findByIdAndIsDeletedFalse(id) + .orElseThrow(() -> new RatingNotFoundException(id)); + + return ratingMapper.toResponse(rating); + } + + @Override + public List getAllRatings() { + log.debug("Fetching all ratings"); + + return ratingMapper.toResponseList(ratingRepository.findAllByIsDeletedFalse()); + } + + @Override + public List getRatingsByTargetId(UUID targetId) { + log.debug("Fetching ratings for target: {}", targetId); + + return ratingMapper.toResponseList(ratingRepository.findAllByTargetIdAndIsDeletedFalse(targetId)); + } + + @Override + public List getRatingsByGuestId(UUID guestId) { + log.debug("Fetching ratings by guest: {}", guestId); + + return ratingMapper.toResponseList(ratingRepository.findAllByGuestIdAndIsDeletedFalse(guestId)); + } + + @Override + public RatingResponse updateRating(UUID id, UpdateRatingRequest request) { + log.debug("Updating rating with id: {}", id); + + Rating rating = ratingRepository.findByIdAndIsDeletedFalse(id) + .orElseThrow(() -> new RatingNotFoundException(id)); + + rating.setScore(request.score()); + rating.setUpdatedAt(LocalDateTime.now()); + + Rating updatedRating = ratingRepository.save(rating); + log.info("Updated rating with id: {}", updatedRating.getId()); + + return ratingMapper.toResponse(updatedRating); + } + + @Override + public void deleteRating(UUID id) { + log.debug("Soft deleting rating with id: {}", id); + + Rating rating = ratingRepository.findByIdAndIsDeletedFalse(id) + .orElseThrow(() -> new RatingNotFoundException(id)); + + rating.setDeleted(true); + rating.setUpdatedAt(LocalDateTime.now()); + ratingRepository.save(rating); + + log.info("Soft deleted rating with id: {}", id); + } +} + diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a4386c7..a431b4d 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,15 @@ spring.application.name=rating server.port=${SERVER_PORT:8080} + +# MongoDB Configuration +spring.mongodb.host=${MONGODB_HOST:devoops-mongodb} +spring.mongodb.port=${MONGODB_PORT:27017} +spring.mongodb.database=rating_db +spring.mongodb.username=${MONGODB_USERNAME:devoops} +spring.mongodb.password=${MONGODB_PASSWORD:devoops} +spring.mongodb.authentication-database=admin +spring.mongodb.representation.uuid=standard + # Logging configuration logging.logstash.host=${LOGSTASH_HOST:localhost:5000} logging.level.root=INFO