From f804299180411a4ffcc056c218dd2d85b5899333 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Wed, 29 Jan 2025 08:48:12 -0800 Subject: [PATCH] Add actions to items --- core/action_test.go | 32 +++++++++++++++++++++++++++ core/db_test.go | 6 +++--- core/item.go | 30 ++++++++++++++++++-------- core/item_test.go | 8 ++++++- core/source.go | 37 +++++++++++++++++++++----------- core/source_test.go | 9 ++++---- core/sql/0001_initial_schema.sql | 1 + 7 files changed, 93 insertions(+), 30 deletions(-) diff --git a/core/action_test.go b/core/action_test.go index 8f001e1..3546e9b 100644 --- a/core/action_test.go +++ b/core/action_test.go @@ -147,4 +147,36 @@ func TestExecute(t *testing.T) { res, err = execute([]string{"jq", "-cn", `["a", "a"] | .[] | {id: .}`}) assertNil(err) assertLen(res, 2) + + // Action keys are detected even with empty values + res, err = execute([]string{"jq", "-cn", `{id: "test", action: {"hello": null}}`}) + assertNil(err) + assertLen(res, 1) + if res[0].Action["hello"] == nil { + t.Fatal("missing hello action") + } + if res[0].Action["goodbye"] != nil { + t.Fatal("nonexistent action should key to nil in Action") + } + + res, err = execute([]string{"jq", "-cn", `{id: "test", action: {"hello": ""}}`}) + assertNil(err) + assertLen(res, 1) + if res[0].Action["hello"] == nil { + t.Fatal("missing hello action") + } + + res, err = execute([]string{"jq", "-cn", `{id: "test", action: {"hello": []}}`}) + assertNil(err) + assertLen(res, 1) + if res[0].Action["hello"] == nil { + t.Fatal("missing hello action") + } + + res, err = execute([]string{"jq", "-cn", `{id: "test", action: {"hello": {}}}`}) + assertNil(err) + assertLen(res, 1) + if res[0].Action["hello"] == nil { + t.Fatal("missing hello action") + } } diff --git a/core/db_test.go b/core/db_test.go index 1e73670..555b8a2 100644 --- a/core/db_test.go +++ b/core/db_test.go @@ -17,8 +17,8 @@ func TestDeleteSourceCascade(t *testing.T) { t.Fatalf("failed to add source2: %v", err) } if err := AddItems(db, []Item{ - {"source1", "item1", 0, true, "", "", "", "", 0}, - {"source2", "item2", 0, true, "", "", "", "", 0}, + {"source1", "item1", 0, true, "", "", "", "", 0, nil}, + {"source2", "item2", 0, true, "", "", "", "", 0, nil}, }); err != nil { t.Fatalf("failed to add items: %v", err) } @@ -42,7 +42,7 @@ func TestDeleteSourceCascade(t *testing.T) { t.Fatalf("Expected only 1 item after source delete, got %d", len(items)) } - err = AddItems(db, []Item{{"source1", "item3", 0, true, "", "", "", "", 0}}) + err = AddItems(db, []Item{{"source1", "item3", 0, true, "", "", "", "", 0, nil}}) if err == nil { t.Fatal("Unexpected success adding item for nonexistent source") } diff --git a/core/item.go b/core/item.go index 87d79d8..2002d3b 100644 --- a/core/item.go +++ b/core/item.go @@ -1,21 +1,33 @@ package core import ( + "database/sql/driver" "encoding/json" "fmt" "log" ) +type Actions map[string]json.RawMessage + +func (a Actions) Value() (driver.Value, error) { + return json.Marshal(a) +} + +func (a *Actions) Scan(value interface{}) error { + return json.Unmarshal([]byte(value.(string)), a) +} + type Item struct { - Source string `json:"source"` - Id string `json:"id"` - Created int `json:"created"` - Active bool `json:"active"` - Title string `json:"title"` - Author string `json:"author"` - Body string `json:"body"` - Link string `json:"link"` - Time int `json:"time"` + Source string `json:"source"` + Id string `json:"id"` + Created int `json:"created"` + Active bool `json:"active"` + Title string `json:"title"` + Author string `json:"author"` + Body string `json:"body"` + Link string `json:"link"` + Time int `json:"time"` + Action Actions `json:"action"` } // Whether an item that no longer appears in a fetch can be deleted. diff --git a/core/item_test.go b/core/item_test.go index 2a5557f..6d32b88 100644 --- a/core/item_test.go +++ b/core/item_test.go @@ -1,6 +1,9 @@ package core -import "testing" +import ( + "encoding/json" + "testing" +) func TestItemFormatsExist(t *testing.T) { for name := range AvailableFormats { @@ -30,6 +33,9 @@ func TestItemRoundTrip(t *testing.T) { Body: "body", Link: "link", Time: 123456, + Action: map[string]json.RawMessage{ + "hello": json.RawMessage(`"world"`), + }, } if err := AddItems(db, []Item{item1}); err != nil { t.Fatalf("update failed: %v", err) diff --git a/core/source.go b/core/source.go index c677add..3e7c87b 100644 --- a/core/source.go +++ b/core/source.go @@ -51,16 +51,20 @@ func DeleteSource(db *DB, name string) error { func AddItems(db *DB, items []Item) error { return db.Transact(func(tx *sql.Tx) error { stmt, err := tx.Prepare(` - insert into items (source, id, active, title, author, body, link, time) - values (?, ?, ?, ?, ?, ?, ?, ?) + insert into items (source, id, active, title, author, body, link, time, action) + values (?, ?, ?, ?, ?, ?, ?, ?, jsonb(?)) `) if err != nil { - return err + return fmt.Errorf("failed to prepare insert: %v", err) } for _, item := range items { - _, err = stmt.Exec(item.Source, item.Id, true, item.Title, item.Author, item.Body, item.Link, item.Time) + actions, err := json.Marshal(item.Action) if err != nil { - return err + 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 @@ -100,7 +104,8 @@ func UpdateItems(db *DB, items []Item) error { author = ?, body = ?, link = ?, - time = ? + time = ?, + action = jsonb(?) where source = ? and id = ? `) @@ -108,7 +113,11 @@ func UpdateItems(db *DB, items []Item) error { return err } for _, item := range items { - _, err = stmt.Exec(item.Title, item.Author, item.Body, item.Link, item.Time, item.Source, item.Id) + 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 } @@ -161,7 +170,7 @@ func getItems(db *DB, query string, args ...any) ([]Item, error) { 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) + 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 } @@ -172,7 +181,7 @@ func getItems(db *DB, query string, args ...any) ([]Item, error) { func GetItem(db *DB, source string, id string) (Item, error) { items, err := getItems(db, ` - select source, id, created, active, title, author, body, link, time + select source, id, created, active, title, author, body, link, time, json(action) from items where source = ? and id = ? @@ -189,7 +198,7 @@ func GetItem(db *DB, source string, id string) (Item, error) { func GetAllActiveItems(db *DB) ([]Item, error) { return getItems(db, ` select - source, id, created, active, title, author, body, link, time + source, id, created, active, title, author, body, link, time, json(action) from items where active <> 0 `) @@ -198,7 +207,7 @@ func GetAllActiveItems(db *DB) ([]Item, error) { func GetAllItems(db *DB) ([]Item, error) { return getItems(db, ` select - source, id, created, active, title, author, body, link, time + source, id, created, active, title, author, body, link, time, json(action) from items `) } @@ -206,7 +215,7 @@ func GetAllItems(db *DB) ([]Item, error) { func GetActiveItemsForSource(db *DB, source string) ([]Item, error) { return getItems(db, ` select - source, id, created, active, title, author, body, link, time + source, id, created, active, title, author, body, link, time, json(action) from items where source = ? @@ -217,7 +226,7 @@ func GetActiveItemsForSource(db *DB, source string) ([]Item, error) { func GetAllItemsForSource(db *DB, source string) ([]Item, error) { return getItems(db, ` select - source, id, created, active, title, author, body, link, time + source, id, created, active, title, author, body, link, time, json(action) from items where source = ? @@ -225,6 +234,8 @@ func GetAllItemsForSource(db *DB, source string) ([]Item, error) { } // Given the results of a fetch, add new items, update existing items, and delete expired items. +// +// Returns the number of new and deleted items on success. func UpdateWithFetchedItems(db *DB, source string, items []Item) (int, int, error) { // Get the existing items existingItems, err := GetAllItemsForSource(db, source) diff --git a/core/source_test.go b/core/source_test.go index f8bc0cd..e5af85e 100644 --- a/core/source_test.go +++ b/core/source_test.go @@ -61,8 +61,8 @@ func TestAddItem(t *testing.T) { } if err := AddItems(db, []Item{ - {"test", "one", 0, true, "", "", "", "", 0}, - {"test", "two", 0, true, "title", "author", "body", "link", 123456}, + {"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) } @@ -219,8 +219,9 @@ func TestOnCreateAction(t *testing.T) { if add != 1 || err != nil { t.Fatal("failed update with alter oncreate") } - if getItem("two").Title != "Goodbye, World" { - t.Fatal("title not updated") + two := getItem("two") + if two.Title != "Goodbye, World" { + t.Fatalf("title not updated, is: %s", two.Title) } // on_create can add a field diff --git a/core/sql/0001_initial_schema.sql b/core/sql/0001_initial_schema.sql index 2e299d1..6b53563 100644 --- a/core/sql/0001_initial_schema.sql +++ b/core/sql/0001_initial_schema.sql @@ -19,6 +19,7 @@ create table items( body text, link text, time int, + action blob, primary key (source, id), foreign key (source) references sources (name) on delete cascade ) strict;