From f67c07b178cd90539f1cc934def5a99be203e44a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Feb 2026 23:03:41 +0000 Subject: Exclude full_content from stream list API responses The /stream endpoint now skips selecting full_content and header_image from the database, reducing memory allocation and response payload size for list views. Full content remains available via /item/:id endpoint. Adds omitempty to JSON tags so empty fields are omitted from responses, and a test verifying full_content is excluded from stream responses. https://claude.ai/code/session_019Z4VJxzY7tcAuNkPAkvry9 --- models/item/item.go | 43 +++++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 14 deletions(-) (limited to 'models') diff --git a/models/item/item.go b/models/item/item.go index 189cb4a..e1e740e 100644 --- a/models/item/item.go +++ b/models/item/item.go @@ -45,8 +45,8 @@ type Item struct { ReadState bool `json:"read"` Starred bool `json:"starred"` - FullContent string `json:"full_content"` - HeaderImage string `json:"header_image"` + FullContent string `json:"full_content,omitempty"` + HeaderImage string `json:"header_image,omitempty"` } func (i *Item) Print() { @@ -97,7 +97,7 @@ func filterPolicy() *bluemonday.Policy { } func ItemById(id int64) *Item { - items, err := Filter(0, nil, "", false, false, id, "") + items, err := Filter(0, nil, "", false, false, id, "", true) if err != nil || len(items) == 0 { return nil } @@ -134,7 +134,12 @@ func (i *Item) GetFullContent() { } } -func Filter(max_id int64, feed_ids []int64, category string, unread_only bool, starred_only bool, item_id int64, search_query string) ([]*Item, error) { +func Filter(max_id int64, feed_ids []int64, category string, unread_only bool, starred_only bool, item_id int64, search_query string, includeContent ...bool) ([]*Item, error) { + + withContent := false + if len(includeContent) > 0 { + withContent = includeContent[0] + } var args []interface{} tables := " feed,item" @@ -142,12 +147,15 @@ func Filter(max_id int64, feed_ids []int64, category string, unread_only bool, s tables = tables + ",fts_item" } - query := `SELECT item.id, item.feed_id, item.title, - item.url, item.description, - item.read_state, item.starred, item.publish_date, - item.full_content, item.header_image, - feed.url, feed.title, feed.category - FROM ` + selectCols := `item.id, item.feed_id, item.title, + item.url, item.description, + item.read_state, item.starred, item.publish_date` + if withContent { + selectCols += `, item.full_content, item.header_image` + } + selectCols += `, feed.url, feed.title, feed.category` + + query := `SELECT ` + selectCols + ` FROM ` query = query + tables + ` WHERE item.feed_id=feed.id AND item.id!=0 ` if max_id != 0 { @@ -206,7 +214,12 @@ func Filter(max_id int64, feed_ids []int64, category string, unread_only bool, s for rows.Next() { i := new(Item) var feed_id int64 - err := rows.Scan(&i.Id, &feed_id, &i.Title, &i.Url, &i.Description, &i.ReadState, &i.Starred, &i.PublishDate, &i.FullContent, &i.HeaderImage, &i.FeedUrl, &i.FeedTitle, &i.FeedCategory) + var err error + if withContent { + err = rows.Scan(&i.Id, &feed_id, &i.Title, &i.Url, &i.Description, &i.ReadState, &i.Starred, &i.PublishDate, &i.FullContent, &i.HeaderImage, &i.FeedUrl, &i.FeedTitle, &i.FeedCategory) + } else { + err = rows.Scan(&i.Id, &feed_id, &i.Title, &i.Url, &i.Description, &i.ReadState, &i.Starred, &i.PublishDate, &i.FeedUrl, &i.FeedTitle, &i.FeedCategory) + } if err != nil { vlog.Println(err) return nil, err @@ -223,9 +236,11 @@ func Filter(max_id int64, feed_ids []int64, category string, unread_only bool, s i.Url = p.Sanitize(i.Url) i.FeedTitle = p.Sanitize(i.FeedTitle) i.FeedUrl = p.Sanitize(i.FeedUrl) - i.FullContent = p.Sanitize(i.FullContent) - i.HeaderImage = p.Sanitize(i.HeaderImage) - i.CleanHeaderImage() + if withContent { + i.FullContent = p.Sanitize(i.FullContent) + i.HeaderImage = p.Sanitize(i.HeaderImage) + i.CleanHeaderImage() + } items = append(items, i) } if err = rows.Err(); err != nil { -- cgit v1.2.3 From 5f5f8be8c6ca78f5d61372544bb24d692d9597f0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Feb 2026 23:31:54 +0000 Subject: Add tests for CSRF exclusions, Filter includeContent, multi-feed, and routing - CSRF: test excluded paths (/api/login, /api/logout), PUT/DELETE methods - Item model: test Filter includeContent flag, ItemById returns content, multiple feed_ids filtering - API: test read_filter=all param, feed_ids comma-separated filter, full_content exclusion from stream - Routing: add v3 frontend route test https://claude.ai/code/session_019Z4VJxzY7tcAuNkPAkvry9 --- models/item/item_test.go | 93 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) (limited to 'models') diff --git a/models/item/item_test.go b/models/item/item_test.go index 241a650..c5b557a 100644 --- a/models/item/item_test.go +++ b/models/item/item_test.go @@ -622,6 +622,99 @@ func TestGetFullContentWithMock(t *testing.T) { } } +func TestFilterIncludeContent(t *testing.T) { + setupTestDB(t) + feedId := createTestFeed(t) + + i := &Item{ + Title: "Content Item", + Url: "https://example.com/content", + Description: "desc", + PublishDate: "2024-01-01 00:00:00", + FeedId: feedId, + } + i.Create() + models.DB.Exec("UPDATE item SET full_content=?, header_image=? WHERE id=?", + "

Full article

", "https://example.com/img.jpg", i.Id) + + // Without includeContent (default) — should NOT have full_content + items, err := Filter(0, nil, "", false, false, 0, "") + if err != nil { + t.Fatal(err) + } + if len(items) != 1 { + t.Fatalf("Expected 1 item, got %d", len(items)) + } + if items[0].FullContent != "" { + t.Errorf("Expected empty FullContent without includeContent, got %q", items[0].FullContent) + } + if items[0].HeaderImage != "" { + t.Errorf("Expected empty HeaderImage without includeContent, got %q", items[0].HeaderImage) + } + + // With includeContent=true — should have full_content + items, err = Filter(0, nil, "", false, false, 0, "", true) + if err != nil { + t.Fatal(err) + } + if len(items) != 1 { + t.Fatalf("Expected 1 item, got %d", len(items)) + } + if items[0].FullContent == "" { + t.Error("Expected FullContent with includeContent=true") + } + if items[0].HeaderImage == "" { + t.Error("Expected HeaderImage with includeContent=true") + } +} + +func TestItemByIdIncludesContent(t *testing.T) { + setupTestDB(t) + feedId := createTestFeed(t) + + i := &Item{ + Title: "ById Content", + Url: "https://example.com/byidcontent", + Description: "desc", + PublishDate: "2024-01-01 00:00:00", + FeedId: feedId, + } + i.Create() + models.DB.Exec("UPDATE item SET full_content=? WHERE id=?", "

Article body

", i.Id) + + found := ItemById(i.Id) + if found == nil { + t.Fatal("ItemById should return an item") + } + if found.FullContent == "" { + t.Error("ItemById should include full_content") + } +} + +func TestFilterMultipleFeedIds(t *testing.T) { + setupTestDB(t) + + res1, _ := models.DB.Exec("INSERT INTO feed(url, title) VALUES(?, ?)", "https://feed1.com", "Feed 1") + feedId1, _ := res1.LastInsertId() + res2, _ := models.DB.Exec("INSERT INTO feed(url, title) VALUES(?, ?)", "https://feed2.com", "Feed 2") + feedId2, _ := res2.LastInsertId() + res3, _ := models.DB.Exec("INSERT INTO feed(url, title) VALUES(?, ?)", "https://feed3.com", "Feed 3") + feedId3, _ := res3.LastInsertId() + + (&Item{Title: "F1 Item", Url: "https://feed1.com/1", Description: "d", PublishDate: "2024-01-01", FeedId: feedId1}).Create() + (&Item{Title: "F2 Item", Url: "https://feed2.com/1", Description: "d", PublishDate: "2024-01-01", FeedId: feedId2}).Create() + (&Item{Title: "F3 Item", Url: "https://feed3.com/1", Description: "d", PublishDate: "2024-01-01", FeedId: feedId3}).Create() + + // Filter by feed IDs 1 and 2 + items, err := Filter(0, []int64{feedId1, feedId2}, "", false, false, 0, "") + if err != nil { + t.Fatal(err) + } + if len(items) != 2 { + t.Errorf("Expected 2 items for two feed IDs, got %d", len(items)) + } +} + func TestRewriteImagesNoSrc(t *testing.T) { input := `no src` result := rewriteImages(input) -- cgit v1.2.3