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 {
Close() error
Migrate() error
CountItems() int
CreateFeed(params CreateFeedParams) *Feed
CreateFeed(params model.CreateFeedParams) *model.Feed
CreateFolder(title string) *Folder
CreateItems(items []Item) bool
DeleteFeed(feedId int64) bool
DeleteFolder(folderId int64) bool
DeleteOldItems()
FeedStats() []FeedStat
GetFeed(id int64) *Feed
GetFeed(id int64) *model.Feed
GetFeedState(feedID int64) (*FeedState, error)
GetItem(id int64) *Item
GetSettings() Settings
ListFeedStates() ([]FeedState, error)
ListFeeds() []Feed
ListFolders() []Folder
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)
@@ -25,4 +30,3 @@ type Storage interface {
UpdateItemStatus(item_id int64, status ItemStatus) 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 (
"database/sql"
"log"
"github.com/nkanaev/yarr/src/storage/model"
)
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
}
func (s *SQLiteStorage) CreateFeed(params CreateFeedParams) *Feed {
func (s *SQLiteStorage) CreateFeed(params model.CreateFeedParams) *model.Feed {
title := params.Title
if title == "" {
title = params.FeedLink
@@ -73,13 +56,6 @@ func (s *SQLiteStorage) DeleteFeed(feedId int64) bool {
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) {
_, err := s.db.Exec(`
update feeds set
@@ -104,8 +80,8 @@ func (s *SQLiteStorage) UpdateFeed(feedId int64, params UpdateFeedParams) (bool,
return true, nil
}
func (s *SQLiteStorage) ListFeeds() []Feed {
result := make([]Feed, 0)
func (s *SQLiteStorage) ListFeeds() []model.Feed {
result := make([]model.Feed, 0)
rows, err := s.db.Query(`
select id, folder_id, title, description, link, feed_link,
ifnull(length(icon), 0) > 0 as has_icon
@@ -117,7 +93,7 @@ func (s *SQLiteStorage) ListFeeds() []Feed {
return result
}
for rows.Next() {
var f Feed
var f model.Feed
err = rows.Scan(
&f.Id,
&f.FolderId,

View File

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

View File

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

View File

@@ -1,57 +1,20 @@
package sqlite
import (
"cmp"
"database/sql"
"database/sql/driver"
"encoding/json"
"fmt"
"log"
"sort"
"slices"
"strings"
"time"
"github.com/nkanaev/yarr/src/storage/model"
)
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
// TODO: serialize/deserialize
func (m *MediaLinks) Scan(src any) error {
switch data := src.(type) {
case []byte:
@@ -67,55 +30,6 @@ func (m MediaLinks) Value() (driver.Value, error) {
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 {
tx, err := s.db.Begin()
if err != nil {
@@ -125,10 +39,13 @@ func (s *SQLiteStorage) CreateItems(items []Item) bool {
now := time.Now().UTC()
itemsSorted := ItemList(items)
sort.Sort(itemsSorted)
slices.SortStableFunc(items, func(a, b model.Item) int {
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(`
insert into items (
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("date_arrived", now),
sql.Named("last_arrived", now),
sql.Named("status", UNREAD),
sql.Named("status", model.UNREAD),
)
if err != nil {
log.Print(err)

View File

@@ -6,34 +6,6 @@ import (
"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 {
return Settings{
Filter: "",
@@ -89,19 +61,6 @@ func (s *SQLiteStorage) GetSettings() Settings {
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 {
tx, err := s.db.Begin()
if err != nil {

View File

@@ -21,15 +21,6 @@ type SQLiteStorage struct {
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) {
if pos := strings.IndexRune(path, '?'); pos == -1 {
params := "_journal=WAL&_sync=NORMAL&_busy_timeout=5000&cache=shared"