From 86de118d6332fc04cfa66b980dee2c281c3a65a3 Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 8 May 2026 11:42:38 +0200 Subject: [PATCH] refactor(documents): bundle density filters into a record (#385) Co-Authored-By: Claude Sonnet 4.6 --- .../document/DensityFilters.java | 23 ++++++++++++ .../document/DocumentController.java | 2 +- .../document/DocumentService.java | 10 +++--- .../document/DocumentControllerTest.java | 35 ++++++++----------- .../DocumentDensityIntegrationTest.java | 18 ++++++---- .../document/DocumentServiceTest.java | 13 ++++--- 6 files changed, 63 insertions(+), 38 deletions(-) create mode 100644 backend/src/main/java/org/raddatz/familienarchiv/document/DensityFilters.java diff --git a/backend/src/main/java/org/raddatz/familienarchiv/document/DensityFilters.java b/backend/src/main/java/org/raddatz/familienarchiv/document/DensityFilters.java new file mode 100644 index 00000000..aa3e7f80 --- /dev/null +++ b/backend/src/main/java/org/raddatz/familienarchiv/document/DensityFilters.java @@ -0,0 +1,23 @@ +package org.raddatz.familienarchiv.document; + +import org.raddatz.familienarchiv.tag.TagOperator; + +import java.util.List; +import java.util.UUID; + +/** + * The non-date filters honoured by {@link DocumentService#getDensity(DensityFilters)}. + * Date bounds (from/to) are deliberately excluded — see the service Javadoc for why. + * + * Kept as a record so the seven values are passed as one named bundle instead of a + * positional argument list where two UUIDs (sender vs. receiver) can be swapped by + * accident at the call site. + */ +public record DensityFilters( + String text, + UUID sender, + UUID receiver, + List tags, + String tagQ, + DocumentStatus status, + TagOperator tagOperator) {} diff --git a/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentController.java b/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentController.java index 53b50f03..f4bf72d3 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentController.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentController.java @@ -401,7 +401,7 @@ public class DocumentController { @Parameter(description = "Tag operator: AND (default) or OR") @RequestParam(required = false) String tagOp) { TagOperator operator = "OR".equalsIgnoreCase(tagOp) ? TagOperator.OR : TagOperator.AND; DocumentDensityResult result = documentService.getDensity( - q, senderId, receiverId, tags, tagQ, status, operator); + new DensityFilters(q, senderId, receiverId, tags, tagQ, status, operator)); return ResponseEntity.ok() .cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES).cachePrivate()) .body(result); diff --git a/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentService.java b/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentService.java index 2446a0ab..3f0d5ece 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentService.java @@ -146,10 +146,8 @@ public class DocumentService { * 'YYYY-MM')) and accept that the criteria/specification surface needs a * parallel native-query path. */ - public DocumentDensityResult getDensity( - String text, UUID sender, UUID receiver, - List tags, String tagQ, - DocumentStatus status, TagOperator tagOperator) { + public DocumentDensityResult getDensity(DensityFilters filters) { + String text = filters.text(); boolean hasText = StringUtils.hasText(text); List rankedIds = null; if (hasText) { @@ -160,7 +158,9 @@ public class DocumentService { } Specification spec = buildSearchSpec( hasText, rankedIds, null, null, - sender, receiver, tags, tagQ, status, tagOperator); + filters.sender(), filters.receiver(), + filters.tags(), filters.tagQ(), + filters.status(), filters.tagOperator()); List dates = documentRepository.findAll(spec).stream() .map(Document::getDocumentDate) diff --git a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentControllerTest.java index 82e60083..9d85491a 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentControllerTest.java @@ -1253,7 +1253,7 @@ class DocumentControllerTest { @Test @WithMockUser void density_returns200_withResultBody_whenAuthenticated() throws Exception { - when(documentService.getDensity(any(), any(), any(), any(), any(), any(), any())).thenReturn( + when(documentService.getDensity(any())).thenReturn( new DocumentDensityResult( List.of(new MonthBucket("1915-08", 2), new MonthBucket("1915-09", 1)), java.time.LocalDate.of(1915, 8, 3), @@ -1278,7 +1278,7 @@ class DocumentControllerTest { @Test @WithMockUser void density_declaresApplicationJsonContentType() throws Exception { - when(documentService.getDensity(any(), any(), any(), any(), any(), any(), any())).thenReturn( + when(documentService.getDensity(any())).thenReturn( new DocumentDensityResult(List.of(), null, null)); mockMvc.perform(get("/api/documents/density") @@ -1289,7 +1289,7 @@ class DocumentControllerTest { @Test @WithMockUser void density_emitsPrivateCacheControlHeader() throws Exception { - when(documentService.getDensity(any(), any(), any(), any(), any(), any(), any())).thenReturn( + when(documentService.getDensity(any())).thenReturn( new DocumentDensityResult(List.of(), null, null)); mockMvc.perform(get("/api/documents/density")) @@ -1303,7 +1303,7 @@ class DocumentControllerTest { @Test @WithMockUser void density_forwardsSenderAndTagFilters() throws Exception { - when(documentService.getDensity(any(), any(), any(), any(), any(), any(), any())).thenReturn( + when(documentService.getDensity(any())).thenReturn( new DocumentDensityResult(List.of(), null, null)); UUID senderId = UUID.randomUUID(); @@ -1314,20 +1314,17 @@ class DocumentControllerTest { .param("tagOp", "OR")) .andExpect(status().isOk()); - verify(documentService).getDensity( - org.mockito.ArgumentMatchers.isNull(), - eq(senderId), - org.mockito.ArgumentMatchers.isNull(), - eq(List.of("Familie", "Urlaub")), - org.mockito.ArgumentMatchers.isNull(), - org.mockito.ArgumentMatchers.isNull(), - eq(org.raddatz.familienarchiv.tag.TagOperator.OR)); + verify(documentService).getDensity(eq(new DensityFilters( + null, senderId, null, + List.of("Familie", "Urlaub"), + null, null, + org.raddatz.familienarchiv.tag.TagOperator.OR))); } @Test @WithMockUser void density_forwardsStatusAndQueryText() throws Exception { - when(documentService.getDensity(any(), any(), any(), any(), any(), any(), any())).thenReturn( + when(documentService.getDensity(any())).thenReturn( new DocumentDensityResult(List.of(), null, null)); mockMvc.perform(get("/api/documents/density") @@ -1335,13 +1332,9 @@ class DocumentControllerTest { .param("status", "REVIEWED")) .andExpect(status().isOk()); - verify(documentService).getDensity( - eq("Brief"), - org.mockito.ArgumentMatchers.isNull(), - org.mockito.ArgumentMatchers.isNull(), - org.mockito.ArgumentMatchers.isNull(), - org.mockito.ArgumentMatchers.isNull(), - eq(DocumentStatus.REVIEWED), - eq(org.raddatz.familienarchiv.tag.TagOperator.AND)); + verify(documentService).getDensity(eq(new DensityFilters( + "Brief", null, null, null, null, + DocumentStatus.REVIEWED, + org.raddatz.familienarchiv.tag.TagOperator.AND))); } } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentDensityIntegrationTest.java b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentDensityIntegrationTest.java index 649938e0..3110a503 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentDensityIntegrationTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentDensityIntegrationTest.java @@ -56,13 +56,17 @@ class DocumentDensityIntegrationTest { urlaubTag = tagRepository.save(Tag.builder().name("Urlaub").build()); } + private static DensityFilters noFilters() { + return new DensityFilters(null, null, null, null, null, null, null); + } + @Test void getDensity_returnsAllMonths_whenNoFiltersApplied() { save("a", LocalDate.of(1915, 8, 3), null, Set.of()); save("b", LocalDate.of(1915, 8, 17), null, Set.of()); save("c", LocalDate.of(1915, 9, 1), null, Set.of()); - DocumentDensityResult result = documentService.getDensity(null, null, null, null, null, null, null); + DocumentDensityResult result = documentService.getDensity(noFilters()); assertThat(result.buckets()).extracting(MonthBucket::month) .containsExactly("1915-08", "1915-09"); @@ -78,7 +82,7 @@ class DocumentDensityIntegrationTest { save("c", LocalDate.of(1920, 5, 1), anna, Set.of()); DocumentDensityResult result = documentService.getDensity( - null, hans.getId(), null, null, null, null, null); + new DensityFilters(null, hans.getId(), null, null, null, null, null)); assertThat(result.buckets()).extracting(MonthBucket::month) .containsExactly("1915-08", "1916-01"); @@ -91,7 +95,7 @@ class DocumentDensityIntegrationTest { save("b", LocalDate.of(1920, 5, 1), null, Set.of(urlaubTag)); DocumentDensityResult result = documentService.getDensity( - null, null, null, List.of("Familie"), null, null, TagOperator.AND); + new DensityFilters(null, null, null, List.of("Familie"), null, null, TagOperator.AND)); assertThat(result.buckets()).extracting(MonthBucket::month).containsExactly("1915-08"); } @@ -103,7 +107,7 @@ class DocumentDensityIntegrationTest { save("c", LocalDate.of(1920, 5, 1), anna, Set.of(familieTag)); DocumentDensityResult result = documentService.getDensity( - null, hans.getId(), null, List.of("Familie"), null, null, TagOperator.AND); + new DensityFilters(null, hans.getId(), null, List.of("Familie"), null, null, TagOperator.AND)); assertThat(result.buckets()).extracting(MonthBucket::month).containsExactly("1915-08"); } @@ -114,7 +118,7 @@ class DocumentDensityIntegrationTest { save("b", LocalDate.of(1916, 1, 4), null, Set.of(), DocumentStatus.PLACEHOLDER); DocumentDensityResult result = documentService.getDensity( - null, null, null, null, null, DocumentStatus.UPLOADED, null); + new DensityFilters(null, null, null, null, null, DocumentStatus.UPLOADED, null)); assertThat(result.buckets()).extracting(MonthBucket::month).containsExactly("1915-08"); } @@ -124,7 +128,7 @@ class DocumentDensityIntegrationTest { save("a", LocalDate.of(1915, 8, 3), hans, Set.of()); DocumentDensityResult result = documentService.getDensity( - null, anna.getId(), null, null, null, null, null); + new DensityFilters(null, anna.getId(), null, null, null, null, null)); assertThat(result.buckets()).isEmpty(); assertThat(result.minDate()).isNull(); @@ -136,7 +140,7 @@ class DocumentDensityIntegrationTest { save("dated", LocalDate.of(1915, 8, 3), null, Set.of()); save("undated", null, null, Set.of()); - DocumentDensityResult result = documentService.getDensity(null, null, null, null, null, null, null); + DocumentDensityResult result = documentService.getDensity(noFilters()); assertThat(result.buckets()).extracting(MonthBucket::count).containsExactly(1); } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentServiceTest.java b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentServiceTest.java index cec66dfc..2637a9e7 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentServiceTest.java @@ -2325,11 +2325,15 @@ class DocumentServiceTest { // ─── getDensity ──────────────────────────────────────────────────────────── + private static DensityFilters anyFilters() { + return new DensityFilters(null, null, null, null, null, null, null); + } + @Test void getDensity_returnsEmptyResult_whenNoDocumentsMatch() { when(documentRepository.findAll(any(Specification.class))).thenReturn(List.of()); - DocumentDensityResult result = documentService.getDensity(null, null, null, null, null, null, null); + DocumentDensityResult result = documentService.getDensity(anyFilters()); assertThat(result.buckets()).isEmpty(); assertThat(result.minDate()).isNull(); @@ -2344,7 +2348,7 @@ class DocumentServiceTest { when(documentRepository.findAll(any(Specification.class))).thenReturn(List.of(a, b, c)); when(tagService.expandTagNamesToDescendantIdSets(any())).thenReturn(List.of()); - DocumentDensityResult result = documentService.getDensity(null, null, null, null, null, null, null); + DocumentDensityResult result = documentService.getDensity(anyFilters()); assertThat(result.buckets()).extracting(MonthBucket::month) .containsExactly("1915-08", "1915-09"); @@ -2360,7 +2364,7 @@ class DocumentServiceTest { when(documentRepository.findAll(any(Specification.class))).thenReturn(List.of(dated, undated)); when(tagService.expandTagNamesToDescendantIdSets(any())).thenReturn(List.of()); - DocumentDensityResult result = documentService.getDensity(null, null, null, null, null, null, null); + DocumentDensityResult result = documentService.getDensity(anyFilters()); assertThat(result.buckets()).extracting(MonthBucket::count).containsExactly(1); } @@ -2369,7 +2373,8 @@ class DocumentServiceTest { void getDensity_shortCircuits_whenFtsReturnsNoMatches() { when(documentRepository.findRankedIdsByFts("xyz")).thenReturn(List.of()); - DocumentDensityResult result = documentService.getDensity("xyz", null, null, null, null, null, null); + DocumentDensityResult result = documentService.getDensity( + new DensityFilters("xyz", null, null, null, null, null, null)); assertThat(result.buckets()).isEmpty(); verify(documentRepository, org.mockito.Mockito.never()).findAll(any(Specification.class));