posts/mutations_add_comment.go

116 lines
2.9 KiB
Go

package main
import (
"fmt"
"strings"
)
// AddComment — appends a comment to a post. Bumps the post. Computes the
// comment path from the parent comment's path. Pre-validated; the lock is
// held during apply so the path computation + index update is atomic.
type AddComment struct {
PostID string
ParentID string // empty for top-level
AuthorID string
Body string
// Set by Apply.
resolvedCommentID string
resolvedPath string
resolvedDepth int
}
func (m *AddComment) Kind() string { return "add_comment" }
func (m *AddComment) AffectedAuthors() []string {
return []string{m.AuthorID}
}
func (m *AddComment) Validate(s *Store) error {
if strings.TrimSpace(m.AuthorID) == "" {
return &ValidationError{Field: "author_id", Message: "required"}
}
if _, ok := s.GetAuthor(m.AuthorID); !ok {
return &ValidationError{Field: "author_id", Message: fmt.Sprintf("unknown author %q", m.AuthorID)}
}
if strings.TrimSpace(m.PostID) == "" {
return &ValidationError{Field: "post_id", Message: "required"}
}
post, ok := s.GetPost(m.PostID)
if !ok {
return &ValidationError{Field: "post_id", Message: fmt.Sprintf("unknown post %q", m.PostID)}
}
if post.IsDeleted {
return &ValidationError{Field: "post_id", Message: "post is deleted"}
}
if m.ParentID != "" {
comments := s.ListComments(m.PostID)
found := false
for _, c := range comments {
if c.ID == m.ParentID && !c.IsDeleted {
found = true
break
}
}
if !found {
return &ValidationError{Field: "parent_id", Message: fmt.Sprintf("unknown parent %q on post %q", m.ParentID, m.PostID)}
}
}
if strings.TrimSpace(m.Body) == "" {
return &ValidationError{Field: "body", Message: "required"}
}
return nil
}
func (m *AddComment) Apply(s *Store, lock LockRegion) error {
now := s.Now()
id := s.NewID()
// Compute path under the lock. Look up parent's path if any.
s.mu.Lock()
defer s.mu.Unlock()
var path string
var depth int
if m.ParentID != "" {
parent, ok := s.comments[m.ParentID]
if !ok {
return fmt.Errorf("parent comment disappeared mid-apply: %s", m.ParentID)
}
path = parent.Path + "." + id
depth = parent.Depth + 1
} else {
path = id
depth = 0
}
c := &Comment{
ID: id,
PostID: m.PostID,
ParentID: m.ParentID,
Path: path,
Depth: depth,
AuthorID: m.AuthorID,
Body: m.Body,
CreatedAt: now,
UpdatedAt: now,
}
m.resolvedCommentID = id
m.resolvedPath = path
m.resolvedDepth = depth
s.comments[id] = c
s.indexInsertComment(c)
// Bump the post.
post, ok := s.posts[m.PostID]
if !ok {
return fmt.Errorf("post disappeared mid-apply: %s", m.PostID)
}
post.BumpedAt = now
post.UpdatedAt = now
s.indexUpdatePost(post)
return nil
}
func (m *AddComment) ResolvedCommentID() string { return m.resolvedCommentID }
func (m *AddComment) ResolvedPath() string { return m.resolvedPath }
func (m *AddComment) ResolvedDepth() int { return m.resolvedDepth }