diff --git a/core/source.go b/core/source.go index 3eb0c5f..689ddb3 100644 --- a/core/source.go +++ b/core/source.go @@ -1,9 +1,6 @@ package core import ( - "database/sql" - "encoding/json" - "errors" "fmt" "log" "time" @@ -55,196 +52,6 @@ func DeleteSource(db DB, name string) error { return err } -func AddItems(db DB, items []Item) error { - return db.Transact(func(tx DB) error { - stmt, err := tx.Prepare(` - insert into items (source, id, active, title, author, body, link, time, action) - values (?, ?, ?, ?, ?, ?, ?, ?, jsonb(?)) - `) - if err != nil { - return fmt.Errorf("failed to prepare insert: %v", err) - } - for _, item := range items { - actions, err := json.Marshal(item.Action) - if err != nil { - return fmt.Errorf("failed to marshal actions for %s/%s: %v", item.Source, item.Id, err) - } - _, err = stmt.Exec(item.Source, item.Id, true, item.Title, item.Author, item.Body, item.Link, item.Time, actions) - if err != nil { - return fmt.Errorf("failed to insert %s/%s: %v", item.Source, item.Id, err) - } - } - return nil - }) -} - -// Set fields in the new item to match the old item where the new item's fields are zero-valued. -// This allows sources to omit fields and let an action set them without a later fetch overwriting -// the value from the action, e.g. an on-create action archiving a page and setting the link to -// point to the archive. -func BackfillItem(new *Item, old *Item) { - new.Active = old.Active - new.Created = old.Created - if new.Author == "" { - new.Author = old.Author - } - if new.Body == "" { - new.Body = old.Body - } - if new.Link == "" { - new.Link = old.Link - } - if new.Time == 0 { - new.Time = old.Time - } - if new.Title == "" { - new.Title = old.Title - } -} - -func UpdateItems(db DB, items []Item) error { - return db.Transact(func(tx DB) error { - stmt, err := tx.Prepare(` - update items - set - title = ?, - author = ?, - body = ?, - link = ?, - time = ?, - action = jsonb(?) - where source = ? - and id = ? - `) - if err != nil { - return err - } - for _, item := range items { - actions, err := json.Marshal(item.Action) - if err != nil { - return fmt.Errorf("failed to marshal actions for %s/%s: %v", item.Source, item.Id, err) - } - _, err = stmt.Exec(item.Title, item.Author, item.Body, item.Link, item.Time, actions, item.Source, item.Id) - if err != nil { - return err - } - } - return nil - }) -} - -// Deactivate an item, returning its previous active state. -func DeactivateItem(db DB, source string, id string) (bool, error) { - row := db.QueryRow(` - select active - from items - where source = ? and id = ? - `, source, id) - var active bool - err := row.Scan(&active) - if err != nil && errors.Is(err, sql.ErrNoRows) { - return false, fmt.Errorf("item %s/%s not found", source, id) - } - - _, err = db.Exec(` - update items - set active = 0 - where source = ? and id = ? - `, source, id) - if err != nil { - return false, err - } - return active, nil -} - -func DeleteItem(db DB, source string, id string) (int64, error) { - res, err := db.Exec(` - delete from items - where source = ? - and id = ? - `, source, id) - if err != nil { - return 0, err - } - return res.RowsAffected() -} - -func getItems(db DB, query string, args ...any) ([]Item, error) { - rows, err := db.Query(query, args...) - if err != nil { - return nil, err - } - var items []Item - for rows.Next() { - var item Item - err = rows.Scan(&item.Source, &item.Id, &item.Created, &item.Active, &item.Title, &item.Author, &item.Body, &item.Link, &item.Time, &item.Action) - if err != nil { - return nil, err - } - items = append(items, item) - } - return items, nil -} - -func GetItem(db DB, source string, id string) (Item, error) { - items, err := getItems(db, ` - select source, id, created, active, title, author, body, link, time, json(action) - from items - where source = ? - and id = ? - order by case when time = 0 then created else time end, id - `, source, id) - if err != nil { - return Item{}, err - } - if len(items) == 0 { - return Item{}, fmt.Errorf("no item in %s with id %s", source, id) - } - return items[0], nil -} - -func GetAllActiveItems(db DB) ([]Item, error) { - return getItems(db, ` - select - source, id, created, active, title, author, body, link, time, json(action) - from items - where active <> 0 - order by case when time = 0 then created else time end, id - `) -} - -func GetAllItems(db DB) ([]Item, error) { - return getItems(db, ` - select - source, id, created, active, title, author, body, link, time, json(action) - from items - order by case when time = 0 then created else time end, id - `) -} - -func GetActiveItemsForSource(db DB, source string) ([]Item, error) { - return getItems(db, ` - select - source, id, created, active, title, author, body, link, time, json(action) - from items - where - source = ? - and active <> 0 - order by case when time = 0 then created else time end, id - `, source) -} - -func GetAllItemsForSource(db DB, source string) ([]Item, error) { - return getItems(db, ` - select - source, id, created, active, title, author, body, link, time, json(action) - from items - where - source = ? - order by case when time = 0 then created else time end, id - `, source) -} - func GetState(db DB, source string) ([]byte, error) { row := db.QueryRow("select state from sources where name = ?", source) var state []byte diff --git a/core/source_test.go b/core/source_test.go index 520cb77..b17d8a3 100644 --- a/core/source_test.go +++ b/core/source_test.go @@ -2,7 +2,6 @@ package core import ( "errors" - "fmt" "slices" "strings" "testing" @@ -46,90 +45,6 @@ func TestCreateSource(t *testing.T) { } } -func AssertItemIs(t *testing.T, item Item, expected string) { - actual := fmt.Sprintf( - "%s/%s/%t/%s/%s/%s/%s/%d", - item.Source, - item.Id, - item.Active, - item.Title, - item.Author, - item.Body, - item.Link, - item.Time, - ) - if actual != expected { - t.Fatalf("expected %s, got %s", expected, actual) - } -} - -func TestAddItem(t *testing.T) { - db := EphemeralDb(t) - if err := AddSource(db, "test"); err != nil { - t.Fatalf("failed to add source: %v", err) - } - - if err := AddItems(db, []Item{ - {"test", "one", 0, true, "", "", "", "", 0, nil}, - {"test", "two", 0, true, "title", "author", "body", "link", 123456, nil}, - }); err != nil { - t.Fatalf("failed to add items: %v", err) - } - items, err := GetActiveItemsForSource(db, "test") - if err != nil { - t.Fatalf("failed to get active items: %v", err) - } - if len(items) != 2 { - t.Fatal("should get two items") - } - // order is by (time ?? created) so this ordering is correct as long as you don't run it in early 1970 - AssertItemIs(t, items[0], "test/two/true/title/author/body/link/123456") - AssertItemIs(t, items[1], "test/one/true/////0") - - if _, err = DeactivateItem(db, "test", "one"); err != nil { - t.Fatal(err) - } - items, err = GetActiveItemsForSource(db, "test") - if err != nil { - t.Fatal(err) - } - if len(items) != 1 { - t.Fatal("should get one item") - } - - items, err = GetAllItemsForSource(db, "test") - if err != nil { - t.Fatal(err) - } - if len(items) != 2 { - t.Fatal("should get two items") - } - - deleted, err := DeleteItem(db, "test", "one") - if err != nil { - t.Fatal(err) - } - if deleted != 1 { - t.Fatal("expected one deletion") - } - - deleted, err = DeleteItem(db, "test", "one") - if err != nil { - t.Fatal(err) - } - if deleted != 0 { - t.Fatal("expected no deletion") - } - - items, err = GetAllItemsForSource(db, "test") - if err != nil { - t.Fatal(err) - } - if len(items) != 1 { - t.Fatal("should get one item") - } -} - func TestUpdateSourceAddAndDelete(t *testing.T) { db := EphemeralDb(t) if err := AddSource(db, "test"); err != nil {