From fe7a8ed9adad08008fe9640f6f27144de8a75450 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 21 Apr 2026 20:59:56 +0200 Subject: [PATCH] feat(audit): add kinds param to findRolledUpActivityFeed Filter is applied at the innermost events CTE to reduce rows entering the LAG/session CTEs. Existing callers pass ROLLUP_ELIGIBLE by default so behaviour is unchanged. Co-Authored-By: Claude Sonnet 4.6 --- .../audit/AuditLogQueryRepository.java | 7 +- .../audit/AuditLogQueryService.java | 3 +- ...uditLogQueryRepositoryIntegrationTest.java | 3 +- .../AuditLogQueryRepositoryRolledUpTest.java | 116 +++++++++++++++--- 4 files changed, 109 insertions(+), 20 deletions(-) diff --git a/backend/src/main/java/org/raddatz/familienarchiv/audit/AuditLogQueryRepository.java b/backend/src/main/java/org/raddatz/familienarchiv/audit/AuditLogQueryRepository.java index 9c0957b8..c502a0db 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/audit/AuditLogQueryRepository.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/audit/AuditLogQueryRepository.java @@ -5,6 +5,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.time.OffsetDateTime; +import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -35,8 +36,7 @@ public interface AuditLogQueryRepository extends JpaRepository { ORDER BY a.happened_at ) AS prev_happened_at FROM audit_log a - WHERE a.kind IN ('TEXT_SAVED','FILE_UPLOADED','ANNOTATION_CREATED', - 'BLOCK_REVIEWED','COMMENT_ADDED','MENTION_CREATED') + WHERE a.kind IN (:kinds) AND a.document_id IS NOT NULL ), sessions_marked AS ( @@ -108,7 +108,8 @@ public interface AuditLogQueryRepository extends JpaRepository { """, nativeQuery = true) List findRolledUpActivityFeed( @Param("currentUserId") String currentUserId, - @Param("limit") int limit); + @Param("limit") int limit, + @Param("kinds") Collection kinds); @Query(value = """ SELECT diff --git a/backend/src/main/java/org/raddatz/familienarchiv/audit/AuditLogQueryService.java b/backend/src/main/java/org/raddatz/familienarchiv/audit/AuditLogQueryService.java index 930ef1a4..d026c9fc 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/audit/AuditLogQueryService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/audit/AuditLogQueryService.java @@ -17,7 +17,8 @@ public class AuditLogQueryService { } public List findActivityFeed(UUID currentUserId, int limit) { - return queryRepository.findRolledUpActivityFeed(currentUserId.toString(), limit); + return queryRepository.findRolledUpActivityFeed(currentUserId.toString(), limit, + AuditKind.ROLLUP_ELIGIBLE.stream().map(Enum::name).toList()); } public PulseStatsRow getPulseStats(OffsetDateTime weekStart, UUID userId) { diff --git a/backend/src/test/java/org/raddatz/familienarchiv/dashboard/AuditLogQueryRepositoryIntegrationTest.java b/backend/src/test/java/org/raddatz/familienarchiv/dashboard/AuditLogQueryRepositoryIntegrationTest.java index e73553c5..a3a3b3ff 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/dashboard/AuditLogQueryRepositoryIntegrationTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/dashboard/AuditLogQueryRepositoryIntegrationTest.java @@ -50,7 +50,8 @@ class AuditLogQueryRepositoryIntegrationTest { "INSERT INTO audit_log (kind, actor_id, document_id, payload) VALUES ('ANNOTATION_CREATED', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '{\"pageNumber\":1}')" }) void findRolledUpActivityFeed_returnsAnnotationEntry() { - List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 10); + List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 10, + List.of("TEXT_SAVED","FILE_UPLOADED","ANNOTATION_CREATED","BLOCK_REVIEWED","COMMENT_ADDED","MENTION_CREATED")); assertThat(rows).hasSize(1); assertThat(rows.get(0).getKind()).isEqualTo("ANNOTATION_CREATED"); diff --git a/backend/src/test/java/org/raddatz/familienarchiv/dashboard/AuditLogQueryRepositoryRolledUpTest.java b/backend/src/test/java/org/raddatz/familienarchiv/dashboard/AuditLogQueryRepositoryRolledUpTest.java index 02e66943..5e09c187 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/dashboard/AuditLogQueryRepositoryRolledUpTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/dashboard/AuditLogQueryRepositoryRolledUpTest.java @@ -36,6 +36,10 @@ class AuditLogQueryRepositoryRolledUpTest { static final UUID DOC_ID = UUID.fromString("eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee"); static final UUID OTHER_DOC_ID = UUID.fromString("ffffffff-ffff-ffff-ffff-ffffffffffff"); + static final List ALL_ELIGIBLE_KINDS = List.of( + "TEXT_SAVED", "FILE_UPLOADED", "ANNOTATION_CREATED", + "BLOCK_REVIEWED", "COMMENT_ADDED", "MENTION_CREATED"); + private static final ObjectMapper MAPPER = new ObjectMapper(); @Autowired AuditLogQueryRepository auditLogQueryRepository; @@ -97,7 +101,7 @@ class AuditLogQueryRepositoryRolledUpTest { insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED", base.plusSeconds(i * 480L)); } - List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40); + List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS); assertThat(rows).hasSize(1); ActivityFeedRow row = rows.get(0); @@ -119,7 +123,7 @@ class AuditLogQueryRepositoryRolledUpTest { insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED", sessionTwoStart); insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED", sessionTwoStart.plusSeconds(300)); - List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40); + List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS); assertThat(rows).hasSize(2); assertThat(rows.get(0).getCount()).isEqualTo(2); @@ -136,7 +140,7 @@ class AuditLogQueryRepositoryRolledUpTest { insertAuditEvent(USER_ID, DOC_ID, "ANNOTATION_CREATED", base.plusSeconds(i * 60L * 30L)); } - List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40); + List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS); assertThat(rows).hasSize(1); assertThat(rows.get(0).getCount()).isEqualTo(30); @@ -152,7 +156,7 @@ class AuditLogQueryRepositoryRolledUpTest { insertAuditEvent(USER_ID, DOC_ID, "COMMENT_ADDED", base.plusSeconds(60)); insertAuditEvent(USER_ID, DOC_ID, "COMMENT_ADDED", base.plusSeconds(120)); - List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40); + List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS); assertThat(rows).hasSize(3); assertThat(rows).allSatisfy(r -> { @@ -170,7 +174,7 @@ class AuditLogQueryRepositoryRolledUpTest { insertAuditEvent(USER_ID, DOC_ID, "METADATA_UPDATED", base.plusSeconds(60)); insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED", base.plusSeconds(120)); - List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40); + List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS); assertThat(rows).hasSize(1); assertThat(rows.get(0).getKind()).isEqualTo("TEXT_SAVED"); @@ -184,7 +188,7 @@ class AuditLogQueryRepositoryRolledUpTest { insertAuditEvent(USER_ID, DOC_ID, "FILE_UPLOADED", rollupStart.plusSeconds(300)); insertAuditEvent(USER_ID, OTHER_DOC_ID, "FILE_UPLOADED", rollupStart.plusSeconds(900)); - List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40); + List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS); assertThat(rows).hasSize(2); assertThat(rows).anySatisfy(r -> { @@ -209,7 +213,7 @@ class AuditLogQueryRepositoryRolledUpTest { Instant.parse("2026-04-20T10:00:00Z"), Map.of("commentId", commentId.toString())); insertReplyNotification(USER_ID, DOC_ID, commentId); - List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40); + List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS); assertThat(rows).anySatisfy(r -> assertThat(r.isYouParticipated()).isTrue() @@ -223,7 +227,7 @@ class AuditLogQueryRepositoryRolledUpTest { insertAuditEvent(OTHER_USER_ID, DOC_ID, "COMMENT_ADDED", Instant.parse("2026-04-20T10:00:00Z"), Map.of("commentId", commentId.toString())); - List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40); + List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS); assertThat(rows).allSatisfy(r -> assertThat(r.isYouParticipated()).isFalse() @@ -236,7 +240,7 @@ class AuditLogQueryRepositoryRolledUpTest { insertAuditEvent(OTHER_USER_ID, DOC_ID, "COMMENT_ADDED", Instant.parse("2026-04-20T10:00:00Z"), Map.of()); - List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40); + List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS); assertThat(rows).allSatisfy(r -> assertThat(r.isYouParticipated()).isFalse() @@ -251,7 +255,7 @@ class AuditLogQueryRepositoryRolledUpTest { Instant.parse("2026-04-20T10:00:00Z"), Map.of("commentId", commentId.toString())); insertReplyNotification(OTHER_USER_ID, DOC_ID, commentId); - List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40); + List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS); assertThat(rows).allSatisfy(r -> assertThat(r.isYouParticipated()).isFalse() @@ -264,7 +268,7 @@ class AuditLogQueryRepositoryRolledUpTest { insertAuditEvent(OTHER_USER_ID, DOC_ID, "MENTION_CREATED", Instant.parse("2026-04-20T10:00:00Z"), Map.of("mentionedUserId", USER_ID.toString())); - List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40); + List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS); assertThat(rows).anySatisfy(r -> assertThat(r.isYouMentioned()).isTrue() @@ -278,7 +282,7 @@ class AuditLogQueryRepositoryRolledUpTest { insertAuditEvent(USER_ID, DOC_ID, "COMMENT_ADDED", Instant.parse("2026-04-20T10:00:00Z"), Map.of("commentId", commentId.toString())); - List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40); + List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS); assertThat(rows).hasSize(1); assertThat(rows.get(0).getCommentId()).isEqualTo(commentId); @@ -292,7 +296,7 @@ class AuditLogQueryRepositoryRolledUpTest { Instant.parse("2026-04-20T10:00:00Z"), Map.of("commentId", commentId.toString(), "mentionedUserId", USER_ID.toString())); - List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40); + List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS); assertThat(rows).hasSize(1); assertThat(rows.get(0).getCommentId()).isEqualTo(commentId); @@ -304,7 +308,7 @@ class AuditLogQueryRepositoryRolledUpTest { insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED", Instant.parse("2026-04-20T10:00:00Z"), Map.of("blockId", "ccc", "pageNumber", "1")); - List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40); + List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS); assertThat(rows).hasSize(1); assertThat(rows.get(0).getCommentId()).isNull(); @@ -316,10 +320,92 @@ class AuditLogQueryRepositoryRolledUpTest { insertAuditEvent(USER_ID, DOC_ID, "MENTION_CREATED", Instant.parse("2026-04-20T10:00:00Z"), Map.of("mentionedUserId", OTHER_USER_ID.toString())); - List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40); + List rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS); assertThat(rows).allSatisfy(r -> assertThat(r.isYouMentioned()).isFalse() ); } + + // ─── kinds filter ───────────────────────────────────────────────────────── + + @Test + void rolledUpFeed_with_single_kind_returns_only_that_kind() { + insertUserAndDocs(); + Instant base = Instant.parse("2026-04-20T10:00:00Z"); + insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED", base); + insertAuditEvent(USER_ID, DOC_ID, "FILE_UPLOADED", base.plusSeconds(60)); + + List rows = auditLogQueryRepository.findRolledUpActivityFeed( + USER_ID.toString(), 40, List.of("FILE_UPLOADED")); + + assertThat(rows).hasSize(1); + assertThat(rows.get(0).getKind()).isEqualTo("FILE_UPLOADED"); + } + + @Test + void rolledUpFeed_with_multiple_kinds_returns_union() { + insertUserAndDocs(); + Instant base = Instant.parse("2026-04-20T10:00:00Z"); + insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED", base); + insertAuditEvent(USER_ID, OTHER_DOC_ID, "FILE_UPLOADED", base.plusSeconds(60)); + insertAuditEvent(USER_ID, DOC_ID, "ANNOTATION_CREATED", base.plusSeconds(120)); + + List rows = auditLogQueryRepository.findRolledUpActivityFeed( + USER_ID.toString(), 40, List.of("TEXT_SAVED", "FILE_UPLOADED")); + + assertThat(rows).hasSize(2); + assertThat(rows).extracting(ActivityFeedRow::getKind) + .containsExactlyInAnyOrder("TEXT_SAVED", "FILE_UPLOADED"); + } + + @Test + void rolledUpFeed_with_default_returns_all_six_eligible_kinds() { + insertUserAndDocs(); + Instant base = Instant.parse("2026-04-20T10:00:00Z"); + insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED", base); + insertAuditEvent(USER_ID, DOC_ID, "FILE_UPLOADED", base.plusSeconds(60)); + insertAuditEvent(USER_ID, DOC_ID, "ANNOTATION_CREATED", base.plusSeconds(120)); + insertAuditEvent(USER_ID, DOC_ID, "BLOCK_REVIEWED", base.plusSeconds(7300)); + insertAuditEvent(USER_ID, DOC_ID, "COMMENT_ADDED", base.plusSeconds(7360)); + insertAuditEvent(USER_ID, DOC_ID, "MENTION_CREATED", base.plusSeconds(7420)); + + List rows = auditLogQueryRepository.findRolledUpActivityFeed( + USER_ID.toString(), 40, + List.of("TEXT_SAVED", "FILE_UPLOADED", "ANNOTATION_CREATED", + "BLOCK_REVIEWED", "COMMENT_ADDED", "MENTION_CREATED")); + + assertThat(rows).hasSize(6); + } + + @Test + void rolledUpFeed_excludes_rows_not_in_filter_set() { + insertUserAndDocs(); + Instant base = Instant.parse("2026-04-20T10:00:00Z"); + insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED", base); + insertAuditEvent(USER_ID, OTHER_DOC_ID, "FILE_UPLOADED", base.plusSeconds(60)); + + List rows = auditLogQueryRepository.findRolledUpActivityFeed( + USER_ID.toString(), 40, List.of("TEXT_SAVED")); + + assertThat(rows).hasSize(1); + assertThat(rows.get(0).getKind()).isEqualTo("TEXT_SAVED"); + } + + @Test + void rolledUpFeed_rollup_still_works_when_kind_set_is_filtered_to_single_rollable_kind() { + insertUserAndDocs(); + Instant base = Instant.parse("2026-04-20T09:00:00Z"); + for (int i = 0; i < 10; i++) { + insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED", base.plusSeconds(i * 480L)); + } + insertAuditEvent(USER_ID, DOC_ID, "FILE_UPLOADED", base.plusSeconds(20)); + + List rows = auditLogQueryRepository.findRolledUpActivityFeed( + USER_ID.toString(), 40, List.of("TEXT_SAVED")); + + assertThat(rows).hasSize(1); + assertThat(rows.get(0).getKind()).isEqualTo("TEXT_SAVED"); + assertThat(rows.get(0).getCount()).isEqualTo(10); + } }