Add API endpoint to update entry title and content
This commit is contained in:
parent
7b541af253
commit
e4285c2cba
10 changed files with 182 additions and 33 deletions
|
@ -484,6 +484,22 @@ func (c *Client) UpdateEntries(entryIDs []int64, status string) error {
|
||||||
return err
|
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.
|
// ToggleBookmark toggles entry bookmark value.
|
||||||
func (c *Client) ToggleBookmark(entryID int64) error {
|
func (c *Client) ToggleBookmark(entryID int64) error {
|
||||||
_, err := c.request.Put(fmt.Sprintf("/v1/entries/%d/bookmark", entryID), nil)
|
_, err := c.request.Put(fmt.Sprintf("/v1/entries/%d/bookmark", entryID), nil)
|
||||||
|
|
|
@ -222,6 +222,12 @@ type Entry struct {
|
||||||
Tags []string `json:"tags"`
|
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.
|
// Entries represents a list of entries.
|
||||||
type Entries []*Entry
|
type Entries []*Entry
|
||||||
|
|
||||||
|
|
27
contrib/bruno/miniflux/Update entry.bru
Normal file
27
contrib/bruno/miniflux/Update entry.bru
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -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.getEntries).Methods(http.MethodGet)
|
||||||
sr.HandleFunc("/entries", handler.setEntryStatus).Methods(http.MethodPut)
|
sr.HandleFunc("/entries", handler.setEntryStatus).Methods(http.MethodPut)
|
||||||
sr.HandleFunc("/entries/{entryID}", handler.getEntry).Methods(http.MethodGet)
|
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}/bookmark", handler.toggleBookmark).Methods(http.MethodPut)
|
||||||
sr.HandleFunc("/entries/{entryID}/save", handler.saveEntry).Methods(http.MethodPost)
|
sr.HandleFunc("/entries/{entryID}/save", handler.saveEntry).Methods(http.MethodPost)
|
||||||
sr.HandleFunc("/entries/{entryID}/fetch-content", handler.fetchContent).Methods(http.MethodGet)
|
sr.HandleFunc("/entries/{entryID}/fetch-content", handler.fetchContent).Methods(http.MethodGet)
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"miniflux.app/v2/internal/model"
|
"miniflux.app/v2/internal/model"
|
||||||
"miniflux.app/v2/internal/proxy"
|
"miniflux.app/v2/internal/proxy"
|
||||||
"miniflux.app/v2/internal/reader/processor"
|
"miniflux.app/v2/internal/reader/processor"
|
||||||
|
"miniflux.app/v2/internal/reader/readingtime"
|
||||||
"miniflux.app/v2/internal/storage"
|
"miniflux.app/v2/internal/storage"
|
||||||
"miniflux.app/v2/internal/urllib"
|
"miniflux.app/v2/internal/urllib"
|
||||||
"miniflux.app/v2/internal/validator"
|
"miniflux.app/v2/internal/validator"
|
||||||
|
@ -232,6 +233,58 @@ func (h *handler) saveEntry(w http.ResponseWriter, r *http.Request) {
|
||||||
json.Accepted(w, r)
|
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) {
|
func (h *handler) fetchContent(w http.ResponseWriter, r *http.Request) {
|
||||||
loggedUserID := request.UserID(r)
|
loggedUserID := request.UserID(r)
|
||||||
entryID := request.RouteInt64Param(r, "entryID")
|
entryID := request.RouteInt64Param(r, "entryID")
|
||||||
|
@ -251,7 +304,7 @@ func (h *handler) fetchContent(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := h.store.UserByID(entry.UserID)
|
user, err := h.store.UserByID(loggedUserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
json.ServerError(w, r, err)
|
json.ServerError(w, r, err)
|
||||||
return
|
return
|
||||||
|
|
|
@ -58,3 +58,19 @@ type EntriesStatusUpdateRequest struct {
|
||||||
EntryIDs []int64 `json:"entry_ids"`
|
EntryIDs []int64 `json:"entry_ids"`
|
||||||
Status string `json:"status"`
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -67,42 +67,25 @@ func (s *Storage) NewEntryQueryBuilder(userID int64) *EntryQueryBuilder {
|
||||||
return NewEntryQueryBuilder(s, userID)
|
return NewEntryQueryBuilder(s, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateEntryContent updates entry content.
|
// UpdateEntryTitleAndContent updates entry title and content.
|
||||||
func (s *Storage) UpdateEntryContent(entry *model.Entry) error {
|
func (s *Storage) UpdateEntryTitleAndContent(entry *model.Entry) error {
|
||||||
tx, err := s.db.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
query := `
|
query := `
|
||||||
UPDATE
|
UPDATE
|
||||||
entries
|
entries
|
||||||
SET
|
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
|
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 {
|
if _, err := s.db.Exec(query, entry.Title, entry.Content, entry.ReadingTime, entry.ID, entry.UserID); err != nil {
|
||||||
tx.Rollback()
|
return fmt.Errorf(`store: unable to update entry #%d: %v`, entry.ID, err)
|
||||||
return fmt.Errorf(`store: unable to update content of entry #%d: %v`, entry.ID, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
query = `
|
return nil
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx.Commit()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// createEntry add a new entry.
|
// createEntry add a new entry.
|
||||||
|
|
|
@ -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) {
|
func TestToggleBookmark(t *testing.T) {
|
||||||
client := createClient(t)
|
client := createClient(t)
|
||||||
createFeed(t, client)
|
createFeed(t, client)
|
||||||
|
|
|
@ -58,7 +58,7 @@ func (h *handler) fetchContent(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.store.UpdateEntryContent(entry); err != nil {
|
if err := h.store.UpdateEntryTitleAndContent(entry); err != nil {
|
||||||
json.ServerError(w, r, err)
|
json.ServerError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import (
|
||||||
// ValidateEntriesStatusUpdateRequest validates a status update for a list of entries.
|
// ValidateEntriesStatusUpdateRequest validates a status update for a list of entries.
|
||||||
func ValidateEntriesStatusUpdateRequest(request *model.EntriesStatusUpdateRequest) error {
|
func ValidateEntriesStatusUpdateRequest(request *model.EntriesStatusUpdateRequest) error {
|
||||||
if len(request.EntryIDs) == 0 {
|
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)
|
return ValidateEntryStatus(request.Status)
|
||||||
|
@ -25,7 +25,7 @@ func ValidateEntryStatus(status string) error {
|
||||||
return nil
|
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.
|
// ValidateEntryOrder makes sure the sorting order is valid.
|
||||||
|
@ -35,5 +35,18 @@ func ValidateEntryOrder(order string) error {
|
||||||
return nil
|
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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue