feat(search-result): extend DocumentSearchResult with pageNumber/pageSize/totalPages
Rename `total` → `totalElements` for Spring-Page parity and add three new required paging fields: pageNumber, pageSize, totalPages. Adds a `paged( slice, pageable, totalElements)` factory alongside the existing single-page `of(list)` shortcut. Enables offset pagination of /documents search (#315). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package org.raddatz.familienarchiv.dto;
|
package org.raddatz.familienarchiv.dto;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -8,9 +9,30 @@ public record DocumentSearchResult(
|
|||||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
List<DocumentSearchItem> items,
|
List<DocumentSearchItem> items,
|
||||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
long total
|
long totalElements,
|
||||||
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
int pageNumber,
|
||||||
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
int pageSize,
|
||||||
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
int totalPages
|
||||||
) {
|
) {
|
||||||
|
/**
|
||||||
|
* Single-page convenience factory used by empty-result shortcuts and by tests that
|
||||||
|
* don't care about paging. Treats the whole list as page 0 of itself.
|
||||||
|
*/
|
||||||
public static DocumentSearchResult of(List<DocumentSearchItem> items) {
|
public static DocumentSearchResult of(List<DocumentSearchItem> items) {
|
||||||
return new DocumentSearchResult(items, items.size());
|
int size = items.size();
|
||||||
|
return new DocumentSearchResult(items, size, 0, size, size == 0 ? 0 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paged factory used by the service when it has a real Pageable + full match count
|
||||||
|
* (e.g. from Spring's Page<T> or from an in-memory sort-then-slice).
|
||||||
|
*/
|
||||||
|
public static DocumentSearchResult paged(List<DocumentSearchItem> slice, Pageable pageable, long totalElements) {
|
||||||
|
int pageSize = pageable.getPageSize();
|
||||||
|
int totalPages = pageSize == 0 ? 0 : (int) ((totalElements + pageSize - 1) / pageSize);
|
||||||
|
return new DocumentSearchResult(slice, totalElements, pageable.getPageNumber(), pageSize, totalPages);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test;
|
|||||||
import org.raddatz.familienarchiv.audit.ActivityActorDTO;
|
import org.raddatz.familienarchiv.audit.ActivityActorDTO;
|
||||||
import org.raddatz.familienarchiv.model.Document;
|
import org.raddatz.familienarchiv.model.Document;
|
||||||
import org.raddatz.familienarchiv.model.DocumentStatus;
|
import org.raddatz.familienarchiv.model.DocumentStatus;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -24,10 +25,43 @@ class DocumentSearchResultTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void of_total_equals_list_size() {
|
void of_totalElements_equals_list_size_for_unpaged_shortcut() {
|
||||||
DocumentSearchResult result = DocumentSearchResult.of(List.of(item(UUID.randomUUID()), item(UUID.randomUUID())));
|
DocumentSearchResult result = DocumentSearchResult.of(
|
||||||
|
List.of(item(UUID.randomUUID()), item(UUID.randomUUID())));
|
||||||
|
|
||||||
assertThat(result.total()).isEqualTo(2L);
|
assertThat(result.totalElements()).isEqualTo(2L);
|
||||||
|
assertThat(result.pageNumber()).isZero();
|
||||||
|
assertThat(result.pageSize()).isEqualTo(2);
|
||||||
|
assertThat(result.totalPages()).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void of_empty_shortcut_has_zero_totalPages() {
|
||||||
|
DocumentSearchResult result = DocumentSearchResult.of(List.of());
|
||||||
|
|
||||||
|
assertThat(result.totalElements()).isZero();
|
||||||
|
assertThat(result.totalPages()).isZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void paged_factory_populates_paging_fields_from_pageable_and_total() {
|
||||||
|
List<DocumentSearchItem> slice = List.of(item(UUID.randomUUID()), item(UUID.randomUUID()));
|
||||||
|
|
||||||
|
DocumentSearchResult result = DocumentSearchResult.paged(slice, PageRequest.of(1, 50), 120L);
|
||||||
|
|
||||||
|
assertThat(result.items()).hasSize(2);
|
||||||
|
assertThat(result.totalElements()).isEqualTo(120L);
|
||||||
|
assertThat(result.pageNumber()).isEqualTo(1);
|
||||||
|
assertThat(result.pageSize()).isEqualTo(50);
|
||||||
|
assertThat(result.totalPages()).isEqualTo(3); // ceil(120 / 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void paged_factory_totalPages_rounds_up_on_remainder() {
|
||||||
|
DocumentSearchResult result =
|
||||||
|
DocumentSearchResult.paged(List.of(), PageRequest.of(0, 7), 30L);
|
||||||
|
|
||||||
|
assertThat(result.totalPages()).isEqualTo(5); // ceil(30 / 7)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -53,9 +87,18 @@ class DocumentSearchResultTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void total_component_is_annotated_as_required_in_openapi_schema() throws NoSuchFieldException {
|
void totalElements_component_is_annotated_as_required_in_openapi_schema() throws NoSuchFieldException {
|
||||||
Schema schema = DocumentSearchResult.class.getDeclaredField("total").getAnnotation(Schema.class);
|
Schema schema = DocumentSearchResult.class.getDeclaredField("totalElements").getAnnotation(Schema.class);
|
||||||
assertThat(schema).isNotNull();
|
assertThat(schema).isNotNull();
|
||||||
assertThat(schema.requiredMode()).isEqualTo(Schema.RequiredMode.REQUIRED);
|
assertThat(schema.requiredMode()).isEqualTo(Schema.RequiredMode.REQUIRED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void paging_components_are_annotated_as_required_in_openapi_schema() throws NoSuchFieldException {
|
||||||
|
for (String name : List.of("pageNumber", "pageSize", "totalPages")) {
|
||||||
|
Schema schema = DocumentSearchResult.class.getDeclaredField(name).getAnnotation(Schema.class);
|
||||||
|
assertThat(schema).as(name + " must have @Schema").isNotNull();
|
||||||
|
assertThat(schema.requiredMode()).isEqualTo(Schema.RequiredMode.REQUIRED);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user