116 lines
2.9 KiB
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 }
|