Browse Source

Add API endpoint to update entry title and content

Frédéric Guillot 2 years ago
parent
commit
e4285c2cba

+ 16 - 0
client/client.go

@@ -484,6 +484,22 @@ func (c *Client) UpdateEntries(entryIDs []int64, status string) error {
 	return err
 }
 
+// UpdateEntry updates an entry.
+func (c *Client) UpdateEntry(entryID int64, entryChanges *EntryModificationRequest) (*Entry, error) {
+	body, err := c.request.Put(fmt.Sprintf("/v1/entries/%d", entryID), entryChanges)
+	if err != nil {
+		return nil, err
+	}
+	defer body.Close()
+
+	var entry *Entry
+	if err := json.NewDecoder(body).Decode(&entry); err != nil {
+		return nil, fmt.Errorf("miniflux: response error (%v)", err)
+	}
+
+	return entry, nil
+}
+
 // ToggleBookmark toggles entry bookmark value.
 func (c *Client) ToggleBookmark(entryID int64) error {
 	_, err := c.request.Put(fmt.Sprintf("/v1/entries/%d/bookmark", entryID), nil)

+ 6 - 0
client/model.go

@@ -222,6 +222,12 @@ type Entry struct {
 	Tags        []string   `json:"tags"`
 }
 
+// EntryModificationRequest represents a request to modify an entry.
+type EntryModificationRequest struct {
+	Title   *string `json:"title"`
+	Content *string `json:"content"`
+}
+
 // Entries represents a list of entries.
 type Entries []*Entry
 

+ 27 - 0
contrib/bruno/miniflux/Update entry.bru

@@ -0,0 +1,27 @@
+meta {
+  name: Update entry
+  type: http
+  seq: 41
+}
+
+put {
+  url: {{minifluxBaseURL}}/v1/entries/{{entryID}}
+  body: json
+  auth: basic
+}
+
+auth:basic {
+  username: {{minifluxUsername}}
+  password: {{minifluxPassword}}
+}
+
+body:json {
+  {
+    "title": "New title",
+    "content": "Some text"
+  }
+}
+
+vars:pre-request {
+  entryID: 1789
+}

+ 1 - 0
internal/api/api.go

@@ -63,6 +63,7 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool) {
 	sr.HandleFunc("/entries", handler.getEntries).Methods(http.MethodGet)
 	sr.HandleFunc("/entries", handler.setEntryStatus).Methods(http.MethodPut)
 	sr.HandleFunc("/entries/{entryID}", handler.getEntry).Methods(http.MethodGet)
+	sr.HandleFunc("/entries/{entryID}", handler.updateEntry).Methods(http.MethodPut)
 	sr.HandleFunc("/entries/{entryID}/bookmark", handler.toggleBookmark).Methods(http.MethodPut)
 	sr.HandleFunc("/entries/{entryID}/save", handler.saveEntry).Methods(http.MethodPost)
 	sr.HandleFunc("/entries/{entryID}/fetch-content", handler.fetchContent).Methods(http.MethodGet)

+ 54 - 1
internal/api/entry.go

@@ -18,6 +18,7 @@ import (
 	"miniflux.app/v2/internal/model"
 	"miniflux.app/v2/internal/proxy"
 	"miniflux.app/v2/internal/reader/processor"
+	"miniflux.app/v2/internal/reader/readingtime"
 	"miniflux.app/v2/internal/storage"
 	"miniflux.app/v2/internal/urllib"
 	"miniflux.app/v2/internal/validator"
@@ -232,6 +233,58 @@ func (h *handler) saveEntry(w http.ResponseWriter, r *http.Request) {
 	json.Accepted(w, r)
 }
 
+func (h *handler) updateEntry(w http.ResponseWriter, r *http.Request) {
+	var entryUpdateRequest model.EntryUpdateRequest
+	if err := json_parser.NewDecoder(r.Body).Decode(&entryUpdateRequest); err != nil {
+		json.BadRequest(w, r, err)
+		return
+	}
+
+	if err := validator.ValidateEntryModification(&entryUpdateRequest); err != nil {
+		json.BadRequest(w, r, err)
+		return
+	}
+
+	loggedUserID := request.UserID(r)
+	entryID := request.RouteInt64Param(r, "entryID")
+
+	entryBuilder := h.store.NewEntryQueryBuilder(loggedUserID)
+	entryBuilder.WithEntryID(entryID)
+	entryBuilder.WithoutStatus(model.EntryStatusRemoved)
+
+	entry, err := entryBuilder.GetEntry()
+	if err != nil {
+		json.ServerError(w, r, err)
+		return
+	}
+
+	if entry == nil {
+		json.NotFound(w, r)
+		return
+	}
+
+	user, err := h.store.UserByID(loggedUserID)
+	if err != nil {
+		json.ServerError(w, r, err)
+		return
+	}
+
+	if user == nil {
+		json.NotFound(w, r)
+		return
+	}
+
+	entryUpdateRequest.Patch(entry)
+	entry.ReadingTime = readingtime.EstimateReadingTime(entry.Content, user.DefaultReadingSpeed, user.CJKReadingSpeed)
+
+	if err := h.store.UpdateEntryTitleAndContent(entry); err != nil {
+		json.ServerError(w, r, err)
+		return
+	}
+
+	json.Created(w, r, entry)
+}
+
 func (h *handler) fetchContent(w http.ResponseWriter, r *http.Request) {
 	loggedUserID := request.UserID(r)
 	entryID := request.RouteInt64Param(r, "entryID")
@@ -251,7 +304,7 @@ func (h *handler) fetchContent(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	user, err := h.store.UserByID(entry.UserID)
+	user, err := h.store.UserByID(loggedUserID)
 	if err != nil {
 		json.ServerError(w, r, err)
 		return

+ 16 - 0
internal/model/entry.go

@@ -58,3 +58,19 @@ type EntriesStatusUpdateRequest struct {
 	EntryIDs []int64 `json:"entry_ids"`
 	Status   string  `json:"status"`
 }
+
+// EntryUpdateRequest represents a request to update an entry.
+type EntryUpdateRequest struct {
+	Title   *string `json:"title"`
+	Content *string `json:"content"`
+}
+
+func (e *EntryUpdateRequest) Patch(entry *Entry) {
+	if e.Title != nil && *e.Title != "" {
+		entry.Title = *e.Title
+	}
+
+	if e.Content != nil && *e.Content != "" {
+		entry.Content = *e.Content
+	}
+}

+ 10 - 27
internal/storage/entry.go

@@ -67,42 +67,25 @@ func (s *Storage) NewEntryQueryBuilder(userID int64) *EntryQueryBuilder {
 	return NewEntryQueryBuilder(s, userID)
 }
 
-// UpdateEntryContent updates entry content.
-func (s *Storage) UpdateEntryContent(entry *model.Entry) error {
-	tx, err := s.db.Begin()
-	if err != nil {
-		return err
-	}
-
+// UpdateEntryTitleAndContent updates entry title and content.
+func (s *Storage) UpdateEntryTitleAndContent(entry *model.Entry) error {
 	query := `
 		UPDATE
 			entries
 		SET
-			content=$1, reading_time=$2
+			title=$1,
+			content=$2,
+			reading_time=$3,
+			document_vectors = setweight(to_tsvector(left(coalesce($1, ''), 500000)), 'A') || setweight(to_tsvector(left(coalesce($2, ''), 500000)), 'B')
 		WHERE
-			id=$3 AND user_id=$4
+			id=$4 AND user_id=$5
 	`
-	_, err = tx.Exec(query, entry.Content, entry.ReadingTime, entry.ID, entry.UserID)
-	if err != nil {
-		tx.Rollback()
-		return fmt.Errorf(`store: unable to update content of entry #%d: %v`, entry.ID, err)
-	}
 
-	query = `
-		UPDATE
-			entries
-		SET
-			document_vectors = setweight(to_tsvector(left(coalesce(title, ''), 500000)), 'A') || setweight(to_tsvector(left(coalesce(content, ''), 500000)), 'B')
-		WHERE
-			id=$1 AND user_id=$2
-	`
-	_, err = tx.Exec(query, entry.ID, entry.UserID)
-	if err != nil {
-		tx.Rollback()
-		return fmt.Errorf(`store: unable to update content of entry #%d: %v`, entry.ID, err)
+	if _, err := s.db.Exec(query, entry.Title, entry.Content, entry.ReadingTime, entry.ID, entry.UserID); err != nil {
+		return fmt.Errorf(`store: unable to update entry #%d: %v`, entry.ID, err)
 	}
 
-	return tx.Commit()
+	return nil
 }
 
 // createEntry add a new entry.

+ 34 - 0
internal/tests/entry_test.go

@@ -397,6 +397,40 @@ func TestUpdateStatus(t *testing.T) {
 	}
 }
 
+func TestUpdateEntry(t *testing.T) {
+	client := createClient(t)
+	createFeed(t, client)
+
+	result, err := client.Entries(&miniflux.Filter{Limit: 1})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	title := "New title"
+	content := "New content"
+
+	_, err = client.UpdateEntry(result.Entries[0].ID, &miniflux.EntryModificationRequest{
+		Title:   &title,
+		Content: &content,
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	entry, err := client.Entry(result.Entries[0].ID)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if entry.Title != title {
+		t.Fatal("The entry title should be updated")
+	}
+
+	if entry.Content != content {
+		t.Fatal("The entry content should be updated")
+	}
+}
+
 func TestToggleBookmark(t *testing.T) {
 	client := createClient(t)
 	createFeed(t, client)

+ 1 - 1
internal/ui/entry_scraper.go

@@ -58,7 +58,7 @@ func (h *handler) fetchContent(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if err := h.store.UpdateEntryContent(entry); err != nil {
+	if err := h.store.UpdateEntryTitleAndContent(entry); err != nil {
 		json.ServerError(w, r, err)
 		return
 	}

+ 16 - 3
internal/validator/entry.go

@@ -12,7 +12,7 @@ import (
 // ValidateEntriesStatusUpdateRequest validates a status update for a list of entries.
 func ValidateEntriesStatusUpdateRequest(request *model.EntriesStatusUpdateRequest) error {
 	if len(request.EntryIDs) == 0 {
-		return fmt.Errorf(`The list of entries cannot be empty`)
+		return fmt.Errorf(`the list of entries cannot be empty`)
 	}
 
 	return ValidateEntryStatus(request.Status)
@@ -25,7 +25,7 @@ func ValidateEntryStatus(status string) error {
 		return nil
 	}
 
-	return fmt.Errorf(`Invalid entry status, valid status values are: "%s", "%s" and "%s"`, model.EntryStatusRead, model.EntryStatusUnread, model.EntryStatusRemoved)
+	return fmt.Errorf(`invalid entry status, valid status values are: "%s", "%s" and "%s"`, model.EntryStatusRead, model.EntryStatusUnread, model.EntryStatusRemoved)
 }
 
 // ValidateEntryOrder makes sure the sorting order is valid.
@@ -35,5 +35,18 @@ func ValidateEntryOrder(order string) error {
 		return nil
 	}
 
-	return fmt.Errorf(`Invalid entry order, valid order values are: "id", "status", "changed_at", "published_at", "created_at", "category_title", "category_id", "title", "author"`)
+	return fmt.Errorf(`invalid entry order, valid order values are: "id", "status", "changed_at", "published_at", "created_at", "category_title", "category_id", "title", "author"`)
+}
+
+// ValidateEntryModification makes sure the entry modification is valid.
+func ValidateEntryModification(request *model.EntryUpdateRequest) error {
+	if request.Title != nil && *request.Title == "" {
+		return fmt.Errorf(`the entry title cannot be empty`)
+	}
+
+	if request.Content != nil && *request.Content == "" {
+		return fmt.Errorf(`the entry content cannot be empty`)
+	}
+
+	return nil
 }