move structs to model

This commit is contained in:
nkanaev
2026-06-09 16:04:13 +01:00
parent dc836ed4fd
commit dee386b586
8 changed files with 210 additions and 207 deletions

View File

@@ -1,22 +1,27 @@
package storage package factory
import (
"github.com/nkanaev/yarr/src/storage/model"
)
type Storage interface { type Storage interface {
Close() error Close() error
Migrate() error
CountItems() int CountItems() int
CreateFeed(params CreateFeedParams) *Feed CreateFeed(params model.CreateFeedParams) *model.Feed
CreateFolder(title string) *Folder CreateFolder(title string) *Folder
CreateItems(items []Item) bool CreateItems(items []Item) bool
DeleteFeed(feedId int64) bool DeleteFeed(feedId int64) bool
DeleteFolder(folderId int64) bool DeleteFolder(folderId int64) bool
DeleteOldItems() DeleteOldItems()
FeedStats() []FeedStat FeedStats() []FeedStat
GetFeed(id int64) *Feed GetFeed(id int64) *model.Feed
GetFeedState(feedID int64) (*FeedState, error) GetFeedState(feedID int64) (*FeedState, error)
GetItem(id int64) *Item GetItem(id int64) *Item
GetSettings() Settings GetSettings() Settings
ListFeedStates() ([]FeedState, error) ListFeedStates() ([]FeedState, error)
ListFeeds() []Feed ListFeeds() []model.Feed
ListFolders() []Folder ListFolders() []model.Folder
ListItems(filter ItemFilter, limit int, newestFirst bool, withContent bool) []Item ListItems(filter ItemFilter, limit int, newestFirst bool, withContent bool) []Item
MarkItemsRead(filter MarkFilter) bool MarkItemsRead(filter MarkFilter) bool
UpdateFeed(feedId int64, params UpdateFeedParams) (bool, error) UpdateFeed(feedId int64, params UpdateFeedParams) (bool, error)
@@ -25,4 +30,3 @@ type Storage interface {
UpdateItemStatus(item_id int64, status ItemStatus) bool UpdateItemStatus(item_id int64, status ItemStatus) bool
UpdateSettings(params UpdateSettingsParams) bool UpdateSettings(params UpdateSettingsParams) bool
} }

182
src/storage/model/model.go Normal file
View File

@@ -0,0 +1,182 @@
package model
import (
"encoding/json"
"time"
)
type Feed struct {
Id int64 `json:"id"`
FolderId *int64 `json:"folder_id"`
Title string `json:"title"`
Description string `json:"description"`
Link string `json:"link"`
FeedLink string `json:"feed_link"`
Icon *[]byte `json:"icon,omitempty"`
HasIcon bool `json:"has_icon"`
}
type CreateFeedParams struct {
Title string
Description string
Link string
FeedLink string
FolderID *int64
}
type Item struct {
Id int64 `json:"id"`
GUID string `json:"guid"`
FeedId int64 `json:"feed_id"`
Title string `json:"title"`
Link string `json:"link"`
Content string `json:"content,omitempty"`
Date time.Time `json:"date"`
Status ItemStatus `json:"status"`
MediaLinks MediaLinks `json:"media_links"`
}
type ItemStatus int
const (
UNREAD ItemStatus = 0
READ ItemStatus = 1
STARRED ItemStatus = 2
)
var StatusRepresentations = map[ItemStatus]string{
UNREAD: "unread",
READ: "read",
STARRED: "starred",
}
var StatusValues = map[string]ItemStatus{
"unread": UNREAD,
"read": READ,
"starred": STARRED,
}
func (s ItemStatus) MarshalJSON() ([]byte, error) {
return json.Marshal(StatusRepresentations[s])
}
func (s *ItemStatus) UnmarshalJSON(b []byte) error {
var str string
if err := json.Unmarshal(b, &str); err != nil {
return err
}
*s = StatusValues[str]
return nil
}
type MediaLink struct {
URL string `json:"url"`
Type string `json:"type"`
Description string `json:"description,omitempty"`
}
type MediaLinks []MediaLink
type ItemFilter struct {
FolderID *int64
FeedID *int64
Status *ItemStatus
Search *string
After *int64
IDs *[]int64
SinceID *int64
MaxID *int64
Before *time.Time
}
type MarkFilter struct {
FolderID *int64
FeedID *int64
Before *time.Time
}
type Folder struct {
Id int64 `json:"id"`
Title string `json:"title"`
IsExpanded bool `json:"is_expanded"`
}
type UpdateFolderParams struct {
Title *string
IsExpanded *bool
}
type Settings struct {
Filter string `json:"filter"`
Feed string `json:"feed"`
FeedListWidth int `json:"feed_list_width"`
ItemListWidth int `json:"item_list_width"`
SortNewestFirst bool `json:"sort_newest_first"`
ThemeName string `json:"theme_name"`
ThemeFont string `json:"theme_font"`
ThemeSize int `json:"theme_size"`
RefreshRate int64 `json:"refresh_rate"`
Language string `json:"language"`
}
type UpdateSettingsParams struct {
Filter *string `json:"filter"`
Feed *string `json:"feed"`
FeedListWidth *int `json:"feed_list_width"`
ItemListWidth *int `json:"item_list_width"`
SortNewestFirst *bool `json:"sort_newest_first"`
ThemeName *string `json:"theme_name"`
ThemeFont *string `json:"theme_font"`
ThemeSize *int `json:"theme_size"`
RefreshRate *int64 `json:"refresh_rate"`
Language *string `json:"language"`
}
func (s Settings) Map() map[string]any {
return map[string]any{
"filter": s.Filter,
"feed": s.Feed,
"feed_list_width": s.FeedListWidth,
"item_list_width": s.ItemListWidth,
"sort_newest_first": s.SortNewestFirst,
"theme_name": s.ThemeName,
"theme_font": s.ThemeFont,
"theme_size": s.ThemeSize,
"refresh_rate": s.RefreshRate,
"language": s.Language,
}
}
type FeedState struct {
FeedID int64
LastRefreshed time.Time
LastError string
HTTPLastModified string
HTTPEtag string
}
type UpdateFeedStateParams struct {
LastRefreshed *time.Time
LastError *string
HTTPLastModified *string
HTTPEtag *string
}
type UpdateFeedParams struct {
Title *string
FeedLink *string
FolderID Nullable[int64]
Icon Nullable[[]byte]
}
type Nullable[T any] struct {
Set bool
Value *T
}
func SetNullable[T any](v *T) Nullable[T] {
return Nullable[T]{Set: true, Value: v}
}

View File

@@ -3,28 +3,11 @@ package sqlite
import ( import (
"database/sql" "database/sql"
"log" "log"
"github.com/nkanaev/yarr/src/storage/model"
) )
type Feed struct { func (s *SQLiteStorage) CreateFeed(params model.CreateFeedParams) *model.Feed {
Id int64 `json:"id"`
FolderId *int64 `json:"folder_id"`
Title string `json:"title"`
Description string `json:"description"`
Link string `json:"link"`
FeedLink string `json:"feed_link"`
Icon *[]byte `json:"icon,omitempty"`
HasIcon bool `json:"has_icon"`
}
type CreateFeedParams struct {
Title string
Description string
Link string
FeedLink string
FolderID *int64
}
func (s *SQLiteStorage) CreateFeed(params CreateFeedParams) *Feed {
title := params.Title title := params.Title
if title == "" { if title == "" {
title = params.FeedLink title = params.FeedLink
@@ -73,13 +56,6 @@ func (s *SQLiteStorage) DeleteFeed(feedId int64) bool {
return nrows == 1 return nrows == 1
} }
type UpdateFeedParams struct {
Title *string
FeedLink *string
FolderID Nullable[int64]
Icon Nullable[[]byte]
}
func (s *SQLiteStorage) UpdateFeed(feedId int64, params UpdateFeedParams) (bool, error) { func (s *SQLiteStorage) UpdateFeed(feedId int64, params UpdateFeedParams) (bool, error) {
_, err := s.db.Exec(` _, err := s.db.Exec(`
update feeds set update feeds set
@@ -104,8 +80,8 @@ func (s *SQLiteStorage) UpdateFeed(feedId int64, params UpdateFeedParams) (bool,
return true, nil return true, nil
} }
func (s *SQLiteStorage) ListFeeds() []Feed { func (s *SQLiteStorage) ListFeeds() []model.Feed {
result := make([]Feed, 0) result := make([]model.Feed, 0)
rows, err := s.db.Query(` rows, err := s.db.Query(`
select id, folder_id, title, description, link, feed_link, select id, folder_id, title, description, link, feed_link,
ifnull(length(icon), 0) > 0 as has_icon ifnull(length(icon), 0) > 0 as has_icon
@@ -117,7 +93,7 @@ func (s *SQLiteStorage) ListFeeds() []Feed {
return result return result
} }
for rows.Next() { for rows.Next() {
var f Feed var f model.Feed
err = rows.Scan( err = rows.Scan(
&f.Id, &f.Id,
&f.FolderId, &f.FolderId,

View File

@@ -5,14 +5,6 @@ import (
"time" "time"
) )
type FeedState struct {
FeedID int64
LastRefreshed time.Time
LastError string
HTTPLastModified string
HTTPEtag string
}
func (s *SQLiteStorage) ListFeedStates() ([]FeedState, error) { func (s *SQLiteStorage) ListFeedStates() ([]FeedState, error) {
rows, err := s.db.Query(` rows, err := s.db.Query(`
select select
@@ -72,13 +64,6 @@ func (s *SQLiteStorage) GetFeedState(feedID int64) (*FeedState, error) {
return &state, nil return &state, nil
} }
type UpdateFeedStateParams struct {
LastRefreshed *time.Time
LastError *string
HTTPLastModified *string
HTTPEtag *string
}
func (s *SQLiteStorage) UpdateFeedState(feedID int64, params UpdateFeedStateParams) (bool, error) { func (s *SQLiteStorage) UpdateFeedState(feedID int64, params UpdateFeedStateParams) (bool, error) {
lastError := params.LastError lastError := params.LastError
if lastError != nil && *lastError == "" { if lastError != nil && *lastError == "" {

View File

@@ -5,12 +5,6 @@ import (
"log" "log"
) )
type Folder struct {
Id int64 `json:"id"`
Title string `json:"title"`
IsExpanded bool `json:"is_expanded"`
}
func (s *SQLiteStorage) CreateFolder(title string) *Folder { func (s *SQLiteStorage) CreateFolder(title string) *Folder {
expanded := true expanded := true
row := s.db.QueryRow(` row := s.db.QueryRow(`
@@ -38,11 +32,6 @@ func (s *SQLiteStorage) DeleteFolder(folderId int64) bool {
return err == nil return err == nil
} }
type UpdateFolderParams struct {
Title *string
IsExpanded *bool
}
func (s *SQLiteStorage) UpdateFolder(folderId int64, params UpdateFolderParams) (bool, error) { func (s *SQLiteStorage) UpdateFolder(folderId int64, params UpdateFolderParams) (bool, error) {
_, err := s.db.Exec(` _, err := s.db.Exec(`
update folders set update folders set

View File

@@ -1,57 +1,20 @@
package sqlite package sqlite
import ( import (
"cmp"
"database/sql" "database/sql"
"database/sql/driver" "database/sql/driver"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log"
"sort" "slices"
"strings" "strings"
"time" "time"
"github.com/nkanaev/yarr/src/storage/model"
) )
type ItemStatus int // TODO: serialize/deserialize
const (
UNREAD ItemStatus = 0
READ ItemStatus = 1
STARRED ItemStatus = 2
)
var StatusRepresentations = map[ItemStatus]string{
UNREAD: "unread",
READ: "read",
STARRED: "starred",
}
var StatusValues = map[string]ItemStatus{
"unread": UNREAD,
"read": READ,
"starred": STARRED,
}
func (s ItemStatus) MarshalJSON() ([]byte, error) {
return json.Marshal(StatusRepresentations[s])
}
func (s *ItemStatus) UnmarshalJSON(b []byte) error {
var str string
if err := json.Unmarshal(b, &str); err != nil {
return err
}
*s = StatusValues[str]
return nil
}
type MediaLink struct {
URL string `json:"url"`
Type string `json:"type"`
Description string `json:"description,omitempty"`
}
type MediaLinks []MediaLink
func (m *MediaLinks) Scan(src any) error { func (m *MediaLinks) Scan(src any) error {
switch data := src.(type) { switch data := src.(type) {
case []byte: case []byte:
@@ -67,55 +30,6 @@ func (m MediaLinks) Value() (driver.Value, error) {
return json.Marshal(m) return json.Marshal(m)
} }
type Item struct {
Id int64 `json:"id"`
GUID string `json:"guid"`
FeedId int64 `json:"feed_id"`
Title string `json:"title"`
Link string `json:"link"`
Content string `json:"content,omitempty"`
Date time.Time `json:"date"`
Status ItemStatus `json:"status"`
MediaLinks MediaLinks `json:"media_links"`
}
type ItemFilter struct {
FolderID *int64
FeedID *int64
Status *ItemStatus
Search *string
After *int64
IDs *[]int64
SinceID *int64
MaxID *int64
Before *time.Time
}
type MarkFilter struct {
FolderID *int64
FeedID *int64
Before *time.Time
}
type ItemList []Item
func (list ItemList) Len() int {
return len(list)
}
func (list ItemList) SortKey(i int) string {
return list[i].Date.Format(time.RFC3339) + "::" + list[i].GUID
}
func (list ItemList) Less(i, j int) bool {
return list.SortKey(i) < list.SortKey(j)
}
func (list ItemList) Swap(i, j int) {
list[i], list[j] = list[j], list[i]
}
func (s *SQLiteStorage) CreateItems(items []Item) bool { func (s *SQLiteStorage) CreateItems(items []Item) bool {
tx, err := s.db.Begin() tx, err := s.db.Begin()
if err != nil { if err != nil {
@@ -125,10 +39,13 @@ func (s *SQLiteStorage) CreateItems(items []Item) bool {
now := time.Now().UTC() now := time.Now().UTC()
itemsSorted := ItemList(items) slices.SortStableFunc(items, func(a, b model.Item) int {
sort.Sort(itemsSorted) sa := a.Date.Format(time.RFC3339) + "::" + a.GUID
sb := b.Date.Format(time.RFC3339) + "::" + b.GUID
return cmp.Compare(sa, sb)
})
for _, item := range itemsSorted { for _, item := range items {
_, err = tx.Exec(` _, err = tx.Exec(`
insert into items ( insert into items (
guid, feed_id, title, link, date, guid, feed_id, title, link, date,
@@ -151,7 +68,7 @@ func (s *SQLiteStorage) CreateItems(items []Item) bool {
sql.Named("media_links", item.MediaLinks), sql.Named("media_links", item.MediaLinks),
sql.Named("date_arrived", now), sql.Named("date_arrived", now),
sql.Named("last_arrived", now), sql.Named("last_arrived", now),
sql.Named("status", UNREAD), sql.Named("status", model.UNREAD),
) )
if err != nil { if err != nil {
log.Print(err) log.Print(err)

View File

@@ -6,34 +6,6 @@ import (
"log" "log"
) )
type Settings struct {
Filter string `json:"filter"`
Feed string `json:"feed"`
FeedListWidth int `json:"feed_list_width"`
ItemListWidth int `json:"item_list_width"`
SortNewestFirst bool `json:"sort_newest_first"`
ThemeName string `json:"theme_name"`
ThemeFont string `json:"theme_font"`
ThemeSize int `json:"theme_size"`
RefreshRate int64 `json:"refresh_rate"`
Language string `json:"language"`
}
func (s Settings) Map() map[string]any {
return map[string]any{
"filter": s.Filter,
"feed": s.Feed,
"feed_list_width": s.FeedListWidth,
"item_list_width": s.ItemListWidth,
"sort_newest_first": s.SortNewestFirst,
"theme_name": s.ThemeName,
"theme_font": s.ThemeFont,
"theme_size": s.ThemeSize,
"refresh_rate": s.RefreshRate,
"language": s.Language,
}
}
func settingsDefaults() Settings { func settingsDefaults() Settings {
return Settings{ return Settings{
Filter: "", Filter: "",
@@ -89,19 +61,6 @@ func (s *SQLiteStorage) GetSettings() Settings {
return result return result
} }
type UpdateSettingsParams struct {
Filter *string `json:"filter"`
Feed *string `json:"feed"`
FeedListWidth *int `json:"feed_list_width"`
ItemListWidth *int `json:"item_list_width"`
SortNewestFirst *bool `json:"sort_newest_first"`
ThemeName *string `json:"theme_name"`
ThemeFont *string `json:"theme_font"`
ThemeSize *int `json:"theme_size"`
RefreshRate *int64 `json:"refresh_rate"`
Language *string `json:"language"`
}
func (s *SQLiteStorage) UpdateSettings(params UpdateSettingsParams) bool { func (s *SQLiteStorage) UpdateSettings(params UpdateSettingsParams) bool {
tx, err := s.db.Begin() tx, err := s.db.Begin()
if err != nil { if err != nil {

View File

@@ -21,15 +21,6 @@ type SQLiteStorage struct {
db *sql.DB db *sql.DB
} }
type Nullable[T any] struct {
Set bool
Value *T
}
func SetNullable[T any](v *T) Nullable[T] {
return Nullable[T]{Set: true, Value: v}
}
func New(path string) (*SQLiteStorage, error) { func New(path string) (*SQLiteStorage, error) {
if pos := strings.IndexRune(path, '?'); pos == -1 { if pos := strings.IndexRune(path, '?'); pos == -1 {
params := "_journal=WAL&_sync=NORMAL&_busy_timeout=5000&cache=shared" params := "_journal=WAL&_sync=NORMAL&_busy_timeout=5000&cache=shared"