Skip to content

Commit 5ec113f

Browse files
authored
feat(comment): add 'comment update' and 'comment delete' (#44)
Wraps two endpoints added in Notion's 2025 API that the CLI didn't yet expose: - PATCH /v1/comments/:id → 'notion comment update <id> --text ...' - DELETE /v1/comments/:id → 'notion comment delete <id> [<id> ...]' 'update' reuses the existing buildCommentRichText helper so the --mention-user flag works the same way as 'comment add'. 'delete' follows the 'block delete' pattern: variadic args, per-id error isolation, and a summary line (N comment(s) deleted). When the target id is the anchor of a discussion, Notion removes the whole thread; deleting a reply removes just that one comment — this is documented in the Long help. Two new client methods (UpdateComment, DeleteComment) keep the API plumbing in internal/client alongside AddComment. Smoke-tested against a real workspace: create comment → update text → verify new text via 'comment get' → delete → raw DELETE on the same id returns object_not_found as expected (the synchronous GET path is eventually consistent server-side, which is a Notion behavior, not a CLI issue). Closes #33
1 parent ac8850f commit 5ec113f

3 files changed

Lines changed: 152 additions & 0 deletions

File tree

cmd/comment.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cmd
33
import (
44
"encoding/json"
55
"fmt"
6+
"os"
67
"strings"
78

89
"github.com/4ier/notion-cli/internal/client"
@@ -317,9 +318,106 @@ func init() {
317318
commentListCmd.Flags().Bool("all", false, "Fetch all pages of results")
318319
commentAddCmd.Flags().String("text", "", "Comment text")
319320
commentAddCmd.Flags().StringArray("mention-user", nil, "Mention a Notion user by ID (repeatable)")
321+
commentUpdateCmd.Flags().String("text", "", "New comment text (required)")
322+
commentUpdateCmd.Flags().StringArray("mention-user", nil, "Mention a Notion user by ID (repeatable)")
320323

321324
commentCmd.AddCommand(commentListCmd)
322325
commentCmd.AddCommand(commentAddCmd)
323326
commentCmd.AddCommand(commentGetCmd)
324327
commentCmd.AddCommand(commentReplyCmd)
328+
commentCmd.AddCommand(commentUpdateCmd)
329+
commentCmd.AddCommand(commentDeleteCmd)
330+
}
331+
332+
var commentUpdateCmd = &cobra.Command{
333+
Use: "update <comment-id>",
334+
Short: "Edit an existing comment's text",
335+
Long: `Edit the text of an existing comment.
336+
337+
Wraps PATCH /v1/comments/:id (added in Notion's 2025 API). The new
338+
rich_text is built the same way as 'comment add' — --mention-user works
339+
if you need to keep or add @mentions.
340+
341+
Examples:
342+
notion comment update abc123 --text "Fixed typo in previous comment"
343+
notion comment update abc123 --text "with mention" --mention-user <user-id>`,
344+
Args: cobra.ExactArgs(1),
345+
RunE: func(cmd *cobra.Command, args []string) error {
346+
token, err := getToken()
347+
if err != nil {
348+
return err
349+
}
350+
351+
commentID := strings.TrimSpace(args[0])
352+
text, _ := cmd.Flags().GetString("text")
353+
mentionUserIDs, _ := cmd.Flags().GetStringArray("mention-user")
354+
355+
if text == "" && len(mentionUserIDs) == 0 {
356+
return fmt.Errorf("--text or --mention-user is required")
357+
}
358+
359+
c := client.New(token)
360+
c.SetDebug(debugMode)
361+
362+
data, err := c.UpdateComment(commentID, text, mentionUserIDs)
363+
if err != nil {
364+
return fmt.Errorf("update comment: %w", err)
365+
}
366+
367+
if outputFormat == "json" {
368+
var result map[string]interface{}
369+
if err := json.Unmarshal(data, &result); err != nil {
370+
return fmt.Errorf("parse response: %w", err)
371+
}
372+
return render.JSON(result)
373+
}
374+
375+
fmt.Println("✓ Comment updated")
376+
return nil
377+
},
378+
}
379+
380+
var commentDeleteCmd = &cobra.Command{
381+
Use: "delete <comment-id ...>",
382+
Short: "Delete one or more comments",
383+
Long: `Delete comments by id. Accepts multiple ids for bulk removal,
384+
mirroring 'block delete' — per-id errors are printed but do not stop
385+
the batch.
386+
387+
Note: deleting the anchor (first) comment of a discussion removes the
388+
whole thread. Deleting a reply removes just that one reply.
389+
390+
Wraps DELETE /v1/comments/:id (added in Notion's 2025 API).
391+
392+
Examples:
393+
notion comment delete abc123
394+
notion comment delete abc123 def456 ghi789`,
395+
Args: cobra.MinimumNArgs(1),
396+
RunE: func(cmd *cobra.Command, args []string) error {
397+
token, err := getToken()
398+
if err != nil {
399+
return err
400+
}
401+
402+
c := client.New(token)
403+
c.SetDebug(debugMode)
404+
405+
deleted := 0
406+
for _, id := range args {
407+
id = strings.TrimSpace(id)
408+
if id == "" {
409+
continue
410+
}
411+
if _, err := c.DeleteComment(id); err != nil {
412+
fmt.Fprintf(os.Stderr, "✗ Failed to delete %s: %v\n", id, err)
413+
continue
414+
}
415+
deleted++
416+
}
417+
418+
if outputFormat != "json" {
419+
fmt.Printf("✓ %d comment(s) deleted\n", deleted)
420+
}
421+
return nil
422+
},
325423
}

cmd/comment_update_delete_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package cmd
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestCommentUpdateCmd_RequiresTextOrMention(t *testing.T) {
8+
if commentUpdateCmd.Short == "" {
9+
t.Error("update short help missing")
10+
}
11+
// Sanity: the command is registered under the parent.
12+
for _, expected := range []string{"update", "delete"} {
13+
found := false
14+
for _, sub := range commentCmd.Commands() {
15+
if sub.Name() == expected {
16+
found = true
17+
break
18+
}
19+
}
20+
if !found {
21+
t.Errorf("comment %s subcommand not registered", expected)
22+
}
23+
}
24+
}
25+
26+
func TestCommentDeleteCmd_AcceptsVariadic(t *testing.T) {
27+
// Args validator should accept 1+ ids.
28+
if err := commentDeleteCmd.Args(commentDeleteCmd, []string{"id1"}); err != nil {
29+
t.Errorf("one id should be valid: %v", err)
30+
}
31+
if err := commentDeleteCmd.Args(commentDeleteCmd, []string{"id1", "id2", "id3"}); err != nil {
32+
t.Errorf("multiple ids should be valid: %v", err)
33+
}
34+
if err := commentDeleteCmd.Args(commentDeleteCmd, []string{}); err == nil {
35+
t.Errorf("zero ids should fail")
36+
}
37+
}

internal/client/client.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,23 @@ func (c *Client) AddComment(pageID, text string, mentionUserIDs []string) ([]byt
301301
return c.Post("/v1/comments", body)
302302
}
303303

304+
// UpdateComment edits the rich_text body of an existing comment.
305+
// Wraps PATCH /v1/comments/:id (added in Notion API 2025).
306+
func (c *Client) UpdateComment(commentID, text string, mentionUserIDs []string) ([]byte, error) {
307+
body := map[string]interface{}{
308+
"rich_text": buildCommentRichText(text, mentionUserIDs),
309+
}
310+
return c.Patch("/v1/comments/"+commentID, body)
311+
}
312+
313+
// DeleteComment removes a comment by id.
314+
// Wraps DELETE /v1/comments/:id (added in Notion API 2025). When the
315+
// target is the anchor comment of a discussion, Notion removes the whole
316+
// thread; otherwise it removes just that one reply.
317+
func (c *Client) DeleteComment(commentID string) ([]byte, error) {
318+
return c.Delete("/v1/comments/" + commentID)
319+
}
320+
304321
func buildCommentRichText(text string, mentionUserIDs []string) []map[string]interface{} {
305322
var richText []map[string]interface{}
306323

0 commit comments

Comments
 (0)