From 5863e1ece6cdf4ef75e5d679f256925acb5e7c0e Mon Sep 17 00:00:00 2001 From: Graham Vasquez Date: Mon, 13 Oct 2025 01:48:42 -0400 Subject: [PATCH 1/3] fix: allowing reading items from preview feeds - Added another constructor for an in memory sqlite db for previews - Some small error handling cleanup - A bit of other conditionals to handle preview feeds --- cmd/nom/main.go | 10 +++++-- internal/commands/commands.go | 13 ++++----- internal/commands/feeds.go | 11 +++++-- internal/commands/tui.go | 1 - internal/store/store.go | 54 ++++++++++++++++++++++++----------- 5 files changed, 59 insertions(+), 30 deletions(-) diff --git a/cmd/nom/main.go b/cmd/nom/main.go index b534073..6ba2140 100755 --- a/cmd/nom/main.go +++ b/cmd/nom/main.go @@ -8,6 +8,7 @@ import ( "github.com/guyfedwards/nom/v2/internal/commands" "github.com/guyfedwards/nom/v2/internal/config" + "github.com/guyfedwards/nom/v2/internal/constants" "github.com/guyfedwards/nom/v2/internal/store" ) @@ -123,8 +124,13 @@ func getCmds() (*commands.Commands, error) { if err = cfg.Load(); err != nil { return nil, err } - - s, err := store.NewSQLiteStore(cfg.ConfigDir, cfg.Database) + var s store.Store + if cfg.IsPreviewMode() { + cfg.Ordering = constants.DescendingOrdering + s, err = store.NewInMemorySQLiteStore() + } else { + s, err = store.NewSQLiteStore(cfg.ConfigDir, cfg.Database) + } if err != nil { return nil, fmt.Errorf("main.go: %w", err) } diff --git a/internal/commands/commands.go b/internal/commands/commands.go index 0d9638e..29c6c57 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -248,15 +248,14 @@ func (c Commands) fetchAllFeeds() ([]store.Item, []ErrorItem, error) { Title: r.Title, } - // only store if non-preview feed - if !includes(c.config.PreviewFeeds, config.Feed{URL: result.url}) { - err := c.store.UpsertItem(i) - if err != nil { - log.Fatalf("[commands.go] fetchAllFeeds: %e", err) - continue - } + id, err := c.store.UpsertItem(i) + if err != nil { + log.Fatalf("[commands.go] fetchAllFeeds: %e", err) + continue } + i.ID = id + items = append(items, i) } } diff --git a/internal/commands/feeds.go b/internal/commands/feeds.go index 1c700af..120bea4 100644 --- a/internal/commands/feeds.go +++ b/internal/commands/feeds.go @@ -17,9 +17,16 @@ func (c Commands) CleanFeeds() error { var urlsToRemove []string + var feeds []config.Feed + if c.config.IsPreviewMode() { + feeds = c.config.PreviewFeeds + } else { + feeds = c.config.Feeds + } + for _, u := range urls { inFeeds := false - for _, f := range c.config.Feeds { + for _, f := range feeds { if f.URL == u { inFeeds = true } @@ -29,7 +36,6 @@ func (c Commands) CleanFeeds() error { urlsToRemove = append(urlsToRemove, u) } } - for _, url := range urlsToRemove { err := c.store.DeleteByFeedURL(url, false) if err != nil { @@ -100,7 +106,6 @@ func fetchFeed(ch chan FetchResultError, wg *sync.WaitGroup, feed config.Feed, h defer wg.Done() r, err := rss.Fetch(feed, httpOpts, version) - if err != nil { ch <- FetchResultError{res: rss.RSS{}, err: err, url: feed.URL} return diff --git a/internal/commands/tui.go b/internal/commands/tui.go index a68f9ac..07f4cfd 100644 --- a/internal/commands/tui.go +++ b/internal/commands/tui.go @@ -217,7 +217,6 @@ func (c *Commands) TUI() error { } prog, err := Render(items, c, es, c.config) - if err != nil { return fmt.Errorf("commands.TUI: %w", err) } diff --git a/internal/store/store.go b/internal/store/store.go index f164ac3..c189267 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -33,7 +33,7 @@ func (i Item) Read() bool { } type Store interface { - UpsertItem(item Item) error + UpsertItem(item Item) (int, error) BeginBatch() error EndBatch() error GetAllItems(ordering string) ([]Item, error) @@ -52,6 +52,27 @@ type SQLiteStore struct { batch *sql.Tx } +func NewInMemorySQLiteStore() (*SQLiteStore, error) { + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + return nil, fmt.Errorf("NewInMemorySQLiteStore: %w", err) + } + + err = dbSetup(db) + if err != nil { + return nil, fmt.Errorf("NewInMemorySQLiteStore: %w", err) + } + + err = runMigrations(db) + if err != nil { + return nil, fmt.Errorf("NewInMemorySQLiteStore: %w", err) + } + + return &SQLiteStore{ + db: db, + }, nil +} + func NewSQLiteStore(basePath string, dbName string) (*SQLiteStore, error) { dbpath := filepath.Join(basePath, dbName) @@ -59,7 +80,7 @@ func NewSQLiteStore(basePath string, dbName string) (*SQLiteStore, error) { db, err := sql.Open("sqlite3", dbpath) if err != nil { - return nil, fmt.Errorf("NewSQLiteCache: %w", err) + return nil, fmt.Errorf("NewSQLiteStore: %w", err) } // if there was no db file before we create the connection then we want to run @@ -67,13 +88,13 @@ func NewSQLiteStore(basePath string, dbName string) (*SQLiteStore, error) { if info == nil { err = dbSetup(db) if err != nil { - return nil, fmt.Errorf("NewSQLiteCache: %w", err) + return nil, fmt.Errorf("NewSQLiteStore: %w", err) } } err = runMigrations(db) if err != nil { - return nil, fmt.Errorf("NewSQLiteCache: %w", err) + return nil, fmt.Errorf("NewSQLiteStore: %w", err) } return &SQLiteStore{ @@ -177,7 +198,7 @@ func (sls *SQLiteStore) EndBatch() error { return nil } -func (sls *SQLiteStore) UpsertItem(item Item) error { +func (sls *SQLiteStore) UpsertItem(item Item) (int, error) { if sls.batch != nil { return sls.upsertItem(sls.batch, item) } @@ -189,42 +210,42 @@ type statementPreparer interface { Prepare(query string) (*sql.Stmt, error) } -func (sls *SQLiteStore) upsertItem(db statementPreparer, item Item) error { +func (sls *SQLiteStore) upsertItem(db statementPreparer, item Item) (int, error) { stmt, err := db.Prepare(`select count(id), id from items where feedurl = ? and title = ?;`) if err != nil { - return fmt.Errorf("sqlite.go: could not prepare query: %w", err) + return 0, fmt.Errorf("sqlite.go: could not prepare query: %w", err) } var count int var id sql.NullInt32 err = stmt.QueryRow(item.FeedURL, item.Title).Scan(&count, &id) if err != nil && !errors.Is(err, sql.ErrNoRows) { - return fmt.Errorf("store.go: write %w", err) + return 0, fmt.Errorf("store.go: write %w", err) } - if count == 0 { stmt, err = db.Prepare(`insert into items (feedurl, link, title, content, author, publishedat, createdat, updatedat) values (?, ?, ?, ?, ?, ?, ?, ?)`) if err != nil { - return fmt.Errorf("sqlite.go: could not prepare query: %w", err) + return 0, fmt.Errorf("sqlite.go: could not prepare query: %w", err) } - _, err = stmt.Exec(item.FeedURL, item.Link, item.Title, item.Content, item.Author, item.PublishedAt, time.Now(), time.Now()) + result, err := stmt.Exec(item.FeedURL, item.Link, item.Title, item.Content, item.Author, item.PublishedAt, time.Now(), time.Now()) if err != nil { - return fmt.Errorf("sqlite.go: Upsert failed: %w", err) + return 0, fmt.Errorf("sqlite.go: Upsert failed: %w", err) } + lastID, err := result.LastInsertId() + return int(lastID), err } else { stmt, err = db.Prepare(`update items set content = ?, updatedat = ? where id = ?`) if err != nil { - return fmt.Errorf("sqlite.go: could not prepare query: %w", err) + return 0, fmt.Errorf("sqlite.go: could not prepare query: %w", err) } _, err = stmt.Exec(item.Content, time.Now(), id) if err != nil { - return fmt.Errorf("sqlite.go: Upsert failed: %w", err) + return 0, fmt.Errorf("sqlite.go: Upsert failed: %w", err) } + return item.ID, nil } - - return nil } // TODO: pagination @@ -327,7 +348,6 @@ func (sls SQLiteStore) GetAllFeedURLs() ([]string, error) { } func (sls SQLiteStore) DeleteByFeedURL(feedurl string, incFavourites bool) error { - var stmt *sql.Stmt if incFavourites { stmt, _ = sls.db.Prepare(`delete from items where feedurl = ?;`) From 339dd4c6c9b8d025f6e3581c53366a1e16be3f0e Mon Sep 17 00:00:00 2001 From: Graham Vasquez Date: Mon, 13 Oct 2025 13:12:30 -0400 Subject: [PATCH 2/3] refactor: pass pointer to item for upsert instead of returning id --- internal/commands/commands.go | 4 +--- internal/store/store.go | 25 ++++++++++++++----------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/internal/commands/commands.go b/internal/commands/commands.go index 29c6c57..3ee6887 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -248,14 +248,12 @@ func (c Commands) fetchAllFeeds() ([]store.Item, []ErrorItem, error) { Title: r.Title, } - id, err := c.store.UpsertItem(i) + err := c.store.UpsertItem(&i) if err != nil { log.Fatalf("[commands.go] fetchAllFeeds: %e", err) continue } - i.ID = id - items = append(items, i) } } diff --git a/internal/store/store.go b/internal/store/store.go index c189267..4a968d6 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -33,7 +33,7 @@ func (i Item) Read() bool { } type Store interface { - UpsertItem(item Item) (int, error) + UpsertItem(item *Item) error BeginBatch() error EndBatch() error GetAllItems(ordering string) ([]Item, error) @@ -198,7 +198,7 @@ func (sls *SQLiteStore) EndBatch() error { return nil } -func (sls *SQLiteStore) UpsertItem(item Item) (int, error) { +func (sls *SQLiteStore) UpsertItem(item *Item) error { if sls.batch != nil { return sls.upsertItem(sls.batch, item) } @@ -210,42 +210,45 @@ type statementPreparer interface { Prepare(query string) (*sql.Stmt, error) } -func (sls *SQLiteStore) upsertItem(db statementPreparer, item Item) (int, error) { +func (sls *SQLiteStore) upsertItem(db statementPreparer, item *Item) error { stmt, err := db.Prepare(`select count(id), id from items where feedurl = ? and title = ?;`) if err != nil { - return 0, fmt.Errorf("sqlite.go: could not prepare query: %w", err) + return fmt.Errorf("sqlite.go: could not prepare query: %w", err) } var count int var id sql.NullInt32 err = stmt.QueryRow(item.FeedURL, item.Title).Scan(&count, &id) if err != nil && !errors.Is(err, sql.ErrNoRows) { - return 0, fmt.Errorf("store.go: write %w", err) + return fmt.Errorf("store.go: write %w", err) } if count == 0 { stmt, err = db.Prepare(`insert into items (feedurl, link, title, content, author, publishedat, createdat, updatedat) values (?, ?, ?, ?, ?, ?, ?, ?)`) if err != nil { - return 0, fmt.Errorf("sqlite.go: could not prepare query: %w", err) + return fmt.Errorf("sqlite.go: could not prepare query: %w", err) } result, err := stmt.Exec(item.FeedURL, item.Link, item.Title, item.Content, item.Author, item.PublishedAt, time.Now(), time.Now()) if err != nil { - return 0, fmt.Errorf("sqlite.go: Upsert failed: %w", err) + return fmt.Errorf("sqlite.go: Upsert failed: %w", err) } lastID, err := result.LastInsertId() - return int(lastID), err + if err != nil { + return fmt.Errorf("sqlite.go: No inserted ID: %w", err) + } + item.ID = int(lastID) } else { stmt, err = db.Prepare(`update items set content = ?, updatedat = ? where id = ?`) if err != nil { - return 0, fmt.Errorf("sqlite.go: could not prepare query: %w", err) + return fmt.Errorf("sqlite.go: could not prepare query: %w", err) } _, err = stmt.Exec(item.Content, time.Now(), id) if err != nil { - return 0, fmt.Errorf("sqlite.go: Upsert failed: %w", err) + return fmt.Errorf("sqlite.go: Upsert failed: %w", err) } - return item.ID, nil } + return nil } // TODO: pagination From 96f42059da2a06c91a74abf7d3d6b0875118584c Mon Sep 17 00:00:00 2001 From: Graham Vasquez Date: Mon, 13 Oct 2025 20:45:49 -0400 Subject: [PATCH 3/3] fix: use default ordering, fix bug with previews not using it --- cmd/nom/main.go | 2 -- internal/commands/tui.go | 8 +------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/cmd/nom/main.go b/cmd/nom/main.go index 6ba2140..a765439 100755 --- a/cmd/nom/main.go +++ b/cmd/nom/main.go @@ -8,7 +8,6 @@ import ( "github.com/guyfedwards/nom/v2/internal/commands" "github.com/guyfedwards/nom/v2/internal/config" - "github.com/guyfedwards/nom/v2/internal/constants" "github.com/guyfedwards/nom/v2/internal/store" ) @@ -126,7 +125,6 @@ func getCmds() (*commands.Commands, error) { } var s store.Store if cfg.IsPreviewMode() { - cfg.Ordering = constants.DescendingOrdering s, err = store.NewInMemorySQLiteStore() } else { s, err = store.NewSQLiteStore(cfg.ConfigDir, cfg.Database) diff --git a/internal/commands/tui.go b/internal/commands/tui.go index 07f4cfd..50a8823 100644 --- a/internal/commands/tui.go +++ b/internal/commands/tui.go @@ -191,13 +191,7 @@ func (c *Commands) TUI() error { var errorItems []ErrorItem // if no feeds in store, fetchAllFeeds, which will return previews - if len(c.config.PreviewFeeds) > 0 { - its, errorItems, err = c.fetchAllFeeds() - if err != nil { - return fmt.Errorf("[commands.go] TUI: %w", err) - } - // if no items, fetchAllFeeds and GetAllFeeds - } else if len(its) == 0 { + if len(c.config.PreviewFeeds) > 0 || len(its) == 0 { _, errorItems, err = c.fetchAllFeeds() if err != nil { return fmt.Errorf("[commands.go] TUI: %w", err)