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:
Marcel
2026-04-24 08:05:05 +02:00
committed by marcel
parent 8f28a99e00
commit 1299bd5938
2 changed files with 72 additions and 7 deletions

View File

@@ -1,6 +1,7 @@
package org.raddatz.familienarchiv.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.data.domain.Pageable;
import java.util.List;
@@ -8,9 +9,30 @@ public record DocumentSearchResult(
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
List<DocumentSearchItem> items,
@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) {
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);
}
}

View File

@@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test;
import org.raddatz.familienarchiv.audit.ActivityActorDTO;
import org.raddatz.familienarchiv.model.Document;
import org.raddatz.familienarchiv.model.DocumentStatus;
import org.springframework.data.domain.PageRequest;
import java.util.List;
import java.util.UUID;
@@ -24,10 +25,43 @@ class DocumentSearchResultTest {
}
@Test
void of_total_equals_list_size() {
DocumentSearchResult result = DocumentSearchResult.of(List.of(item(UUID.randomUUID()), item(UUID.randomUUID())));
void of_totalElements_equals_list_size_for_unpaged_shortcut() {
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
@@ -53,9 +87,18 @@ class DocumentSearchResultTest {
}
@Test
void total_component_is_annotated_as_required_in_openapi_schema() throws NoSuchFieldException {
Schema schema = DocumentSearchResult.class.getDeclaredField("total").getAnnotation(Schema.class);
void totalElements_component_is_annotated_as_required_in_openapi_schema() throws NoSuchFieldException {
Schema schema = DocumentSearchResult.class.getDeclaredField("totalElements").getAnnotation(Schema.class);
assertThat(schema).isNotNull();
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);
}
}
}