diff --git a/src/storage/factory/factory.go b/src/storage/factory/factory.go index 4d4b315..a709b8c 100644 --- a/src/storage/factory/factory.go +++ b/src/storage/factory/factory.go @@ -9,24 +9,24 @@ type Storage interface { Migrate() error CountItems() int CreateFeed(params model.CreateFeedParams) *model.Feed - CreateFolder(title string) *Folder - CreateItems(items []Item) bool + CreateFolder(title string) *model.Folder + CreateItems(items []model.Item) bool DeleteFeed(feedId int64) bool DeleteFolder(folderId int64) bool DeleteOldItems() - FeedStats() []FeedStat + FeedStats() []model.FeedStat GetFeed(id int64) *model.Feed - GetFeedState(feedID int64) (*FeedState, error) - GetItem(id int64) *Item - GetSettings() Settings - ListFeedStates() ([]FeedState, error) + GetFeedState(feedID int64) (*model.FeedState, error) + GetItem(id int64) *model.Item + GetSettings() model.Settings + ListFeedStates() ([]model.FeedState, error) ListFeeds() []model.Feed ListFolders() []model.Folder - ListItems(filter ItemFilter, limit int, newestFirst bool, withContent bool) []Item - MarkItemsRead(filter MarkFilter) bool - UpdateFeed(feedId int64, params UpdateFeedParams) (bool, error) - UpdateFeedState(feedID int64, params UpdateFeedStateParams) (bool, error) - UpdateFolder(folderId int64, params UpdateFolderParams) (bool, error) - UpdateItemStatus(item_id int64, status ItemStatus) bool - UpdateSettings(params UpdateSettingsParams) bool + ListItems(filter model.ItemFilter, limit int, newestFirst bool, withContent bool) []model.Item + MarkItemsRead(filter model.MarkFilter) bool + UpdateFeed(feedId int64, params model.UpdateFeedParams) (bool, error) + UpdateFeedState(feedID int64, params model.UpdateFeedStateParams) (bool, error) + UpdateFolder(folderId int64, params model.UpdateFolderParams) (bool, error) + UpdateItemStatus(item_id int64, status model.ItemStatus) bool + UpdateSettings(params model.UpdateSettingsParams) bool } diff --git a/src/storage/model/model.go b/src/storage/model/model.go index 24a1eeb..fc232dc 100644 --- a/src/storage/model/model.go +++ b/src/storage/model/model.go @@ -108,6 +108,12 @@ type UpdateFolderParams struct { IsExpanded *bool } +type FeedStat struct { + FeedId int64 `json:"feed_id"` + UnreadCount int64 `json:"unread"` + StarredCount int64 `json:"starred"` +} + type Settings struct { Filter string `json:"filter"` diff --git a/src/storage/sqlite/feed.go b/src/storage/sqlite/feed.go index e1decc2..f1fe760 100644 --- a/src/storage/sqlite/feed.go +++ b/src/storage/sqlite/feed.go @@ -30,7 +30,7 @@ func (s *SQLiteStorage) CreateFeed(params model.CreateFeedParams) *model.Feed { log.Print(err) return nil } - return &Feed{ + return &model.Feed{ Id: id, Title: title, Description: params.Description, @@ -56,7 +56,7 @@ func (s *SQLiteStorage) DeleteFeed(feedId int64) bool { return nrows == 1 } -func (s *SQLiteStorage) UpdateFeed(feedId int64, params UpdateFeedParams) (bool, error) { +func (s *SQLiteStorage) UpdateFeed(feedId int64, params model.UpdateFeedParams) (bool, error) { _, err := s.db.Exec(` update feeds set title = coalesce(:title, title), @@ -112,8 +112,8 @@ func (s *SQLiteStorage) ListFeeds() []model.Feed { return result } -func (s *SQLiteStorage) GetFeed(id int64) *Feed { - var f Feed +func (s *SQLiteStorage) GetFeed(id int64) *model.Feed { + var f model.Feed err := s.db.QueryRow(` select id, folder_id, title, link, feed_link, diff --git a/src/storage/sqlite/feed_test.go b/src/storage/sqlite/feed_test.go index a836358..cf6f830 100644 --- a/src/storage/sqlite/feed_test.go +++ b/src/storage/sqlite/feed_test.go @@ -3,11 +3,13 @@ package sqlite import ( "reflect" "testing" + + "github.com/nkanaev/yarr/src/storage/model" ) func TestCreateFeed(t *testing.T) { db := testDB() - feed1 := db.CreateFeed(CreateFeedParams{Title: "title", Link: "http://example.com", FeedLink: "http://example.com/feed.xml"}) + feed1 := db.CreateFeed(model.CreateFeedParams{Title: "title", Link: "http://example.com", FeedLink: "http://example.com/feed.xml"}) if feed1 == nil || feed1.Id == 0 { t.Fatal("expected feed") } @@ -19,16 +21,16 @@ func TestCreateFeed(t *testing.T) { func TestCreateFeedSameLink(t *testing.T) { db := testDB() - feed1 := db.CreateFeed(CreateFeedParams{Title: "title", FeedLink: "http://example1.com/feed.xml"}) + feed1 := db.CreateFeed(model.CreateFeedParams{Title: "title", FeedLink: "http://example1.com/feed.xml"}) if feed1 == nil || feed1.Id == 0 { t.Fatal("expected feed") } for range 10 { - db.CreateFeed(CreateFeedParams{Title: "title", FeedLink: "http://example2.com/feed.xml"}) + db.CreateFeed(model.CreateFeedParams{Title: "title", FeedLink: "http://example2.com/feed.xml"}) } - feed2 := db.CreateFeed(CreateFeedParams{Title: "title", Link: "http://example.com", FeedLink: "http://example1.com/feed.xml"}) + feed2 := db.CreateFeed(model.CreateFeedParams{Title: "title", Link: "http://example.com", FeedLink: "http://example1.com/feed.xml"}) if feed1.Id != feed2.Id { t.Fatalf("expected the same feed.\nwant: %#v\nhave: %#v", feed1, feed2) } @@ -40,25 +42,25 @@ func TestReadFeed(t *testing.T) { t.Fatal("cannot get nonexistent feed") } - feed1 := db.CreateFeed(CreateFeedParams{Title: "feed 1", Link: "http://example1.com", FeedLink: "http://example1.com/feed.xml"}) - feed2 := db.CreateFeed(CreateFeedParams{Title: "feed 2", Link: "http://example2.com", FeedLink: "http://example2.com/feed.xml"}) + feed1 := db.CreateFeed(model.CreateFeedParams{Title: "feed 1", Link: "http://example1.com", FeedLink: "http://example1.com/feed.xml"}) + feed2 := db.CreateFeed(model.CreateFeedParams{Title: "feed 2", Link: "http://example2.com", FeedLink: "http://example2.com/feed.xml"}) feeds := db.ListFeeds() - if !reflect.DeepEqual(feeds, []Feed{*feed1, *feed2}) { + if !reflect.DeepEqual(feeds, []model.Feed{*feed1, *feed2}) { t.Fatalf("invalid feed list: %#v", feeds) } } func TestUpdateFeed(t *testing.T) { db := testDB() - feed1 := db.CreateFeed(CreateFeedParams{Title: "feed 1", Link: "http://example1.com", FeedLink: "http://example1.com/feed.xml"}) + feed1 := db.CreateFeed(model.CreateFeedParams{Title: "feed 1", Link: "http://example1.com", FeedLink: "http://example1.com/feed.xml"}) folder := db.CreateFolder("test") icon := []byte("icon") title := "newtitle" - db.UpdateFeed(feed1.Id, UpdateFeedParams{ + db.UpdateFeed(feed1.Id, model.UpdateFeedParams{ Title: &title, - FolderID: SetNullable(&folder.Id), - Icon: SetNullable(&icon), + FolderID: model.SetNullable(&folder.Id), + Icon: model.SetNullable(&icon), }) feed2 := db.GetFeed(feed1.Id) @@ -75,7 +77,7 @@ func TestUpdateFeed(t *testing.T) { func TestDeleteFeed(t *testing.T) { db := testDB() - feed1 := db.CreateFeed(CreateFeedParams{Title: "title", Link: "http://example.com", FeedLink: "http://example.com/feed.xml"}) + feed1 := db.CreateFeed(model.CreateFeedParams{Title: "title", Link: "http://example.com", FeedLink: "http://example.com/feed.xml"}) if db.DeleteFeed(100500) { t.Error("cannot delete what does not exist") diff --git a/src/storage/sqlite/feedstate.go b/src/storage/sqlite/feedstate.go index 1b4ba4b..d80bcb4 100644 --- a/src/storage/sqlite/feedstate.go +++ b/src/storage/sqlite/feedstate.go @@ -2,10 +2,11 @@ package sqlite import ( "database/sql" - "time" + + "github.com/nkanaev/yarr/src/storage/model" ) -func (s *SQLiteStorage) ListFeedStates() ([]FeedState, error) { +func (s *SQLiteStorage) ListFeedStates() ([]model.FeedState, error) { rows, err := s.db.Query(` select feed_id @@ -20,9 +21,9 @@ func (s *SQLiteStorage) ListFeedStates() ([]FeedState, error) { } defer rows.Close() - states := make([]FeedState, 0) + states := make([]model.FeedState, 0) for rows.Next() { - var state FeedState + var state model.FeedState err := rows.Scan( &state.FeedID, &state.LastRefreshed, @@ -38,8 +39,8 @@ func (s *SQLiteStorage) ListFeedStates() ([]FeedState, error) { return states, nil } -func (s *SQLiteStorage) GetFeedState(feedID int64) (*FeedState, error) { - var state FeedState +func (s *SQLiteStorage) GetFeedState(feedID int64) (*model.FeedState, error) { + var state model.FeedState err := s.db.QueryRow(` select feed_id @@ -64,7 +65,7 @@ func (s *SQLiteStorage) GetFeedState(feedID int64) (*FeedState, error) { return &state, nil } -func (s *SQLiteStorage) UpdateFeedState(feedID int64, params UpdateFeedStateParams) (bool, error) { +func (s *SQLiteStorage) UpdateFeedState(feedID int64, params model.UpdateFeedStateParams) (bool, error) { lastError := params.LastError if lastError != nil && *lastError == "" { lastError = nil diff --git a/src/storage/sqlite/feedstate_test.go b/src/storage/sqlite/feedstate_test.go index 47a3f56..43b0043 100644 --- a/src/storage/sqlite/feedstate_test.go +++ b/src/storage/sqlite/feedstate_test.go @@ -3,20 +3,22 @@ package sqlite import ( "testing" "time" + + "github.com/nkanaev/yarr/src/storage/model" ) func TestUpdateFeedState_Full(t *testing.T) { s := testDB() defer s.Close() - f := s.CreateFeed(CreateFeedParams{Title: "Test", FeedLink: "http://example.com"}) + f := s.CreateFeed(model.CreateFeedParams{Title: "Test", FeedLink: "http://example.com"}) now := time.Now().UTC().Truncate(time.Second) errMsg := "error" lmod := "today" etag := "v1" - ok, err := s.UpdateFeedState(f.Id, UpdateFeedStateParams{ + ok, err := s.UpdateFeedState(f.Id, model.UpdateFeedStateParams{ LastRefreshed: &now, LastError: &errMsg, HTTPLastModified: &lmod, @@ -54,12 +56,12 @@ func TestUpdateFeedState_Partial(t *testing.T) { s := testDB() defer s.Close() - f := s.CreateFeed(CreateFeedParams{Title: "Test", FeedLink: "http://example.com"}) + f := s.CreateFeed(model.CreateFeedParams{Title: "Test", FeedLink: "http://example.com"}) etag := "v1" - s.UpdateFeedState(f.Id, UpdateFeedStateParams{HTTPEtag: &etag}) + s.UpdateFeedState(f.Id, model.UpdateFeedStateParams{HTTPEtag: &etag}) newErr := "new error" - _, err := s.UpdateFeedState(f.Id, UpdateFeedStateParams{ + _, err := s.UpdateFeedState(f.Id, model.UpdateFeedStateParams{ LastError: &newErr, }) if err != nil { @@ -82,12 +84,12 @@ func TestUpdateFeedState_ClearError(t *testing.T) { s := testDB() defer s.Close() - f := s.CreateFeed(CreateFeedParams{Title: "Test", FeedLink: "http://example.com"}) + f := s.CreateFeed(model.CreateFeedParams{Title: "Test", FeedLink: "http://example.com"}) errMsg := "error" - s.UpdateFeedState(f.Id, UpdateFeedStateParams{LastError: &errMsg}) + s.UpdateFeedState(f.Id, model.UpdateFeedStateParams{LastError: &errMsg}) empty := "" - _, err := s.UpdateFeedState(f.Id, UpdateFeedStateParams{ + _, err := s.UpdateFeedState(f.Id, model.UpdateFeedStateParams{ LastError: &empty, }) if err != nil { @@ -107,12 +109,12 @@ func TestListFeedStates(t *testing.T) { s := testDB() defer s.Close() - f1 := s.CreateFeed(CreateFeedParams{Title: "F1", FeedLink: "L1"}) - f2 := s.CreateFeed(CreateFeedParams{Title: "F2", FeedLink: "L2"}) + f1 := s.CreateFeed(model.CreateFeedParams{Title: "F1", FeedLink: "L1"}) + f2 := s.CreateFeed(model.CreateFeedParams{Title: "F2", FeedLink: "L2"}) errMsg := "fail" - s.UpdateFeedState(f1.Id, UpdateFeedStateParams{LastError: &errMsg}) - s.UpdateFeedState(f2.Id, UpdateFeedStateParams{HTTPEtag: ptr("e")}) + s.UpdateFeedState(f1.Id, model.UpdateFeedStateParams{LastError: &errMsg}) + s.UpdateFeedState(f2.Id, model.UpdateFeedStateParams{HTTPEtag: ptr("e")}) states, err := s.ListFeedStates() if err != nil { diff --git a/src/storage/sqlite/folder.go b/src/storage/sqlite/folder.go index 9ce66ea..33d7971 100644 --- a/src/storage/sqlite/folder.go +++ b/src/storage/sqlite/folder.go @@ -3,9 +3,11 @@ package sqlite import ( "database/sql" "log" + + "github.com/nkanaev/yarr/src/storage/model" ) -func (s *SQLiteStorage) CreateFolder(title string) *Folder { +func (s *SQLiteStorage) CreateFolder(title string) *model.Folder { expanded := true row := s.db.QueryRow(` insert into folders (title, is_expanded) values (:title, :is_expanded) @@ -21,7 +23,7 @@ func (s *SQLiteStorage) CreateFolder(title string) *Folder { log.Print(err) return nil } - return &Folder{Id: id, Title: title, IsExpanded: expanded} + return &model.Folder{Id: id, Title: title, IsExpanded: expanded} } func (s *SQLiteStorage) DeleteFolder(folderId int64) bool { @@ -32,7 +34,7 @@ func (s *SQLiteStorage) DeleteFolder(folderId int64) bool { return err == nil } -func (s *SQLiteStorage) UpdateFolder(folderId int64, params UpdateFolderParams) (bool, error) { +func (s *SQLiteStorage) UpdateFolder(folderId int64, params model.UpdateFolderParams) (bool, error) { _, err := s.db.Exec(` update folders set title = coalesce(:title, title), @@ -50,8 +52,8 @@ func (s *SQLiteStorage) UpdateFolder(folderId int64, params UpdateFolderParams) return true, nil } -func (s *SQLiteStorage) ListFolders() []Folder { - result := make([]Folder, 0) +func (s *SQLiteStorage) ListFolders() []model.Folder { + result := make([]model.Folder, 0) rows, err := s.db.Query(` select id, title, is_expanded from folders @@ -62,7 +64,7 @@ func (s *SQLiteStorage) ListFolders() []Folder { return result } for rows.Next() { - var f Folder + var f model.Folder err = rows.Scan(&f.Id, &f.Title, &f.IsExpanded) if err != nil { log.Print(err) diff --git a/src/storage/sqlite/folder_test.go b/src/storage/sqlite/folder_test.go index 2912f0d..de84a01 100644 --- a/src/storage/sqlite/folder_test.go +++ b/src/storage/sqlite/folder_test.go @@ -2,6 +2,8 @@ package sqlite import ( "testing" + + "github.com/nkanaev/yarr/src/storage/model" ) func TestUpdateFolder(t *testing.T) { @@ -13,7 +15,7 @@ func TestUpdateFolder(t *testing.T) { t.Run("rename only", func(t *testing.T) { newTitle := "new title" - ok, err := db.UpdateFolder(folder.Id, UpdateFolderParams{ + ok, err := db.UpdateFolder(folder.Id, model.UpdateFolderParams{ Title: &newTitle, }) if !ok || err != nil { @@ -31,7 +33,7 @@ func TestUpdateFolder(t *testing.T) { t.Run("toggle expanded only", func(t *testing.T) { isExpanded := false - ok, err := db.UpdateFolder(folder.Id, UpdateFolderParams{ + ok, err := db.UpdateFolder(folder.Id, model.UpdateFolderParams{ IsExpanded: &isExpanded, }) if !ok || err != nil { @@ -50,7 +52,7 @@ func TestUpdateFolder(t *testing.T) { t.Run("update both", func(t *testing.T) { bothTitle := "both" isExpanded := true - ok, err := db.UpdateFolder(folder.Id, UpdateFolderParams{ + ok, err := db.UpdateFolder(folder.Id, model.UpdateFolderParams{ Title: &bothTitle, IsExpanded: &isExpanded, }) @@ -65,7 +67,7 @@ func TestUpdateFolder(t *testing.T) { }) t.Run("update none", func(t *testing.T) { - ok, err := db.UpdateFolder(folder.Id, UpdateFolderParams{}) + ok, err := db.UpdateFolder(folder.Id, model.UpdateFolderParams{}) if !ok || err != nil { t.Fatalf("UpdateFolder failed: %v", err) } diff --git a/src/storage/sqlite/item.go b/src/storage/sqlite/item.go index f161fba..fbb0762 100644 --- a/src/storage/sqlite/item.go +++ b/src/storage/sqlite/item.go @@ -14,7 +14,8 @@ import ( "github.com/nkanaev/yarr/src/storage/model" ) -// TODO: serialize/deserialize +type MediaLinks model.MediaLinks + func (m *MediaLinks) Scan(src any) error { switch data := src.(type) { case []byte: @@ -30,7 +31,7 @@ func (m MediaLinks) Value() (driver.Value, error) { return json.Marshal(m) } -func (s *SQLiteStorage) CreateItems(items []Item) bool { +func (s *SQLiteStorage) CreateItems(items []model.Item) bool { tx, err := s.db.Begin() if err != nil { log.Print(err) @@ -65,7 +66,7 @@ func (s *SQLiteStorage) CreateItems(items []Item) bool { sql.Named("link", item.Link), sql.Named("date", item.Date), sql.Named("content", item.Content), - sql.Named("media_links", item.MediaLinks), + sql.Named("media_links", MediaLinks(item.MediaLinks)), sql.Named("date_arrived", now), sql.Named("last_arrived", now), sql.Named("status", model.UNREAD), @@ -86,7 +87,7 @@ func (s *SQLiteStorage) CreateItems(items []Item) bool { return true } -func listQueryPredicate(filter ItemFilter, newestFirst bool) (string, []any) { +func listQueryPredicate(filter model.ItemFilter, newestFirst bool) (string, []any) { cond := make([]string, 0) args := make([]any, 0) if filter.FolderID != nil { @@ -169,13 +170,13 @@ func (s *SQLiteStorage) CountItems() int { } func (s *SQLiteStorage) ListItems( - filter ItemFilter, + filter model.ItemFilter, limit int, newestFirst bool, withContent bool, -) []Item { +) []model.Item { predicate, args := listQueryPredicate(filter, newestFirst) - result := make([]Item, 0) + result := make([]model.Item, 0) order := "date desc, id desc" if !newestFirst { @@ -207,11 +208,11 @@ func (s *SQLiteStorage) ListItems( return result } for rows.Next() { - var x Item + var x model.Item err = rows.Scan( &x.Id, &x.GUID, &x.FeedId, &x.Title, &x.Link, &x.Date, - &x.Status, &x.MediaLinks, &x.Content, + &x.Status, (*MediaLinks)(&x.MediaLinks), &x.Content, ) if err != nil { log.Print(err) @@ -222,8 +223,8 @@ func (s *SQLiteStorage) ListItems( return result } -func (s *SQLiteStorage) GetItem(id int64) *Item { - i := &Item{} +func (s *SQLiteStorage) GetItem(id int64) *model.Item { + i := &model.Item{} err := s.db.QueryRow(` select i.id, i.guid, i.feed_id, i.title, i.link, i.content, @@ -232,7 +233,7 @@ func (s *SQLiteStorage) GetItem(id int64) *Item { where i.id = :id `, sql.Named("id", id)).Scan( &i.Id, &i.GUID, &i.FeedId, &i.Title, &i.Link, &i.Content, - &i.Date, &i.Status, &i.MediaLinks, + &i.Date, &i.Status, (*MediaLinks)(&i.MediaLinks), ) if err != nil { log.Print(err) @@ -241,7 +242,7 @@ func (s *SQLiteStorage) GetItem(id int64) *Item { return i } -func (s *SQLiteStorage) UpdateItemStatus(item_id int64, status ItemStatus) bool { +func (s *SQLiteStorage) UpdateItemStatus(item_id int64, status model.ItemStatus) bool { _, err := s.db.Exec(`update items set status = :status where id = :id`, sql.Named("status", status), sql.Named("id", item_id), @@ -249,8 +250,8 @@ func (s *SQLiteStorage) UpdateItemStatus(item_id int64, status ItemStatus) bool return err == nil } -func (s *SQLiteStorage) MarkItemsRead(filter MarkFilter) bool { - predicate, args := listQueryPredicate(ItemFilter{ +func (s *SQLiteStorage) MarkItemsRead(filter model.MarkFilter) bool { + predicate, args := listQueryPredicate(model.ItemFilter{ FolderID: filter.FolderID, FeedID: filter.FeedID, Before: filter.Before, @@ -258,7 +259,7 @@ func (s *SQLiteStorage) MarkItemsRead(filter MarkFilter) bool { query := fmt.Sprintf(` update items as i set status = %d where %s and i.status != %d - `, READ, predicate, STARRED) + `, model.READ, predicate, model.STARRED) _, err := s.db.Exec(query, args...) if err != nil { log.Print(err) @@ -266,14 +267,8 @@ func (s *SQLiteStorage) MarkItemsRead(filter MarkFilter) bool { return err == nil } -type FeedStat struct { - FeedId int64 `json:"feed_id"` - UnreadCount int64 `json:"unread"` - StarredCount int64 `json:"starred"` -} - -func (s *SQLiteStorage) FeedStats() []FeedStat { - result := make([]FeedStat, 0) +func (s *SQLiteStorage) FeedStats() []model.FeedStat { + result := make([]model.FeedStat, 0) rows, err := s.db.Query(fmt.Sprintf(` select feed_id, @@ -281,13 +276,13 @@ func (s *SQLiteStorage) FeedStats() []FeedStat { sum(case status when %d then 1 else 0 end) from items group by feed_id - `, UNREAD, STARRED)) + `, model.UNREAD, model.STARRED)) if err != nil { log.Print(err) return result } for rows.Next() { - stat := FeedStat{} + stat := model.FeedStat{} rows.Scan(&stat.FeedId, &stat.UnreadCount, &stat.StarredCount) result = append(result, stat) } @@ -322,7 +317,7 @@ func (s *SQLiteStorage) DeleteOldItems() { where rn > :keep_size and last_arrived < datetime(max_la, :keep_days_limit) )`, - sql.Named("starred_status", STARRED), + sql.Named("starred_status", model.STARRED), sql.Named("keep_size", itemsKeepSize), sql.Named("keep_days_limit", fmt.Sprintf("-%d days", itemsKeepDays)), ) diff --git a/src/storage/sqlite/item_test.go b/src/storage/sqlite/item_test.go index 06a5180..a5fac68 100644 --- a/src/storage/sqlite/item_test.go +++ b/src/storage/sqlite/item_test.go @@ -8,6 +8,8 @@ import ( "testing" "testing/synctest" "time" + + "github.com/nkanaev/yarr/src/storage/model" ) /* @@ -30,22 +32,22 @@ import ( */ type testItemScope struct { - feed11, feed12 *Feed - feed21, feed01 *Feed - folder1, folder2 *Folder + feed11, feed12 *model.Feed + feed21, feed01 *model.Feed + folder1, folder2 *model.Folder } func testItemsSetup(db *SQLiteStorage) testItemScope { folder1 := db.CreateFolder("folder1") folder2 := db.CreateFolder("folder2") - feed11 := db.CreateFeed(CreateFeedParams{Title: "feed11", FeedLink: "http://test.com/feed11.xml", FolderID: &folder1.Id}) - feed12 := db.CreateFeed(CreateFeedParams{Title: "feed12", FeedLink: "http://test.com/feed12.xml", FolderID: &folder1.Id}) - feed21 := db.CreateFeed(CreateFeedParams{Title: "feed21", FeedLink: "http://test.com/feed21.xml", FolderID: &folder2.Id}) - feed01 := db.CreateFeed(CreateFeedParams{Title: "feed01", FeedLink: "http://test.com/feed01.xml"}) + feed11 := db.CreateFeed(model.CreateFeedParams{Title: "feed11", FeedLink: "http://test.com/feed11.xml", FolderID: &folder1.Id}) + feed12 := db.CreateFeed(model.CreateFeedParams{Title: "feed12", FeedLink: "http://test.com/feed12.xml", FolderID: &folder1.Id}) + feed21 := db.CreateFeed(model.CreateFeedParams{Title: "feed21", FeedLink: "http://test.com/feed21.xml", FolderID: &folder2.Id}) + feed01 := db.CreateFeed(model.CreateFeedParams{Title: "feed01", FeedLink: "http://test.com/feed01.xml"}) now := time.Now() - db.CreateItems([]Item{ + db.CreateItems([]model.Item{ // feed11 {GUID: "item111", FeedId: feed11.Id, Title: "title111", Date: now.Add(time.Hour * 24 * 1)}, { @@ -98,11 +100,11 @@ func testItemsSetup(db *SQLiteStorage) testItemScope { }) db.db.Exec( `update items set status = :status where guid in ("item112", "item122", "item211", "item012")`, - sql.Named("status", READ), + sql.Named("status", model.READ), ) db.db.Exec( `update items set status = :status where guid in ("item113", "item212", "item013")`, - sql.Named("status", STARRED), + sql.Named("status", model.STARRED), ) return testItemScope{ @@ -115,8 +117,8 @@ func testItemsSetup(db *SQLiteStorage) testItemScope { } } -func getItem(db *SQLiteStorage, guid string) *Item { - i := &Item{} +func getItem(db *SQLiteStorage, guid string) *model.Item { + i := &model.Item{} err := db.db.QueryRow(` select i.id, i.guid, i.feed_id, i.title, i.link, i.content, @@ -125,7 +127,7 @@ func getItem(db *SQLiteStorage, guid string) *Item { where i.guid = :guid `, sql.Named("guid", guid)).Scan( &i.Id, &i.GUID, &i.FeedId, &i.Title, &i.Link, &i.Content, - &i.Date, &i.Status, &i.MediaLinks, + &i.Date, &i.Status, (*MediaLinks)(&i.MediaLinks), ) if err != nil { log.Fatal(err) @@ -133,7 +135,7 @@ func getItem(db *SQLiteStorage, guid string) *Item { return i } -func getItemGuids(items []Item) []string { +func getItemGuids(items []model.Item) []string { guids := make([]string, 0) for _, item := range items { guids = append(guids, item.GUID) @@ -147,7 +149,7 @@ func TestListItems(t *testing.T) { // filter by folder_id - have := getItemGuids(db.ListItems(ItemFilter{FolderID: &scope.folder1.Id}, 10, false, false)) + have := getItemGuids(db.ListItems(model.ItemFilter{FolderID: &scope.folder1.Id}, 10, false, false)) want := []string{"item111", "item112", "item113", "item121", "item122"} if !reflect.DeepEqual(have, want) { t.Logf("want: %#v", want) @@ -155,7 +157,7 @@ func TestListItems(t *testing.T) { t.Fail() } - have = getItemGuids(db.ListItems(ItemFilter{FolderID: &scope.folder2.Id}, 10, false, false)) + have = getItemGuids(db.ListItems(model.ItemFilter{FolderID: &scope.folder2.Id}, 10, false, false)) want = []string{"item211", "item212"} if !reflect.DeepEqual(have, want) { t.Logf("want: %#v", want) @@ -165,7 +167,7 @@ func TestListItems(t *testing.T) { // filter by feed_id - have = getItemGuids(db.ListItems(ItemFilter{FeedID: &scope.feed11.Id}, 10, false, false)) + have = getItemGuids(db.ListItems(model.ItemFilter{FeedID: &scope.feed11.Id}, 10, false, false)) want = []string{"item111", "item112", "item113"} if !reflect.DeepEqual(have, want) { t.Logf("want: %#v", want) @@ -173,7 +175,7 @@ func TestListItems(t *testing.T) { t.Fail() } - have = getItemGuids(db.ListItems(ItemFilter{FeedID: &scope.feed01.Id}, 10, false, false)) + have = getItemGuids(db.ListItems(model.ItemFilter{FeedID: &scope.feed01.Id}, 10, false, false)) want = []string{"item011", "item012", "item013"} if !reflect.DeepEqual(have, want) { t.Logf("want: %#v", want) @@ -183,8 +185,8 @@ func TestListItems(t *testing.T) { // filter by status - var starred ItemStatus = STARRED - have = getItemGuids(db.ListItems(ItemFilter{Status: &starred}, 10, false, false)) + var starred model.ItemStatus = model.STARRED + have = getItemGuids(db.ListItems(model.ItemFilter{Status: &starred}, 10, false, false)) want = []string{"item113", "item212", "item013"} if !reflect.DeepEqual(have, want) { t.Logf("want: %#v", want) @@ -192,8 +194,8 @@ func TestListItems(t *testing.T) { t.Fail() } - var unread ItemStatus = UNREAD - have = getItemGuids(db.ListItems(ItemFilter{Status: &unread}, 10, false, false)) + var unread model.ItemStatus = model.UNREAD + have = getItemGuids(db.ListItems(model.ItemFilter{Status: &unread}, 10, false, false)) want = []string{"item111", "item121", "item011"} if !reflect.DeepEqual(have, want) { t.Logf("want: %#v", want) @@ -203,7 +205,7 @@ func TestListItems(t *testing.T) { // limit - have = getItemGuids(db.ListItems(ItemFilter{}, 2, false, false)) + have = getItemGuids(db.ListItems(model.ItemFilter{}, 2, false, false)) want = []string{"item111", "item112"} if !reflect.DeepEqual(have, want) { t.Logf("want: %#v", want) @@ -213,7 +215,7 @@ func TestListItems(t *testing.T) { // filter by search search1 := "title111" - have = getItemGuids(db.ListItems(ItemFilter{Search: &search1}, 4, true, false)) + have = getItemGuids(db.ListItems(model.ItemFilter{Search: &search1}, 4, true, false)) want = []string{"item111"} if !reflect.DeepEqual(have, want) { t.Logf("want: %#v", want) @@ -222,7 +224,7 @@ func TestListItems(t *testing.T) { } // sort by date - have = getItemGuids(db.ListItems(ItemFilter{}, 4, true, false)) + have = getItemGuids(db.ListItems(model.ItemFilter{}, 4, true, false)) want = []string{"item013", "item012", "item011", "item212"} if !reflect.DeepEqual(have, want) { t.Logf("want: %#v", want) @@ -239,7 +241,7 @@ func TestListItemsPaginated(t *testing.T) { item121 := getItem(db, "item121") // all, newest first - have := getItemGuids(db.ListItems(ItemFilter{After: &item012.Id}, 3, true, false)) + have := getItemGuids(db.ListItems(model.ItemFilter{After: &item012.Id}, 3, true, false)) want := []string{"item011", "item212", "item211"} if !reflect.DeepEqual(have, want) { t.Logf("want: %#v", want) @@ -248,9 +250,9 @@ func TestListItemsPaginated(t *testing.T) { } // unread, newest first - unread := UNREAD + unread := model.UNREAD have = getItemGuids( - db.ListItems(ItemFilter{After: &item012.Id, Status: &unread}, 3, true, false), + db.ListItems(model.ItemFilter{After: &item012.Id, Status: &unread}, 3, true, false), ) want = []string{"item011", "item121", "item111"} if !reflect.DeepEqual(have, want) { @@ -260,9 +262,9 @@ func TestListItemsPaginated(t *testing.T) { } // starred, oldest first - starred := STARRED + starred := model.STARRED have = getItemGuids( - db.ListItems(ItemFilter{After: &item121.Id, Status: &starred}, 3, false, false), + db.ListItems(model.ItemFilter{After: &item121.Id, Status: &starred}, 3, false, false), ) want = []string{"item212", "item013"} if !reflect.DeepEqual(have, want) { @@ -274,12 +276,12 @@ func TestListItemsPaginated(t *testing.T) { func TestMarkItemsRead(t *testing.T) { // NOTE: starred items must not be marked as read - var read ItemStatus = READ + var read model.ItemStatus = model.READ db1 := testDB() testItemsSetup(db1) - db1.MarkItemsRead(MarkFilter{}) - have := getItemGuids(db1.ListItems(ItemFilter{Status: &read}, 10, false, false)) + db1.MarkItemsRead(model.MarkFilter{}) + have := getItemGuids(db1.ListItems(model.ItemFilter{Status: &read}, 10, false, false)) want := []string{ "item111", "item112", "item121", "item122", "item211", "item011", "item012", @@ -292,8 +294,8 @@ func TestMarkItemsRead(t *testing.T) { db2 := testDB() scope2 := testItemsSetup(db2) - db2.MarkItemsRead(MarkFilter{FolderID: &scope2.folder1.Id}) - have = getItemGuids(db2.ListItems(ItemFilter{Status: &read}, 10, false, false)) + db2.MarkItemsRead(model.MarkFilter{FolderID: &scope2.folder1.Id}) + have = getItemGuids(db2.ListItems(model.ItemFilter{Status: &read}, 10, false, false)) want = []string{ "item111", "item112", "item121", "item122", "item211", "item012", @@ -306,8 +308,8 @@ func TestMarkItemsRead(t *testing.T) { db3 := testDB() scope3 := testItemsSetup(db3) - db3.MarkItemsRead(MarkFilter{FeedID: &scope3.feed11.Id}) - have = getItemGuids(db3.ListItems(ItemFilter{Status: &read}, 10, false, false)) + db3.MarkItemsRead(model.MarkFilter{FeedID: &scope3.feed11.Id}) + have = getItemGuids(db3.ListItems(model.ItemFilter{Status: &read}, 10, false, false)) want = []string{ "item111", "item112", "item122", "item211", "item012", @@ -321,14 +323,14 @@ func TestMarkItemsRead(t *testing.T) { func TestDeleteOldItems(t *testing.T) { now := time.Now().UTC() - starred := STARRED + starred := model.STARRED t.Run("keeps at least 50 items", func(t *testing.T) { db := testDB() - feed := db.CreateFeed(CreateFeedParams{Title: "f", FeedLink: "http://f.xml"}) - items := make([]Item, 100) + feed := db.CreateFeed(model.CreateFeedParams{Title: "f", FeedLink: "http://f.xml"}) + items := make([]model.Item, 100) for i := range 100 { - items[i] = Item{GUID: strconv.Itoa(i), FeedId: feed.Id, Date: now.Add(time.Duration(i) * time.Hour * 24)} + items[i] = model.Item{GUID: strconv.Itoa(i), FeedId: feed.Id, Date: now.Add(time.Duration(i) * time.Hour * 24)} } db.CreateItems(items) @@ -346,10 +348,10 @@ func TestDeleteOldItems(t *testing.T) { t.Run("keeps all less than 90 days old", func(t *testing.T) { db := testDB() - feed := db.CreateFeed(CreateFeedParams{Title: "f", FeedLink: "http://f.xml"}) - items := make([]Item, 100) + feed := db.CreateFeed(model.CreateFeedParams{Title: "f", FeedLink: "http://f.xml"}) + items := make([]model.Item, 100) for i := 0; i < 100; i++ { - items[i] = Item{GUID: strconv.Itoa(i), FeedId: feed.Id, Date: now.Add(time.Duration(i) * time.Second)} + items[i] = model.Item{GUID: strconv.Itoa(i), FeedId: feed.Id, Date: now.Add(time.Duration(i) * time.Second)} } db.CreateItems(items) @@ -368,10 +370,10 @@ func TestDeleteOldItems(t *testing.T) { t.Run("keeps starred", func(t *testing.T) { db := testDB() - feed := db.CreateFeed(CreateFeedParams{Title: "f", FeedLink: "http://f.xml"}) - items := make([]Item, 100) + feed := db.CreateFeed(model.CreateFeedParams{Title: "f", FeedLink: "http://f.xml"}) + items := make([]model.Item, 100) for i := 0; i < 100; i++ { - items[i] = Item{GUID: strconv.Itoa(i), FeedId: feed.Id, Date: now.Add(time.Duration(i) * time.Second)} + items[i] = model.Item{GUID: strconv.Itoa(i), FeedId: feed.Id, Date: now.Add(time.Duration(i) * time.Second)} } db.CreateItems(items) @@ -397,9 +399,9 @@ func TestCreateItemsLastArrived(t *testing.T) { synctest.Test(t, func(t *testing.T) { db := testDB() defer db.db.Close() - feed := db.CreateFeed(CreateFeedParams{Title: "test feed", FeedLink: "http://example.com/feed"}) + feed := db.CreateFeed(model.CreateFeedParams{Title: "test feed", FeedLink: "http://example.com/feed"}) - item := Item{ + item := model.Item{ GUID: "item1", FeedId: feed.Id, Title: "Title 1", @@ -407,7 +409,7 @@ func TestCreateItemsLastArrived(t *testing.T) { } // 1. Initial creation - db.CreateItems([]Item{item}) + db.CreateItems([]model.Item{item}) var lastArrived1 time.Time err := db.db.QueryRow("select last_arrived from items where guid = ?", item.GUID).Scan(&lastArrived1) @@ -418,7 +420,7 @@ func TestCreateItemsLastArrived(t *testing.T) { time.Sleep(time.Second * 10) // 2. Update on conflict - db.CreateItems([]Item{item}) + db.CreateItems([]model.Item{item}) var lastArrived2 time.Time err = db.db.QueryRow("select last_arrived from items where guid = ?", item.GUID).Scan(&lastArrived2) @@ -435,9 +437,9 @@ func TestCreateItemsLastArrived(t *testing.T) { func TestSearch(t *testing.T) { db := testDB() defer db.Close() - feed := db.CreateFeed(CreateFeedParams{Title: "f", FeedLink: "http://f.xml"}) + feed := db.CreateFeed(model.CreateFeedParams{Title: "f", FeedLink: "http://f.xml"}) - db.CreateItems([]Item{ + db.CreateItems([]model.Item{ { GUID: "i1", FeedId: feed.Id, @@ -460,40 +462,40 @@ func TestSearch(t *testing.T) { // 1. Basic search s1 := "emergency" - have := getItemGuids(db.ListItems(ItemFilter{Search: &s1}, 10, true, false)) + have := getItemGuids(db.ListItems(model.ItemFilter{Search: &s1}, 10, true, false)) if !reflect.DeepEqual(have, []string{"i1"}) { t.Errorf("basic search failed: expected [i1], got %v", have) } // 2. HTML stripping: Should find text, but NOT the tags s2 := "test" - have = getItemGuids(db.ListItems(ItemFilter{Search: &s2}, 10, true, false)) + have = getItemGuids(db.ListItems(model.ItemFilter{Search: &s2}, 10, true, false)) if !reflect.DeepEqual(have, []string{"i1"}) { t.Errorf("html text search failed: expected [i1], got %v", have) } s3 := "secret-class" - have = getItemGuids(db.ListItems(ItemFilter{Search: &s3}, 10, true, false)) + have = getItemGuids(db.ListItems(model.ItemFilter{Search: &s3}, 10, true, false)) if len(have) > 0 { t.Errorf("html tag search should have failed but found: %v", have) } // 3. Multi-word (AND) s4 := "broadcast system" - have = getItemGuids(db.ListItems(ItemFilter{Search: &s4}, 10, true, false)) + have = getItemGuids(db.ListItems(model.ItemFilter{Search: &s4}, 10, true, false)) if !reflect.DeepEqual(have, []string{"i1"}) { t.Errorf("multi-word search failed: expected [i1], got %v", have) } // 4. Unicode s5 := "Привет" - have = getItemGuids(db.ListItems(ItemFilter{Search: &s5}, 10, true, false)) + have = getItemGuids(db.ListItems(model.ItemFilter{Search: &s5}, 10, true, false)) if !reflect.DeepEqual(have, []string{"i2"}) { t.Errorf("unicode search failed: expected [i2], got %v", have) } s6 := "世界" - have = getItemGuids(db.ListItems(ItemFilter{Search: &s6}, 10, true, false)) + have = getItemGuids(db.ListItems(model.ItemFilter{Search: &s6}, 10, true, false)) if !reflect.DeepEqual(have, []string{"i2"}) { t.Errorf("unicode search (CJK) failed: expected [i2], got %v", have) } @@ -501,14 +503,14 @@ func TestSearch(t *testing.T) { // 5. Trigger: Update db.db.Exec("update items set title = 'Updated Title' where guid = 'i1'") s7 := "Updated" - have = getItemGuids(db.ListItems(ItemFilter{Search: &s7}, 10, true, false)) + have = getItemGuids(db.ListItems(model.ItemFilter{Search: &s7}, 10, true, false)) if !reflect.DeepEqual(have, []string{"i1"}) { t.Errorf("update trigger failed: expected [i1], got %v", have) } // 6. Trigger: Delete db.db.Exec("delete from items where guid = 'i1'") - have = getItemGuids(db.ListItems(ItemFilter{Search: &s7}, 10, true, false)) + have = getItemGuids(db.ListItems(model.ItemFilter{Search: &s7}, 10, true, false)) if len(have) > 0 { t.Errorf("delete trigger failed: found deleted item: %v", have) } diff --git a/src/storage/sqlite/settings.go b/src/storage/sqlite/settings.go index c1e65ac..fac3d82 100644 --- a/src/storage/sqlite/settings.go +++ b/src/storage/sqlite/settings.go @@ -4,10 +4,12 @@ import ( "database/sql" "encoding/json" "log" + + "github.com/nkanaev/yarr/src/storage/model" ) -func settingsDefaults() Settings { - return Settings{ +func settingsDefaults() model.Settings { + return model.Settings{ Filter: "", Feed: "", FeedListWidth: 300, @@ -21,7 +23,7 @@ func settingsDefaults() Settings { } } -func (s *SQLiteStorage) GetSettings() Settings { +func (s *SQLiteStorage) GetSettings() model.Settings { result := settingsDefaults() rows, err := s.db.Query(`select key, val from settings;`) if err != nil { @@ -61,7 +63,7 @@ func (s *SQLiteStorage) GetSettings() Settings { return result } -func (s *SQLiteStorage) UpdateSettings(params UpdateSettingsParams) bool { +func (s *SQLiteStorage) UpdateSettings(params model.UpdateSettingsParams) bool { tx, err := s.db.Begin() if err != nil { log.Print(err) diff --git a/src/storage/sqlite/settings_test.go b/src/storage/sqlite/settings_test.go index 1251de0..aaf5497 100644 --- a/src/storage/sqlite/settings_test.go +++ b/src/storage/sqlite/settings_test.go @@ -4,6 +4,8 @@ import ( "reflect" "strings" "testing" + + "github.com/nkanaev/yarr/src/storage/model" ) func TestSettingsDefaults(t *testing.T) { @@ -22,7 +24,7 @@ func TestUpdateSettings(t *testing.T) { s := testDB() defer s.Close() - params := UpdateSettingsParams{ + params := model.UpdateSettingsParams{ ThemeName: ptr("night"), FeedListWidth: ptr(400), RefreshRate: ptr(int64(15)), @@ -49,7 +51,7 @@ func TestGetSettings(t *testing.T) { s := testDB() defer s.Close() - s.UpdateSettings(UpdateSettingsParams{Language: ptr("fr")}) + s.UpdateSettings(model.UpdateSettingsParams{Language: ptr("fr")}) settings := s.GetSettings() if settings.Language != "fr" { @@ -64,8 +66,8 @@ func TestSettingsExhaustive(t *testing.T) { s := testDB() defer s.Close() - settingsType := reflect.TypeOf(Settings{}) - paramsType := reflect.TypeOf(UpdateSettingsParams{}) + settingsType := reflect.TypeOf(model.Settings{}) + paramsType := reflect.TypeOf(model.UpdateSettingsParams{}) settings := s.GetSettings() m := settings.Map() @@ -125,7 +127,7 @@ func TestSettingsExhaustive(t *testing.T) { } } - if ok := s.UpdateSettings(paramsValue.Interface().(UpdateSettingsParams)); !ok { + if ok := s.UpdateSettings(paramsValue.Interface().(model.UpdateSettingsParams)); !ok { t.Errorf("UpdateSettings failed for %q", jsonKey) }