refactor(documents): bundle density filters into a record (#385)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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<String> tags,
|
||||||
|
String tagQ,
|
||||||
|
DocumentStatus status,
|
||||||
|
TagOperator tagOperator) {}
|
||||||
@@ -401,7 +401,7 @@ public class DocumentController {
|
|||||||
@Parameter(description = "Tag operator: AND (default) or OR") @RequestParam(required = false) String tagOp) {
|
@Parameter(description = "Tag operator: AND (default) or OR") @RequestParam(required = false) String tagOp) {
|
||||||
TagOperator operator = "OR".equalsIgnoreCase(tagOp) ? TagOperator.OR : TagOperator.AND;
|
TagOperator operator = "OR".equalsIgnoreCase(tagOp) ? TagOperator.OR : TagOperator.AND;
|
||||||
DocumentDensityResult result = documentService.getDensity(
|
DocumentDensityResult result = documentService.getDensity(
|
||||||
q, senderId, receiverId, tags, tagQ, status, operator);
|
new DensityFilters(q, senderId, receiverId, tags, tagQ, status, operator));
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
.cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES).cachePrivate())
|
.cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES).cachePrivate())
|
||||||
.body(result);
|
.body(result);
|
||||||
|
|||||||
@@ -146,10 +146,8 @@ public class DocumentService {
|
|||||||
* 'YYYY-MM')) and accept that the criteria/specification surface needs a
|
* 'YYYY-MM')) and accept that the criteria/specification surface needs a
|
||||||
* parallel native-query path.
|
* parallel native-query path.
|
||||||
*/
|
*/
|
||||||
public DocumentDensityResult getDensity(
|
public DocumentDensityResult getDensity(DensityFilters filters) {
|
||||||
String text, UUID sender, UUID receiver,
|
String text = filters.text();
|
||||||
List<String> tags, String tagQ,
|
|
||||||
DocumentStatus status, TagOperator tagOperator) {
|
|
||||||
boolean hasText = StringUtils.hasText(text);
|
boolean hasText = StringUtils.hasText(text);
|
||||||
List<UUID> rankedIds = null;
|
List<UUID> rankedIds = null;
|
||||||
if (hasText) {
|
if (hasText) {
|
||||||
@@ -160,7 +158,9 @@ public class DocumentService {
|
|||||||
}
|
}
|
||||||
Specification<Document> spec = buildSearchSpec(
|
Specification<Document> spec = buildSearchSpec(
|
||||||
hasText, rankedIds, null, null,
|
hasText, rankedIds, null, null,
|
||||||
sender, receiver, tags, tagQ, status, tagOperator);
|
filters.sender(), filters.receiver(),
|
||||||
|
filters.tags(), filters.tagQ(),
|
||||||
|
filters.status(), filters.tagOperator());
|
||||||
|
|
||||||
List<LocalDate> dates = documentRepository.findAll(spec).stream()
|
List<LocalDate> dates = documentRepository.findAll(spec).stream()
|
||||||
.map(Document::getDocumentDate)
|
.map(Document::getDocumentDate)
|
||||||
|
|||||||
@@ -1253,7 +1253,7 @@ class DocumentControllerTest {
|
|||||||
@Test
|
@Test
|
||||||
@WithMockUser
|
@WithMockUser
|
||||||
void density_returns200_withResultBody_whenAuthenticated() throws Exception {
|
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(
|
new DocumentDensityResult(
|
||||||
List.of(new MonthBucket("1915-08", 2), new MonthBucket("1915-09", 1)),
|
List.of(new MonthBucket("1915-08", 2), new MonthBucket("1915-09", 1)),
|
||||||
java.time.LocalDate.of(1915, 8, 3),
|
java.time.LocalDate.of(1915, 8, 3),
|
||||||
@@ -1278,7 +1278,7 @@ class DocumentControllerTest {
|
|||||||
@Test
|
@Test
|
||||||
@WithMockUser
|
@WithMockUser
|
||||||
void density_declaresApplicationJsonContentType() throws Exception {
|
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));
|
new DocumentDensityResult(List.of(), null, null));
|
||||||
|
|
||||||
mockMvc.perform(get("/api/documents/density")
|
mockMvc.perform(get("/api/documents/density")
|
||||||
@@ -1289,7 +1289,7 @@ class DocumentControllerTest {
|
|||||||
@Test
|
@Test
|
||||||
@WithMockUser
|
@WithMockUser
|
||||||
void density_emitsPrivateCacheControlHeader() throws Exception {
|
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));
|
new DocumentDensityResult(List.of(), null, null));
|
||||||
|
|
||||||
mockMvc.perform(get("/api/documents/density"))
|
mockMvc.perform(get("/api/documents/density"))
|
||||||
@@ -1303,7 +1303,7 @@ class DocumentControllerTest {
|
|||||||
@Test
|
@Test
|
||||||
@WithMockUser
|
@WithMockUser
|
||||||
void density_forwardsSenderAndTagFilters() throws Exception {
|
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));
|
new DocumentDensityResult(List.of(), null, null));
|
||||||
UUID senderId = UUID.randomUUID();
|
UUID senderId = UUID.randomUUID();
|
||||||
|
|
||||||
@@ -1314,20 +1314,17 @@ class DocumentControllerTest {
|
|||||||
.param("tagOp", "OR"))
|
.param("tagOp", "OR"))
|
||||||
.andExpect(status().isOk());
|
.andExpect(status().isOk());
|
||||||
|
|
||||||
verify(documentService).getDensity(
|
verify(documentService).getDensity(eq(new DensityFilters(
|
||||||
org.mockito.ArgumentMatchers.isNull(),
|
null, senderId, null,
|
||||||
eq(senderId),
|
List.of("Familie", "Urlaub"),
|
||||||
org.mockito.ArgumentMatchers.isNull(),
|
null, null,
|
||||||
eq(List.of("Familie", "Urlaub")),
|
org.raddatz.familienarchiv.tag.TagOperator.OR)));
|
||||||
org.mockito.ArgumentMatchers.isNull(),
|
|
||||||
org.mockito.ArgumentMatchers.isNull(),
|
|
||||||
eq(org.raddatz.familienarchiv.tag.TagOperator.OR));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithMockUser
|
@WithMockUser
|
||||||
void density_forwardsStatusAndQueryText() throws Exception {
|
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));
|
new DocumentDensityResult(List.of(), null, null));
|
||||||
|
|
||||||
mockMvc.perform(get("/api/documents/density")
|
mockMvc.perform(get("/api/documents/density")
|
||||||
@@ -1335,13 +1332,9 @@ class DocumentControllerTest {
|
|||||||
.param("status", "REVIEWED"))
|
.param("status", "REVIEWED"))
|
||||||
.andExpect(status().isOk());
|
.andExpect(status().isOk());
|
||||||
|
|
||||||
verify(documentService).getDensity(
|
verify(documentService).getDensity(eq(new DensityFilters(
|
||||||
eq("Brief"),
|
"Brief", null, null, null, null,
|
||||||
org.mockito.ArgumentMatchers.isNull(),
|
DocumentStatus.REVIEWED,
|
||||||
org.mockito.ArgumentMatchers.isNull(),
|
org.raddatz.familienarchiv.tag.TagOperator.AND)));
|
||||||
org.mockito.ArgumentMatchers.isNull(),
|
|
||||||
org.mockito.ArgumentMatchers.isNull(),
|
|
||||||
eq(DocumentStatus.REVIEWED),
|
|
||||||
eq(org.raddatz.familienarchiv.tag.TagOperator.AND));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,13 +56,17 @@ class DocumentDensityIntegrationTest {
|
|||||||
urlaubTag = tagRepository.save(Tag.builder().name("Urlaub").build());
|
urlaubTag = tagRepository.save(Tag.builder().name("Urlaub").build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static DensityFilters noFilters() {
|
||||||
|
return new DensityFilters(null, null, null, null, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getDensity_returnsAllMonths_whenNoFiltersApplied() {
|
void getDensity_returnsAllMonths_whenNoFiltersApplied() {
|
||||||
save("a", LocalDate.of(1915, 8, 3), null, Set.of());
|
save("a", LocalDate.of(1915, 8, 3), null, Set.of());
|
||||||
save("b", LocalDate.of(1915, 8, 17), null, Set.of());
|
save("b", LocalDate.of(1915, 8, 17), null, Set.of());
|
||||||
save("c", LocalDate.of(1915, 9, 1), 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)
|
assertThat(result.buckets()).extracting(MonthBucket::month)
|
||||||
.containsExactly("1915-08", "1915-09");
|
.containsExactly("1915-08", "1915-09");
|
||||||
@@ -78,7 +82,7 @@ class DocumentDensityIntegrationTest {
|
|||||||
save("c", LocalDate.of(1920, 5, 1), anna, Set.of());
|
save("c", LocalDate.of(1920, 5, 1), anna, Set.of());
|
||||||
|
|
||||||
DocumentDensityResult result = documentService.getDensity(
|
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)
|
assertThat(result.buckets()).extracting(MonthBucket::month)
|
||||||
.containsExactly("1915-08", "1916-01");
|
.containsExactly("1915-08", "1916-01");
|
||||||
@@ -91,7 +95,7 @@ class DocumentDensityIntegrationTest {
|
|||||||
save("b", LocalDate.of(1920, 5, 1), null, Set.of(urlaubTag));
|
save("b", LocalDate.of(1920, 5, 1), null, Set.of(urlaubTag));
|
||||||
|
|
||||||
DocumentDensityResult result = documentService.getDensity(
|
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");
|
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));
|
save("c", LocalDate.of(1920, 5, 1), anna, Set.of(familieTag));
|
||||||
|
|
||||||
DocumentDensityResult result = documentService.getDensity(
|
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");
|
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);
|
save("b", LocalDate.of(1916, 1, 4), null, Set.of(), DocumentStatus.PLACEHOLDER);
|
||||||
|
|
||||||
DocumentDensityResult result = documentService.getDensity(
|
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");
|
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());
|
save("a", LocalDate.of(1915, 8, 3), hans, Set.of());
|
||||||
|
|
||||||
DocumentDensityResult result = documentService.getDensity(
|
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.buckets()).isEmpty();
|
||||||
assertThat(result.minDate()).isNull();
|
assertThat(result.minDate()).isNull();
|
||||||
@@ -136,7 +140,7 @@ class DocumentDensityIntegrationTest {
|
|||||||
save("dated", LocalDate.of(1915, 8, 3), null, Set.of());
|
save("dated", LocalDate.of(1915, 8, 3), null, Set.of());
|
||||||
save("undated", null, 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);
|
assertThat(result.buckets()).extracting(MonthBucket::count).containsExactly(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2325,11 +2325,15 @@ class DocumentServiceTest {
|
|||||||
|
|
||||||
// ─── getDensity ────────────────────────────────────────────────────────────
|
// ─── getDensity ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static DensityFilters anyFilters() {
|
||||||
|
return new DensityFilters(null, null, null, null, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getDensity_returnsEmptyResult_whenNoDocumentsMatch() {
|
void getDensity_returnsEmptyResult_whenNoDocumentsMatch() {
|
||||||
when(documentRepository.findAll(any(Specification.class))).thenReturn(List.of());
|
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.buckets()).isEmpty();
|
||||||
assertThat(result.minDate()).isNull();
|
assertThat(result.minDate()).isNull();
|
||||||
@@ -2344,7 +2348,7 @@ class DocumentServiceTest {
|
|||||||
when(documentRepository.findAll(any(Specification.class))).thenReturn(List.of(a, b, c));
|
when(documentRepository.findAll(any(Specification.class))).thenReturn(List.of(a, b, c));
|
||||||
when(tagService.expandTagNamesToDescendantIdSets(any())).thenReturn(List.of());
|
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)
|
assertThat(result.buckets()).extracting(MonthBucket::month)
|
||||||
.containsExactly("1915-08", "1915-09");
|
.containsExactly("1915-08", "1915-09");
|
||||||
@@ -2360,7 +2364,7 @@ class DocumentServiceTest {
|
|||||||
when(documentRepository.findAll(any(Specification.class))).thenReturn(List.of(dated, undated));
|
when(documentRepository.findAll(any(Specification.class))).thenReturn(List.of(dated, undated));
|
||||||
when(tagService.expandTagNamesToDescendantIdSets(any())).thenReturn(List.of());
|
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);
|
assertThat(result.buckets()).extracting(MonthBucket::count).containsExactly(1);
|
||||||
}
|
}
|
||||||
@@ -2369,7 +2373,8 @@ class DocumentServiceTest {
|
|||||||
void getDensity_shortCircuits_whenFtsReturnsNoMatches() {
|
void getDensity_shortCircuits_whenFtsReturnsNoMatches() {
|
||||||
when(documentRepository.findRankedIdsByFts("xyz")).thenReturn(List.of());
|
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();
|
assertThat(result.buckets()).isEmpty();
|
||||||
verify(documentRepository, org.mockito.Mockito.never()).findAll(any(Specification.class));
|
verify(documentRepository, org.mockito.Mockito.never()).findAll(any(Specification.class));
|
||||||
|
|||||||
Reference in New Issue
Block a user