diff --git a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/config/ProjectWebClientConfiguration.java b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/config/WebClientConfig.java similarity index 73% rename from issue-service/src/main/java/com/github/devraghav/bugtracker/issue/config/ProjectWebClientConfiguration.java rename to issue-service/src/main/java/com/github/devraghav/bugtracker/issue/config/WebClientConfig.java index 375d8c1..ec855a9 100644 --- a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/config/ProjectWebClientConfiguration.java +++ b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/config/WebClientConfig.java @@ -2,6 +2,7 @@ import io.netty.channel.ChannelOption; import java.time.Duration; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; @@ -11,9 +12,10 @@ import reactor.netty.http.client.HttpClient; @Configuration -class ProjectWebClientConfiguration { +public class WebClientConfig { @Bean - WebClient projectWebClient(WebClient.Builder webClientBuilder) { + WebClient projectWebClient( + WebClient.Builder webClientBuilder, @Value("${app.edge-service.url}") String edgeServiceUrl) { HttpClient httpClient = HttpClient.create() @@ -21,11 +23,12 @@ WebClient projectWebClient(WebClient.Builder webClientBuilder) { .wiretap(true) // it is the time we wait to receive a response after sending a // request. - .responseTimeout(Duration.ofMillis(500)) + .responseTimeout(Duration.ofMillis(5_00)) // Period within which a connection between a client and a server must be established - .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1_000); + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2_000); return webClientBuilder + .baseUrl(edgeServiceUrl) .defaultHeader(HttpHeaders.USER_AGENT, "project-service") .clientConnector(new ReactorClientHttpConnector(httpClient)) .build(); diff --git a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/dto/Project.java b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/dto/Project.java deleted file mode 100644 index 8bc4037..0000000 --- a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/dto/Project.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.github.devraghav.bugtracker.issue.dto; - -import java.time.LocalDateTime; -import java.util.Set; - -public record Project( - String id, - String name, - String description, - Boolean enabled, - ProjectStatus status, - String author, - LocalDateTime createdAt, - Set versions) {} diff --git a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/dto/ProjectStatus.java b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/dto/ProjectStatus.java deleted file mode 100644 index bd571dc..0000000 --- a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/dto/ProjectStatus.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.github.devraghav.bugtracker.issue.dto; - -import java.util.Arrays; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; -import lombok.Getter; - -public enum ProjectStatus { - UNKNOWN(-1), - POC(0), - IN_PROGRESS(1), - DEPLOYED(2); - @Getter private int value; - - private static Map reverseLookup = - Arrays.stream(ProjectStatus.values()) - .collect(Collectors.toUnmodifiableMap(ProjectStatus::getValue, Function.identity())); - - ProjectStatus(int value) { - this.value = value; - } - - public static ProjectStatus fromValue(int value) { - return reverseLookup.getOrDefault(value, ProjectStatus.UNKNOWN); - } -} diff --git a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/dto/ProjectVersion.java b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/dto/ProjectVersion.java deleted file mode 100644 index ffddc46..0000000 --- a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/dto/ProjectVersion.java +++ /dev/null @@ -1,3 +0,0 @@ -package com.github.devraghav.bugtracker.issue.dto; - -public record ProjectVersion(String id, String version) {} diff --git a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/entity/IssueEntity.java b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/entity/IssueEntity.java index 4a8194d..5d824be 100644 --- a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/entity/IssueEntity.java +++ b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/entity/IssueEntity.java @@ -2,8 +2,7 @@ import java.time.LocalDateTime; import java.util.*; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.*; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; @@ -15,14 +14,33 @@ public class IssueEntity { private Integer priority; private Integer severity; private String businessUnit; - private Set projects; private String header; private String description; private String reporter; private String assignee; + private Set attachments; private Set watchers; private Map tags; - private String lastUpdateBy; private LocalDateTime createdAt; private LocalDateTime endedAt; + private String lastUpdateBy; + + @Data + @NoArgsConstructor + @AllArgsConstructor(access = AccessLevel.PRIVATE) + @Builder + public static class ProjectAttachment { + private String projectId; + private String name; + private Version version; + } + + @Getter + @NoArgsConstructor + @AllArgsConstructor(access = AccessLevel.PRIVATE) + @Builder + public static class Version { + private String id; + private String version; + } } diff --git a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/entity/ProjectInfoRef.java b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/entity/ProjectInfoRef.java deleted file mode 100644 index c5c3147..0000000 --- a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/entity/ProjectInfoRef.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.github.devraghav.bugtracker.issue.entity; - -import com.github.devraghav.bugtracker.issue.request.IssueRequest; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -public class ProjectInfoRef { - private String projectId; - private String versionId; - - public ProjectInfoRef(IssueRequest.ProjectInfo projectInfo) { - this.projectId = projectInfo.projectId(); - this.versionId = projectInfo.versionId(); - } -} diff --git a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/event/IssueCreatedEventConverter.java b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/event/IssueCreatedEventConverter.java index f08191f..94b3113 100644 --- a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/event/IssueCreatedEventConverter.java +++ b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/event/IssueCreatedEventConverter.java @@ -1,9 +1,7 @@ package com.github.devraghav.bugtracker.issue.event; import com.github.devraghav.bugtracker.issue.event.internal.IssueEvent; -import com.github.devraghav.bugtracker.issue.request.IssueRequest; import com.github.devraghav.bugtracker.issue.response.IssueResponse; -import com.github.devraghav.data_model.domain.issue.ProjectAttachment; import com.github.devraghav.data_model.event.issue.IssueCreated; import com.github.devraghav.data_model.schema.issue.IssueCreatedSchema; import java.time.ZoneOffset; @@ -13,14 +11,16 @@ class IssueCreatedEventConverter implements EventConverter { - private ProjectAttachment getProject(IssueRequest.ProjectInfo project) { - return ProjectAttachment.newBuilder() + private com.github.devraghav.data_model.domain.issue.ProjectAttachment getProject( + IssueResponse.ProjectAttachment project) { + return com.github.devraghav.data_model.domain.issue.ProjectAttachment.newBuilder() .setProjectId(project.projectId()) - .setProjectVersionId(project.versionId()) + .setProjectVersionId(project.version().id()) .build(); } - private List getProjects(Set projects) { + private List getProjects( + Set projects) { return projects.stream().map(this::getProject).collect(Collectors.toList()); } @@ -34,7 +34,7 @@ private com.github.devraghav.data_model.domain.issue.Issue getIssue(IssueRespons .setPriority(issue.priority().name()) .setSeverity(issue.severity().name()) .setAssignee(issue.assignee()) - .setProjects(getProjects(issue.projects())) + .setProjects(getProjects(issue.attachments())) .setWatchers(List.copyOf(issue.watchers())) .setReporter(issue.reporter()) .setTags(issue.tags()) diff --git a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/event/IssueUpdatedEventConverter.java b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/event/IssueUpdatedEventConverter.java index 1495e39..0bdf2c1 100644 --- a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/event/IssueUpdatedEventConverter.java +++ b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/event/IssueUpdatedEventConverter.java @@ -1,9 +1,7 @@ package com.github.devraghav.bugtracker.issue.event; import com.github.devraghav.bugtracker.issue.event.internal.IssueEvent; -import com.github.devraghav.bugtracker.issue.request.IssueRequest; import com.github.devraghav.bugtracker.issue.response.IssueResponse; -import com.github.devraghav.data_model.domain.issue.ProjectAttachment; import com.github.devraghav.data_model.event.issue.IssueUpdated; import com.github.devraghav.data_model.schema.issue.IssueUpdatedSchema; import java.time.ZoneOffset; @@ -13,14 +11,16 @@ class IssueUpdatedEventConverter implements EventConverter { - private ProjectAttachment getProject(IssueRequest.ProjectInfo project) { - return ProjectAttachment.newBuilder() + private com.github.devraghav.data_model.domain.issue.ProjectAttachment getProject( + IssueResponse.ProjectAttachment project) { + return com.github.devraghav.data_model.domain.issue.ProjectAttachment.newBuilder() .setProjectId(project.projectId()) - .setProjectVersionId(project.versionId()) + .setProjectVersionId(project.version().id()) .build(); } - private List getProjects(Set projects) { + private List getProjects( + Set projects) { return projects.stream().map(this::getProject).collect(Collectors.toList()); } @@ -34,7 +34,7 @@ private com.github.devraghav.data_model.domain.issue.Issue getIssue(IssueRespons .setPriority(issue.priority().name()) .setSeverity(issue.severity().name()) .setAssignee(issue.assignee()) - .setProjects(getProjects(issue.projects())) + .setProjects(getProjects(issue.attachments())) .setWatchers(List.copyOf(issue.watchers())) .setReporter(issue.reporter()) .setTags(issue.tags()) diff --git a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/exception/ProjectClientException.java b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/exception/ProjectClientException.java index 2e08b02..18be2a6 100644 --- a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/exception/ProjectClientException.java +++ b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/exception/ProjectClientException.java @@ -16,10 +16,8 @@ public static ProjectClientException invalidProject(String projectId) { return new ProjectClientException("Project not found", Map.of("projectId", projectId)); } - public static ProjectClientException invalidProjectOrVersion(String projectId, String versionId) { - return new ProjectClientException( - "Either project or version not found", - Map.of("projectId", projectId, "versionId", versionId)); + public static ProjectClientException invalidVersion(String versionId) { + return new ProjectClientException("Version not found", Map.of("versionId", versionId)); } public static ProjectClientException unableToConnect(WebClientRequestException exception) { diff --git a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/exception/UserClientException.java b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/exception/UserClientException.java new file mode 100644 index 0000000..0675884 --- /dev/null +++ b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/exception/UserClientException.java @@ -0,0 +1,23 @@ +package com.github.devraghav.bugtracker.issue.exception; + +import java.util.Map; +import lombok.Getter; +import org.springframework.web.reactive.function.client.WebClientRequestException; + +public class UserClientException extends RuntimeException { + @Getter private final Map meta; + + private UserClientException(String message, Map meta) { + super(message); + this.meta = meta; + } + + public static UserClientException invalidUser(String userId) { + return new UserClientException("User not found", Map.of("userId", userId)); + } + + public static UserClientException unableToConnect(WebClientRequestException exception) { + return new UserClientException( + "Unable to connect with Project Service", Map.of("path", exception.getUri().getPath())); + } +} diff --git a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/excpetion/IssueException.java b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/excpetion/IssueException.java index c55e63a..f34c9f6 100644 --- a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/excpetion/IssueException.java +++ b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/excpetion/IssueException.java @@ -18,11 +18,6 @@ private IssueException(String message) { this(message, Map.of()); } - public static IssueException invalidComment(String content) { - return new IssueException( - "Comment should not be null & less than 256 character length", Map.of("comment", content)); - } - public static IssueException nullSeverity() { return new IssueException("Severity not found"); } @@ -58,10 +53,10 @@ public static IssueException noProjectAttach() { return new IssueException("No project attached"); } - public static IssueException invalidProject(IssueRequest.ProjectInfo projectInfo) { + public static IssueException invalidProject(IssueRequest.ProjectAttachment projectAttachment) { var meta = new HashMap(); - meta.put("projectId", projectInfo.projectId()); - meta.put("versionId", projectInfo.versionId()); + meta.put("projectId", projectAttachment.projectId()); + meta.put("versionId", projectAttachment.versionId()); return new IssueException("Either projectId or versionId is invalid", meta); } } diff --git a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/mapper/IssueMapper.java b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/mapper/IssueMapper.java index 15fef51..ee892c7 100644 --- a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/mapper/IssueMapper.java +++ b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/mapper/IssueMapper.java @@ -1,13 +1,17 @@ package com.github.devraghav.bugtracker.issue.mapper; -import com.github.devraghav.bugtracker.issue.dto.*; import com.github.devraghav.bugtracker.issue.entity.IssueEntity; +import com.github.devraghav.bugtracker.issue.project.ProjectResponse; import com.github.devraghav.bugtracker.issue.request.IssueRequest; import com.github.devraghav.bugtracker.issue.response.IssueResponse; import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; import org.mapstruct.*; +import reactor.util.function.Tuple2; @Mapper( componentModel = "spring", @@ -35,9 +39,33 @@ public interface IssueMapper { @Mapping(target = "watchers", expression = "java(Set.of())"), @Mapping(target = "lastUpdateBy", source = "reporter"), @Mapping(target = "assignee", ignore = true), - @Mapping(target = "endedAt", ignore = true) + @Mapping(target = "endedAt", ignore = true), + @Mapping(target = "attachments", source = "attachments", qualifiedByName = "convertAttachment") }) - IssueEntity issueRequestToIssueEntity(String reporter, IssueRequest.CreateIssue createIssue); + IssueEntity issueRequestToIssueEntity( + String reporter, + IssueRequest.CreateIssue createIssue, + List> attachments); + + @Named("convertAttachment") + default Set convertAttachment( + List> attachments) { + + return attachments.stream().map(this::getProjectAttachment).collect(Collectors.toSet()); + } + + private IssueEntity.ProjectAttachment getProjectAttachment( + Tuple2 tuple2) { + return IssueEntity.ProjectAttachment.builder() + .projectId(tuple2.getT1().id()) + .name(tuple2.getT1().name()) + .version( + IssueEntity.Version.builder() + .id(tuple2.getT2().id()) + .version(tuple2.getT2().version()) + .build()) + .build(); + } @Mappings({ @Mapping( diff --git a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/project/DefaultProjectClient.java b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/project/DefaultProjectClient.java new file mode 100644 index 0000000..5c8b030 --- /dev/null +++ b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/project/DefaultProjectClient.java @@ -0,0 +1,47 @@ +package com.github.devraghav.bugtracker.issue.project; + +import com.github.devraghav.bugtracker.issue.exception.ProjectClientException; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientRequestException; +import reactor.core.publisher.Mono; + +@Component +class DefaultProjectClient implements ProjectClient { + private final WebClient webClient; + + public DefaultProjectClient(WebClient webClient) { + this.webClient = webClient; + } + + @Override + public Mono getProjectById(String projectId) { + return webClient + .get() + .uri("/api/rest/internal/v1/project/{projectId}", projectId) + .retrieve() + .onStatus( + httpStatusCode -> httpStatusCode.value() == HttpStatus.NOT_FOUND.value(), + clientResponse -> Mono.error(ProjectClientException.invalidProject(projectId))) + .bodyToMono(ProjectResponse.Project.class) + .onErrorResume( + WebClientRequestException.class, + exception -> Mono.error(ProjectClientException.unableToConnect(exception))); + } + + @Override + public Mono getVersionById(String projectId, String versionId) { + return webClient + .get() + .uri("/api/rest/internal/v1/project/{projectId}/version/{versionId}", projectId, versionId) + .retrieve() + .onStatus( + httpStatusCode -> httpStatusCode.value() == HttpStatus.NOT_FOUND.value(), + clientResponse -> Mono.error(ProjectClientException.invalidVersion(versionId))) + .bodyToMono(ProjectResponse.Project.Version.class) + .onErrorResume( + WebClientRequestException.class, + exception -> Mono.error(ProjectClientException.unableToConnect(exception))); + } +} diff --git a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/project/ProjectClient.java b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/project/ProjectClient.java new file mode 100644 index 0000000..0b7e220 --- /dev/null +++ b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/project/ProjectClient.java @@ -0,0 +1,10 @@ +package com.github.devraghav.bugtracker.issue.project; + +import reactor.core.publisher.Mono; + +public interface ProjectClient { + + Mono getProjectById(String projectId); + + Mono getVersionById(String projectId, String versionId); +} diff --git a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/project/ProjectResponse.java b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/project/ProjectResponse.java new file mode 100644 index 0000000..4c2dc27 --- /dev/null +++ b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/project/ProjectResponse.java @@ -0,0 +1,36 @@ +package com.github.devraghav.bugtracker.issue.project; + +import java.util.Arrays; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import lombok.Getter; + +public final class ProjectResponse { + + public static enum ProjectStatus { + UNKNOWN(-1), + POC(0), + IN_PROGRESS(1), + DEPLOYED(2); + @Getter private final int value; + + private static final Map VALUE_TO_ENUM = + Arrays.stream(ProjectStatus.values()) + .collect(Collectors.toUnmodifiableMap(ProjectStatus::getValue, Function.identity())); + + ProjectStatus(int value) { + this.value = value; + } + + public static ProjectStatus fromValue(int value) { + return VALUE_TO_ENUM.getOrDefault(value, ProjectStatus.UNKNOWN); + } + } + + public record Project( + String id, String name, Boolean enabled, ProjectStatus status, Set versions) { + public record Version(String id, String version) {} + } +} diff --git a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/repository/IssueRepository.java b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/repository/IssueRepository.java index dc17478..81177f8 100644 --- a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/repository/IssueRepository.java +++ b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/repository/IssueRepository.java @@ -28,7 +28,7 @@ public interface IssueRepository @Update("{ '$pull' : { 'watchers' : '?1' } }") Mono findAndPullWatcherById(String id, String userId); - @Query("{ 'projects.projectId' : '?0'}") + @Query("{ 'attachments.projectId' : '?0'}") Flux findAllByProjectId(String projectId); Flux findAllByReporter(String reporter); diff --git a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/request/IssueRequest.java b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/request/IssueRequest.java index f756b38..42d2d18 100644 --- a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/request/IssueRequest.java +++ b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/request/IssueRequest.java @@ -10,7 +10,7 @@ public final class IssueRequest { - public record ProjectInfo(String projectId, String versionId) { + public record ProjectAttachment(String projectId, String versionId) { @JsonIgnore public boolean isValid() { @@ -29,12 +29,12 @@ public static record CreateIssue( IssueResponse.Priority priority, IssueResponse.Severity severity, String businessUnit, - Set projects, + Set attachments, String header, String description, Map tags) { public CreateIssue { - projects = Set.copyOf(projects == null ? Set.of() : projects); + attachments = Set.copyOf(attachments == null ? Set.of() : attachments); tags = Map.copyOf(tags == null ? Map.of() : tags); } } @@ -62,5 +62,5 @@ public static Page of(ServerRequest request) { } public static record Monitor( - String issueId, String user, MonitorType monitorType, String requestedBy) {} + String issueId, String user, MonitorType monitorType, String requestBy) {} } diff --git a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/response/IssueResponse.java b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/response/IssueResponse.java index 0778670..404a58f 100644 --- a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/response/IssueResponse.java +++ b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/response/IssueResponse.java @@ -2,7 +2,6 @@ import com.github.devraghav.bugtracker.issue.excpetion.IssueException; import com.github.devraghav.bugtracker.issue.repository.IssueNotFoundException; -import com.github.devraghav.bugtracker.issue.request.IssueRequest; import java.net.URI; import java.time.LocalDateTime; import java.util.Arrays; @@ -20,12 +19,16 @@ public final class IssueResponse { + public record ProjectAttachment(String projectId, String name, Version version) {} + + public record Version(String id, String version) {} + public static record Issue( String id, Priority priority, Severity severity, String businessUnit, - Set projects, + Set attachments, String header, String description, String assignee, diff --git a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/route/handler/IssueRouteV1Handler.java b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/route/handler/IssueRouteV1Handler.java index bbdc324..a35e0e7 100644 --- a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/route/handler/IssueRouteV1Handler.java +++ b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/route/handler/IssueRouteV1Handler.java @@ -90,7 +90,7 @@ public Mono monitor(ServerRequest request, IssueRequest.MonitorT getAuthenticatedPrincipal(request), request.bodyToMono(new ParameterizedTypeReference>() {})) .map(tuple2 -> new IssueRequest.Monitor(issueId, tuple2.getT2().get("user"), monitorType, tuple2.getT1())) - .flatMap(monitor -> issueCommandService.monitor(issueId, monitor)) + .flatMap(issueCommandService::monitor) .then(IssueResponse.noContent()) .onErrorResume(IssueException.class, exception -> IssueResponse.invalid(request, exception)); // @spotless:on diff --git a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/service/IssueCommandService.java b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/service/IssueCommandService.java index e245048..e006901 100644 --- a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/service/IssueCommandService.java +++ b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/service/IssueCommandService.java @@ -2,7 +2,6 @@ import com.github.devraghav.bugtracker.event.internal.DomainEvent; import com.github.devraghav.bugtracker.event.internal.EventBus; -import com.github.devraghav.bugtracker.issue.dto.*; import com.github.devraghav.bugtracker.issue.entity.IssueEntity; import com.github.devraghav.bugtracker.issue.event.internal.*; import com.github.devraghav.bugtracker.issue.excpetion.IssueException; @@ -11,8 +10,10 @@ import com.github.devraghav.bugtracker.issue.repository.IssueRepository; import com.github.devraghav.bugtracker.issue.request.IssueRequest; import com.github.devraghav.bugtracker.issue.response.IssueResponse; +import com.github.devraghav.bugtracker.issue.user.UserClient; import java.time.LocalDateTime; import java.util.Objects; +import java.util.function.Consumer; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.codec.multipart.FilePart; @@ -27,56 +28,63 @@ public class IssueCommandService { private final RequestValidator requestValidator; private final IssueMapper issueMapper; private final IssueQueryService issueQueryService; + private final UserClient userClient; private final IssueRepository issueRepository; private final IssueAttachmentRepository issueAttachmentRepository; private final EventBus.ReactivePublisher domainEventPublisher; public Mono create(String userId, IssueRequest.CreateIssue createIssue) { + // @spotless:off return requestValidator .validate(createIssue) - .map(validateRequest -> issueMapper.issueRequestToIssueEntity(userId, validateRequest)) - .flatMap(this::save); + .map(validateRequest -> issueMapper.issueRequestToIssueEntity(userId, + validateRequest.getT1(), validateRequest.getT2())) + .flatMap(issueEntity -> + upsert(issueEntity, issue -> domainEventPublisher.publish(new IssueEvent.Created(issue)))); + // @spotless:on } public Mono update( String userId, String issueId, IssueRequest.UpdateIssue updateRequest) { + // @spotless:off return issueQueryService .findById(issueId) .filter(issueEntity -> Objects.nonNull(issueEntity.getEndedAt())) - .map( - issueEntity -> - issueMapper.issueRequestToIssueEntity(userId, issueEntity, updateRequest)) - .flatMap(issueEntity -> update(userId, issueEntity)) + .map(issueEntity -> issueMapper.issueRequestToIssueEntity(userId, issueEntity, updateRequest)) + .flatMap(issueEntity -> + upsert(issueEntity, issue -> domainEventPublisher.publish(new IssueEvent.Updated(userId, issue)))) .switchIfEmpty(Mono.error(() -> IssueException.alreadyEnded(issueId))); + // @spotless:on } - public Mono monitor(String issueId, IssueRequest.Monitor monitor) { + public Mono monitor(IssueRequest.Monitor monitor) { log.info("monitor {} with assignRequest {}", monitor.monitorType(), monitor); - var issueMono = issueQueryService.exists(issueId).map(unused -> issueId); + var issueMono = issueQueryService.exists(monitor.issueId()).map(unused -> monitor.issueId()); if (IssueRequest.MonitorType.UNASSIGN == monitor.monitorType()) { - return unassigned(issueMono, monitor.requestedBy()); + return unassigned(issueMono, monitor.requestBy()); } else { if (IssueRequest.MonitorType.ASSIGN == monitor.monitorType()) { - return assignee(issueMono, monitor.user(), monitor.requestedBy()); + return assignee(issueMono, monitor.user(), monitor.requestBy()); } else if (IssueRequest.MonitorType.WATCH == monitor.monitorType()) { - return watch(issueMono, monitor.user(), monitor.requestedBy()); + return watch(issueMono, monitor.user(), monitor.requestBy()); } else { - return unwatch(issueMono, monitor.user(), monitor.requestedBy()); + return unwatch(issueMono, monitor.user(), monitor.requestBy()); } } } - private Mono assignee(Mono issueMono, String userId, String requestedBy) { - return issueMono.flatMap(issueId -> assignee(issueId, userId, requestedBy)); + private Mono assignee(Mono issueMono, String userId, String requestBy) { + return issueMono.flatMap(issueId -> assignee(issueId, userId, requestBy)); } - private Mono assignee(String issueId, String userId, String requestedBy) { + private Mono assignee(String issueId, String userId, String requestBy) { + // @spotless:off return issueRepository .findAndSetAssigneeById(issueId, userId) - .doOnSuccess( - unused -> - domainEventPublisher.publish(new IssueEvent.Assigned(issueId, userId, requestedBy))) + .doOnSuccess(unused -> + domainEventPublisher.publish(new IssueEvent.Assigned(issueId, userId, requestBy))) .then(); + // @spotless:on } private Mono unassigned(Mono issueMono, String requestedBy) { @@ -96,14 +104,13 @@ private Mono watch(Mono issueMono, String userId, String requested } private Mono watch(String issueId, String userId, String requestedBy) { - log.info("Watch by issueId {} and user {}", issueId, userId); + // @spotless:off return issueRepository .findAndAddWatcherById(issueId, userId) - .doOnSuccess( - unused -> - domainEventPublisher.publish( - new IssueEvent.WatchStarted(issueId, userId, requestedBy))) + .doOnSuccess(unused -> + domainEventPublisher.publish(new IssueEvent.WatchStarted(issueId, userId, requestedBy))) .then(); + // @spotless:on } private Mono unwatch(Mono issueMono, String userId, String requestedBy) { @@ -111,13 +118,12 @@ private Mono unwatch(Mono issueMono, String userId, String request } private Mono unwatch(String issueId, String userId, String requestedBy) { + // @spotless:off return issueRepository .findAndPullWatcherById(issueId, userId) - .doOnSuccess( - unused -> - domainEventPublisher.publish( - new IssueEvent.WatchEnded(issueId, userId, requestedBy))) + .doOnSuccess(unused -> domainEventPublisher.publish(new IssueEvent.WatchEnded(issueId, userId, requestedBy))) .then(); + // @spotless:on } public Mono resolve(String issueId, String resolvedBy) { @@ -135,17 +141,11 @@ public Mono uploadAttachment(String issueId, FilePart filePart) { return issueAttachmentRepository.upload(issueId, filePart.filename(), filePart.content()); } - private Mono save(IssueEntity issueEntity) { - return issueRepository - .save(issueEntity) - .map(issueMapper::issueEntityToIssue) - .doOnSuccess(issue -> domainEventPublisher.publish(new IssueEvent.Created(issue))); - } - - private Mono update(String userId, IssueEntity issueEntity) { + private Mono upsert( + IssueEntity issueEntity, Consumer onSuccessConsumer) { return issueRepository .save(issueEntity) .map(issueMapper::issueEntityToIssue) - .doOnSuccess(issue -> domainEventPublisher.publish(new IssueEvent.Updated(userId, issue))); + .doOnSuccess(onSuccessConsumer); } } diff --git a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/service/IssueQueryService.java b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/service/IssueQueryService.java index 390e2cb..bcdf872 100644 --- a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/service/IssueQueryService.java +++ b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/service/IssueQueryService.java @@ -2,7 +2,6 @@ import com.github.devraghav.bugtracker.issue.dto.*; import com.github.devraghav.bugtracker.issue.entity.IssueEntity; -import com.github.devraghav.bugtracker.issue.exception.ProjectClientException; import com.github.devraghav.bugtracker.issue.excpetion.IssueException; import com.github.devraghav.bugtracker.issue.mapper.IssueMapper; import com.github.devraghav.bugtracker.issue.repository.IssueAttachmentRepository; @@ -16,7 +15,6 @@ @Service public record IssueQueryService( IssueMapper issueMapper, - ProjectReactiveClient projectReactiveClient, IssueRepository issueRepository, IssueAttachmentRepository issueAttachmentRepository) { @@ -60,19 +58,8 @@ public Mono findById(String issueId) { private Flux getAllByReporter(String reporter) { return issueRepository.findAllByReporter(reporter).map(issueMapper::issueEntityToIssue); } - + // TODO: validate projectId private Flux getAllByProjectId(String projectId) { - return validateProjectId(projectId) - .flatMapMany( - unused -> - issueRepository.findAllByProjectId(projectId).map(issueMapper::issueEntityToIssue)); - } - - private Mono validateProjectId(String projectId) { - return projectReactiveClient - .isProjectExists(projectId) - .onErrorResume( - ProjectClientException.class, - exception -> Mono.error(IssueException.projectServiceException(exception))); + return issueRepository.findAllByProjectId(projectId).map(issueMapper::issueEntityToIssue); } } diff --git a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/service/ProjectReactiveClient.java b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/service/ProjectReactiveClient.java deleted file mode 100644 index 08b1673..0000000 --- a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/service/ProjectReactiveClient.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.github.devraghav.bugtracker.issue.service; - -import com.github.devraghav.bugtracker.issue.dto.Project; -import com.github.devraghav.bugtracker.issue.dto.ProjectVersion; -import com.github.devraghav.bugtracker.issue.exception.ProjectClientException; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Service; -import org.springframework.web.reactive.function.client.WebClient; -import org.springframework.web.reactive.function.client.WebClientRequestException; -import reactor.core.publisher.Mono; - -@Service -public class ProjectReactiveClient { - private final String projectFindByIdURL; - private final String projectVersionFindByIdURL; - private final WebClient projectWebClient; - - public ProjectReactiveClient( - @Value("${app.external.project-service.url}") String projectServiceURL, - WebClient projectWebClient) { - this.projectWebClient = projectWebClient; - this.projectFindByIdURL = projectServiceURL + "/api/rest/v1/project/{id}"; - this.projectVersionFindByIdURL = - projectServiceURL + "/api/rest/v1/project/{id}/version/{versionId}"; - } - - public Mono fetchProject(String projectId) { - return projectWebClient - .get() - .uri(projectFindByIdURL, projectId) - .retrieve() - .onStatus( - httpStatusCode -> httpStatusCode.value() == HttpStatus.NOT_FOUND.value(), - clientResponse -> Mono.error(ProjectClientException.invalidProject(projectId))) - .bodyToMono(Project.class) - .onErrorResume( - WebClientRequestException.class, - exception -> Mono.error(ProjectClientException.unableToConnect(exception))); - } - - public Mono isProjectExists(String projectId) { - return fetchProject(projectId).hasElement(); - } - - public Mono fetchProjectVersion(String projectId, String versionId) { - return projectWebClient - .get() - .uri(projectVersionFindByIdURL, projectId, versionId) - .retrieve() - .onStatus( - httpStatusCode -> httpStatusCode.value() == HttpStatus.NOT_FOUND.value(), - clientResponse -> - Mono.error(ProjectClientException.invalidProjectOrVersion(projectId, versionId))) - .bodyToMono(ProjectVersion.class) - .onErrorResume( - WebClientRequestException.class, - exception -> Mono.error(ProjectClientException.unableToConnect(exception))); - } - - public Mono isProjectVersionExists(String projectId, String versionId) { - return fetchProjectVersion(projectId, versionId).hasElement(); - } -} diff --git a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/service/RequestValidator.java b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/service/RequestValidator.java index 3c23f02..fcedee0 100644 --- a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/service/RequestValidator.java +++ b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/service/RequestValidator.java @@ -1,24 +1,35 @@ package com.github.devraghav.bugtracker.issue.service; +import com.github.devraghav.bugtracker.issue.project.ProjectResponse; import com.github.devraghav.bugtracker.issue.request.CommentRequest; import com.github.devraghav.bugtracker.issue.request.IssueRequest; import com.github.devraghav.bugtracker.issue.validation.Validator; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; @Component @RequiredArgsConstructor public class RequestValidator { - private final Validator + private final Validator< + IssueRequest.CreateIssue, + Tuple2< + IssueRequest.CreateIssue, + List>>> createIssueRequestValidator; private final Validator createCommentRequestValidator; private final Validator updateCommentRequestValidator; - public Mono validate(final IssueRequest.CreateIssue request) { + public Mono< + Tuple2< + IssueRequest.CreateIssue, + List>>> + validate(final IssueRequest.CreateIssue request) { return createIssueRequestValidator.validate(request); } diff --git a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/user/DefaultUserClient.java b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/user/DefaultUserClient.java new file mode 100644 index 0000000..c961343 --- /dev/null +++ b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/user/DefaultUserClient.java @@ -0,0 +1,32 @@ +package com.github.devraghav.bugtracker.issue.user; + +import com.github.devraghav.bugtracker.issue.exception.UserClientException; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientRequestException; +import reactor.core.publisher.Mono; + +@Component +class DefaultUserClient implements UserClient { + private final WebClient webClient; + + public DefaultUserClient(WebClient webClient) { + this.webClient = webClient; + } + + @Override + public Mono getUserById(String userId) { + return webClient + .get() + .uri("/api/rest/internal/v1/user/{userId}", userId) + .retrieve() + .onStatus( + httpStatusCode -> httpStatusCode.value() == HttpStatus.NOT_FOUND.value(), + clientResponse -> Mono.error(UserClientException.invalidUser(userId))) + .bodyToMono(User.class) + .onErrorResume( + WebClientRequestException.class, + exception -> Mono.error(UserClientException.unableToConnect(exception))); + } +} diff --git a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/user/User.java b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/user/User.java new file mode 100644 index 0000000..2ffc2d0 --- /dev/null +++ b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/user/User.java @@ -0,0 +1,3 @@ +package com.github.devraghav.bugtracker.issue.user; + +public record User(String id, String firstName, String lastName) {} diff --git a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/user/UserClient.java b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/user/UserClient.java new file mode 100644 index 0000000..37ff359 --- /dev/null +++ b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/user/UserClient.java @@ -0,0 +1,7 @@ +package com.github.devraghav.bugtracker.issue.user; + +import reactor.core.publisher.Mono; + +public interface UserClient { + Mono getUserById(String userId); +} diff --git a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/validation/CreateIssueRequestValidator.java b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/validation/CreateIssueRequestValidator.java index 37759b0..80ad51c 100644 --- a/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/validation/CreateIssueRequestValidator.java +++ b/issue-service/src/main/java/com/github/devraghav/bugtracker/issue/validation/CreateIssueRequestValidator.java @@ -1,34 +1,45 @@ package com.github.devraghav.bugtracker.issue.validation; -import com.github.devraghav.bugtracker.issue.dto.*; import com.github.devraghav.bugtracker.issue.exception.ProjectClientException; import com.github.devraghav.bugtracker.issue.excpetion.IssueException; +import com.github.devraghav.bugtracker.issue.project.ProjectClient; +import com.github.devraghav.bugtracker.issue.project.ProjectResponse; import com.github.devraghav.bugtracker.issue.request.IssueRequest; import com.github.devraghav.bugtracker.issue.response.IssueResponse; -import com.github.devraghav.bugtracker.issue.service.ProjectReactiveClient; import java.util.Collection; +import java.util.List; import java.util.Objects; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; @Component @RequiredArgsConstructor class CreateIssueRequestValidator - implements Validator { - - private final ProjectReactiveClient projectReactiveClient; + implements Validator< + IssueRequest.CreateIssue, + Tuple2< + IssueRequest.CreateIssue, + List>>> { + private final ProjectClient projectClient; @Override - public Mono validate(IssueRequest.CreateIssue createIssue) { - return validateHeader(createIssue.header()) - .and(validateDescription(createIssue.description())) - .and(validatePriority(createIssue.priority())) - .and(validateSeverity(createIssue.severity())) - .and(validatedProjectInfo(createIssue.projects())) - .thenReturn(createIssue); + public Mono< + Tuple2< + IssueRequest.CreateIssue, + List>>> + validate(IssueRequest.CreateIssue createIssue) { + var validRequest = + validateHeader(createIssue.header()) + .and(validateDescription(createIssue.description())) + .and(validatePriority(createIssue.priority())) + .and(validateSeverity(createIssue.severity())) + .thenReturn(createIssue); + var projectAttachments = validatedProjectAttachments(createIssue.attachments()); + return validRequest.zipWith(projectAttachments); } private Mono validateHeader(String header) { @@ -61,40 +72,33 @@ private Mono validateSeverity(IssueResponse.Severity severity) { .then(); } - private Mono validatedProjectInfo(Collection projectInfos) { - return Flux.fromIterable(projectInfos) + private Mono>> + validatedProjectAttachments(Collection projectAttachments) { + return Flux.fromIterable(projectAttachments) .switchIfEmpty(Mono.error(IssueException::noProjectAttach)) - .flatMap(this::validateProjectInfo) - .last() - .then(); - } - - private Mono validateProjectInfo(IssueRequest.ProjectInfo projectInfo) { - return Mono.just(projectInfo) - .filter(IssueRequest.ProjectInfo::isValid) - .flatMap(this::isProjectInfoExists) - .switchIfEmpty(Mono.error(() -> IssueException.invalidProject(projectInfo))); + .flatMap(this::validateProjectAttachments) + .collectList(); } - private Mono isProjectInfoExists(IssueRequest.ProjectInfo projectInfo) { + private Mono> + validateProjectAttachments(IssueRequest.ProjectAttachment projectAttachment) { return Mono.zip( - validateProjectId(projectInfo.projectId()), - validateProjectVersion(projectInfo.projectId(), projectInfo.versionId()), - Boolean::logicalAnd) - .map(Boolean::booleanValue); + getProject(projectAttachment.projectId()), + getProjectVersion(projectAttachment.projectId(), projectAttachment.versionId())); } - private Mono validateProjectId(String projectId) { - return projectReactiveClient - .isProjectExists(projectId) + private Mono getProject(String projectId) { + return projectClient + .getProjectById(projectId) .onErrorResume( ProjectClientException.class, exception -> Mono.error(IssueException.projectServiceException(exception))); } - private Mono validateProjectVersion(String projectId, String versionId) { - return projectReactiveClient - .isProjectVersionExists(projectId, versionId) + private Mono getProjectVersion( + String projectId, String versionId) { + return projectClient + .getVersionById(projectId, versionId) .onErrorResume( ProjectClientException.class, exception -> Mono.error(IssueException.projectServiceException(exception))); diff --git a/issue-service/src/main/resources/application.properties b/issue-service/src/main/resources/application.properties index 2df9025..be8708f 100644 --- a/issue-service/src/main/resources/application.properties +++ b/issue-service/src/main/resources/application.properties @@ -14,8 +14,7 @@ springdoc.use-management-port=true springdoc.api-docs.groups.enabled=true #springdoc.swagger-ui.url=/open-api.json -app.external.user-service.url=http://user-service:8080 -app.external.project-service.url=http://project-service:8080 +app.edge-service.url=http://edge-service:8080 #managment management.server.port=9090 diff --git a/issue-service/src/test/java/com/github/devraghav/bugtracker/issue/IssueCommandServiceApplicationTests.java b/issue-service/src/test/java/com/github/devraghav/bugtracker/issue/IssueCommandServiceApplicationTests.java index 65c3934..bd01d79 100644 --- a/issue-service/src/test/java/com/github/devraghav/bugtracker/issue/IssueCommandServiceApplicationTests.java +++ b/issue-service/src/test/java/com/github/devraghav/bugtracker/issue/IssueCommandServiceApplicationTests.java @@ -3,7 +3,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -@SpringBootTest +@SpringBootTest(properties = {"app.edge-service.url=http://localhost:8000"}) class IssueCommandServiceApplicationTests { @Test diff --git a/jmeter/user_service_test_plan.jmx b/jmeter/user_service_test_plan.jmx new file mode 100644 index 0000000..bcc4d4f --- /dev/null +++ b/jmeter/user_service_test_plan.jmx @@ -0,0 +1,393 @@ + + + + + + false + true + false + + + + + + + + continue + + false + 1000 + + 10 + 0 + false + + + true + + + + + + + false + + + false + + + + + localhost + 8081 + + + api/rest/v1/user + GET + true + false + true + false + + + + + + + + + + false + + + false + + + + + localhost + 8082 + + + api/rest/v1/project + GET + true + false + true + false + + + + + + + + + + false + + + false + + + + + localhost + 8083 + + + api/rest/v1/issue + GET + true + false + true + false + + + + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + + + + + true + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + /Users/raghav.joshi/Documents/practice/project/bug-tracker/jmeter/report + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + + + + + + continue + + false + 1 + + 8 + 0 + false + + + true + + + + true + + + + false + { + "firstName": "EventUser", + "lastName": "EventUserTest", + "email": "event@event.com", + "password": "TEST123", + "access": "ADMIN" +} + = + + + + localhost + 8083 + + + api/rest/v1/user + POST + true + false + true + false + true + + + + + + + + + Content-type + application/json + + + + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + + + + + + + diff --git a/project-service/src/main/java/com/github/devraghav/bugtracker/project/entity/ProjectEntity.java b/project-service/src/main/java/com/github/devraghav/bugtracker/project/entity/ProjectEntity.java index 41435d1..18d3739 100644 --- a/project-service/src/main/java/com/github/devraghav/bugtracker/project/entity/ProjectEntity.java +++ b/project-service/src/main/java/com/github/devraghav/bugtracker/project/entity/ProjectEntity.java @@ -14,6 +14,7 @@ public class ProjectEntity { @Id private String id; + private String key; private String name; private String description; private Boolean enabled = true; @@ -21,5 +22,7 @@ public class ProjectEntity { private Map tags; private String author; private List versions; + private String createdBy; + private String lastUpdateBy; private LocalDateTime createdAt; } diff --git a/project-service/src/main/java/com/github/devraghav/bugtracker/project/entity/ProjectVersionEntity.java b/project-service/src/main/java/com/github/devraghav/bugtracker/project/entity/ProjectVersionEntity.java index 0d23d62..fb34231 100644 --- a/project-service/src/main/java/com/github/devraghav/bugtracker/project/entity/ProjectVersionEntity.java +++ b/project-service/src/main/java/com/github/devraghav/bugtracker/project/entity/ProjectVersionEntity.java @@ -1,5 +1,6 @@ package com.github.devraghav.bugtracker.project.entity; +import java.time.LocalDateTime; import lombok.Data; import lombok.NoArgsConstructor; @@ -8,5 +9,6 @@ public class ProjectVersionEntity { private String id; private String version; - private String userId; + private LocalDateTime createdAt; + private String createdBy; } diff --git a/project-service/src/main/java/com/github/devraghav/bugtracker/project/event/internal/ProjectEvent.java b/project-service/src/main/java/com/github/devraghav/bugtracker/project/event/internal/ProjectEvent.java index 3ecc49d..512022c 100644 --- a/project-service/src/main/java/com/github/devraghav/bugtracker/project/event/internal/ProjectEvent.java +++ b/project-service/src/main/java/com/github/devraghav/bugtracker/project/event/internal/ProjectEvent.java @@ -18,6 +18,19 @@ public Created(ProjectResponse.Project project) { } } + @Getter + class Updated extends DomainEvent { + private final ProjectResponse.Project project; + + public Updated(ProjectResponse.Project project) { + super( + project.id(), + "Updated", + new PublisherInfo("Project", ProjectResponse.Project.class, project.author())); + this.project = project; + } + } + @Getter class VersionCreated extends DomainEvent { private final String projectId; diff --git a/project-service/src/main/java/com/github/devraghav/bugtracker/project/mapper/ProjectMapper.java b/project-service/src/main/java/com/github/devraghav/bugtracker/project/mapper/ProjectMapper.java index 5eeca1e..bbec1b6 100644 --- a/project-service/src/main/java/com/github/devraghav/bugtracker/project/mapper/ProjectMapper.java +++ b/project-service/src/main/java/com/github/devraghav/bugtracker/project/mapper/ProjectMapper.java @@ -5,29 +5,62 @@ import com.github.devraghav.bugtracker.project.request.ProjectRequest; import com.github.devraghav.bugtracker.project.response.ProjectResponse; import java.time.LocalDateTime; +import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.UUID; import org.mapstruct.*; @Mapper( uses = {ProjectVersionMapper.class}, componentModel = "spring", - imports = {List.class, LocalDateTime.class, UUID.class}) + imports = {List.class, LocalDateTime.class, UUID.class, Optional.class}) public interface ProjectMapper { @Mappings({ @Mapping(target = "id", expression = "java(UUID.randomUUID().toString())"), + @Mapping(target = "key", source = "name", qualifiedByName = "getKey"), @Mapping(target = "enabled", constant = "true"), @Mapping(target = "status", source = "createProject.status", qualifiedByName = "statusToValue"), @Mapping(target = "versions", expression = "java(List.of())"), @Mapping(target = "createdAt", expression = "java(LocalDateTime.now())"), - @Mapping(target = "author", source = "author") + @Mapping(target = "author", source = "author"), + @Mapping(target = "createdBy", source = "author"), + @Mapping(target = "lastUpdateBy", source = "author") }) ProjectEntity requestToEntity(String author, ProjectRequest.CreateProject createProject); + @Mappings({ + @Mapping(target = "lastUpdateBy", source = "updateBy"), + @Mapping( + target = "description", + expression = + "java(Optional.ofNullable(updateProject.description()).orElseGet(() ->projectEntity.getDescription()))"), + @Mapping( + target = "status", + expression = + "java(Optional.ofNullable(updateProject.status()).map(ProjectRequest.ProjectStatus::getValue).orElseGet(() ->projectEntity.getStatus()))"), + @Mapping( + target = "tags", + expression = + "java(Optional.ofNullable(updateProject.tags()).orElseGet(() ->projectEntity.getTags()))") + }) + ProjectEntity requestToEntity( + String updateBy, ProjectEntity projectEntity, ProjectRequest.UpdateProject updateProject); + @Mappings({@Mapping(target = "status", source = "status", qualifiedByName = "valueToStatus")}) ProjectResponse.Project entityToResponse(ProjectEntity projectEntity); + @Named("getKey") + default String getKey(String name) { + return Arrays.stream(name.split(" ")) + .map(string -> string.substring(0, 1)) + .map(String::toUpperCase) + .reduce((first, second) -> first + "" + second) + .filter(initial -> initial.length() > 1) + .orElse(name); + } + @Named("statusToValue") default Integer statusToValue(ProjectRequest.ProjectStatus projectStatus) { return switch (projectStatus) { diff --git a/project-service/src/main/java/com/github/devraghav/bugtracker/project/mapper/ProjectVersionMapper.java b/project-service/src/main/java/com/github/devraghav/bugtracker/project/mapper/ProjectVersionMapper.java index 0668fe2..da13fe2 100644 --- a/project-service/src/main/java/com/github/devraghav/bugtracker/project/mapper/ProjectVersionMapper.java +++ b/project-service/src/main/java/com/github/devraghav/bugtracker/project/mapper/ProjectVersionMapper.java @@ -3,6 +3,7 @@ import com.github.devraghav.bugtracker.project.entity.ProjectVersionEntity; import com.github.devraghav.bugtracker.project.request.ProjectRequest; import com.github.devraghav.bugtracker.project.response.ProjectResponse; +import java.time.LocalDateTime; import java.util.UUID; import org.mapstruct.Mapper; import org.mapstruct.Mapping; @@ -10,12 +11,14 @@ @Mapper( componentModel = "spring", - imports = {UUID.class}) + imports = {UUID.class, LocalDateTime.class}) public interface ProjectVersionMapper { @Mappings({ @Mapping(target = "id", expression = "java(UUID.randomUUID().toString())"), - @Mapping(target = "userId", source = "userId") + @Mapping(target = "userId", source = "userId"), + @Mapping(target = "createdAt", expression = "java(LocalDateTime.now())"), + @Mapping(target = "createdBy", source = "userId") }) ProjectVersionEntity requestToEntity(String userId, ProjectRequest.CreateVersion projectRequest); diff --git a/project-service/src/main/java/com/github/devraghav/bugtracker/project/request/ProjectRequest.java b/project-service/src/main/java/com/github/devraghav/bugtracker/project/request/ProjectRequest.java index 0692979..63f69ac 100644 --- a/project-service/src/main/java/com/github/devraghav/bugtracker/project/request/ProjectRequest.java +++ b/project-service/src/main/java/com/github/devraghav/bugtracker/project/request/ProjectRequest.java @@ -35,5 +35,12 @@ public static record CreateProject( } } + public static record UpdateProject( + String description, ProjectStatus status, Map tags) { + public UpdateProject { + tags = Map.copyOf(tags == null ? Map.of() : tags); + } + } + public static record CreateVersion(String version) {} } diff --git a/project-service/src/main/java/com/github/devraghav/bugtracker/project/route/defintition/ProjectOpenAPIDocHelper.java b/project-service/src/main/java/com/github/devraghav/bugtracker/project/route/defintition/ProjectOpenAPIDocHelper.java index b650db9..6def358 100644 --- a/project-service/src/main/java/com/github/devraghav/bugtracker/project/route/defintition/ProjectOpenAPIDocHelper.java +++ b/project-service/src/main/java/com/github/devraghav/bugtracker/project/route/defintition/ProjectOpenAPIDocHelper.java @@ -49,6 +49,19 @@ void saveVersionOperationDoc(org.springdoc.core.fn.builders.operation.Builder op .implementation(ProjectRequest.CreateVersion.class)))); } + void updateProjectOperationDoc(org.springdoc.core.fn.builders.operation.Builder ops) { + ops.operationId("update") + .security(securityRequirementBuilder().name("bearerAuth")) + .response(saveProject201ResponseDoc()) + .response(project404ResponseDoc()) + .parameter(parameterBuilder() + .in(ParameterIn.PATH) + .name("id") + .schema(schemaBuilder().type("string"))) + .requestBody(requestBodyBuilder().content(contentBuilder().schema(schemaBuilder() + .implementation(ProjectRequest.UpdateProject.class)))); + } + void getProjectByIdOperationDoc(org.springdoc.core.fn.builders.operation.Builder ops) { ops.operationId("get") .security(securityRequirementBuilder().name("bearerAuth")) diff --git a/project-service/src/main/java/com/github/devraghav/bugtracker/project/route/defintition/ProjectRouteDefinition.java b/project-service/src/main/java/com/github/devraghav/bugtracker/project/route/defintition/ProjectRouteDefinition.java index c20f59d..c18c788 100644 --- a/project-service/src/main/java/com/github/devraghav/bugtracker/project/route/defintition/ProjectRouteDefinition.java +++ b/project-service/src/main/java/com/github/devraghav/bugtracker/project/route/defintition/ProjectRouteDefinition.java @@ -26,6 +26,7 @@ RouterFunction projectRoutes( Supplier> routerByIdSupplier = () -> SpringdocRouteBuilder.route() + .PATCH("", projectRouteHandler::updateProject, docHelper::updateProjectOperationDoc) .GET("", projectRouteHandler::getProject, docHelper::getProjectByIdOperationDoc) .GET( "/version", @@ -58,6 +59,17 @@ RouterFunction projectRoutes( .and(accept(APPLICATION_JSON).or(contentType(APPLICATION_JSON))), routerFunctionSupplier, emptyOperationsConsumer) + .nest( + path("/api/rest/internal/v1/project") + .and(accept(APPLICATION_JSON).or(contentType(APPLICATION_JSON))), + () -> + SpringdocRouteBuilder.route() + .GET( + "/{id}", + projectRouteHandler::getProject, + docHelper::getProjectByIdOperationDoc) + .build(), + ops -> ops.hidden(true)) .build(); } } diff --git a/project-service/src/main/java/com/github/devraghav/bugtracker/project/route/handler/ProjectRouteHandler.java b/project-service/src/main/java/com/github/devraghav/bugtracker/project/route/handler/ProjectRouteHandler.java index a056006..fcbcf19 100644 --- a/project-service/src/main/java/com/github/devraghav/bugtracker/project/route/handler/ProjectRouteHandler.java +++ b/project-service/src/main/java/com/github/devraghav/bugtracker/project/route/handler/ProjectRouteHandler.java @@ -11,6 +11,8 @@ public interface ProjectRouteHandler { Mono createProject(ServerRequest request); + Mono updateProject(ServerRequest request); + Mono getProject(ServerRequest request); Mono addVersionToProject(ServerRequest request); diff --git a/project-service/src/main/java/com/github/devraghav/bugtracker/project/route/handler/ProjectRouteV1Handler.java b/project-service/src/main/java/com/github/devraghav/bugtracker/project/route/handler/ProjectRouteV1Handler.java index 3f31cc7..57d4f7c 100644 --- a/project-service/src/main/java/com/github/devraghav/bugtracker/project/route/handler/ProjectRouteV1Handler.java +++ b/project-service/src/main/java/com/github/devraghav/bugtracker/project/route/handler/ProjectRouteV1Handler.java @@ -41,6 +41,19 @@ public Mono createProject(ServerRequest request) { // @spotless:on } + @Override + public Mono updateProject(ServerRequest request) { + var projectId = request.pathVariable("id"); + return Mono.zip( + getAuthenticatedPrincipal(request), + request.bodyToMono(ProjectRequest.UpdateProject.class)) + .flatMap(tuple2 -> projectService.update(tuple2.getT1(), projectId, tuple2.getT2())) + .flatMap(user -> ProjectResponse.create(request, user)) + .switchIfEmpty(ProjectResponse.noBody(request)) + .onErrorResume( + ProjectException.class, exception -> ProjectResponse.invalid(request, exception)); + } + @Override public Mono getProject(ServerRequest request) { var projectId = request.pathVariable("id"); diff --git a/project-service/src/main/java/com/github/devraghav/bugtracker/project/security/SecurityConfig.java b/project-service/src/main/java/com/github/devraghav/bugtracker/project/security/SecurityConfig.java index 2527e2c..b8faec4 100644 --- a/project-service/src/main/java/com/github/devraghav/bugtracker/project/security/SecurityConfig.java +++ b/project-service/src/main/java/com/github/devraghav/bugtracker/project/security/SecurityConfig.java @@ -46,7 +46,7 @@ public SecurityConfig(@Value("${app.security.jwt.secret}") String secret) { SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { // spotless:off return http.authorizeExchange() - .pathMatchers("/actuator/**", "/favicon.ico") + .pathMatchers("/actuator/**", "/favicon.ico", "/api/rest/internal/**") .permitAll() .pathMatchers(HttpMethod.POST, "/api/rest/v1/project") .hasAnyAuthority(Role.ROLE_ADMIN.name(),Role.ROLE_WRITE.name()) diff --git a/project-service/src/main/java/com/github/devraghav/bugtracker/project/service/ProjectService.java b/project-service/src/main/java/com/github/devraghav/bugtracker/project/service/ProjectService.java index ff59cf3..71a4cf1 100644 --- a/project-service/src/main/java/com/github/devraghav/bugtracker/project/service/ProjectService.java +++ b/project-service/src/main/java/com/github/devraghav/bugtracker/project/service/ProjectService.java @@ -11,19 +11,22 @@ import com.github.devraghav.bugtracker.project.repository.ProjectRepository; import com.github.devraghav.bugtracker.project.request.ProjectRequest; import com.github.devraghav.bugtracker.project.response.ProjectResponse; -import com.github.devraghav.bugtracker.project.validation.RequestValidator; +import java.util.function.Consumer; +import lombok.RequiredArgsConstructor; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @Service -public record ProjectService( - ProjectMapper projectMapper, - ProjectVersionMapper projectVersionMapper, - RequestValidator requestValidator, - ProjectRepository projectRepository, - EventBus.ReactivePublisher eventReactivePublisher) { +@RequiredArgsConstructor +public class ProjectService { + + private final ProjectMapper projectMapper; + private final ProjectVersionMapper projectVersionMapper; + private final RequestValidator requestValidator; + private final ProjectRepository projectRepository; + private final EventBus.ReactivePublisher eventReactivePublisher; public Mono save( String requestBy, ProjectRequest.CreateProject createProject) { @@ -31,13 +34,25 @@ public Mono save( return requestValidator .validate(createProject) .map(validRequest -> projectMapper.requestToEntity(requestBy, validRequest)) - .flatMap(this::save) + .flatMap(projectEntity -> + upsert(projectEntity,project -> eventReactivePublisher.publish(new ProjectEvent.Created(project)))) .onErrorResume( DuplicateKeyException.class, exception -> Mono.error(ProjectException.alreadyExistsByName(createProject.name()))); // @spotless:on } + public Mono update( + String requestBy, String projectId, ProjectRequest.UpdateProject updateProject) { + // @spotless:off + return requestValidator + .validate(updateProject).zipWith(projectRepository.findById(projectId)) + .map(tuple2 -> projectMapper.requestToEntity(requestBy, tuple2.getT2(), tuple2.getT1())) + .flatMap(projectEntity -> + upsert(projectEntity,project -> eventReactivePublisher.publish(new ProjectEvent.Updated(project)))); + // @spotless:on + } + public Flux findAll() { return projectRepository.findAll().map(projectMapper::entityToResponse); } @@ -77,11 +92,12 @@ public Mono findVersionByProjectIdAndVersionId( .map(projectVersionMapper::entityToResponse); } - private Mono save(ProjectEntity projectEntity) { + private Mono upsert( + ProjectEntity projectEntity, Consumer onSuccessConsumer) { return projectRepository .save(projectEntity) .map(author -> projectMapper.entityToResponse(projectEntity)) - .doOnSuccess(project -> eventReactivePublisher.publish(new ProjectEvent.Created(project))); + .doOnSuccess(onSuccessConsumer); } private Mono addVersion( diff --git a/project-service/src/main/java/com/github/devraghav/bugtracker/project/validation/RequestValidator.java b/project-service/src/main/java/com/github/devraghav/bugtracker/project/service/RequestValidator.java similarity index 58% rename from project-service/src/main/java/com/github/devraghav/bugtracker/project/validation/RequestValidator.java rename to project-service/src/main/java/com/github/devraghav/bugtracker/project/service/RequestValidator.java index 1c28194..7eaad1d 100644 --- a/project-service/src/main/java/com/github/devraghav/bugtracker/project/validation/RequestValidator.java +++ b/project-service/src/main/java/com/github/devraghav/bugtracker/project/service/RequestValidator.java @@ -1,6 +1,7 @@ -package com.github.devraghav.bugtracker.project.validation; +package com.github.devraghav.bugtracker.project.service; import com.github.devraghav.bugtracker.project.request.ProjectRequest; +import com.github.devraghav.bugtracker.project.validation.Validator; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; @@ -9,8 +10,13 @@ @RequiredArgsConstructor public class RequestValidator { private final Validator createUserRequestValidator; + private final Validator updateProjectValidator; public Mono validate(final ProjectRequest.CreateProject request) { return createUserRequestValidator.validate(request); } + + public Mono validate(final ProjectRequest.UpdateProject request) { + return updateProjectValidator.validate(request); + } } diff --git a/project-service/src/main/java/com/github/devraghav/bugtracker/project/validation/CreateProjectRequestValidator.java b/project-service/src/main/java/com/github/devraghav/bugtracker/project/validation/CreateProjectRequestValidator.java index 2f5ff2b..6fcf166 100644 --- a/project-service/src/main/java/com/github/devraghav/bugtracker/project/validation/CreateProjectRequestValidator.java +++ b/project-service/src/main/java/com/github/devraghav/bugtracker/project/validation/CreateProjectRequestValidator.java @@ -7,7 +7,7 @@ import reactor.core.publisher.Mono; @Component -public class CreateProjectRequestValidator implements Validator { +class CreateProjectRequestValidator implements Validator { @Override public Mono validate(ProjectRequest.CreateProject createProject) { return validateName(createProject.name()) @@ -25,7 +25,7 @@ private Mono validateName(String name) { private Mono validateDescription(String description) { return Mono.justOrEmpty(description) - .filter(projectDesc -> StringUtils.hasLength(projectDesc) && projectDesc.length() <= 200) + .filter(projectDesc -> StringUtils.hasLength(projectDesc) && projectDesc.length() <= 500) .switchIfEmpty(Mono.error(() -> ProjectException.invalidDescription(description))) .then(); } diff --git a/project-service/src/main/java/com/github/devraghav/bugtracker/project/validation/UpdateProjectRequestValidator.java b/project-service/src/main/java/com/github/devraghav/bugtracker/project/validation/UpdateProjectRequestValidator.java new file mode 100644 index 0000000..00f68f9 --- /dev/null +++ b/project-service/src/main/java/com/github/devraghav/bugtracker/project/validation/UpdateProjectRequestValidator.java @@ -0,0 +1,21 @@ +package com.github.devraghav.bugtracker.project.validation; + +import com.github.devraghav.bugtracker.project.request.ProjectRequest; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Mono; + +@Component +class UpdateProjectRequestValidator implements Validator { + @Override + public Mono validate(ProjectRequest.UpdateProject updateProject) { + return validateDescription(updateProject.description()).thenReturn(updateProject); + } + + private Mono validateDescription(String description) { + return Mono.just(description) + .filter(projectDesc -> StringUtils.hasLength(projectDesc) && projectDesc.length() <= 500) + .onErrorResume(NullPointerException.class, npe -> Mono.empty()) + .then(); + } +} diff --git a/user-service/src/main/java/com/github/devraghav/bugtracker/user/event/internal/UserEvent.java b/user-service/src/main/java/com/github/devraghav/bugtracker/user/event/internal/UserEvent.java index cdc6834..472a275 100644 --- a/user-service/src/main/java/com/github/devraghav/bugtracker/user/event/internal/UserEvent.java +++ b/user-service/src/main/java/com/github/devraghav/bugtracker/user/event/internal/UserEvent.java @@ -14,7 +14,20 @@ public Created(UserResponse.User createdUser) { super( createdUser.id(), "Created", - new PublisherInfo("User", UserResponse.User.class, "SYSTEM")); + new PublisherInfo("User", UserResponse.User.class, createdUser.id())); + this.createdUser = createdUser; + } + } + + @Getter + class Updated extends DomainEvent { + private final UserResponse.User createdUser; + + public Updated(UserResponse.User createdUser) { + super( + createdUser.id(), + "Updated", + new PublisherInfo("User", UserResponse.User.class, createdUser.id())); this.createdUser = createdUser; } } diff --git a/user-service/src/main/java/com/github/devraghav/bugtracker/user/mapper/UserMapper.java b/user-service/src/main/java/com/github/devraghav/bugtracker/user/mapper/UserMapper.java index 885f821..eccb719 100644 --- a/user-service/src/main/java/com/github/devraghav/bugtracker/user/mapper/UserMapper.java +++ b/user-service/src/main/java/com/github/devraghav/bugtracker/user/mapper/UserMapper.java @@ -3,6 +3,7 @@ import com.github.devraghav.bugtracker.user.entity.UserEntity; import com.github.devraghav.bugtracker.user.request.UserRequest; import com.github.devraghav.bugtracker.user.response.UserResponse; +import java.util.Optional; import java.util.UUID; import org.mapstruct.Mapper; import org.mapstruct.Mapping; @@ -13,7 +14,7 @@ @Mapper( componentModel = "spring", - imports = {UUID.class}) + imports = {UUID.class, Optional.class}) public abstract class UserMapper { @Autowired private PasswordEncoder passwordEncoder; @@ -25,6 +26,23 @@ public abstract class UserMapper { }) public abstract UserEntity requestToEntity(UserRequest.CreateUser createUserUserRequest); + @Mappings({ + @Mapping( + target = "firstName", + expression = + "java(Optional.ofNullable(updateUser.firstName()).orElse(userEntity.getFirstName()))"), + @Mapping( + target = "lastName", + expression = + "java(Optional.ofNullable(updateUser.lastName()).orElse(userEntity.getLastName()))"), + @Mapping( + target = "role", + expression = + "java(Optional.ofNullable(updateUser.role()).map(UserRequest.Role::getValue).orElseGet(userEntity::getRole))") + }) + public abstract UserEntity requestToEntity( + UserEntity userEntity, UserRequest.UpdateUser updateUser); + @Mappings({@Mapping(target = "role", source = "role", qualifiedByName = "valueToRole")}) public abstract UserResponse.User entityToResponse(UserEntity userEntity); diff --git a/user-service/src/main/java/com/github/devraghav/bugtracker/user/request/UserRequest.java b/user-service/src/main/java/com/github/devraghav/bugtracker/user/request/UserRequest.java index c6abe06..4123d58 100644 --- a/user-service/src/main/java/com/github/devraghav/bugtracker/user/request/UserRequest.java +++ b/user-service/src/main/java/com/github/devraghav/bugtracker/user/request/UserRequest.java @@ -13,19 +13,29 @@ public static record AuthRequest(String email, String password) {} public static record AuthResponse(String token) {} + public static sealed interface User { + String firstName(); + + String lastName(); + + Role role(); + } + public static record CreateUser( - String firstName, String lastName, String email, String password, Role role) { + String firstName, String lastName, String email, String password, Role role) implements User { public Role role() { return Objects.isNull(role) ? Role.ROLE_READ : role; } } + public static record UpdateUser(String firstName, String lastName, Role role) implements User {} + public enum Role { ROLE_ADMIN(0), ROLE_READ(1), ROLE_WRITE(2); - @Getter private int value; - private static Map reverseLookup = + @Getter private final int value; + private static Map VALUE_TO_ENUM = Arrays.stream(Role.values()) .collect(Collectors.toUnmodifiableMap(Role::getValue, Function.identity())); @@ -34,7 +44,7 @@ public enum Role { } public static Role fromValue(int value) { - return reverseLookup.getOrDefault(value, Role.ROLE_READ); + return VALUE_TO_ENUM.getOrDefault(value, Role.ROLE_READ); } } } diff --git a/user-service/src/main/java/com/github/devraghav/bugtracker/user/route/definition/UserOpenAPIDocHelper.java b/user-service/src/main/java/com/github/devraghav/bugtracker/user/route/definition/UserOpenAPIDocHelper.java index f0debba..3e642d8 100644 --- a/user-service/src/main/java/com/github/devraghav/bugtracker/user/route/definition/UserOpenAPIDocHelper.java +++ b/user-service/src/main/java/com/github/devraghav/bugtracker/user/route/definition/UserOpenAPIDocHelper.java @@ -68,6 +68,23 @@ void getUserByIdOperationDoc(org.springdoc.core.fn.builders.operation.Builder op .build(); } + void updateUserOperationDoc(org.springdoc.core.fn.builders.operation.Builder ops) { + ops.operationId("update") + .summary("Update user by its id") + .security(securityRequirementBuilder().name("bearerAuth")) + .requestBody(requestBodyBuilder() + .content(contentBuilder() + .schema(schemaBuilder().implementation(UserRequest.UpdateUser.class)))) + .response(getUserById200ResponseDoc()) + .response(getUserById404ResponseDoc()) + .response(responseBuilder().responseCode("401").description("Unauthorized user")) + .parameter(parameterBuilder() + .in(ParameterIn.PATH) + .name("id") + .schema(schemaBuilder().type("string"))) + .build(); + } + private Builder saveUser201ResponseDoc() { return responseBuilder() .responseCode("201") diff --git a/user-service/src/main/java/com/github/devraghav/bugtracker/user/route/definition/UserRouteDefinition.java b/user-service/src/main/java/com/github/devraghav/bugtracker/user/route/definition/UserRouteDefinition.java index 5e19862..6091a9d 100644 --- a/user-service/src/main/java/com/github/devraghav/bugtracker/user/route/definition/UserRouteDefinition.java +++ b/user-service/src/main/java/com/github/devraghav/bugtracker/user/route/definition/UserRouteDefinition.java @@ -28,6 +28,7 @@ RouterFunction userRoutes( .POST("/login", userRouteHandler::login, docHelper::loginUserOperationDoc) .POST("/signup", userRouteHandler::create, docHelper::signUpUserOperationDoc) .GET("/{id}", userRouteHandler::get, docHelper::getUserByIdOperationDoc) + .PATCH("/{id}", userRouteHandler::update, docHelper::updateUserOperationDoc) .build(); return SpringdocRouteBuilder.route() .nest( @@ -35,6 +36,14 @@ RouterFunction userRoutes( .and(accept(APPLICATION_JSON).or(contentType(APPLICATION_JSON))), routerFunctionSupplier, emptyOperationsConsumer) + .nest( + path("/api/rest/internal/v1/user") + .and(accept(APPLICATION_JSON).or(contentType(APPLICATION_JSON))), + () -> + SpringdocRouteBuilder.route() + .GET("/{id}", userRouteHandler::get, docHelper::getUserByIdOperationDoc) + .build(), + ops -> ops.hidden(true)) .build(); } } diff --git a/user-service/src/main/java/com/github/devraghav/bugtracker/user/route/handler/UserRouteHandler.java b/user-service/src/main/java/com/github/devraghav/bugtracker/user/route/handler/UserRouteHandler.java index 872b137..a5bd3e2 100644 --- a/user-service/src/main/java/com/github/devraghav/bugtracker/user/route/handler/UserRouteHandler.java +++ b/user-service/src/main/java/com/github/devraghav/bugtracker/user/route/handler/UserRouteHandler.java @@ -10,6 +10,8 @@ public interface UserRouteHandler { Mono create(ServerRequest request); + Mono update(ServerRequest request); + Mono get(ServerRequest request); Mono login(ServerRequest request); diff --git a/user-service/src/main/java/com/github/devraghav/bugtracker/user/route/handler/UserRouteV1Handler.java b/user-service/src/main/java/com/github/devraghav/bugtracker/user/route/handler/UserRouteV1Handler.java index 531ee98..73ea0c7 100644 --- a/user-service/src/main/java/com/github/devraghav/bugtracker/user/route/handler/UserRouteV1Handler.java +++ b/user-service/src/main/java/com/github/devraghav/bugtracker/user/route/handler/UserRouteV1Handler.java @@ -35,6 +35,17 @@ public Mono create(ServerRequest request) { .onErrorResume(UserException.class, exception -> UserResponse.invalid(request, exception)); } + @Override + public Mono update(ServerRequest request) { + var userId = request.pathVariable("id"); + return request + .bodyToMono(UserRequest.UpdateUser.class) + .flatMap(updateUser -> userService.update(userId, updateUser)) + .flatMap(user -> UserResponse.create(request, user)) + .switchIfEmpty(UserResponse.noBody(request)) + .onErrorResume(UserException.class, exception -> UserResponse.invalid(request, exception)); + } + @Override public Mono get(ServerRequest request) { return userService diff --git a/user-service/src/main/java/com/github/devraghav/bugtracker/user/security/SecurityConfig.java b/user-service/src/main/java/com/github/devraghav/bugtracker/user/security/SecurityConfig.java index 7cb2710..675daa8 100644 --- a/user-service/src/main/java/com/github/devraghav/bugtracker/user/security/SecurityConfig.java +++ b/user-service/src/main/java/com/github/devraghav/bugtracker/user/security/SecurityConfig.java @@ -48,11 +48,8 @@ public class SecurityConfig { SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { // spotless:off return http.authorizeExchange() - .pathMatchers( - "/api/rest/v1/user/login", - "/api/rest/v1/user/signup", - "/favicon.ico" , - "/actuator/**") + .pathMatchers("/api/rest/v1/user/login", "/api/rest/v1/user/signup", "/favicon.ico", + "/actuator/**", "/api/rest/internal/**") .permitAll() .pathMatchers(HttpMethod.GET,"/api/rest/v1/user") .hasAuthority("ROLE_ADMIN") diff --git a/user-service/src/main/java/com/github/devraghav/bugtracker/user/service/RequestValidator.java b/user-service/src/main/java/com/github/devraghav/bugtracker/user/service/RequestValidator.java index 3aa9702..e0590d7 100644 --- a/user-service/src/main/java/com/github/devraghav/bugtracker/user/service/RequestValidator.java +++ b/user-service/src/main/java/com/github/devraghav/bugtracker/user/service/RequestValidator.java @@ -6,9 +6,15 @@ import reactor.core.publisher.Mono; @Component -public record RequestValidator(Validator createUserRequestValidator) { +public record RequestValidator( + Validator createUserValidator, + Validator updateUserValidator) { public Mono validate(final UserRequest.CreateUser request) { - return createUserRequestValidator.validate(request); + return createUserValidator.validate(request); + } + + public Mono validate(final UserRequest.UpdateUser request) { + return updateUserValidator.validate(request); } } diff --git a/user-service/src/main/java/com/github/devraghav/bugtracker/user/service/UserService.java b/user-service/src/main/java/com/github/devraghav/bugtracker/user/service/UserService.java index c1599c4..cdff8d6 100644 --- a/user-service/src/main/java/com/github/devraghav/bugtracker/user/service/UserService.java +++ b/user-service/src/main/java/com/github/devraghav/bugtracker/user/service/UserService.java @@ -9,28 +9,43 @@ import com.github.devraghav.bugtracker.user.repository.UserRepository; import com.github.devraghav.bugtracker.user.request.UserRequest; import com.github.devraghav.bugtracker.user.response.UserResponse; +import java.util.function.Consumer; +import lombok.RequiredArgsConstructor; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @Service -public record UserService( - RequestValidator requestValidator, - UserMapper userMapper, - UserRepository userRepository, - EventBus.ReactivePublisher domainEventReactivePublisher) { - - // @spotless:off - public Mono save(UserRequest.CreateUser createUserUserRequest) { +@RequiredArgsConstructor +public class UserService { + + private final RequestValidator requestValidator; + private final UserMapper userMapper; + private final UserRepository userRepository; + private final EventBus.ReactivePublisher domainEventPublisher; + + public Mono save(UserRequest.CreateUser createUser) { + // @spotless:off return requestValidator - .validate(createUserUserRequest) + .validate(createUser) .map(userMapper::requestToEntity) - .flatMap(this::save) + .flatMap(userEntity -> upsert(userEntity,user -> domainEventPublisher.publish(new UserEvent.Created(user)))) .onErrorResume(DuplicateKeyException.class, - exception -> Mono.error(UserException.alreadyExistsWithEmail(createUserUserRequest.email()))); + exception -> Mono.error(UserException.alreadyExistsWithEmail(createUser.email()))); + // @spotless:on + } + + public Mono update(String userId, UserRequest.UpdateUser updateUser) { + // @spotless:off + return requestValidator + .validate(updateUser) + .zipWith(userRepository.findById(userId)) + .map(tuple2 -> userMapper.requestToEntity(tuple2.getT2(), tuple2.getT1())) + .flatMap(userEntity -> + upsert(userEntity, user -> domainEventPublisher.publish(new UserEvent.Updated(user)))); + // @spotless:on } - // @spotless:on public Flux findAll() { return userRepository.findAll().map(userMapper::entityToResponse); @@ -50,10 +65,11 @@ public Mono findById(String userId) { .switchIfEmpty(Mono.error(() -> UserException.notFoundById(userId))); } - private Mono save(UserEntity userEntity) { + private Mono upsert( + UserEntity userEntity, Consumer onSuccessConsumer) { return userRepository .save(userEntity) .map(userMapper::entityToResponse) - .doOnSuccess(user -> domainEventReactivePublisher.publish(new UserEvent.Created(user))); + .doOnSuccess(onSuccessConsumer); } } diff --git a/user-service/src/main/java/com/github/devraghav/bugtracker/user/validation/UpdateUserRequestValidator.java b/user-service/src/main/java/com/github/devraghav/bugtracker/user/validation/UpdateUserRequestValidator.java new file mode 100644 index 0000000..cb505ec --- /dev/null +++ b/user-service/src/main/java/com/github/devraghav/bugtracker/user/validation/UpdateUserRequestValidator.java @@ -0,0 +1,32 @@ +package com.github.devraghav.bugtracker.user.validation; + +import com.github.devraghav.bugtracker.user.exception.UserException; +import com.github.devraghav.bugtracker.user.request.UserRequest; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Mono; + +@Component +class UpdateUserRequestValidator implements Validator { + + @Override + public Mono validate(UserRequest.UpdateUser updateUser) { + return validateFirstName(updateUser.firstName()) + .and(validateLastName(updateUser.lastName())) + .thenReturn(updateUser); + } + + private Mono validateLastName(String lastName) { + return Mono.justOrEmpty(lastName) + .filter(StringUtils::hasLength) + .switchIfEmpty(Mono.error(UserException.nullLastName())) + .then(); + } + + private Mono validateFirstName(String firstName) { + return Mono.justOrEmpty(firstName) + .filter(StringUtils::hasLength) + .switchIfEmpty(Mono.error(UserException.nullFirstName())) + .then(); + } +}