diff --git a/backend/src/main/resources/db/migration/V51__backfill_block_comment_annotation_id.sql b/backend/src/main/resources/db/migration/V51__backfill_block_comment_annotation_id.sql new file mode 100644 index 00000000..f136adb7 --- /dev/null +++ b/backend/src/main/resources/db/migration/V51__backfill_block_comment_annotation_id.sql @@ -0,0 +1,24 @@ +-- Backfill annotation_id on block comments and their notifications. +-- +-- Before the upstream fix, CommentService.postBlockComment did not set +-- DocumentComment.annotationId, so block comments were stored with +-- annotation_id = NULL and every notification built from them inherited +-- that NULL (see NotificationService.notifyMentions/notifyReply). +-- +-- The frontend deep-link flow needs annotationId in the URL query string +-- to open the correct annotation panel and scroll to the comment. +-- Without this backfill, previously issued notifications would still +-- carry annotation_id = NULL even after the code fix lands. + +UPDATE document_comments dc +SET annotation_id = tb.annotation_id +FROM transcription_blocks tb +WHERE dc.block_id = tb.id + AND dc.annotation_id IS NULL; + +UPDATE notifications n +SET annotation_id = dc.annotation_id +FROM document_comments dc +WHERE n.reference_id = dc.id + AND n.annotation_id IS NULL + AND dc.annotation_id IS NOT NULL; diff --git a/backend/src/test/java/org/raddatz/familienarchiv/repository/MigrationIntegrationTest.java b/backend/src/test/java/org/raddatz/familienarchiv/repository/MigrationIntegrationTest.java index 7709b486..b482a0be 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/repository/MigrationIntegrationTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/repository/MigrationIntegrationTest.java @@ -302,6 +302,57 @@ class MigrationIntegrationTest { ).isInstanceOf(DataIntegrityViolationException.class); } + // ─── V51: backfill annotation_id on block comments and notifications ───── + + @Test + void v51_backfillsAnnotationIdOnBlockCommentsFromTheirBlocks() { + UUID docId = createDocument(); + UUID annotationId = insertAnnotation(docId); + UUID blockId = insertBlock(docId, annotationId); + UUID commentId = insertBlockCommentWithNullAnnotationId(docId, blockId); + + jdbc.update(V51_BACKFILL_COMMENTS_SQL); + + UUID stored = jdbc.queryForObject( + "SELECT annotation_id FROM document_comments WHERE id = ?", + UUID.class, commentId); + assertThat(stored).isEqualTo(annotationId); + } + + @Test + void v51_backfillsAnnotationIdOnNotificationsFromTheirReferencedComment() { + UUID docId = createDocument(); + UUID userId = insertUser("recipient-" + UUID.randomUUID() + "@example.com"); + UUID annotationId = insertAnnotation(docId); + UUID blockId = insertBlock(docId, annotationId); + UUID commentId = insertBlockCommentWithAnnotationId(docId, blockId, annotationId); + UUID notificationId = insertNotificationWithNullAnnotationId(docId, commentId, userId); + + jdbc.update(V51_BACKFILL_NOTIFICATIONS_SQL); + + UUID stored = jdbc.queryForObject( + "SELECT annotation_id FROM notifications WHERE id = ?", + UUID.class, notificationId); + assertThat(stored).isEqualTo(annotationId); + } + + private static final String V51_BACKFILL_COMMENTS_SQL = """ + UPDATE document_comments dc + SET annotation_id = tb.annotation_id + FROM transcription_blocks tb + WHERE dc.block_id = tb.id + AND dc.annotation_id IS NULL + """; + + private static final String V51_BACKFILL_NOTIFICATIONS_SQL = """ + UPDATE notifications n + SET annotation_id = dc.annotation_id + FROM document_comments dc + WHERE n.reference_id = dc.id + AND n.annotation_id IS NULL + AND dc.annotation_id IS NOT NULL + """; + // ─── helpers ───────────────────────────────────────────────────────────── private UUID createPerson(String firstName, String lastName) { @@ -326,4 +377,63 @@ class MigrationIntegrationTest { em.flush(); return doc.getId(); } + + private UUID insertAnnotation(UUID docId) { + UUID id = UUID.randomUUID(); + jdbc.update(""" + INSERT INTO document_annotations + (id, document_id, page_number, x, y, width, height, color) + VALUES (?, ?, 1, 0.1, 0.1, 0.3, 0.1, '#00C7B1') + """, id, docId); + return id; + } + + private UUID insertBlock(UUID docId, UUID annotationId) { + UUID id = UUID.randomUUID(); + jdbc.update(""" + INSERT INTO transcription_blocks + (id, annotation_id, document_id, text, sort_order) + VALUES (?, ?, ?, '', 0) + """, id, annotationId, docId); + return id; + } + + private UUID insertUser(String email) { + UUID id = UUID.randomUUID(); + jdbc.update(""" + INSERT INTO users (id, email, password, enabled, notify_on_reply, notify_on_mention) + VALUES (?, ?, 'hash', true, false, false) + """, id, email); + return id; + } + + private UUID insertBlockCommentWithNullAnnotationId(UUID docId, UUID blockId) { + UUID id = UUID.randomUUID(); + jdbc.update(""" + INSERT INTO document_comments + (id, document_id, block_id, annotation_id, author_name, content) + VALUES (?, ?, ?, NULL, 'Tester', 'Hi') + """, id, docId, blockId); + return id; + } + + private UUID insertBlockCommentWithAnnotationId(UUID docId, UUID blockId, UUID annotationId) { + UUID id = UUID.randomUUID(); + jdbc.update(""" + INSERT INTO document_comments + (id, document_id, block_id, annotation_id, author_name, content) + VALUES (?, ?, ?, ?, 'Tester', 'Hi') + """, id, docId, blockId, annotationId); + return id; + } + + private UUID insertNotificationWithNullAnnotationId(UUID docId, UUID commentId, UUID recipientId) { + UUID id = UUID.randomUUID(); + jdbc.update(""" + INSERT INTO notifications + (id, recipient_id, type, document_id, reference_id, annotation_id, read, actor_name) + VALUES (?, ?, 'MENTION', ?, ?, NULL, false, 'Tester') + """, id, recipientId, docId, commentId); + return id; + } }