From cec32d70f1237a56fc0be86439df915ac92cd2a7 Mon Sep 17 00:00:00 2001 From: Raja Kolli Date: Thu, 11 Apr 2024 17:58:39 +0530 Subject: [PATCH] feat : expose endpoint to retrieve customer revision history by ID, with pagination support. (#1185) * feat : expose endpoint to fetch history by pagination * Use existing exception rather than creating newer one --- jpa/boot-data-envers/pom.xml | 30 ++++++++++-- .../example/envers/config/WebMvcConfig.java | 12 +++-- .../com/example/envers/entities/Customer.java | 24 ++++++++-- .../envers/services/CustomerService.java | 31 +++++++++++++ .../web/controllers/CustomerController.java | 8 ++++ .../envers/ApplicationIntegrationTest.java | 29 ++++++------ .../web/controllers/CustomerControllerIT.java | 46 +++++++++++++++++-- .../controllers/CustomerControllerTest.java | 8 ++-- 8 files changed, 151 insertions(+), 37 deletions(-) diff --git a/jpa/boot-data-envers/pom.xml b/jpa/boot-data-envers/pom.xml index 69ac4006f..0344a52b8 100644 --- a/jpa/boot-data-envers/pom.xml +++ b/jpa/boot-data-envers/pom.xml @@ -7,7 +7,7 @@ org.springframework.boot spring-boot-starter-parent - 3.2.4 + 3.3.0-M3 com.example.envers @@ -223,11 +223,11 @@ - 2.39.0 + 2.40.0 - - - + + + @@ -336,4 +336,24 @@ + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + diff --git a/jpa/boot-data-envers/src/main/java/com/example/envers/config/WebMvcConfig.java b/jpa/boot-data-envers/src/main/java/com/example/envers/config/WebMvcConfig.java index 524db2a3f..38963390a 100644 --- a/jpa/boot-data-envers/src/main/java/com/example/envers/config/WebMvcConfig.java +++ b/jpa/boot-data-envers/src/main/java/com/example/envers/config/WebMvcConfig.java @@ -8,14 +8,16 @@ @Configuration @RequiredArgsConstructor public class WebMvcConfig implements WebMvcConfigurer { + private final ApplicationProperties properties; @Override public void addCorsMappings(CorsRegistry registry) { - registry.addMapping(properties.getCors().getPathPattern()) - .allowedMethods(properties.getCors().getAllowedMethods()) - .allowedHeaders(properties.getCors().getAllowedHeaders()) - .allowedOriginPatterns(properties.getCors().getAllowedOriginPatterns()) - .allowCredentials(properties.getCors().isAllowCredentials()); + ApplicationProperties.Cors propertiesCors = properties.getCors(); + registry.addMapping(propertiesCors.getPathPattern()) + .allowedMethods(propertiesCors.getAllowedMethods()) + .allowedHeaders(propertiesCors.getAllowedHeaders()) + .allowedOriginPatterns(propertiesCors.getAllowedOriginPatterns()) + .allowCredentials(propertiesCors.isAllowCredentials()); } } diff --git a/jpa/boot-data-envers/src/main/java/com/example/envers/entities/Customer.java b/jpa/boot-data-envers/src/main/java/com/example/envers/entities/Customer.java index 573f4ce22..8c3c5bac1 100644 --- a/jpa/boot-data-envers/src/main/java/com/example/envers/entities/Customer.java +++ b/jpa/boot-data-envers/src/main/java/com/example/envers/entities/Customer.java @@ -11,7 +11,6 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; import lombok.ToString; import org.hibernate.Hibernate; import org.hibernate.envers.Audited; @@ -19,7 +18,6 @@ @Entity @Table(name = "customers") @Getter -@Setter @NoArgsConstructor @AllArgsConstructor @Audited @@ -36,7 +34,27 @@ public class Customer { private String address; @Version - Short version = 0; + Short version; + + public Customer setId(Long id) { + this.id = id; + return this; + } + + public Customer setName(String name) { + this.name = name; + return this; + } + + public Customer setAddress(String address) { + this.address = address; + return this; + } + + public Customer setVersion(Short version) { + this.version = version; + return this; + } @Override public boolean equals(Object o) { diff --git a/jpa/boot-data-envers/src/main/java/com/example/envers/services/CustomerService.java b/jpa/boot-data-envers/src/main/java/com/example/envers/services/CustomerService.java index b6e6f5529..1b15f654b 100644 --- a/jpa/boot-data-envers/src/main/java/com/example/envers/services/CustomerService.java +++ b/jpa/boot-data-envers/src/main/java/com/example/envers/services/CustomerService.java @@ -18,6 +18,8 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.history.Revision; +import org.springframework.data.history.RevisionSort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -65,6 +67,35 @@ public List findCustomerRevisionsById(Long id) { return revisionDtoCF.stream().map(CompletableFuture::join).toList(); } + public PagedResult findCustomerHistoryById(Long id, Pageable pageRequest) { + if (customerRepository.findById(id).isEmpty()) { + throw new CustomerNotFoundException(id); + } + + RevisionSort sortDir; + Optional direction = + pageRequest.getSort().stream().map(Sort.Order::getDirection).findFirst(); + if (direction.isPresent()) { + if (Sort.Direction.ASC.name().equalsIgnoreCase(direction.get().name())) { + sortDir = RevisionSort.asc(); + } else { + sortDir = RevisionSort.desc(); + } + } else { + sortDir = RevisionSort.desc(); + } + + Pageable pageable = PageRequest.of(pageRequest.getPageNumber(), pageRequest.getPageSize(), sortDir); + Page> customerRevisions = customerRepository.findRevisions(id, pageable); + List> revisionCFResultList = customerRevisions.getContent().stream() + .map(customerRevision -> CompletableFuture.supplyAsync( + () -> customerRevisionToRevisionDTOMapper.convert(customerRevision))) + .toList(); + List revisionResultList = + revisionCFResultList.stream().map(CompletableFuture::join).toList(); + return new PagedResult<>(customerRevisions, revisionResultList); + } + @Transactional public CustomerResponse saveCustomer(CustomerRequest customerRequest) { Customer customer = customerMapper.toEntity(customerRequest); diff --git a/jpa/boot-data-envers/src/main/java/com/example/envers/web/controllers/CustomerController.java b/jpa/boot-data-envers/src/main/java/com/example/envers/web/controllers/CustomerController.java index 8dcfefb01..b07ffbc4e 100644 --- a/jpa/boot-data-envers/src/main/java/com/example/envers/web/controllers/CustomerController.java +++ b/jpa/boot-data-envers/src/main/java/com/example/envers/web/controllers/CustomerController.java @@ -8,10 +8,12 @@ import com.example.envers.model.response.RevisionResult; import com.example.envers.services.CustomerService; import com.example.envers.utils.AppConstants; +import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import java.net.URI; import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; @@ -55,6 +57,12 @@ public ResponseEntity> findCustomerRevisionsById(@PathVaria return ResponseEntity.ok(customerService.findCustomerRevisionsById(id)); } + @Operation(summary = "Get Customer Version History By Id") + @GetMapping("/{id}/history") + public PagedResult getCustomerHistoryById(@PathVariable("id") Long id, Pageable pageable) { + return customerService.findCustomerHistoryById(id, pageable); + } + @PostMapping public ResponseEntity createCustomer(@RequestBody @Validated CustomerRequest customerRequest) { CustomerResponse response = customerService.saveCustomer(customerRequest); diff --git a/jpa/boot-data-envers/src/test/java/com/example/envers/ApplicationIntegrationTest.java b/jpa/boot-data-envers/src/test/java/com/example/envers/ApplicationIntegrationTest.java index 3acbe750f..e4e25aa6c 100644 --- a/jpa/boot-data-envers/src/test/java/com/example/envers/ApplicationIntegrationTest.java +++ b/jpa/boot-data-envers/src/test/java/com/example/envers/ApplicationIntegrationTest.java @@ -19,29 +19,27 @@ class ApplicationIntegrationTest extends AbstractIntegrationTest { @Test void initialRevision() { - Customer cust = new Customer(); - cust.setName("junit"); - cust.setAddress("address"); - Customer customer = customerRepository.save(cust); + Customer cust = new Customer().setName("junitName").setAddress("junitAddress"); + Customer savedCustomer = customerRepository.save(cust); - Revisions revisions = customerRepository.findRevisions(customer.getId()); + Revisions revisions = customerRepository.findRevisions(savedCustomer.getId()); assertThat(revisions).isNotEmpty().allSatisfy(revision -> assertThat(revision.getEntity()) .extracting(Customer::getId, Customer::getName, Customer::getVersion) - .containsExactly(customer.getId(), customer.getName(), customer.getVersion())); + .containsExactly(savedCustomer.getId(), savedCustomer.getName(), null)); } @Test void updateIncreasesRevisionNumber() { - var cust = new Customer(); - cust.setName("text"); + Customer cust = new Customer().setName("text"); Customer customer = customerRepository.save(cust); customer.setName("If"); - customerRepository.save(customer); + Customer updatedCustomer = customerRepository.save(customer); - Optional> revision = customerRepository.findLastChangeRevision(customer.getId()); + Optional> revision = + customerRepository.findLastChangeRevision(updatedCustomer.getId()); assertThat(revision) .isPresent() @@ -55,8 +53,7 @@ void updateIncreasesRevisionNumber() { @Test void deletedItemWillHaveRevisionRetained() { - var cust = new Customer(); - cust.setName("junit"); + Customer cust = new Customer().setName("junitName").setAddress("junitAddress"); Customer customer = customerRepository.save(cust); customerRepository.delete(customer); @@ -71,11 +68,11 @@ void deletedItemWillHaveRevisionRetained() { Revision finalRevision = iterator.next(); assertThat(initialRevision).satisfies(rev -> assertThat(rev.getEntity()) - .extracting(Customer::getId, Customer::getName, Customer::getVersion) - .containsExactly(customer.getId(), customer.getName(), customer.getVersion())); + .extracting(Customer::getId, Customer::getName, Customer::getAddress, Customer::getVersion) + .containsExactly(customer.getId(), customer.getName(), customer.getAddress(), null)); assertThat(finalRevision).satisfies(rev -> assertThat(rev.getEntity()) - .extracting(Customer::getId, Customer::getName, Customer::getVersion) - .containsExactly(customer.getId(), null, (short) 0)); + .extracting(Customer::getId, Customer::getName, Customer::getAddress, Customer::getVersion) + .containsExactly(customer.getId(), null, null, null)); } } diff --git a/jpa/boot-data-envers/src/test/java/com/example/envers/web/controllers/CustomerControllerIT.java b/jpa/boot-data-envers/src/test/java/com/example/envers/web/controllers/CustomerControllerIT.java index a455c2b6d..ef996c6f4 100644 --- a/jpa/boot-data-envers/src/test/java/com/example/envers/web/controllers/CustomerControllerIT.java +++ b/jpa/boot-data-envers/src/test/java/com/example/envers/web/controllers/CustomerControllerIT.java @@ -37,9 +37,9 @@ void setUp() { customerRepository.deleteAllInBatch(); customerList = new ArrayList<>(); - customerList.add(new Customer(null, "First Customer", "Junit Address", (short) 0)); - customerList.add(new Customer(null, "Second Customer", "Junit Address", (short) 0)); - customerList.add(new Customer(null, "Third Customer", "Junit Address", (short) 0)); + customerList.add(new Customer().setName("First Customer").setAddress("Junit Address")); + customerList.add(new Customer().setName("First Customer").setAddress("Junit Address")); + customerList.add(new Customer().setName("First Customer").setAddress("Junit Address")); customerList = customerRepository.saveAll(customerList); } @@ -88,6 +88,44 @@ void shouldFindCustomerRevisionsById() throws Exception { .andExpect(jsonPath("$[0].revisionNumber", notNullValue())) .andExpect(jsonPath("$[0].revisionType", is("INSERT"))); } + + @Test + void shouldFindCustomerHistoryById() throws Exception { + Customer customer = customerList.getFirst(); + customerRepository.saveAndFlush(customer.setAddress("newAddress")); + Long customerId = customer.getId(); + + mockMvc.perform(get("/api/customers/{id}/history?page=0&size=10&sort=revision_Number,desc", customerId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.size()", is(2))) + .andExpect(jsonPath("$.totalElements", is(2))) + .andExpect(jsonPath("$.pageNumber", is(1))) + .andExpect(jsonPath("$.totalPages", is(1))) + .andExpect(jsonPath("$.isFirst", is(true))) + .andExpect(jsonPath("$.isLast", is(true))) + .andExpect(jsonPath("$.hasNext", is(false))) + .andExpect(jsonPath("$.hasPrevious", is(false))) + .andExpect(jsonPath("$.data[0].entity.id", is(customer.getId()), Long.class)) + .andExpect(jsonPath("$.data[0].entity.name", is(customer.getName()))) + .andExpect(jsonPath("$.data[0].entity.address", is(customer.getAddress()))) + .andExpect(jsonPath("$.data[0].revisionNumber", notNullValue())) + .andExpect(jsonPath("$.data[0].revisionType", is("UPDATE"))) + .andExpect(jsonPath("$.data[0].revisionInstant", notNullValue())); + } + + @Test + void cantFindCustomerHistoryById() throws Exception { + Customer customer = customerList.getFirst(); + Long customerId = customer.getId() + 10_000; + + mockMvc.perform(get("/api/customers/{id}/history?page=0&size=10&sort=revision_Number,asc", customerId)) + .andExpect(status().isNotFound()) + .andExpect(header().string(HttpHeaders.CONTENT_TYPE, is(MediaType.APPLICATION_PROBLEM_JSON_VALUE))) + .andExpect(jsonPath("$.type", is("http://api.boot-data-envers.com/errors/not-found"))) + .andExpect(jsonPath("$.title", is("Not Found"))) + .andExpect(jsonPath("$.status", is(404))) + .andExpect(jsonPath("$.detail").value("Customer with Id '%d' not found".formatted(customerId))); + } } @Test @@ -113,7 +151,7 @@ void shouldReturn400WhenCreateNewCustomerWithoutName() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(customerRequest))) .andExpect(status().isBadRequest()) - .andExpect(header().string("Content-Type", is("application/problem+json"))) + .andExpect(header().string(HttpHeaders.CONTENT_TYPE, is(MediaType.APPLICATION_PROBLEM_JSON_VALUE))) .andExpect(jsonPath("$.type", is("about:blank"))) .andExpect(jsonPath("$.title", is("Constraint Violation"))) .andExpect(jsonPath("$.status", is(400))) diff --git a/jpa/boot-data-envers/src/test/java/com/example/envers/web/controllers/CustomerControllerTest.java b/jpa/boot-data-envers/src/test/java/com/example/envers/web/controllers/CustomerControllerTest.java index 423093317..1e08e961b 100644 --- a/jpa/boot-data-envers/src/test/java/com/example/envers/web/controllers/CustomerControllerTest.java +++ b/jpa/boot-data-envers/src/test/java/com/example/envers/web/controllers/CustomerControllerTest.java @@ -107,7 +107,7 @@ void shouldReturn404WhenFetchingNonExistingCustomer() throws Exception { mockMvc.perform(get("/api/customers/{id}", customerId)) .andExpect(status().isNotFound()) - .andExpect(header().string("Content-Type", is(MediaType.APPLICATION_PROBLEM_JSON_VALUE))) + .andExpect(header().string(HttpHeaders.CONTENT_TYPE, is(MediaType.APPLICATION_PROBLEM_JSON_VALUE))) .andExpect(jsonPath("$.type", is("http://api.boot-data-envers.com/errors/not-found"))) .andExpect(jsonPath("$.title", is("Not Found"))) .andExpect(jsonPath("$.status", is(404))) @@ -143,7 +143,7 @@ void shouldReturn400WhenCreateNewCustomerWithoutName() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(customerRequest))) .andExpect(status().isBadRequest()) - .andExpect(header().string("Content-Type", is("application/problem+json"))) + .andExpect(header().string(HttpHeaders.CONTENT_TYPE, is("application/problem+json"))) .andExpect(jsonPath("$.type", is("about:blank"))) .andExpect(jsonPath("$.title", is("Constraint Violation"))) .andExpect(jsonPath("$.status", is(400))) @@ -187,7 +187,7 @@ void shouldReturn404WhenUpdatingNonExistingCustomer() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(customerRequest))) .andExpect(status().isNotFound()) - .andExpect(header().string("Content-Type", is(MediaType.APPLICATION_PROBLEM_JSON_VALUE))) + .andExpect(header().string(HttpHeaders.CONTENT_TYPE, is(MediaType.APPLICATION_PROBLEM_JSON_VALUE))) .andExpect(jsonPath("$.type", is("http://api.boot-data-envers.com/errors/not-found"))) .andExpect(jsonPath("$.title", is("Not Found"))) .andExpect(jsonPath("$.status", is(404))) @@ -217,7 +217,7 @@ void shouldReturn404WhenDeletingNonExistingCustomer() throws Exception { given(customerService.findCustomerById(customerId)).willReturn(Optional.empty()); mockMvc.perform(delete("/api/customers/{id}", customerId)) - .andExpect(header().string("Content-Type", is(MediaType.APPLICATION_PROBLEM_JSON_VALUE))) + .andExpect(header().string(HttpHeaders.CONTENT_TYPE, is(MediaType.APPLICATION_PROBLEM_JSON_VALUE))) .andExpect(jsonPath("$.type", is("http://api.boot-data-envers.com/errors/not-found"))) .andExpect(jsonPath("$.title", is("Not Found"))) .andExpect(jsonPath("$.status", is(404)))