Files
pad/internal/models/comment.go
T
xarmian 076fb9b2e7 feat(comments): comment editing backend — user_id, UpdateComment, PATCH, SSE (TASK-1663) (#665)
* feat(comments): comment editing backend — user_id, UpdateComment, PATCH, SSE (TASK-1663)

Foundation for comment editing (PLAN-1662). No migration — comments.user_id
already exists (012_users.sql) but was never written or exposed.

- Populate user_id on create/reply: CreateComment takes an explicit userID
  param (passed from currentUserID by the handlers, not via the request body
  so it can't be spoofed). Expose user_id on models.Comment + all comment
  SELECTs/scans. The workspace export path is left as-is — imported comments
  keep NULL user_id (admin-only edit), matching the pre-identity fallback.
- Store.UpdateComment(id, body): replaces body + bumps updated_at; the
  comments_fts_update trigger re-indexes.
- PATCH /workspaces/{ws}/comments/{commentID}: author-or-admin only
  (canEditComment), rejects empty body. Editing is an authorship op, distinct
  from delete (item editors). NULL user_id → admin-only.
- comment_updated SSE event: broadcast from the handler; added to the web
  sse allowlist + ItemTimeline refresh set.
- web: api.comments.update(), Comment.user_id type.

Tests: author edits own (200), non-author non-admin (403), admin edits
anyone (200), empty body (400), NULL-user_id comment is admin-only.

Parent: PLAN-1662.

* fix(account): detach authored comments on account deletion per Codex review (round 1)

Now that TASK-1663 populates comments.user_id (FK to users.id),
DeleteAccountAtomic would fail on the FK for any user who authored a
comment. Null comments.user_id for the user before deleting the row —
comments live on in soft-deleted/other workspaces; the display-name
author is preserved and the comment just becomes admin-only to edit.
Regression test added.
2026-05-30 12:38:57 -04:00

53 lines
1.8 KiB
Go

package models
import "time"
// Comment represents a comment on an item.
type Comment struct {
ID string `json:"id"`
ItemID string `json:"item_id"`
WorkspaceID string `json:"workspace_id"`
Author string `json:"author"`
// UserID is the authenticated user who authored the comment. Empty for
// pre-identity comments (created before TASK-1663) and agent/system
// comments; the comment-edit permission check treats empty as
// "no provable author" → admin-only.
UserID string `json:"user_id,omitempty"`
Body string `json:"body"`
CreatedBy string `json:"created_by"`
Source string `json:"source"`
ActivityID string `json:"activity_id,omitempty"`
ParentID string `json:"parent_id,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Populated by joins (not stored)
ItemTitle string `json:"item_title,omitempty"`
ItemSlug string `json:"item_slug,omitempty"`
// Populated by handlers for threaded views
Replies []Comment `json:"replies,omitempty"`
Reactions []Reaction `json:"reactions,omitempty"`
}
// CommentCreate is the input for creating a new comment.
type CommentCreate struct {
Author string `json:"author,omitempty"`
Body string `json:"body"`
CreatedBy string `json:"created_by,omitempty"`
Source string `json:"source,omitempty"`
ParentID string `json:"parent_id,omitempty"`
ActivityID string `json:"activity_id,omitempty"`
}
// Reaction represents an emoji reaction on a comment.
type Reaction struct {
ID string `json:"id"`
CommentID string `json:"comment_id"`
UserID string `json:"user_id,omitempty"`
Actor string `json:"actor"`
Emoji string `json:"emoji"`
CreatedAt time.Time `json:"created_at"`
ActorName string `json:"actor_name,omitempty"`
}