Skip to content

Commit

Permalink
feat : expose endpoint to retrieve customer revision history by ID, w…
Browse files Browse the repository at this point in the history
…ith pagination support. (#1185)

* feat : expose endpoint to fetch history by pagination

* Use existing exception rather than creating newer one
  • Loading branch information
rajadilipkolli authored Apr 11, 2024
1 parent af75087 commit cec32d7
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 37 deletions.
30 changes: 25 additions & 5 deletions jpa/boot-data-envers/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
<version>3.3.0-M3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example.envers</groupId>
Expand Down Expand Up @@ -223,11 +223,11 @@
<configuration>
<java>
<palantirJavaFormat>
<version>2.39.0</version>
<version>2.40.0</version>
</palantirJavaFormat>
<importOrder />
<removeUnusedImports />
<formatAnnotations />
<importOrder/>
<removeUnusedImports/>
<formatAnnotations/>
</java>
</configuration>
<executions>
Expand Down Expand Up @@ -336,4 +336,24 @@
</plugins>
</build>

<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,13 @@
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.Hibernate;
import org.hibernate.envers.Audited;

@Entity
@Table(name = "customers")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Audited
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -65,6 +67,35 @@ public List<RevisionResult> findCustomerRevisionsById(Long id) {
return revisionDtoCF.stream().map(CompletableFuture::join).toList();
}

public PagedResult<RevisionResult> findCustomerHistoryById(Long id, Pageable pageRequest) {
if (customerRepository.findById(id).isEmpty()) {
throw new CustomerNotFoundException(id);
}

RevisionSort sortDir;
Optional<Sort.Direction> 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<Revision<Integer, Customer>> customerRevisions = customerRepository.findRevisions(id, pageable);
List<CompletableFuture<RevisionResult>> revisionCFResultList = customerRevisions.getContent().stream()
.map(customerRevision -> CompletableFuture.supplyAsync(
() -> customerRevisionToRevisionDTOMapper.convert(customerRevision)))
.toList();
List<RevisionResult> revisionResultList =
revisionCFResultList.stream().map(CompletableFuture::join).toList();
return new PagedResult<>(customerRevisions, revisionResultList);
}

@Transactional
public CustomerResponse saveCustomer(CustomerRequest customerRequest) {
Customer customer = customerMapper.toEntity(customerRequest);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -55,6 +57,12 @@ public ResponseEntity<List<RevisionResult>> findCustomerRevisionsById(@PathVaria
return ResponseEntity.ok(customerService.findCustomerRevisionsById(id));
}

@Operation(summary = "Get Customer Version History By Id")
@GetMapping("/{id}/history")
public PagedResult<RevisionResult> getCustomerHistoryById(@PathVariable("id") Long id, Pageable pageable) {
return customerService.findCustomerHistoryById(id, pageable);
}

@PostMapping
public ResponseEntity<CustomerResponse> createCustomer(@RequestBody @Validated CustomerRequest customerRequest) {
CustomerResponse response = customerService.saveCustomer(customerRequest);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Integer, Customer> revisions = customerRepository.findRevisions(customer.getId());
Revisions<Integer, Customer> 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<Integer, Customer>> revision = customerRepository.findLastChangeRevision(customer.getId());
Optional<Revision<Integer, Customer>> revision =
customerRepository.findLastChangeRevision(updatedCustomer.getId());

assertThat(revision)
.isPresent()
Expand All @@ -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);
Expand All @@ -71,11 +68,11 @@ void deletedItemWillHaveRevisionRetained() {
Revision<Integer, Customer> 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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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
Expand All @@ -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)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand Down Expand Up @@ -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)))
Expand Down Expand Up @@ -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)))
Expand Down Expand Up @@ -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)))
Expand Down

0 comments on commit cec32d7

Please sign in to comment.