feat(comments): add DocumentComment entity, CommentRepository, and CommentService (green)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,10 @@ public enum ErrorCode {
|
|||||||
/** The new annotation overlaps an existing one on the same page. 409 */
|
/** The new annotation overlaps an existing one on the same page. 409 */
|
||||||
ANNOTATION_OVERLAP,
|
ANNOTATION_OVERLAP,
|
||||||
|
|
||||||
|
// --- Comments ---
|
||||||
|
/** The comment with the given ID does not exist. 404 */
|
||||||
|
COMMENT_NOT_FOUND,
|
||||||
|
|
||||||
// --- Generic ---
|
// --- Generic ---
|
||||||
/** Request validation failed (missing or malformed fields). 400 */
|
/** Request validation failed (missing or malformed fields). 400 */
|
||||||
VALIDATION_ERROR,
|
VALIDATION_ERROR,
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package org.raddatz.familienarchiv.model;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
import org.hibernate.annotations.UpdateTimestamp;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "document_comments")
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class DocumentComment {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private UUID id;
|
||||||
|
|
||||||
|
@Column(name = "document_id", nullable = false)
|
||||||
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private UUID documentId;
|
||||||
|
|
||||||
|
@Column(name = "annotation_id")
|
||||||
|
private UUID annotationId;
|
||||||
|
|
||||||
|
@Column(name = "parent_id")
|
||||||
|
private UUID parentId;
|
||||||
|
|
||||||
|
@Column(name = "author_id")
|
||||||
|
private UUID authorId;
|
||||||
|
|
||||||
|
@Column(name = "author_name", nullable = false)
|
||||||
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String authorName;
|
||||||
|
|
||||||
|
@Column(nullable = false, columnDefinition = "TEXT")
|
||||||
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
|
@CreationTimestamp
|
||||||
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "updated_at", nullable = false)
|
||||||
|
@UpdateTimestamp
|
||||||
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
// Populated by the service — not stored in the database
|
||||||
|
@Transient
|
||||||
|
@Builder.Default
|
||||||
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private List<DocumentComment> replies = new ArrayList<>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package org.raddatz.familienarchiv.repository;
|
||||||
|
|
||||||
|
import org.raddatz.familienarchiv.model.DocumentComment;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface CommentRepository extends JpaRepository<DocumentComment, UUID> {
|
||||||
|
|
||||||
|
List<DocumentComment> findByDocumentIdAndAnnotationIdIsNullAndParentIdIsNull(UUID documentId);
|
||||||
|
|
||||||
|
List<DocumentComment> findByAnnotationIdAndParentIdIsNull(UUID annotationId);
|
||||||
|
|
||||||
|
List<DocumentComment> findByParentId(UUID parentId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package org.raddatz.familienarchiv.service;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.raddatz.familienarchiv.exception.DomainException;
|
||||||
|
import org.raddatz.familienarchiv.exception.ErrorCode;
|
||||||
|
import org.raddatz.familienarchiv.model.AppUser;
|
||||||
|
import org.raddatz.familienarchiv.model.DocumentComment;
|
||||||
|
import org.raddatz.familienarchiv.repository.CommentRepository;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CommentService {
|
||||||
|
|
||||||
|
private final CommentRepository commentRepository;
|
||||||
|
|
||||||
|
public List<DocumentComment> getCommentsForDocument(UUID documentId) {
|
||||||
|
List<DocumentComment> roots =
|
||||||
|
commentRepository.findByDocumentIdAndAnnotationIdIsNullAndParentIdIsNull(documentId);
|
||||||
|
return withReplies(roots);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DocumentComment> getCommentsForAnnotation(UUID annotationId) {
|
||||||
|
List<DocumentComment> roots = commentRepository.findByAnnotationIdAndParentIdIsNull(annotationId);
|
||||||
|
return withReplies(roots);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public DocumentComment postComment(UUID documentId, UUID annotationId, String content, AppUser author) {
|
||||||
|
DocumentComment comment = DocumentComment.builder()
|
||||||
|
.documentId(documentId)
|
||||||
|
.annotationId(annotationId)
|
||||||
|
.content(content)
|
||||||
|
.authorId(author.getId())
|
||||||
|
.authorName(resolveAuthorName(author))
|
||||||
|
.build();
|
||||||
|
return commentRepository.save(comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public DocumentComment replyToComment(UUID documentId, UUID commentId, String content, AppUser author) {
|
||||||
|
DocumentComment target = commentRepository.findById(commentId)
|
||||||
|
.orElseThrow(() -> DomainException.notFound(
|
||||||
|
ErrorCode.COMMENT_NOT_FOUND, "Comment not found: " + commentId));
|
||||||
|
|
||||||
|
UUID rootId = target.getParentId() != null ? target.getParentId() : target.getId();
|
||||||
|
DocumentComment root = commentRepository.findById(rootId)
|
||||||
|
.orElseThrow(() -> DomainException.notFound(
|
||||||
|
ErrorCode.COMMENT_NOT_FOUND, "Comment not found: " + rootId));
|
||||||
|
|
||||||
|
DocumentComment reply = DocumentComment.builder()
|
||||||
|
.documentId(documentId)
|
||||||
|
.annotationId(root.getAnnotationId())
|
||||||
|
.parentId(root.getId())
|
||||||
|
.content(content)
|
||||||
|
.authorId(author.getId())
|
||||||
|
.authorName(resolveAuthorName(author))
|
||||||
|
.build();
|
||||||
|
return commentRepository.save(reply);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public DocumentComment editComment(UUID documentId, UUID commentId, String content, AppUser currentUser) {
|
||||||
|
DocumentComment comment = findComment(documentId, commentId);
|
||||||
|
if (!currentUser.getId().equals(comment.getAuthorId())) {
|
||||||
|
throw DomainException.forbidden("Only the comment author can edit it");
|
||||||
|
}
|
||||||
|
comment.setContent(content);
|
||||||
|
return commentRepository.save(comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void deleteComment(UUID documentId, UUID commentId, AppUser currentUser) {
|
||||||
|
DocumentComment comment = findComment(documentId, commentId);
|
||||||
|
boolean isAuthor = currentUser.getId().equals(comment.getAuthorId());
|
||||||
|
boolean isAdmin = currentUser.hasPermission("ADMIN");
|
||||||
|
if (!isAuthor && !isAdmin) {
|
||||||
|
throw DomainException.forbidden("Only the comment author or an admin can delete it");
|
||||||
|
}
|
||||||
|
commentRepository.delete(comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── private helpers ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private List<DocumentComment> withReplies(List<DocumentComment> roots) {
|
||||||
|
roots.forEach(root -> root.setReplies(commentRepository.findByParentId(root.getId())));
|
||||||
|
return roots;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DocumentComment findComment(UUID documentId, UUID commentId) {
|
||||||
|
return commentRepository.findById(commentId)
|
||||||
|
.filter(c -> documentId.equals(c.getDocumentId()))
|
||||||
|
.orElseThrow(() -> DomainException.notFound(
|
||||||
|
ErrorCode.COMMENT_NOT_FOUND, "Comment not found: " + commentId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveAuthorName(AppUser author) {
|
||||||
|
String first = author.getFirstName();
|
||||||
|
String last = author.getLastName();
|
||||||
|
if ((first == null || first.isBlank()) && (last == null || last.isBlank())) {
|
||||||
|
return author.getUsername();
|
||||||
|
}
|
||||||
|
return ((first != null ? first : "") + " " + (last != null ? last : "")).strip();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user